]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.dnsdist/dnscrypt.py
Merge pull request #3052 from zeha/pdnsutil-consistent-cerr
[thirdparty/pdns.git] / regression-tests.dnsdist / dnscrypt.py
1 #!/usr/bin/env python2
2 import dns
3 import dns.message
4 import socket
5 import struct
6 import time
7 import libnacl
8 import libnacl.utils
9
10 class DNSCryptResolverCertificate:
11 DNSCRYPT_CERT_MAGIC = '\x44\x4e\x53\x43'
12 DNSCRYPT_ES_VERSION = '\x00\x01'
13 DNSCRYPT_PROTOCOL_MIN_VERSION = '\x00\x00'
14
15 def __init__(self, serial, validFrom, validUntil, publicKey, clientMagic):
16 self.serial = serial
17 self.validFrom = validFrom
18 self.validUntil = validUntil
19 self.publicKey = publicKey
20 self.clientMagic = clientMagic
21
22 def isValid(self):
23 now = time.time()
24 return self.validFrom <= now and self.validUntil >= now
25
26 @staticmethod
27 def fromBinary(binary, providerFP):
28 if len(binary) != 124:
29 raise Exception("Invalid binary certificate")
30
31 certMagic = binary[0:4]
32 esVersion = binary[4:6]
33 protocolMinVersion = binary[6:8]
34
35 if certMagic != DNSCryptResolverCertificate.DNSCRYPT_CERT_MAGIC or esVersion != DNSCryptResolverCertificate.DNSCRYPT_ES_VERSION or protocolMinVersion != DNSCryptResolverCertificate.DNSCRYPT_PROTOCOL_MIN_VERSION:
36 raise Exception("Invalid binary certificate")
37
38 orig = libnacl.crypto_sign_open(binary[8:124], providerFP)
39
40 resolverPK = orig[0:32]
41 clientMagic = orig[32:40]
42 serial = struct.unpack_from("I", orig[40:44])
43 validFrom = struct.unpack_from("!I", orig[44:48])[0];
44 validUntil = struct.unpack_from("!I", orig[48:52])[0];
45 return DNSCryptResolverCertificate(serial, validFrom, validUntil, resolverPK, clientMagic)
46
47 class DNSCryptClient:
48 DNSCRYPT_NONCE_SIZE = 24
49 DNSCRYPT_MAC_SIZE = 16
50 DNSCRYPT_PADDED_BLOCK_SIZE = 64
51 DNSCRYPT_MIN_UDP_LENGTH = 256
52 DNSCRYPT_RESOLVER_MAGIC = '\x72\x36\x66\x6e\x76\x57\x6a\x38'
53
54 @staticmethod
55 def _addrToSocketType(addr):
56 result = None
57 try:
58 socket.inet_pton(socket.AF_INET6, addr)
59 result = socket.AF_INET6
60 except socket.error:
61 socket.inet_pton(socket.AF_INET, addr)
62 result = socket.AF_INET
63
64 return result
65
66 def __init__(self, providerName, providerFingerprint, resolverAddress, resolverPort=443, timeout=2):
67 self._providerName = providerName
68 self._providerFingerprint = providerFingerprint.lower().replace(':', '').decode('hex')
69 self._resolverAddress = resolverAddress
70 self._resolverPort = resolverPort
71 self._resolverCertificates = []
72 self._publicKey, self._privateKey = libnacl.crypto_box_keypair()
73
74 addrType = self._addrToSocketType(self._resolverAddress)
75 self._sock = socket.socket(addrType, socket.SOCK_DGRAM)
76 self._sock.settimeout(timeout)
77 self._sock.connect((self._resolverAddress, self._resolverPort))
78
79 def _sendQuery(self, queryContent):
80 self._sock.send(queryContent)
81 data = self._sock.recv(4096)
82 return data
83
84 def _hasValidResolverCertificate(self):
85
86 for cert in self._resolverCertificates:
87 if cert.isValid():
88 return True
89
90 return False
91
92 def _getResolverCertificates(self):
93 query = dns.message.make_query(self._providerName, dns.rdatatype.TXT, dns.rdataclass.IN)
94 data = self._sendQuery(query.to_wire())
95
96 response = dns.message.from_wire(data)
97 if response.rcode() != dns.rcode.NOERROR or len(response.answer) != 1:
98 raise Exception("Invalid response to public key request")
99
100 an = response.answer[0]
101 if an.rdclass != dns.rdataclass.IN or an.rdtype != dns.rdatatype.TXT or len(an.items) == 0:
102 raise Exception("Invalid response to public key request")
103
104 for item in an.items:
105 if len(item.strings) != 1:
106 continue
107
108 cert = DNSCryptResolverCertificate.fromBinary(item.strings[0], self._providerFingerprint)
109 if cert.isValid():
110 self._resolverCertificates.append(cert)
111
112 def _getResolverCertificate(self):
113 certs = self._resolverCertificates
114 result = None
115 for cert in certs:
116 if cert.isValid():
117 if result is None or cert.serial > result.serial:
118 result = cert
119
120 return result
121
122 @staticmethod
123 def _generateNonce():
124 nonce = libnacl.utils.rand_nonce()
125 return nonce[:(DNSCryptClient.DNSCRYPT_NONCE_SIZE / 2)]
126
127 def _encryptQuery(self, queryContent, resolverCert, nonce):
128 header = resolverCert.clientMagic + self._publicKey + nonce
129 requiredSize = len(header) + self.DNSCRYPT_MAC_SIZE + len(queryContent)
130 paddingSize = self.DNSCRYPT_PADDED_BLOCK_SIZE - (len(queryContent) % self.DNSCRYPT_PADDED_BLOCK_SIZE)
131 # padding size should be DNSCRYPT_PADDED_BLOCK_SIZE <= padding size <= 4096
132 if requiredSize < self.DNSCRYPT_MIN_UDP_LENGTH:
133 paddingSize += self.DNSCRYPT_MIN_UDP_LENGTH - requiredSize
134 requiredSize = self.DNSCRYPT_MIN_UDP_LENGTH
135
136 padding = '\x80'
137 idx = 0
138 while idx < (paddingSize - 1):
139 padding = padding + '\x00'
140 idx += 1
141
142 data = queryContent + padding
143 nonce = nonce + ('\x00'*(self.DNSCRYPT_NONCE_SIZE / 2))
144 box = libnacl.crypto_box(data, nonce, resolverCert.publicKey, self._privateKey)
145 return header + box
146
147 def _decryptResponse(self, encryptedResponse, resolverCert, clientNonce):
148 resolverMagic = encryptedResponse[:8]
149 if resolverMagic != self.DNSCRYPT_RESOLVER_MAGIC:
150 raise Exception("Invalid encrypted response: bad resolver magic")
151
152 nonce = encryptedResponse[8:32]
153 if nonce[0:self.DNSCRYPT_NONCE_SIZE / 2] != clientNonce:
154 raise Exception("Invalid encrypted response: bad nonce")
155
156 cleartext = libnacl.crypto_box_open(encryptedResponse[32:], nonce, resolverCert.publicKey, self._privateKey)
157 idx = len(cleartext) - 1
158 while idx > 0:
159 if cleartext[idx] != '\x00':
160 break
161 idx -= 1
162
163 if idx == 0 or cleartext[idx] != '\x80':
164 raise Exception("Invalid encrypted response: invalid padding")
165
166 idx -= 1
167 paddingLen = len(cleartext) - idx
168
169 return cleartext[:idx+1]
170
171 def query(self, queryContent):
172
173 if not self._hasValidResolverCertificate():
174 self._getResolverCertificates()
175
176 nonce = self._generateNonce()
177 resolverCert = self._getResolverCertificate()
178 if resolverCert is None:
179 raise Exception("No valid certificate found")
180 encryptedQuery = self._encryptQuery(queryContent, resolverCert, nonce)
181 encryptedResponse = self._sendQuery(encryptedQuery)
182 response = self._decryptResponse(encryptedResponse, resolverCert, nonce)
183 return response