Skip to content

Commit f60f1a2

Browse files
authored
Fix issue when updating TRC, deleting or adding core AS (#387)
* Fix issue when updating TRC, deleting or adding core AS Fix issue with overwritting certificate with random version of certificate when updating TRC, after changes to the set of core ASes Add regression test and unit test checking the new behavior of bundling the latest cert version and not some cert version * Lint, update comments * relint
1 parent e4557af commit f60f1a2

File tree

4 files changed

+111
-12
lines changed

4 files changed

+111
-12
lines changed

scionlab/models/core.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,18 @@ def isd_as_path_str(self):
264264
return self.isd_as_str().replace(":", "_")
265265

266266
def certificates(self):
267-
""" returns a queryset of all of this AS' certificates """
268-
return Certificate.objects.filter(key__AS=self)
267+
""" returns a queryset of all of this AS' certificates (in random order) """
268+
return Certificate.objects.filter(key__AS=self).order_by('?')
269+
270+
def certificates_latest(self):
271+
""" returns a queryset of all the latest certificates of this AS """
272+
certs = Certificate.objects.none()
273+
for key_usage in Key.USAGES:
274+
latest_version = self.keys.filter(usage=key_usage).aggregate(models.Max('version'))[
275+
'version__max'] or 1
276+
certs = certs | Certificate.objects.filter(key__AS=self, key__usage=key_usage,
277+
key__version__gte=latest_version)
278+
return certs
269279

270280
def generate_keys(self, not_before=None):
271281
Key.objects.create_all_keys(self, not_before=not_before)
@@ -594,14 +604,14 @@ def active(self):
594604
"""
595605
return list of Interfaces from active links
596606
"""
597-
return self.filter(link_as_interfaceA__active=True) |\
607+
return self.filter(link_as_interfaceA__active=True) | \
598608
self.filter(link_as_interfaceB__active=True)
599609

600610
def inactive(self):
601611
"""
602612
return list of Interfaces from inactive links
603613
"""
604-
return self.filter(link_as_interfaceA__active=False) |\
614+
return self.filter(link_as_interfaceA__active=False) | \
605615
self.filter(link_as_interfaceB__active=False)
606616

607617

scionlab/scion/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def _write_trcs(self, dir):
125125
self.archive.write_bytes((dir, CERT_DIR, trc.filename()), trc.format_trcfile())
126126

127127
def _write_certs(self, dir):
128-
for cert in self.AS.certificates().all():
128+
for cert in self.AS.certificates_latest().all():
129129
self.archive.write_text((dir, CRYPTO_DIR, cert.subdir(), cert.filename()),
130130
cert.format_certfile())
131131

scionlab/scion/trcs.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ def verify_trcs(*trcs: bytes):
6363
_run_scion_pki('verify', '--anchor', *[f.name for f in files])
6464

6565

66+
def verify_certificate(cert: bytes, trc: bytes):
67+
"""
68+
Verify that the certificate is valid, using the last TRC as anchor.
69+
Raises VerifyError if the certificate is not valid.
70+
"""
71+
with contextlib.ExitStack() as stack:
72+
trc_file = stack.enter_context(NamedTemporaryFile(suffix=".trc"))
73+
cert_file = stack.enter_context(NamedTemporaryFile(suffix=".pem"))
74+
files = [trc_file, cert_file]
75+
for f, value in zip(files, [trc, cert]):
76+
f.write(value)
77+
f.flush()
78+
_run_scion_pki('verify', '--trc', *[f.name for f in files], command='certificate')
79+
80+
6681
def generate_trc(prev_trc: bytes,
6782
isd_id: int,
6883
base: int,
@@ -256,13 +271,20 @@ def _to_seconds(d: timedelta) -> str:
256271
return f'{int(d.total_seconds())}s'
257272

258273

259-
def _run_scion_pki(*args, cwd=None, check=True):
274+
def _run_scion_pki(*args, cwd=None, command='trcs', check=True):
260275
try:
261-
return subprocess.run([settings.SCION_PKI_COMMAND, 'trcs', *args],
262-
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
263-
encoding='utf-8',
264-
check=check,
265-
cwd=cwd)
276+
if command == 'trcs':
277+
return subprocess.run([settings.SCION_PKI_COMMAND, command, *args],
278+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
279+
encoding='utf-8',
280+
check=check,
281+
cwd=cwd)
282+
elif command == 'certificate':
283+
return subprocess.run([settings.SCION_PKI_COMMAND, command, *args],
284+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
285+
encoding='utf-8',
286+
check=check,
287+
cwd=cwd)
266288
except subprocess.CalledProcessError as e:
267289
raise _CalledProcessErrorWithOutput(e) from None
268290

scionlab/tests/test_trc.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from scionlab.models.pki import Key, Certificate
2222
from scionlab.models.trc import TRC, _coreas_certificates
2323

24-
2524
_ASID_1 = 'ff00:0:1'
2625
_ASID_2 = 'ff00:0:2'
2726
_ASID_3 = 'ff00:0:3'
@@ -250,6 +249,74 @@ def test_create_sensitive_update(self):
250249
self.assertTrue(trc.votes.exists())
251250
self.assertEqual(trc.quorum, prev.quorum)
252251

252+
def test_delete_one_core_as(self):
253+
self._create_ases()
254+
prev = TRC.objects.create(self.isd1)
255+
# remove one core AS
256+
AS.objects.filter(is_core=True, isd=self.isd1).first().delete()
257+
# deleting a core As triggers a generation of a TRC. Get that TRC:
258+
trc = TRC.objects.latest()
259+
260+
# check the trc chain
261+
_check_trc(trc, prev)
262+
# check it's a sensitive update
263+
self.assertEqual(trc.serial_version, prev.serial_version + 1)
264+
self.assertEqual(trc.base_version, prev.base_version)
265+
self.assertEqual(trc.predecessor_trc_or_none(), prev)
266+
self.assertTrue(trc.votes.exists())
267+
self.assertNotEqual(trc.quorum, prev.quorum)
268+
269+
# Check valid latest CP AS certificates regenerated, core
270+
some_core = AS.objects.filter(is_core=True, isd=self.isd1).first()
271+
cert_cp_as = some_core.certificates_latest().filter(key__usage=Key.CP_AS).first()
272+
loaded_certs = bytes(cert_cp_as.format_certfile(), 'ascii')
273+
trcs.verify_certificate(loaded_certs, trcs.decode_trc(trc.trc))
274+
275+
# Check valid latest CP AS certificates regenerated, non-core
276+
any_none_core = AS.objects.filter(is_core=False, isd=self.isd1).first()
277+
cert_cp_as = any_none_core.certificates_latest().filter(key__usage=Key.CP_AS).first()
278+
loaded_certs = bytes(cert_cp_as.format_certfile(), 'ascii')
279+
trcs.verify_certificate(loaded_certs, trcs.decode_trc(trc.trc))
280+
281+
def test_broken_delete_one_core_as(self):
282+
# [regression test] Check that validating an invalid / old certificate fails
283+
# against an updated TRC
284+
self._create_ases()
285+
prev = TRC.objects.create(self.isd1)
286+
# remove one core AS
287+
AS.objects.filter(is_core=True, isd=self.isd1).first().delete()
288+
# deleting a core As triggers a generation of a TRC. Get that TRC:
289+
trc = TRC.objects.latest()
290+
291+
# check the trc chain
292+
_check_trc(trc, prev)
293+
# check it's a sensitive update
294+
self.assertEqual(trc.serial_version, prev.serial_version + 1)
295+
self.assertEqual(trc.base_version, prev.base_version)
296+
self.assertEqual(trc.predecessor_trc_or_none(), prev)
297+
self.assertTrue(trc.votes.exists())
298+
self.assertNotEqual(trc.quorum, prev.quorum)
299+
300+
# Check invalid CP AS certificates when selecting old certificate, core
301+
with self.assertRaises(trcs._CalledProcessErrorWithOutput):
302+
some_core = AS.objects.filter(is_core=True, isd=self.isd1).first()
303+
cert_cp_as = Certificate.objects.filter(key__AS=some_core, key__usage=Key.CP_AS,
304+
key__version=1).get()
305+
loaded_certs = bytes(cert_cp_as.format_certfile(), 'ascii')
306+
trcs.verify_certificate(loaded_certs, trcs.decode_trc(trc.trc))
307+
308+
# Check invalid CP AS certificates when randomly selecting, non-core
309+
with self.assertRaises(AttributeError):
310+
any_none_core = AS.objects.filter(is_core=False, isd=self.isd1).first()
311+
cert_cp_as = Certificate.objects.filter(key__AS=any_none_core, key__usage=Key.CP_AS,
312+
key__version=1).get()
313+
certfile = cert_cp_as.format_certfile()
314+
# We should never get further, Unreachable code
315+
# The first core AS was deleted and the non-core v1 CP AS cert was referring to
316+
# that core AS CA cert
317+
loaded_certs = bytes(certfile, 'ascii')
318+
trcs.verify_certificate(loaded_certs, trcs.decode_trc(trc.trc))
319+
253320
def test_create_less_core_ases(self):
254321
self._create_ases()
255322
prev = TRC.objects.create(self.isd1)

0 commit comments

Comments
 (0)