]> granicus.if.org Git - pdns/commitdiff
dnsdist: Add a basic regression test for DNSCrypt
authorRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 30 Dec 2015 08:20:30 +0000 (09:20 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 4 Jan 2016 09:19:40 +0000 (10:19 +0100)
I could not find any DNSCrypt client implementation in python without
zillions of dependencies, so I wrote a basic one depending only
on dnspython and libnacl bindings.

.travis.yml
pdns/README-dnsdist.md
pdns/dnsdist-dnscrypt.cc
pdns/dnsdist-lua2.cc
regression-tests.dnsdist/.gitignore
regression-tests.dnsdist/DNSCryptProviderPrivate.key [new file with mode: 0644]
regression-tests.dnsdist/DNSCryptProviderPublic.key [new file with mode: 0644]
regression-tests.dnsdist/dnscrypt.py [new file with mode: 0644]
regression-tests.dnsdist/dnsdisttests.py
regression-tests.dnsdist/requirements.txt
regression-tests.dnsdist/test_DNSCrypt.py [new file with mode: 0644]

index 45fd464fb03a4723e4a8838ec9dba0e9c9d267fc..4d227861e202179a01874678914f1408e8aae7bc 100644 (file)
@@ -280,13 +280,14 @@ script:
   - cd pdns/dnsdistdist
   - tar xf dnsdist*.tar.bz2
   - cd dnsdist-*
-  - ./configure --enable-unit-tests
+  - ./configure --enable-unit-tests --enable-libsodium --enable-dnscrypt
   - make -k -j3
   - ./testrunner
   - cp ./dnsdist ../../../regression-tests.dnsdist/
   - cd ../../../regression-tests.dnsdist
   - DNSDISTBIN=./dnsdist ./runtests
   - rm -f ./dnsdist
+  - rm -f ./DNSCryptResolver.cert ./DNSCryptResolver.key
   - cd ..
   - rm -rf pdns/dnsdistdist/dnsdist-*/
 
index 348446895a56bc38cfd5530e26ca66cb3cd8f18e..c9fe5329ec3cf468f7f8a84a1821d7c14d56c80c 100644 (file)
@@ -661,6 +661,7 @@ To generate the provider and resolver certificates and keys, you can simply do:
 
 ```
 > generateDNSCryptProviderKeys("/path/to/providerPublic.key", "/path/to/providerPrivate.key")
+Provider fingerprint is: E1D7:2108:9A59:BF8D:F101:16FA:ED5E:EA6A:9F6C:C78F:7F91:AF6B:027E:62F4:69C3:B1AA
 > generateDNSCryptCertificate("/path/to/providerPrivate.key", "/path/to/resolver.cert", "/path/to/resolver.key", serial, validFrom, validUntil)
 ```
 
@@ -675,6 +676,12 @@ You can display the currently configured DNSCrypt binds with:
 0   127.0.0.1:8443       2.name               14       2016-04-10 08:14:15   0         -
 ```
 
+If you forgot to write down the provider fingerprint value after generating the provider keys, you can use `printDNSCryptProviderFingerprint()` to retrieve it later:
+```
+> printDNSCryptProviderFingerprint("/path/to/providerPublic.key")
+Provider fingerprint is: E1D7:2108:9A59:BF8D:F101:16FA:ED5E:EA6A:9F6C:C78F:7F91:AF6B:027E:62F4:69C3:B1AA
+```
+
 All functions and types
 -----------------------
 Within `dnsdist` several core object types exist:
index 9b9c991a9bb5b8fb9832cdcc175c6fa74a5aa19c..c94394e9a8ca3ae7668d1b5970c6b5297425ca37 100644 (file)
@@ -11,7 +11,7 @@ int handleDnsCryptQuery(DnsCryptContext* ctx, char* packet, uint16_t len, std::s
   ctx->parsePacket(packet, len, query, tcp, decryptedQueryLen);
 
   if (query->valid == false) {
-    vinfolog("Dropping DnsCrypt invalid query");
+    vinfolog("Dropping DNSCrypt invalid query");
     return false;
   }
 
index 8bd81e2a63ca0ab4c667fb8d153856bba970fa0c..ba0310bd5c16e7f07e1d444002384a0c500b74a6 100644 (file)
@@ -380,6 +380,30 @@ void moreLua()
 #endif
     });
 
+    g_lua.writeFunction("printDNSCryptProviderFingerprint", [](const std::string& publicKeyFile) {
+        setLuaNoSideEffect();
+#ifdef HAVE_DNSCRYPT
+        unsigned char publicKey[DNSCRYPT_PROVIDER_PUBLIC_KEY_SIZE];
+
+        try {
+          ifstream file(publicKeyFile);
+          file.read((char *) &publicKey, sizeof(publicKey));
+
+          if (file.fail())
+            throw std::runtime_error("Invalid dnscrypt provider public key file " + publicKeyFile);
+
+          file.close();
+          g_outputBuffer="Provider fingerprint is: " + DnsCryptContext::getProviderFingerprint(publicKey) + "\n";
+        }
+        catch(std::exception& e) {
+          errlog(e.what());
+          g_outputBuffer="Error: "+string(e.what())+"\n";
+        }
+#else
+      g_outputBuffer="Error: DNSCrypt support is not enabled.\n";
+#endif
+    });
+
     g_lua.writeFunction("generateDNSCryptCertificate", [](const std::string& providerPrivateKeyFile, const std::string& certificateFile, const std::string privateKeyFile, uint32_t serial, time_t begin, time_t end) {
         setLuaNoSideEffect();
 #ifdef HAVE_DNSCRYPT
index 5132bb74a865b58ccd7221089be6447543e4a0f2..8fda00e5322049d3873d7498085b9124d4ff7d57 100644 (file)
@@ -3,5 +3,6 @@
 /*.pid
 /*.pyc
 dnsdist_*.conf
+DNSCryptResolver*
 .dnsdist_history
 .history
diff --git a/regression-tests.dnsdist/DNSCryptProviderPrivate.key b/regression-tests.dnsdist/DNSCryptProviderPrivate.key
new file mode 100644 (file)
index 0000000..d6a9c39
Binary files /dev/null and b/regression-tests.dnsdist/DNSCryptProviderPrivate.key differ
diff --git a/regression-tests.dnsdist/DNSCryptProviderPublic.key b/regression-tests.dnsdist/DNSCryptProviderPublic.key
new file mode 100644 (file)
index 0000000..6f9fb4f
--- /dev/null
@@ -0,0 +1 @@
+á×!\b\9aY¿\8dñ\ 1\16úí^êj\9f\8f\7f\91¯k\ 2~bôiñª
\ No newline at end of file
diff --git a/regression-tests.dnsdist/dnscrypt.py b/regression-tests.dnsdist/dnscrypt.py
new file mode 100644 (file)
index 0000000..bb66895
--- /dev/null
@@ -0,0 +1,183 @@
+#!/usr/bin/env python2
+import dns
+import dns.message
+import socket
+import struct
+import time
+import libnacl
+import libnacl.utils
+
+class DNSCryptResolverCertificate:
+    DNSCRYPT_CERT_MAGIC = '\x44\x4e\x53\x43'
+    DNSCRYPT_ES_VERSION = '\x00\x01'
+    DNSCRYPT_PROTOCOL_MIN_VERSION = '\x00\x00'
+
+    def __init__(self, serial, validFrom, validUntil, publicKey, clientMagic):
+        self.serial = serial
+        self.validFrom = validFrom
+        self.validUntil = validUntil
+        self.publicKey = publicKey
+        self.clientMagic = clientMagic
+
+    def isValid(self):
+        now = time.time()
+        return self.validFrom <= now and self.validUntil >= now
+
+    @staticmethod
+    def fromBinary(binary, providerFP):
+        if len(binary) != 124:
+            raise Exception("Invalid binary certificate")
+
+        certMagic = binary[0:4]
+        esVersion = binary[4:6]
+        protocolMinVersion = binary[6:8]
+
+        if certMagic != DNSCryptResolverCertificate.DNSCRYPT_CERT_MAGIC or esVersion != DNSCryptResolverCertificate.DNSCRYPT_ES_VERSION or protocolMinVersion != DNSCryptResolverCertificate.DNSCRYPT_PROTOCOL_MIN_VERSION:
+            raise Exception("Invalid binary certificate")
+
+        orig = libnacl.crypto_sign_open(binary[8:124], providerFP)
+
+        resolverPK = orig[0:32]
+        clientMagic = orig[32:40]
+        serial = struct.unpack_from("I", orig[40:44])
+        validFrom = struct.unpack_from("!I", orig[44:48])[0];
+        validUntil = struct.unpack_from("!I", orig[48:52])[0];
+        return DNSCryptResolverCertificate(serial, validFrom, validUntil, resolverPK, clientMagic)
+
+class DNSCryptClient:
+    DNSCRYPT_NONCE_SIZE = 24
+    DNSCRYPT_MAC_SIZE = 16
+    DNSCRYPT_PADDED_BLOCK_SIZE = 64
+    DNSCRYPT_MIN_UDP_LENGTH = 256
+    DNSCRYPT_RESOLVER_MAGIC = '\x72\x36\x66\x6e\x76\x57\x6a\x38'
+
+    @staticmethod
+    def _addrToSocketType(addr):
+        result = None
+        try:
+            socket.inet_pton(socket.AF_INET6, addr)
+            result = socket.AF_INET6
+        except socket.error:
+            socket.inet_pton(socket.AF_INET, addr)
+            result = socket.AF_INET
+
+        return result
+
+    def __init__(self, providerName, providerFingerprint, resolverAddress, resolverPort=443):
+        self._providerName = providerName
+        self._providerFingerprint = providerFingerprint.lower().replace(':', '').decode('hex')
+        self._resolverAddress = resolverAddress
+        self._resolverPort = resolverPort
+        self._resolverCertificates = []
+        self._publicKey, self._privateKey = libnacl.crypto_box_keypair()
+
+        addrType = self._addrToSocketType(self._resolverAddress)
+        self._sock = socket.socket(addrType, socket.SOCK_DGRAM)
+        self._sock.connect((self._resolverAddress, self._resolverPort))
+        self._sock.settimeout(2)
+
+    def _sendQuery(self, queryContent):
+        self._sock.send(queryContent)
+        data = self._sock.recv(4096)
+        return data
+
+    def _hasValidResolverCertificate(self):
+
+        for cert in self._resolverCertificates:
+            if cert.isValid():
+                return True
+
+        return False
+
+    def _getResolverCertificates(self):
+        query = dns.message.make_query(self._providerName, dns.rdatatype.TXT, dns.rdataclass.IN)
+        data = self._sendQuery(query.to_wire())
+
+        response = dns.message.from_wire(data)
+        if response.rcode() != dns.rcode.NOERROR or len(response.answer) != 1:
+            raise Exception("Invalid response to public key request")
+
+        an = response.answer[0]
+        if an.rdclass != dns.rdataclass.IN or an.rdtype != dns.rdatatype.TXT or len(an.items) == 0:
+            raise Exception("Invalid response to public key request")
+
+        for item in an.items:
+            if len(item.strings) != 1:
+                continue
+            
+            cert = DNSCryptResolverCertificate.fromBinary(item.strings[0], self._providerFingerprint)
+            if cert.isValid():
+                self._resolverCertificates.append(cert)
+
+    def _getResolverCertificate(self):
+        certs = self._resolverCertificates
+        result = None
+        for cert in certs:
+            if cert.isValid():
+                if result is None or cert.serial > result.serial:
+                    result = cert
+
+        return result
+
+    @staticmethod
+    def _generateNonce():
+        nonce = libnacl.utils.rand_nonce()
+        return nonce[:(DNSCryptClient.DNSCRYPT_NONCE_SIZE / 2)]
+
+    def _encryptQuery(self, queryContent, resolverCert, nonce):
+        header = resolverCert.clientMagic + self._publicKey + nonce
+        requiredSize = len(header) + self.DNSCRYPT_MAC_SIZE + len(queryContent)
+        paddingSize = self.DNSCRYPT_PADDED_BLOCK_SIZE - (len(queryContent) % self.DNSCRYPT_PADDED_BLOCK_SIZE)
+        # padding size should be DNSCRYPT_PADDED_BLOCK_SIZE <= padding size <= 4096
+        if requiredSize < self.DNSCRYPT_MIN_UDP_LENGTH:
+            paddingSize += self.DNSCRYPT_MIN_UDP_LENGTH - requiredSize
+            requiredSize = self.DNSCRYPT_MIN_UDP_LENGTH
+
+        padding = '\x80'
+        idx = 0
+        while idx < (paddingSize - 1):
+            padding = padding + '\x00'
+            idx += 1
+
+        data = queryContent + padding
+        nonce = nonce + ('\x00'*(self.DNSCRYPT_NONCE_SIZE / 2))
+        box = libnacl.crypto_box(data, nonce, resolverCert.publicKey, self._privateKey)
+        return header + box
+
+    def _decryptResponse(self, encryptedResponse, resolverCert, clientNonce):
+        resolverMagic = encryptedResponse[:8]
+        if resolverMagic != self.DNSCRYPT_RESOLVER_MAGIC:
+            raise Exception("Invalid encrypted response: bad resolver magic")
+
+        nonce = encryptedResponse[8:32]
+        if nonce[0:self.DNSCRYPT_NONCE_SIZE / 2] != clientNonce:
+            raise Exception("Invalid encrypted response: bad nonce")
+
+        cleartext = libnacl.crypto_box_open(encryptedResponse[32:], nonce, resolverCert.publicKey, self._privateKey)
+        idx = len(cleartext) - 1
+        while idx > 0:
+            if cleartext[idx] != '\x00':
+                break
+            idx -= 1
+
+        if idx == 0 or cleartext[idx] != '\x80':
+            raise Exception("Invalid encrypted response: invalid padding")
+
+        idx -= 1
+        paddingLen = len(cleartext) - idx
+
+        return cleartext[:idx+1]
+
+    def query(self, queryContent):
+
+        if not self._hasValidResolverCertificate():
+            self._getResolverCertificates()
+
+        nonce = self._generateNonce()
+        resolverCert = self._getResolverCertificate()
+        if resolverCert is None:
+            raise Exception("No valid certificate found")
+        encryptedQuery = self._encryptQuery(queryContent, resolverCert, nonce)
+        encryptedResponse = self._sendQuery(encryptedQuery)
+        response = self._decryptResponse(encryptedResponse, resolverCert, nonce)
+        return response
index f66f8c8a0c0e299c106993c3cfe282a7d03fd0be..a650c6c769d8ff1c4d75167caa291f0c32345c52 100644 (file)
@@ -2,6 +2,7 @@
 
 import clientsubnetoption
 import dns
+import dns.message
 import Queue
 import os
 import socket
index 5962a6862c91f451cee373ace2617f19522e35f3..e33025f32eebeb4dc776b6dfa2e94bde58498032 100644 (file)
@@ -1,2 +1,3 @@
 dnspython>=1.11
 nose==1.3.0
+libnacl>=1.4.3
diff --git a/regression-tests.dnsdist/test_DNSCrypt.py b/regression-tests.dnsdist/test_DNSCrypt.py
new file mode 100644 (file)
index 0000000..0b4e233
--- /dev/null
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+import dns
+import dns.message
+import os
+import socket
+import subprocess
+import time
+import unittest
+from dnsdisttests import DNSDistTest
+import dnscrypt
+
+class TestDNSCrypt(DNSDistTest):
+    """
+    dnsdist is configured to accept DNSCrypt queries on 127.0.0.1:_dnsDistPortDNSCrypt.
+    The provider's keys have been generated with:
+    generateDNSCryptProviderKeys("DNSCryptProviderPublic.key", "DNSCryptProviderPrivate.key")
+    Be careful to change the _providerFingerprint below if you want to regenerate the keys.
+    """
+
+    _dnsDistPort = 5340
+    _dnsDistPortDNSCrypt = 8443
+    _config_template = """
+    generateDNSCryptCertificate("DNSCryptProviderPrivate.key", "DNSCryptResolver.cert", "DNSCryptResolver.key", 42, %d, %d)
+    addDNSCryptBind("127.0.0.1:%d", "%s", "DNSCryptResolver.cert", "DNSCryptResolver.key")
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    _dnsdistcmd = (os.environ['DNSDISTBIN'] + " -C dnsdist_DNSCrypt.conf --acl 127.0.0.1/32 -l 127.0.0.1:" + str(_dnsDistPort)).split()
+    _providerFingerprint = 'E1D7:2108:9A59:BF8D:F101:16FA:ED5E:EA6A:9F6C:C78F:7F91:AF6B:027E:62F4:69C3:B1AA'
+    _providerName = "2.provider.name"
+
+    @classmethod
+    def startDNSDist(cls, shutUp=True):
+        print("Launching dnsdist..")
+        # valid from 60s ago until 2h from now
+        validFrom = time.time() - 60
+        validUntil = time.time() + 7200
+        with open('dnsdist_DNSCrypt.conf', 'w') as conf:
+            conf.write(cls._config_template % (validFrom, validUntil, cls._dnsDistPortDNSCrypt, cls._providerName, str(cls._testServerPort)))
+
+        print(' '.join(cls._dnsdistcmd))
+        if shutUp:
+            with open(os.devnull, 'w') as fdDevNull:
+                cls._dnsdist = subprocess.Popen(cls._dnsdistcmd, close_fds=True, stdout=fdDevNull, stderr=fdDevNull)
+        else:
+            cls._dnsdist = subprocess.Popen(cls._dnsdistcmd, close_fds=True)
+
+        time.sleep(1)
+
+        if cls._dnsdist.poll() is not None:
+            cls._dnsdist.terminate()
+            cls._dnsdist.wait()
+            sys.exit(cls._dnsdist.returncode)
+
+
+    def testSimpleA(self):
+        """
+        Send an encrypted A query.
+        """
+        client = dnscrypt.DNSCryptClient(self._providerName, self._providerFingerprint, "127.0.0.1", 8443)
+        name = 'a.dnscrypt.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    3600,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '127.0.0.1')
+        response.answer.append(rrset)
+
+        self._toResponderQueue.put(response)
+        data = client.query(query.to_wire())
+        receivedResponse = dns.message.from_wire(data)
+        receivedQuery = None
+        if not self._fromResponderQueue.empty():
+            receivedQuery = self._fromResponderQueue.get(query)
+
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        receivedResponse.id = response.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(response, receivedResponse)
+
+if __name__ == '__main__':
+    unittest.main()
+    exit(0)