I have spent the last two weeks looking into the problem of doing opportunistic encryption on the IP layer using IPsec Transport Mode and public keys in DNS.


What I have done so far:

  • Designed a system based on existing standards and practises how to make end-to-end opportunistic IP layer encryption a real possibility. See below for details I believe to work.

  • Read standard texts, documentation and looked briefly at least a few existing resolvers and IPsec implementations.

  • Developed a forwarding DNS resolver. When this resolver sees A and AAAA queries it also looks up a public key (IPSECKEY) for the corresponding name and sets up a security policy for the addresses involved to use IPsec when possible.

This forwarding resolver needs to talk to a full resolver to work. This full resolver might implement DNSSEC validation for protection against active attacks. The full resolver can run on the same node.

  • Created a website for the project.

  • Written this text.

What I plan to do next:

  • Patch an IKE daemon (probably racoon but not entirely sure yet) to lookup IPSECKEY records and set up new peers with public keys when given an FQDN as identifier.

  • Patch the IKE daemon to be able to set up new peers and load public keys on demand through commands on an administration socket.

  • Add code to my resolver to use the above command.


Storing public keys in DNS and using them together with an IPsec key daemon makes it possible to use opportunistic encryption on the IP layer. To do this there are already at least two methods to store a public key for use with IPsec in DNS, plain public keys in the IPSECKEY record, RFC 4025 (standards track), and X.509 or SPKI certificates, RFC 4398 (proposed standard). There is also an established method of using IPSECKEY public key records to establish IPsec tunnels, usually between security gateways protecting local networks, RFC 4322 (informational).

The implementations I have found that use RFC 4025 and RFC 4322 are the descendants of the now dead Linux FreeS/WAN project, Openswan and Strongswan. These systems can be used to establish tunnels with opportunistic encryption, typically between security gateways as outlined in RFC 4322.

RFC 4398's CERT records are supported by the well-known and portable racoon IKE daemon but naturally only when a peer initiates contact and not for the use of the initiating node.

I want to fix at least some of this by writing new software or by patching existing software. My focus is on end-to-end encryption and authentication: I want to use Transport Mode instead of tunnels and forward zones in DNS instead of reverse zones. I also want to keep away from the X.509 format and the well known problems of ASN.1 encoding. I intend to work with plain public keys and the IPSECKEY record.

Typical scenario:

  1. A process on Internet node Alice wants to talk to a process on node Bob.

  2. Alice looks up the IPv4 address (A record) or IPv6 address (AAAA) in DNS.

  3. Alice also looks up Bob's public key (IPSECKEY) in DNS.

  4. If the A or AAAA and the IPSECKEY records exist Alice asks her IKE daemon to establish a security association for Bob (or just sets up a peer with the public key in the IKE daemon and sets up a security policy for the involved addresses -- the security association will be negotiated if any traffic ever comes).

  5. Alice's IKE daemon talks to Bob's and checks that Bob knows the private key matching the public key found in DNS and tries to establish a Security Association.

  6. Through IKE Bob realizes that Alice wants to talk and looks up her public key in DNS so he can authenticate her as well. Note that the identifier sent must be the FQDN, not the IP address.

  7. If everything looks fine Alice and Bob have authenticated each other and have established an IPsec Security Association.

The way to make use of this in everyday communications between arbitrary nodes is, of course, to be able to control your node's forward DNS records. This is usually much easier than controlling the PTR record (reverse zone) for the IP address(es) you happen to use.

The usual way of automatically updating your forward zone is by using the DNS Update protocol, RFC 2136. Many DHCP clients support this protocol out of the box. Other systems, for instance those using IPv6 SLAAC, will have to use some other means, perhaps by calling the nsupdate program bundled with BIND whenever an interface is configured.

However, even though DNS Update itself is widely supported it's not certain that the implementations support the IPSECKEY RR. I hope to fix that, too, at least for some cases.

The method outlined above might be used both to protect nodes in an end-to-end scenario and to protect whole networks behind a security gateway in a typical NAT44 network.

Using IPSECKEY public keys stored in plain DNS should protect against passive surveillance. If the IPSECKEY record is signed and validated using DNSSEC it should also protect against an active attack. The weakest part of the scenario is probably the DNS Update authentication but I will need help from others in analysing the security implications. Please feel free to get in touch with me!

To make this all work I will have to:

  • Write a resolver that catches A or AAAA lookups on Alice and initiate a lookup of the IPSECKEY record as well.

  • Have the resolver set up security policies for the addresses involved on the initiating node.

  • Patch the IKE daemon to lookup IPSECKEY and set up a new peer with a public key when it receives an FQDN as an identifier.

  • Possibly patch the IKE daemon to be able to take orders from the resolver and set up new peers and load a public key on demand.

Other interesting methods for IP layer opportunistic encryption are RFC 5386, Better-Than-Nothing Security (BTNS) (standards track), a way of encrypting traffic without authentication, and the use of IPv6 Cryptographically Generated Addresses (CGA) (RFC 3972) for public key authentication purposes.

The CGA hack only works on IPv6 but BTNS should work on both IPv4 and IPv6. Implementing BTNS might be a natural followup on this project.


Forwarding Resolver

I have written a small forwarding resolver in Perl using Net::DNS that catches A and AAAA records and looks up the corresponding IPSECKEY record for the same name. Here's a typical debug run where I do a lookup for the host name ipsec2.hack.org in another window:

ipsec1# ./ns.pl
Adding local address:
Any AAAA records for ipsec2.hack.org?
Oh! It's A or AAAA!
We got an IPSECKEY!
Public key (RSA): AQO69zuWw9eX59xnZjzSN+N9DUcFIm0htb2XRLThLaVZC1uRVJwW2SkeLrlJuio6iZqh3WS2OfHYcn8zE0H0oVnaG5/38um/6LVN/ysOWXt8FPUwV4PVyjlJJYC15NR2+h6kNbR5Z8XPz0IxvtYO2Bes44xq22L09z/cXoog/gW8DzRmQSZ+OrUDPx3G7ej2pf/P88TPcxDhypfXJCgc8IMRaDpGKsLekzDWVOXPQ5NIiJKwfA4WMuphs4BxuLisYJPS6TCl05dGKlSuQ4zaQ4uxMM1qX1RvLRP8tpCfaNsJzAhgciqRbP2vnvecyXHC6yaCuSD0LDz1z2uLXSQBuxrF5F+yUycyqZt7QvBN13Pk5jw5e2YCsMzPydFw6eZVAcrJol9jTr6WxB/Z3T0cAz9D2nBAhCf8H/7kfGqcivY8d+2fuNl3j+8J68E2v7ntJQZu/Djb7Ks6pjECHES1KkQwwL+ICKQhpqCRS2eytdgBXJCjvPGQYqbRKcuCj7wMtxGCeDhQwq07WMVE/09u2xo43kYh0d6QRfi0EmUFWJk8c7UYUTOd+BBIiintkuUCZXWAiPxlkUETlWui5ojmuvOYqpIDTjUD8hLMYMHvljnPc7OeuVuJyCeKTnS+5fHhsZdCYLslwo1NLc2wyzqPa9JCE5+fcYS8hFISLruAURFvKw==
Setting up security policy between 2001:16d8:ffff:1:0:0:0:3 and 2001:16d8:ffff:1:0:0:0:4

Everything here is actually happening and showing real data although the SPD manipulation is a bit crude at the moment. I call setkey(8) for every pair of addresses and feed it on stdin.

A possible followup on this project might be to implement this simple forwarding resolver in C and using the PF_KEY interface directly or, perhaps better, to patch an existing resolver like Unbound.

Please note that there is no DNSSEC validation in this case. We rely on the full resolver, which might run on the same node, to do any validation of the resource records. This might change in the future.

The source code for the resolver is available at the project website.

Full Resolver

The Unbound DNS resolver is extendable using Python. I had an idea to write a Python module to do what the forwarding resolver above does but using Unbound's DNSSEC validation.

I have done initial experiments with a simple Python script but I'm still not sure if I can do the actual IPSECKEY query from within the script or if I have to patch the C code. For now, I'll keep experimenting with my Net::DNS resolver to try to make the entire system work before looking at something else.

Here's some sample code for a skeleton of a Python module:

def init(id, cfg):
    log_info("pythonmod: init called, module id is %d port: %d script: %s" % (id, cfg.port, cfg.python_script))
    return True

def deinit(id):
    log_info("pythonmod: deinit called, module id is %d" % id)
    return True

def inform_super(id, qstate, superqstate, qdata):
    return True

def operate(id, event, qstate, qdata):
    print "operate(): event is ", event

    if event == MODULE_EVENT_NEW or event == MODULE_EVENT_PASS:
        print("Sending original query \"%s\", type %s (%d), class %s (%d) " % (
            qstate.qinfo.qname_str, qstate.qinfo.qtype_str,
            qstate.qinfo.qclass_str, qstate.qinfo.qclass)) 

        # Pass on the new event to the iterator for a lookup.
        qstate.ext_state[id] = MODULE_WAIT_MODULE 
        return True

    if event == MODULE_EVENT_MODDONE:
        # Iterator finished. We might have a reply.

        print("Reply for query \"%s\", type %s (%d), class %s (%d) " % (
            qstate.qinfo.qname_str, qstate.qinfo.qtype_str,
            qstate.qinfo.qclass_str, qstate.qinfo.qclass)) 

        if qstate.return_msg and qstate.qinfo.qtype_str == "A":
            print("Store away reply for A record.")

            # TODO: Store away the original message.

            # Look up IPSEC.
            print("Look up IPSECKEY")

            # XXX Now what?

        # We're done.
        qstate.ext_state[id] = MODULE_FINISHED 
        return True

    qstate.ext_state[id] = MODULE_ERROR
    return True

log_info("pythonmod: script loaded.")

At the "XXX Now what" I tried with what I thought was the obvious: Create a new qstate and fill it with an IPSEC query and then do:

# Pass on the new event to the iterator for a lookup.
newq.ext_state[id] = MODULE_WAIT_MODULE

but it didn't work. I'm sure I made some mistake. I tried a lot of different versions of this but nothing I did worked and I needed to spend time on other things.

Someone more knowledgeable about the insides of Unbound might want to pick this up or at least comment on it. If it's impossible to do from Python I would appreciate if someone told me so I can focus on patching Unbound in C instead, if I can find the time.

Key Daemon

The resolver naturally needs to work in concert with a key daemon to dynamically set up new peers with public keys for authentication.

The key daemon can also be controlled by the resolver to initiate a security association but the initiation might also be triggered by a security policy which, in turn, might have been set by the resolver. I'm not yet sure which is the best alternative. The current implementation of the resolver sets IPsec security policies by calls to setkey on the initiating side.

On the responding side I use racoon's automatic policy generation ("generate_policy on") to set up the security policy for traffic.

Available Key Daemons

Here are some available key daemons I have found:

  • racoon from ipsec-tools/KAME. BSD license. C. Supports public keys. Supports CERT DNS lookups. Supports automatic policy generation. IKEv1.

  • racoon2 from WIDE. BSD license. C. Doesn't support plain public keys? Supports X.509. IKEv1 and v2.

  • OpenBSD isakmpd. BSD license. C. Supports X.509.

  • OpenBSD iked. ISC license. C. Supports X.509 and plain public keys.

  • charon (Strongswan and Openswan). GPLv2. C. Supports public keys. IKEv2. Supports IPSECKEY lookups?

  • pluto (Strongswan and Openswan). GPLv2. C. Supports public keys. IKEv1. Supports IPSECKEY lookups? Not available on FreeBSD.

  • IKEv2. Alpha status. Linux only? Web page http://ikev2.zemris.fer.hr/ not available.

  • OpenIKEv2. C++. IKEv2.

Of these racoon, racoon2, isakmpd and Strongswan's charon are the ones available in the FreeBSD's ports tree.

I chose to look closer at racoon since it has a permissive license, it's multiplatform (FreeBSD, NetBSD and Linux), it's written in a language I understand and it has some nice features like automatic policy generation. Time permitting I will look closer at at least Strongswan's charon and OpenBSD's iked as well.


For reference, here are some notes on how I configured two qemu instances running FreeBSD to use IPsec with public key authentication using racoon and my own resolver to set up the security policy.

I run IPsec enabled FreeBSD kernels. This requires a kernel recompile with "option IPSEC" and "device crypto". They run on two qemu instances connected through a bridged host interface. I start them like this:

qemu -enable-kqemu -nographic -hda /backup/slask/fbsd.img -m 512M -net nic,model=e1000 -net tap,name=tap0,script=no

qemu -enable-kqemu -nographic -hda /backup/slask/fbsd2.img -m 512M -net nic,model=e1000,macaddr=52:54:00:12:34:57 -net tap,name=tap1,script=no

The use of the tap interface was configured like this on the host running the qemu instances:

Add this to /etc/sysctl.conf:



perm    /dev/tap0       0660
perm    /dev/tap1       0660

This will allow users in group wheel (default owner of /dev/tap*) to use the devices. Add yourself to the wheel group.

The bridge was created by adding this to /etc/rc.conf:

cloned_interfaces="tap0 tap1 bridge0"
ifconfig_bridge0="addm em0 addm tap0 addm tap1 up"

I manually configured IPsec with public key authentication using using the racoon IKE daemon from ipsec-tools/KAME.

I generated the key pairs like this:

% plainrsa-gen -b 4096 -f privatekey.rsa

At the top of the generated file there is a public key beginning with "#: PUB". Copy the public key to your public key directory (path certificate in the racoon configuration below) on the other host and remove the "#".

I created this racoon configuration:

path certificate "/usr/local/etc/racoon/certs";

remote anonymous
        exchange_mode main;
        # Doesn't really matter if we use main mode or aggressive
        # mode. Our identy isn't secret and we don't send any
        # hashes of a pre-shared key in the clear.
        # exchange_mode aggressive;
        lifetime time 24 hour;
        my_identifier fqdn "ipsec2.hack.org";
        # On the other host:
        # my_identifier fqdn "ipsec1.hack.org";
        certificate_type plain_rsa "privatekey.rsa";

        # The other host's public key:
        peers_certfile plain_rsa "pubkey1.rsa";
        # On the other side:
        # peers_certfile plain_rsa "pubkey2.rsa";

        # Automatically generate a Security Policy when the other
        # side initiates dialogue.
        generate_policy on;

                encryption_algorithm aes;
                hash_algorithm sha256;

                # Use public key authentication:
                authentication_method rsasig;

                dh_group 2;

sainfo anonymous
         lifetime time 1 hour;
         encryption_algorithm aes;
         authentication_algorithm hmac_sha256;
         compression_algorithm deflate;

This configuration file will automatically generate fitting security policies on the responding side after suggestions from the initiator. If you want to set policies manually instead, create files like these and run them (on one host, reverse it on the other host)::

#!/sbin/setkey -f

spdadd any -P out ipsec esp/transport//require ;
spdadd any -P in ipsec esp/transport//require ;

I tried with a "require" policy on IPv6 as well but for some reason yet to be discovered it didn't work. This, however, did:

spdadd 2001:16d8:ffff:1::3 2001:16d8:ffff:1::4 any -P out ipsec esp/transport//use ;
spdadd 2001:16d8:ffff:1::4  2001:16d8:ffff:1::3 any -P in ipsec esp/transport//use ;

It might be a problem with Neighbor Discovery. I will investigate further.

Using this I can initiate an IKE dialogue between the two racoons by simply pinging the other host. After the IKE dialogue is finished the pings goes through and all traffic between the two hosts is encrypted.

Starting without security policies I can run my forwarding resolver, ns.pl, and have it set up policies as required on the initiating node. When it's running I can ping the other host by name and the resolver catches the name lookup, sets up a security policy and the racoon on my side starts negotiating. With racoon's automatic policy generation on the responding side traffic starts to flow.

I configured both the hosts in my DNS zone like this (one of the hosts):

ipsec1                  IN      AAAA    2001:16d8:ffff:1::3

ipsec1                  IN      IPSECKEY ( 10 0 1

and the corrresponding in the reverse zone: IN PTR ipsec1.hack.org.

That is all for now. Please visit the project web and stay tuned for more.