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