]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.dnsdist/dnscrypt.py
Merge pull request #8713 from rgacogne/auth-strict-caches-size
[thirdparty/pdns.git] / regression-tests.dnsdist / dnscrypt.py
CommitLineData
b8db58a2 1#!/usr/bin/env python2
b8db58a2
RG
2import socket
3import struct
4import time
fe1c60f2
RG
5import dns
6import dns.message
b8db58a2
RG
7import libnacl
8import libnacl.utils
b4f23783 9import binascii
ae99f853 10from builtins import bytes
b8db58a2 11
fe1c60f2 12class DNSCryptResolverCertificate(object):
ae99f853
RG
13 DNSCRYPT_CERT_MAGIC = b'\x44\x4e\x53\x43'
14 DNSCRYPT_ES_VERSION = b'\x00\x01'
15 DNSCRYPT_PROTOCOL_MIN_VERSION = b'\x00\x00'
b8db58a2
RG
16
17 def __init__(self, serial, validFrom, validUntil, publicKey, clientMagic):
18 self.serial = serial
19 self.validFrom = validFrom
20 self.validUntil = validUntil
21 self.publicKey = publicKey
22 self.clientMagic = clientMagic
23
24 def isValid(self):
25 now = time.time()
26 return self.validFrom <= now and self.validUntil >= now
27
28 @staticmethod
29 def fromBinary(binary, providerFP):
30 if len(binary) != 124:
31 raise Exception("Invalid binary certificate")
32
33 certMagic = binary[0:4]
34 esVersion = binary[4:6]
35 protocolMinVersion = binary[6:8]
36
37 if certMagic != DNSCryptResolverCertificate.DNSCRYPT_CERT_MAGIC or esVersion != DNSCryptResolverCertificate.DNSCRYPT_ES_VERSION or protocolMinVersion != DNSCryptResolverCertificate.DNSCRYPT_PROTOCOL_MIN_VERSION:
38 raise Exception("Invalid binary certificate")
39
40 orig = libnacl.crypto_sign_open(binary[8:124], providerFP)
41
42 resolverPK = orig[0:32]
43 clientMagic = orig[32:40]
b7bd0317 44 serial = struct.unpack_from("!I", orig[40:44])[0]
b1bec9f0
RG
45 validFrom = struct.unpack_from("!I", orig[44:48])[0]
46 validUntil = struct.unpack_from("!I", orig[48:52])[0]
b8db58a2
RG
47 return DNSCryptResolverCertificate(serial, validFrom, validUntil, resolverPK, clientMagic)
48
fe1c60f2 49class DNSCryptClient(object):
b8db58a2
RG
50 DNSCRYPT_NONCE_SIZE = 24
51 DNSCRYPT_MAC_SIZE = 16
52 DNSCRYPT_PADDED_BLOCK_SIZE = 64
53 DNSCRYPT_MIN_UDP_LENGTH = 256
ae99f853 54 DNSCRYPT_RESOLVER_MAGIC = b'\x72\x36\x66\x6e\x76\x57\x6a\x38'
b8db58a2
RG
55
56 @staticmethod
57 def _addrToSocketType(addr):
58 result = None
59 try:
60 socket.inet_pton(socket.AF_INET6, addr)
61 result = socket.AF_INET6
62 except socket.error:
63 socket.inet_pton(socket.AF_INET, addr)
64 result = socket.AF_INET
65
66 return result
67
1ade83b2 68 def __init__(self, providerName, providerFingerprint, resolverAddress, resolverPort=443, timeout=2):
b8db58a2 69 self._providerName = providerName
b4f23783 70 self._providerFingerprint = binascii.unhexlify(providerFingerprint.lower().replace(':', ''))
b8db58a2
RG
71 self._resolverAddress = resolverAddress
72 self._resolverPort = resolverPort
73 self._resolverCertificates = []
74 self._publicKey, self._privateKey = libnacl.crypto_box_keypair()
fcffc585 75 self._timeout = timeout
b8db58a2
RG
76
77 addrType = self._addrToSocketType(self._resolverAddress)
78 self._sock = socket.socket(addrType, socket.SOCK_DGRAM)
1ade83b2 79 self._sock.settimeout(timeout)
b8db58a2 80 self._sock.connect((self._resolverAddress, self._resolverPort))
b8db58a2 81
fcffc585
RG
82 def _sendQuery(self, queryContent, tcp=False):
83 if tcp:
84 addrType = self._addrToSocketType(self._resolverAddress)
85 sock = socket.socket(addrType, socket.SOCK_STREAM)
86 sock.settimeout(self._timeout)
87 sock.connect((self._resolverAddress, self._resolverPort))
88 sock.send(struct.pack("!H", len(queryContent)))
89 else:
90 sock = self._sock
91
92 sock.send(queryContent)
93
94 data = None
95 if tcp:
96 got = sock.recv(2)
fcffc585
RG
97 if got:
98 (rlen,) = struct.unpack("!H", got)
99 data = sock.recv(rlen)
100 else:
101 data = sock.recv(4096)
102
b8db58a2
RG
103 return data
104
105 def _hasValidResolverCertificate(self):
106
107 for cert in self._resolverCertificates:
108 if cert.isValid():
109 return True
110
111 return False
112
79500db5
RG
113 def clearExpiredResolverCertificates(self):
114 newCerts = []
115
116 for cert in self._resolverCertificates:
117 if cert.isValid():
118 newCerts.append(cert)
119
120 self._resolverCertificates = newCerts
121
122 def refreshResolverCertificates(self):
123 self.clearExpiredResolverCertificates()
124
b8db58a2
RG
125 query = dns.message.make_query(self._providerName, dns.rdatatype.TXT, dns.rdataclass.IN)
126 data = self._sendQuery(query.to_wire())
127
128 response = dns.message.from_wire(data)
129 if response.rcode() != dns.rcode.NOERROR or len(response.answer) != 1:
130 raise Exception("Invalid response to public key request")
131
132 an = response.answer[0]
133 if an.rdclass != dns.rdataclass.IN or an.rdtype != dns.rdatatype.TXT or len(an.items) == 0:
134 raise Exception("Invalid response to public key request")
135
43234e76
RG
136 self._resolverCertificates = []
137
b8db58a2
RG
138 for item in an.items:
139 if len(item.strings) != 1:
140 continue
b1bec9f0 141
b8db58a2
RG
142 cert = DNSCryptResolverCertificate.fromBinary(item.strings[0], self._providerFingerprint)
143 if cert.isValid():
144 self._resolverCertificates.append(cert)
145
79500db5 146 def getResolverCertificate(self):
b8db58a2
RG
147 certs = self._resolverCertificates
148 result = None
149 for cert in certs:
150 if cert.isValid():
151 if result is None or cert.serial > result.serial:
152 result = cert
153
154 return result
155
43234e76
RG
156 def getAllResolverCertificates(self, onlyValid=False):
157 certs = self._resolverCertificates
158 result = []
159 for cert in certs:
160 if not onlyValid or cert.isValid():
161 result.append(cert)
162
163 return result
164
b8db58a2
RG
165 @staticmethod
166 def _generateNonce():
167 nonce = libnacl.utils.rand_nonce()
ae99f853 168 return nonce[:int(DNSCryptClient.DNSCRYPT_NONCE_SIZE / 2)]
b8db58a2 169
fcffc585 170 def _encryptQuery(self, queryContent, resolverCert, nonce, tcp=False):
b8db58a2
RG
171 header = resolverCert.clientMagic + self._publicKey + nonce
172 requiredSize = len(header) + self.DNSCRYPT_MAC_SIZE + len(queryContent)
173 paddingSize = self.DNSCRYPT_PADDED_BLOCK_SIZE - (len(queryContent) % self.DNSCRYPT_PADDED_BLOCK_SIZE)
174 # padding size should be DNSCRYPT_PADDED_BLOCK_SIZE <= padding size <= 4096
fcffc585 175 if not tcp and requiredSize < self.DNSCRYPT_MIN_UDP_LENGTH:
b8db58a2
RG
176 paddingSize += self.DNSCRYPT_MIN_UDP_LENGTH - requiredSize
177 requiredSize = self.DNSCRYPT_MIN_UDP_LENGTH
178
ae99f853 179 padding = b'\x80'
b8db58a2
RG
180 idx = 0
181 while idx < (paddingSize - 1):
ae99f853 182 padding = padding + b'\x00'
b8db58a2
RG
183 idx += 1
184
185 data = queryContent + padding
ae99f853 186 nonce = nonce + (b'\x00'*int(self.DNSCRYPT_NONCE_SIZE / 2))
b8db58a2
RG
187 box = libnacl.crypto_box(data, nonce, resolverCert.publicKey, self._privateKey)
188 return header + box
189
190 def _decryptResponse(self, encryptedResponse, resolverCert, clientNonce):
191 resolverMagic = encryptedResponse[:8]
192 if resolverMagic != self.DNSCRYPT_RESOLVER_MAGIC:
193 raise Exception("Invalid encrypted response: bad resolver magic")
194
195 nonce = encryptedResponse[8:32]
ae99f853 196 if nonce[0:int(self.DNSCRYPT_NONCE_SIZE / 2)] != clientNonce:
b8db58a2
RG
197 raise Exception("Invalid encrypted response: bad nonce")
198
199 cleartext = libnacl.crypto_box_open(encryptedResponse[32:], nonce, resolverCert.publicKey, self._privateKey)
ae99f853
RG
200 cleartextBytes = bytes(cleartext)
201 idx = len(cleartextBytes) - 1
b8db58a2 202 while idx > 0:
ae99f853 203 if cleartextBytes[idx] != 0:
b8db58a2
RG
204 break
205 idx -= 1
206
ae99f853 207 if idx == 0 or cleartextBytes[idx] != 128:
b8db58a2
RG
208 raise Exception("Invalid encrypted response: invalid padding")
209
210 idx -= 1
ae99f853 211 paddingLen = len(cleartextBytes) - idx
b8db58a2
RG
212
213 return cleartext[:idx+1]
214
fcffc585 215 def query(self, queryContent, tcp=False):
b8db58a2
RG
216
217 if not self._hasValidResolverCertificate():
79500db5 218 self.refreshResolverCertificates()
b8db58a2
RG
219
220 nonce = self._generateNonce()
79500db5 221 resolverCert = self.getResolverCertificate()
b8db58a2
RG
222 if resolverCert is None:
223 raise Exception("No valid certificate found")
fcffc585
RG
224 encryptedQuery = self._encryptQuery(queryContent, resolverCert, nonce, tcp)
225 encryptedResponse = self._sendQuery(encryptedQuery, tcp)
b8db58a2
RG
226 response = self._decryptResponse(encryptedResponse, resolverCert, nonce)
227 return response