]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.dnsdist/dnscrypt.py
Merge pull request #3592 from pieterlexis/issue-3591-secure-delegated-test-broken
[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])
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 _getResolverCertificates(self):
113 query = dns.message.make_query(self._providerName, dns.rdatatype.TXT, dns.rdataclass.IN)
114 data = self._sendQuery(query.to_wire())
115
116 response = dns.message.from_wire(data)
117 if response.rcode() != dns.rcode.NOERROR or len(response.answer) != 1:
118 raise Exception("Invalid response to public key request")
119
120 an = response.answer[0]
121 if an.rdclass != dns.rdataclass.IN or an.rdtype != dns.rdatatype.TXT or len(an.items) == 0:
122 raise Exception("Invalid response to public key request")
123
124 for item in an.items:
125 if len(item.strings) != 1:
126 continue
127
128 cert = DNSCryptResolverCertificate.fromBinary(item.strings[0], self._providerFingerprint)
129 if cert.isValid():
130 self._resolverCertificates.append(cert)
131
132 def _getResolverCertificate(self):
133 certs = self._resolverCertificates
134 result = None
135 for cert in certs:
136 if cert.isValid():
137 if result is None or cert.serial > result.serial:
138 result = cert
139
140 return result
141
142 @staticmethod
143 def _generateNonce():
144 nonce = libnacl.utils.rand_nonce()
145 return nonce[:(DNSCryptClient.DNSCRYPT_NONCE_SIZE / 2)]
146
147 def _encryptQuery(self, queryContent, resolverCert, nonce, tcp=False):
148 header = resolverCert.clientMagic + self._publicKey + nonce
149 requiredSize = len(header) + self.DNSCRYPT_MAC_SIZE + len(queryContent)
150 paddingSize = self.DNSCRYPT_PADDED_BLOCK_SIZE - (len(queryContent) % self.DNSCRYPT_PADDED_BLOCK_SIZE)
151 # padding size should be DNSCRYPT_PADDED_BLOCK_SIZE <= padding size <= 4096
152 if not tcp and requiredSize < self.DNSCRYPT_MIN_UDP_LENGTH:
153 paddingSize += self.DNSCRYPT_MIN_UDP_LENGTH - requiredSize
154 requiredSize = self.DNSCRYPT_MIN_UDP_LENGTH
155
156 padding = '\x80'
157 idx = 0
158 while idx < (paddingSize - 1):
159 padding = padding + '\x00'
160 idx += 1
161
162 data = queryContent + padding
163 nonce = nonce + ('\x00'*(self.DNSCRYPT_NONCE_SIZE / 2))
164 box = libnacl.crypto_box(data, nonce, resolverCert.publicKey, self._privateKey)
165 return header + box
166
167 def _decryptResponse(self, encryptedResponse, resolverCert, clientNonce):
168 resolverMagic = encryptedResponse[:8]
169 if resolverMagic != self.DNSCRYPT_RESOLVER_MAGIC:
170 raise Exception("Invalid encrypted response: bad resolver magic")
171
172 nonce = encryptedResponse[8:32]
173 if nonce[0:self.DNSCRYPT_NONCE_SIZE / 2] != clientNonce:
174 raise Exception("Invalid encrypted response: bad nonce")
175
176 cleartext = libnacl.crypto_box_open(encryptedResponse[32:], nonce, resolverCert.publicKey, self._privateKey)
177 idx = len(cleartext) - 1
178 while idx > 0:
179 if cleartext[idx] != '\x00':
180 break
181 idx -= 1
182
183 if idx == 0 or cleartext[idx] != '\x80':
184 raise Exception("Invalid encrypted response: invalid padding")
185
186 idx -= 1
187 paddingLen = len(cleartext) - idx
188
189 return cleartext[:idx+1]
190
191 def query(self, queryContent, tcp=False):
192
193 if not self._hasValidResolverCertificate():
194 self._getResolverCertificates()
195
196 nonce = self._generateNonce()
197 resolverCert = self._getResolverCertificate()
198 if resolverCert is None:
199 raise Exception("No valid certificate found")
200 encryptedQuery = self._encryptQuery(queryContent, resolverCert, nonce, tcp)
201 encryptedResponse = self._sendQuery(encryptedQuery, tcp)
202 response = self._decryptResponse(encryptedResponse, resolverCert, nonce)
203 return response