]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.dnsdist/test_TLSSessionResumption.py
Merge pull request #14001 from rgacogne/ddist-ffi-policy-no-server
[thirdparty/pdns.git] / regression-tests.dnsdist / test_TLSSessionResumption.py
1 #!/usr/bin/env python
2 import base64
3 import dns
4 import os
5 import shutil
6 import subprocess
7 import tempfile
8 import time
9 import unittest
10 from dnsdisttests import DNSDistTest, pickAvailablePort
11 try:
12 range = xrange
13 except NameError:
14 pass
15
16 class DNSDistTLSSessionResumptionTest(DNSDistTest):
17
18 _consoleKey = DNSDistTest.generateConsoleKey()
19 _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
20
21 @classmethod
22 def checkSessionResumed(cls, addr, port, serverName, caFile, ticketFileOut, ticketFileIn, allowNoTicket=False):
23 outFile = tempfile.NamedTemporaryFile()
24
25 # we force TLS 1.3 because the session file gets updated when an existing ticket encrypted with an older key gets re-encrypted with the active key
26 # whereas in TLS 1.2 the existing ticket is written instead..
27 testcmd = ['openssl', 's_client', '-tls1_3', '-CAfile', caFile, '-connect', '%s:%d' % (addr, port), '-servername', serverName, '-sess_out', outFile.name]
28 if ticketFileIn and os.path.exists(ticketFileIn):
29 testcmd = testcmd + ['-sess_in', ticketFileIn]
30
31 output = None
32 try:
33 process = subprocess.Popen(testcmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
34 # we need to wait just a bit so that the Post-Handshake New Session Ticket has the time to arrive..
35 time.sleep(0.5)
36 output = process.communicate(input=b'')
37 except subprocess.CalledProcessError as exc:
38 raise AssertionError('%s failed (%d): %s' % (testcmd, process.returncode, process.output))
39
40 if process.returncode != 0:
41 raise AssertionError('%s failed (%d): %s' % (testcmd, process.returncode, output))
42
43 if os.stat(outFile.name).st_size == 0:
44 # if tickets have been disabled, or if the session ticket encryption key is exactly the same, we might not get a new ticket
45 if not allowNoTicket:
46 raise AssertionError('%s failed (%d) to write a session to the output file: %s' % (testcmd, process.returncode, output))
47 else:
48 shutil.copyfile(outFile.name, ticketFileOut)
49
50 for line in output[0].decode().splitlines():
51 if line.startswith('Reused, TLSv1.'):
52 return True
53
54 return False
55
56 @staticmethod
57 def generateTicketKeysFile(numberOfTickets, outputFile):
58 with open(outputFile, 'wb') as fp:
59 fp.write(os.urandom(numberOfTickets * 80))
60
61 @unittest.skipIf('SKIP_DOH_TESTS' in os.environ, 'DNS over HTTPS tests are disabled')
62 class TestNoTLSSessionResumptionDOH(DNSDistTLSSessionResumptionTest):
63
64 _serverKey = 'server.key'
65 _serverCert = 'server.chain'
66 _serverName = 'tls.tests.dnsdist.org'
67 _caCert = 'ca.pem'
68 _dohWithNGHTTP2ServerPort = pickAvailablePort()
69 _dohWithH2OServerPort = pickAvailablePort()
70 _numberOfKeys = 0
71 _config_template = """
72 newServer{address="127.0.0.1:%s"}
73
74 addDOHLocal("127.0.0.1:%d", "%s", "%s", { "/" }, { numberOfTicketsKeys=%d, numberOfStoredSessions=0, sessionTickets=false, library='nghttp2' })
75 addDOHLocal("127.0.0.1:%d", "%s", "%s", { "/" }, { numberOfTicketsKeys=%d, numberOfStoredSessions=0, sessionTickets=false, library='h2o' })
76 """
77 _config_params = ['_testServerPort', '_dohWithNGHTTP2ServerPort', '_serverCert', '_serverKey', '_numberOfKeys', '_dohWithH2OServerPort', '_serverCert', '_serverKey', '_numberOfKeys']
78
79 def testNoSessionResumption(self):
80 """
81 Session Resumption: DoH (disabled)
82 """
83 for port in [self._dohWithNGHTTP2ServerPort, self._dohWithH2OServerPort]:
84 self.assertFalse(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/no-session.out.doh', None, allowNoTicket=True))
85 self.assertFalse(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/no-session.out.doh', '/tmp/no-session.out.doh', allowNoTicket=True))
86
87 @unittest.skipIf('SKIP_DOH_TESTS' in os.environ, 'DNS over HTTPS tests are disabled')
88 class TestTLSSessionResumptionDOH(DNSDistTLSSessionResumptionTest):
89
90 _serverKey = 'server.key'
91 _serverCert = 'server.chain'
92 _serverName = 'tls.tests.dnsdist.org'
93 _caCert = 'ca.pem'
94 _dohWithNGHTTP2ServerPort = pickAvailablePort()
95 _dohWithH2OServerPort = pickAvailablePort()
96 _numberOfKeys = 5
97 _config_template = """
98 setKey("%s")
99 controlSocket("127.0.0.1:%s")
100 newServer{address="127.0.0.1:%s"}
101
102 addDOHLocal("127.0.0.1:%d", "%s", "%s", { "/" }, { numberOfTicketsKeys=%d, library='nghttp2' })
103 addDOHLocal("127.0.0.1:%d", "%s", "%s", { "/" }, { numberOfTicketsKeys=%d, library='h2o' })
104 """
105 _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_dohWithNGHTTP2ServerPort', '_serverCert', '_serverKey', '_numberOfKeys', '_dohWithH2OServerPort', '_serverCert', '_serverKey', '_numberOfKeys']
106
107 def testSessionResumption(self):
108 """
109 Session Resumption: DoH
110 """
111 for (port, bindIdx) in [(self._dohWithNGHTTP2ServerPort, 0), (self._dohWithH2OServerPort, 1)]:
112 self.assertFalse(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh', None))
113 self.assertTrue(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh', '/tmp/session.doh', allowNoTicket=True))
114
115 # rotate the TLS session ticket keys several times, but keep the previously active one around so we can resume
116 for _ in range(self._numberOfKeys - 1):
117 self.sendConsoleCommand(f"getDOHFrontend({bindIdx}):rotateTicketsKey()")
118
119 # the session should be resumed and a new ticket, encrypted with the newly active key, should be stored
120 self.assertTrue(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh', '/tmp/session.doh'))
121
122 # rotate the TLS session ticket keys several times, but keep the previously active one around so we can resume
123 for _ in range(self._numberOfKeys - 1):
124 self.sendConsoleCommand(f"getDOHFrontend({bindIdx}):rotateTicketsKey()")
125
126 self.assertTrue(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh', '/tmp/session.doh'))
127
128 # rotate the TLS session ticket keys several times, not keeping any key around this time!
129 for _ in range(self._numberOfKeys):
130 self.sendConsoleCommand(f"getDOHFrontend({bindIdx}):rotateTicketsKey()")
131
132 # we should not be able to resume
133 self.assertFalse(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh', '/tmp/session.doh'))
134
135 # generate a file containing _numberOfKeys ticket keys
136 self.generateTicketKeysFile(self._numberOfKeys, '/tmp/ticketKeys.1')
137 self.generateTicketKeysFile(self._numberOfKeys - 1, '/tmp/ticketKeys.2')
138 # load all ticket keys from the file
139 self.sendConsoleCommand(f"getDOHFrontend({bindIdx}):loadTicketsKeys('/tmp/ticketKeys.1')")
140
141 # create a new session, resume it
142 self.assertFalse(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh', None))
143 self.assertTrue(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh', '/tmp/session.doh', allowNoTicket=True))
144
145 # reload the same keys
146 self.sendConsoleCommand(f"getDOHFrontend({bindIdx}):loadTicketsKeys('/tmp/ticketKeys.1')")
147
148 # should still be able to resume
149 self.assertTrue(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh', '/tmp/session.doh', allowNoTicket=True))
150
151 # rotate the TLS session ticket keys several times, but keep the previously active one around so we can resume
152 for _ in range(self._numberOfKeys - 1):
153 self.sendConsoleCommand(f"getDOHFrontend({bindIdx}):rotateTicketsKey()")
154 # should still be able to resume
155 self.assertTrue(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh', '/tmp/session.doh'))
156
157 # reload the same keys
158 self.sendConsoleCommand(f"getDOHFrontend({bindIdx}):loadTicketsKeys('/tmp/ticketKeys.1')")
159 # since the last key was only present in memory, we should not be able to resume
160 self.assertFalse(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh', '/tmp/session.doh'))
161
162 # but now we can
163 self.assertTrue(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh', '/tmp/session.doh', allowNoTicket=True))
164
165 # generate a file with only _numberOfKeys - 1 keys, so the last active one should still be around after loading that one
166 self.generateTicketKeysFile(self._numberOfKeys - 1, '/tmp/ticketKeys.2')
167 self.sendConsoleCommand(f"getDOHFrontend({bindIdx}):loadTicketsKeys('/tmp/ticketKeys.2')")
168 # we should be able to resume, and the ticket should be re-encrypted with the new key (NOTE THAT we store into a new file!!)
169 self.assertTrue(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh.2', '/tmp/session.doh'))
170 self.assertTrue(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh.2', '/tmp/session.doh.2', allowNoTicket=True))
171
172 # rotate all keys, we should not be able to resume
173 for _ in range(self._numberOfKeys):
174 self.sendConsoleCommand(f"getDOHFrontend({bindIdx}):rotateTicketsKey()")
175 self.assertFalse(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh.3', '/tmp/session.doh.2'))
176
177 # reload from file 1, the old session should resume
178 self.sendConsoleCommand(f"getDOHFrontend({bindIdx}):loadTicketsKeys('/tmp/ticketKeys.1')")
179 self.assertTrue(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh', '/tmp/session.doh', allowNoTicket=True))
180
181 # reload from file 2, the latest session should resume
182 self.sendConsoleCommand(f"getDOHFrontend({bindIdx}):loadTicketsKeys('/tmp/ticketKeys.2')")
183 self.assertTrue(self.checkSessionResumed('127.0.0.1', port, self._serverName, self._caCert, '/tmp/session.doh.2', '/tmp/session.doh.2', allowNoTicket=True))
184
185 class TestNoTLSSessionResumptionDOT(DNSDistTLSSessionResumptionTest):
186
187 _serverKey = 'server.key'
188 _serverCert = 'server.chain'
189 _serverName = 'tls.tests.dnsdist.org'
190 _caCert = 'ca.pem'
191 _tlsServerPort = pickAvailablePort()
192 _numberOfKeys = 0
193 _config_template = """
194 newServer{address="127.0.0.1:%s"}
195
196 addTLSLocal("127.0.0.1:%s", "%s", "%s", { numberOfTicketsKeys=%d, numberOfStoredSessions=0, sessionTickets=false })
197 """
198 _config_params = ['_testServerPort', '_tlsServerPort', '_serverCert', '_serverKey', '_numberOfKeys']
199
200 def testNoSessionResumption(self):
201 """
202 Session Resumption: DoT (disabled)
203 """
204 self.assertFalse(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/no-session.out.dot', None, allowNoTicket=True))
205 self.assertFalse(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/no-session.out.dot', '/tmp/no-session.out.dot', allowNoTicket=True))
206
207 class TestTLSSessionResumptionDOT(DNSDistTLSSessionResumptionTest):
208
209 _serverKey = 'server.key'
210 _serverCert = 'server.chain'
211 _serverName = 'tls.tests.dnsdist.org'
212 _caCert = 'ca.pem'
213 _tlsServerPort = pickAvailablePort()
214 _numberOfKeys = 5
215 _config_template = """
216 setKey("%s")
217 controlSocket("127.0.0.1:%s")
218 newServer{address="127.0.0.1:%s"}
219
220 addTLSLocal("127.0.0.1:%s", "%s", "%s", { provider="openssl", numberOfTicketsKeys=%d })
221 """
222 _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_tlsServerPort', '_serverCert', '_serverKey', '_numberOfKeys']
223
224 def testSessionResumption(self):
225 """
226 Session Resumption: DoT
227 """
228 self.assertFalse(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', None))
229 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot', allowNoTicket=True))
230
231 # rotate the TLS session ticket keys several times, but keep the previously active one around so we can resume
232 for _ in range(self._numberOfKeys - 1):
233 self.sendConsoleCommand("getTLSContext(0):rotateTicketsKey()")
234
235 # the session should be resumed and a new ticket, encrypted with the newly active key, should be stored
236 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot'))
237
238 # rotate the TLS session ticket keys several times, but keep the previously active one around so we can resume
239 for _ in range(self._numberOfKeys - 1):
240 self.sendConsoleCommand("getTLSContext(0):rotateTicketsKey()")
241
242 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot'))
243
244 # rotate the TLS session ticket keys several times, not keeping any key around this time!
245 for _ in range(self._numberOfKeys):
246 self.sendConsoleCommand("getTLSContext(0):rotateTicketsKey()")
247
248 # we should not be able to resume
249 self.assertFalse(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot'))
250
251 # generate a file containing _numberOfKeys ticket keys
252 self.generateTicketKeysFile(self._numberOfKeys, '/tmp/ticketKeys.1')
253 self.generateTicketKeysFile(self._numberOfKeys - 1, '/tmp/ticketKeys.2')
254 # load all ticket keys from the file
255 self.sendConsoleCommand("getTLSContext(0):loadTicketsKeys('/tmp/ticketKeys.1')")
256
257 # create a new session, resume it
258 self.assertFalse(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', None))
259 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot', allowNoTicket=True))
260
261 # reload the same keys
262 self.sendConsoleCommand("getTLSContext(0):loadTicketsKeys('/tmp/ticketKeys.1')")
263
264 # should still be able to resume
265 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot', allowNoTicket=True))
266
267 # rotate the TLS session ticket keys several times, but keep the previously active one around so we can resume
268 for _ in range(self._numberOfKeys - 1):
269 self.sendConsoleCommand("getTLSContext(0):rotateTicketsKey()")
270 # should still be able to resume
271 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot'))
272
273 # reload the same keys
274 self.sendConsoleCommand("getTLSContext(0):loadTicketsKeys('/tmp/ticketKeys.1')")
275 # since the last key was only present in memory, we should not be able to resume
276 self.assertFalse(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot'))
277
278 # but now we can
279 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot', allowNoTicket=True))
280
281 # generate a file with only _numberOfKeys - 1 keys, so the last active one should still be around after loading that one
282 self.generateTicketKeysFile(self._numberOfKeys - 1, '/tmp/ticketKeys.2')
283 self.sendConsoleCommand("getTLSContext(0):loadTicketsKeys('/tmp/ticketKeys.2')")
284 # we should be able to resume, and the ticket should be re-encrypted with the new key (NOTE THAT we store into a new file!!)
285 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot.2', '/tmp/session.dot'))
286 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot.2', '/tmp/session.dot.2', allowNoTicket=True))
287
288 # rotate all keys, we should not be able to resume
289 for _ in range(self._numberOfKeys):
290 self.sendConsoleCommand("getTLSContext(0):rotateTicketsKey()")
291 self.assertFalse(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot.3', '/tmp/session.dot.2'))
292
293 # reload from file 1, the old session should resume
294 self.sendConsoleCommand("getTLSContext(0):loadTicketsKeys('/tmp/ticketKeys.1')")
295 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot', allowNoTicket=True))
296
297 # reload from file 2, the latest session should resume
298 self.sendConsoleCommand("getTLSContext(0):loadTicketsKeys('/tmp/ticketKeys.2')")
299 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot.2', '/tmp/session.dot.2', allowNoTicket=True))