]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.dnsdist/test_TLSSessionResumption.py
Merge pull request #13851 from dwfreed/fix-alpn-selection
[thirdparty/pdns.git] / regression-tests.dnsdist / test_TLSSessionResumption.py
CommitLineData
4ecc5603
RG
1#!/usr/bin/env python
2import base64
3import dns
4import os
31426478 5import shutil
4ecc5603 6import subprocess
31426478 7import tempfile
4ecc5603 8import time
13291274 9import unittest
630eb526 10from dnsdisttests import DNSDistTest, pickAvailablePort
4ecc5603
RG
11try:
12 range = xrange
13except NameError:
14 pass
15
16class DNSDistTLSSessionResumptionTest(DNSDistTest):
17
18 _consoleKey = DNSDistTest.generateConsoleKey()
19 _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
20
21 @classmethod
31426478
RG
22 def checkSessionResumed(cls, addr, port, serverName, caFile, ticketFileOut, ticketFileIn, allowNoTicket=False):
23 outFile = tempfile.NamedTemporaryFile()
24
4ecc5603
RG
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..
31426478 27 testcmd = ['openssl', 's_client', '-tls1_3', '-CAfile', caFile, '-connect', '%s:%d' % (addr, port), '-servername', serverName, '-sess_out', outFile.name]
4ecc5603
RG
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..
8f3568cb 35 time.sleep(0.5)
4ecc5603
RG
36 output = process.communicate(input=b'')
37 except subprocess.CalledProcessError as exc:
31426478
RG
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)
4ecc5603
RG
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
13291274 61@unittest.skipIf('SKIP_DOH_TESTS' in os.environ, 'DNS over HTTPS tests are disabled')
4ecc5603
RG
62class TestNoTLSSessionResumptionDOH(DNSDistTLSSessionResumptionTest):
63
64 _serverKey = 'server.key'
65 _serverCert = 'server.chain'
66 _serverName = 'tls.tests.dnsdist.org'
67 _caCert = 'ca.pem'
188dbe71
RG
68 _dohWithNGHTTP2ServerPort = pickAvailablePort()
69 _dohWithH2OServerPort = pickAvailablePort()
4ecc5603
RG
70 _numberOfKeys = 0
71 _config_template = """
72 newServer{address="127.0.0.1:%s"}
73
188dbe71
RG
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' })
4ecc5603 76 """
188dbe71 77 _config_params = ['_testServerPort', '_dohWithNGHTTP2ServerPort', '_serverCert', '_serverKey', '_numberOfKeys', '_dohWithH2OServerPort', '_serverCert', '_serverKey', '_numberOfKeys']
4ecc5603
RG
78
79 def testNoSessionResumption(self):
80 """
81 Session Resumption: DoH (disabled)
82 """
188dbe71
RG
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))
4ecc5603 86
13291274 87@unittest.skipIf('SKIP_DOH_TESTS' in os.environ, 'DNS over HTTPS tests are disabled')
4ecc5603
RG
88class TestTLSSessionResumptionDOH(DNSDistTLSSessionResumptionTest):
89
90 _serverKey = 'server.key'
91 _serverCert = 'server.chain'
92 _serverName = 'tls.tests.dnsdist.org'
93 _caCert = 'ca.pem'
188dbe71
RG
94 _dohWithNGHTTP2ServerPort = pickAvailablePort()
95 _dohWithH2OServerPort = pickAvailablePort()
4ecc5603
RG
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
188dbe71
RG
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' })
4ecc5603 104 """
188dbe71 105 _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_dohWithNGHTTP2ServerPort', '_serverCert', '_serverKey', '_numberOfKeys', '_dohWithH2OServerPort', '_serverCert', '_serverKey', '_numberOfKeys']
4ecc5603
RG
106
107 def testSessionResumption(self):
108 """
109 Session Resumption: DoH
110 """
188dbe71
RG
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))
4ecc5603
RG
184
185class TestNoTLSSessionResumptionDOT(DNSDistTLSSessionResumptionTest):
186
187 _serverKey = 'server.key'
188 _serverCert = 'server.chain'
189 _serverName = 'tls.tests.dnsdist.org'
190 _caCert = 'ca.pem'
630eb526 191 _tlsServerPort = pickAvailablePort()
4ecc5603
RG
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 """
31426478
RG
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))
4ecc5603
RG
206
207class TestTLSSessionResumptionDOT(DNSDistTLSSessionResumptionTest):
208
209 _serverKey = 'server.key'
210 _serverCert = 'server.chain'
211 _serverName = 'tls.tests.dnsdist.org'
212 _caCert = 'ca.pem'
630eb526 213 _tlsServerPort = pickAvailablePort()
4ecc5603
RG
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 """
31426478
RG
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))
4ecc5603
RG
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
31426478 236 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot'))
4ecc5603
RG
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
31426478 242 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot'))
4ecc5603
RG
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
31426478 249 self.assertFalse(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot'))
4ecc5603
RG
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
31426478
RG
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))
4ecc5603
RG
260
261 # reload the same keys
262 self.sendConsoleCommand("getTLSContext(0):loadTicketsKeys('/tmp/ticketKeys.1')")
263
264 # should still be able to resume
31426478 265 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot', allowNoTicket=True))
4ecc5603
RG
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
31426478 271 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot'))
4ecc5603
RG
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
31426478 276 self.assertFalse(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot'))
4ecc5603
RG
277
278 # but now we can
31426478 279 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot', allowNoTicket=True))
4ecc5603
RG
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!!)
31426478
RG
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))
4ecc5603
RG
287
288 # rotate all keys, we should not be able to resume
289 for _ in range(self._numberOfKeys):
290 self.sendConsoleCommand("getTLSContext(0):rotateTicketsKey()")
31426478 291 self.assertFalse(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot.3', '/tmp/session.dot.2'))
4ecc5603
RG
292
293 # reload from file 1, the old session should resume
294 self.sendConsoleCommand("getTLSContext(0):loadTicketsKeys('/tmp/ticketKeys.1')")
31426478 295 self.assertTrue(self.checkSessionResumed('127.0.0.1', self._tlsServerPort, self._serverName, self._caCert, '/tmp/session.dot', '/tmp/session.dot', allowNoTicket=True))
4ecc5603
RG
296
297 # reload from file 2, the latest session should resume
298 self.sendConsoleCommand("getTLSContext(0):loadTicketsKeys('/tmp/ticketKeys.2')")
31426478 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))