]>
Commit | Line | Data |
---|---|---|
b8db58a2 | 1 | #!/usr/bin/env python2 |
b8db58a2 RG |
2 | import socket |
3 | import struct | |
4 | import time | |
fe1c60f2 RG |
5 | import dns |
6 | import dns.message | |
b8db58a2 RG |
7 | import libnacl |
8 | import libnacl.utils | |
9 | ||
fe1c60f2 | 10 | class DNSCryptResolverCertificate(object): |
b8db58a2 RG |
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] | |
79500db5 | 42 | serial = struct.unpack_from("I", orig[40:44])[0] |
b1bec9f0 RG |
43 | validFrom = struct.unpack_from("!I", orig[44:48])[0] |
44 | validUntil = struct.unpack_from("!I", orig[48:52])[0] | |
b8db58a2 RG |
45 | return DNSCryptResolverCertificate(serial, validFrom, validUntil, resolverPK, clientMagic) |
46 | ||
fe1c60f2 | 47 | class DNSCryptClient(object): |
b8db58a2 RG |
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 | ||
1ade83b2 | 66 | def __init__(self, providerName, providerFingerprint, resolverAddress, resolverPort=443, timeout=2): |
b8db58a2 RG |
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() | |
fcffc585 | 73 | self._timeout = timeout |
b8db58a2 RG |
74 | |
75 | addrType = self._addrToSocketType(self._resolverAddress) | |
76 | self._sock = socket.socket(addrType, socket.SOCK_DGRAM) | |
1ade83b2 | 77 | self._sock.settimeout(timeout) |
b8db58a2 | 78 | self._sock.connect((self._resolverAddress, self._resolverPort)) |
b8db58a2 | 79 | |
fcffc585 RG |
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 | ||
b8db58a2 RG |
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 | ||
79500db5 RG |
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 | ||
b8db58a2 RG |
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 | |
b1bec9f0 | 138 | |
b8db58a2 RG |
139 | cert = DNSCryptResolverCertificate.fromBinary(item.strings[0], self._providerFingerprint) |
140 | if cert.isValid(): | |
141 | self._resolverCertificates.append(cert) | |
142 | ||
79500db5 | 143 | def getResolverCertificate(self): |
b8db58a2 RG |
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 | ||
fcffc585 | 158 | def _encryptQuery(self, queryContent, resolverCert, nonce, tcp=False): |
b8db58a2 RG |
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 | |
fcffc585 | 163 | if not tcp and requiredSize < self.DNSCRYPT_MIN_UDP_LENGTH: |
b8db58a2 RG |
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 | ||
fcffc585 | 202 | def query(self, queryContent, tcp=False): |
b8db58a2 RG |
203 | |
204 | if not self._hasValidResolverCertificate(): | |
79500db5 | 205 | self.refreshResolverCertificates() |
b8db58a2 RG |
206 | |
207 | nonce = self._generateNonce() | |
79500db5 | 208 | resolverCert = self.getResolverCertificate() |
b8db58a2 RG |
209 | if resolverCert is None: |
210 | raise Exception("No valid certificate found") | |
fcffc585 RG |
211 | encryptedQuery = self._encryptQuery(queryContent, resolverCert, nonce, tcp) |
212 | encryptedResponse = self._sendQuery(encryptedQuery, tcp) | |
b8db58a2 RG |
213 | response = self._decryptResponse(encryptedResponse, resolverCert, nonce) |
214 | return response |