--- /dev/null
+#!/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
--- /dev/null
+#!/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)