]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.dnsdist/dnscrypt.py
Merge pull request #5353 from rgacogne/dnsdist-statnode-labels
[thirdparty/pdns.git] / regression-tests.dnsdist / dnscrypt.py
1 #!/usr/bin/env python2
2 import socket
3 import struct
4 import time
5 import dns
6 import dns.message
7 import libnacl
8 import libnacl.utils
9
10 class DNSCryptResolverCertificate(object):
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])[0]
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(object):
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 self._timeout = timeout
74
75 addrType = self._addrToSocketType(self._resolverAddress)
76 self._sock = socket.socket(addrType, socket.SOCK_DGRAM)
77 self._sock.settimeout(timeout)
78 self._sock.connect((self._resolverAddress, self._resolverPort))
79
80 def _sendQuery(self, queryContent, tcp=False):
81 if tcp:
82 addrType = self._addrToSocketType(self._resolverAddress)
83 sock = socket.socket(addrType, socket.SOCK_STREAM)
84 sock.settimeout(self._timeout)
85 sock.connect((self._resolverAddress, self._resolverPort))
86 sock.send(struct.pack("!H", len(queryContent)))
87 else:
88 sock = self._sock
89
90 sock.send(queryContent)
91
92 data = None
93 if tcp:
94 got = sock.recv(2)
95 print(len(got))
96 if got:
97 (rlen,) = struct.unpack("!H", got)
98 data = sock.recv(rlen)
99 else:
100 data = sock.recv(4096)
101
102 return data
103
104 def _hasValidResolverCertificate(self):
105
106 for cert in self._resolverCertificates:
107 if cert.isValid():
108 return True
109
110 return False
111
112 def clearExpiredResolverCertificates(self):
113 newCerts = []
114
115 for cert in self._resolverCertificates:
116 if cert.isValid():
117 newCerts.append(cert)
118
119 self._resolverCertificates = newCerts
120
121 def refreshResolverCertificates(self):
122 self.clearExpiredResolverCertificates()
123
124 query = dns.message.make_query(self._providerName, dns.rdatatype.TXT, dns.rdataclass.IN)
125 data = self._sendQuery(query.to_wire())
126
127 response = dns.message.from_wire(data)
128 if response.rcode() != dns.rcode.NOERROR or len(response.answer) != 1:
129 raise Exception("Invalid response to public key request")
130
131 an = response.answer[0]
132 if an.rdclass != dns.rdataclass.IN or an.rdtype != dns.rdatatype.TXT or len(an.items) == 0:
133 raise Exception("Invalid response to public key request")
134
135 for item in an.items:
136 if len(item.strings) != 1:
137 continue
138
139 cert = DNSCryptResolverCertificate.fromBinary(item.strings[0], self._providerFingerprint)
140 if cert.isValid():
141 self._resolverCertificates.append(cert)
142
143 def getResolverCertificate(self):
144 certs = self._resolverCertificates
145 result = None
146 for cert in certs:
147 if cert.isValid():
148 if result is None or cert.serial > result.serial:
149 result = cert
150
151 return result
152
153 @staticmethod
154 def _generateNonce():
155 nonce = libnacl.utils.rand_nonce()
156 return nonce[:(DNSCryptClient.DNSCRYPT_NONCE_SIZE / 2)]
157
158 def _encryptQuery(self, queryContent, resolverCert, nonce, tcp=False):
159 header = resolverCert.clientMagic + self._publicKey + nonce
160 requiredSize = len(header) + self.DNSCRYPT_MAC_SIZE + len(queryContent)
161 paddingSize = self.DNSCRYPT_PADDED_BLOCK_SIZE - (len(queryContent) % self.DNSCRYPT_PADDED_BLOCK_SIZE)
162 # padding size should be DNSCRYPT_PADDED_BLOCK_SIZE <= padding size <= 4096
163 if not tcp and requiredSize < self.DNSCRYPT_MIN_UDP_LENGTH:
164 paddingSize += self.DNSCRYPT_MIN_UDP_LENGTH - requiredSize
165 requiredSize = self.DNSCRYPT_MIN_UDP_LENGTH
166
167 padding = '\x80'
168 idx = 0
169 while idx < (paddingSize - 1):
170 padding = padding + '\x00'
171 idx += 1
172
173 data = queryContent + padding
174 nonce = nonce + ('\x00'*(self.DNSCRYPT_NONCE_SIZE / 2))
175 box = libnacl.crypto_box(data, nonce, resolverCert.publicKey, self._privateKey)
176 return header + box
177
178 def _decryptResponse(self, encryptedResponse, resolverCert, clientNonce):
179 resolverMagic = encryptedResponse[:8]
180 if resolverMagic != self.DNSCRYPT_RESOLVER_MAGIC:
181 raise Exception("Invalid encrypted response: bad resolver magic")
182
183 nonce = encryptedResponse[8:32]
184 if nonce[0:self.DNSCRYPT_NONCE_SIZE / 2] != clientNonce:
185 raise Exception("Invalid encrypted response: bad nonce")
186
187 cleartext = libnacl.crypto_box_open(encryptedResponse[32:], nonce, resolverCert.publicKey, self._privateKey)
188 idx = len(cleartext) - 1
189 while idx > 0:
190 if cleartext[idx] != '\x00':
191 break
192 idx -= 1
193
194 if idx == 0 or cleartext[idx] != '\x80':
195 raise Exception("Invalid encrypted response: invalid padding")
196
197 idx -= 1
198 paddingLen = len(cleartext) - idx
199
200 return cleartext[:idx+1]
201
202 def query(self, queryContent, tcp=False):
203
204 if not self._hasValidResolverCertificate():
205 self.refreshResolverCertificates()
206
207 nonce = self._generateNonce()
208 resolverCert = self.getResolverCertificate()
209 if resolverCert is None:
210 raise Exception("No valid certificate found")
211 encryptedQuery = self._encryptQuery(queryContent, resolverCert, nonce, tcp)
212 encryptedResponse = self._sendQuery(encryptedQuery, tcp)
213 response = self._decryptResponse(encryptedResponse, resolverCert, nonce)
214 return response