Skip to content

Commit c65af4b

Browse files
authored
Generate TRCs in PEM format (#406)
Change the data format of the TRCs from DER to PEM, both in the generated configuration and in the internal representation in the DB. Add a data migration to convert all existing TRCs. Previously, we've generated the TRCs in DER format as this is currently the default output format of `scion-pki trcs combine`. The (binary) DER data was stored base64-encoded in the text database field. According to the Anapayans working on this, the PEM format is should generally be preferred, and it should become the default format for TRCs eventually. The practical motivation for the change is this; the CS persists any TRCs fetched "over the wire" not only in its sqlite DB, but it also writes them back to the configuration directory, in PEM format. As our configuration has up to now contained TRCs in DER, installing updated configuration using `scionlab-config` after the CS wrote such a TRC file would lead to an apparent file conflict. Using PEM in our configuration avoids this.
1 parent d99cc56 commit c65af4b

File tree

22 files changed

+3097
-1964
lines changed

22 files changed

+3097
-1964
lines changed

scionlab/config_tar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
# the scionlab-config.json manifest file in the configuration tar ball.
3535
# This version number should be incremented whenever code changes globally affect the generated
3636
# configuration of hosts.
37-
CONFIG_GEN_VERSION = 14
37+
CONFIG_GEN_VERSION = 15
3838

3939

4040
def generate_user_as_config_tar(user_as, archive):

scionlab/fixtures/testdata.yaml

Lines changed: 649 additions & 3 deletions
Large diffs are not rendered by default.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright 2021 ETH Zurich
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import textwrap
16+
17+
from django.db import migrations
18+
19+
PEM_WIDTH = 64 # see e.g. RFC 7468
20+
TRC_PEM_HEADER = "-----BEGIN TRC-----\n"
21+
TRC_PEM_FOOTER = "-----END TRC-----\n"
22+
23+
24+
def convert_trcs_to_pem(apps, schema_editor):
25+
"""
26+
Convert TRC data from DER to PEM.
27+
As the DER blob is already stored as base64-encoded text in the DB,
28+
we only need to wrap lines and add the header and footer to turn it into a PEM.
29+
"""
30+
TRC = apps.get_model('scionlab', 'TRC')
31+
for trc in TRC.objects.all():
32+
trc.trc = trc_base64der_to_pem(trc.trc)
33+
trc.save()
34+
35+
36+
def trc_base64der_to_pem(der: str) -> str:
37+
assert not der.startswith(TRC_PEM_HEADER)
38+
assert not der.endswith(TRC_PEM_HEADER)
39+
body = textwrap.fill(der, width=PEM_WIDTH)
40+
return TRC_PEM_HEADER + body + "\n" + TRC_PEM_FOOTER
41+
42+
43+
def revert_trcs_to_der(apps, schema_editor):
44+
"""
45+
Convert TRC from PEM back to base64-encoded DER.
46+
"""
47+
TRC = apps.get_model('scionlab', 'TRC')
48+
for trc in TRC.objects.all():
49+
trc.trc = trc_pem_to_base64der(trc.trc)
50+
trc.save()
51+
52+
53+
def trc_pem_to_base64der(pem: str) -> str:
54+
assert pem.startswith(TRC_PEM_HEADER)
55+
assert pem.endswith(TRC_PEM_FOOTER)
56+
stripped = pem[len(TRC_PEM_HEADER):-len(TRC_PEM_FOOTER)]
57+
return stripped.replace("\n", "")
58+
59+
60+
class Migration(migrations.Migration):
61+
62+
dependencies = [
63+
('scionlab', '0004_remove_link_bandwidth'),
64+
]
65+
66+
operations = [
67+
migrations.RunPython(convert_trcs_to_pem, # forward
68+
revert_trcs_to_der), # backward
69+
]

scionlab/models/core.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from scionlab.scion import as_ids
3636
from scionlab.scion.certs import verify_certificate_valid, verify_cp_as_chain
3737
from scionlab.scion.keys import verify_key
38-
from scionlab.scion.trcs import decode_trc, verify_trcs
38+
from scionlab.scion.trcs import verify_trcs
3939
from scionlab.scion.pkicommand import ScionPkiError
4040
from scionlab.util.django import value_set
4141
from scionlab.defines import (
@@ -117,7 +117,7 @@ def validate_crypto(self):
117117
namely that the trc chain is valid.
118118
Returns the number of valid trcs in this ISD.
119119
"""
120-
ts = [decode_trc(t.trc) for t in self.trcs.order_by('serial_version')]
120+
ts = [t.trc for t in self.trcs.order_by('serial_version')]
121121
verify_trcs(ts[0], *ts)
122122
return len(ts)
123123

@@ -306,7 +306,7 @@ def validate_crypto(self):
306306
latests certificates and keys and checking them against the latest TRC.
307307
Returns the number of certificates (with keys) that passed the check.
308308
"""
309-
trc = decode_trc(self.isd.trcs.latest().trc)
309+
trc = self.isd.trcs.latest().trc
310310
certs = self.certificates_latest()
311311
cert_map = {}
312312
for cert in certs:

scionlab/models/trc.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def create(self, isd):
112112
votes_idx = prev.get_certificate_indices(votes) if prev else []
113113

114114
trc = trcs.generate_trc(
115-
prev_trc=trcs.decode_trc(prev.trc) if prev else None,
115+
prev_trc=prev.trc if prev else None,
116116
isd_id=isd.isd_id,
117117
base=base,
118118
serial=serial,
@@ -128,7 +128,7 @@ def create(self, isd):
128128
)
129129
obj = super().create(isd=isd, serial_version=serial, base_version=base,
130130
not_before=not_before, not_after=not_after,
131-
quorum=quorum, trc=trcs.encode_trc(trc))
131+
quorum=quorum, trc=trc)
132132
obj.core_ases.set(core_ases)
133133
obj.certificates.add(*certificates)
134134
obj.votes.set(votes)
@@ -236,9 +236,6 @@ def get_certificate_indices(self, certs):
236236
def filename(self) -> str:
237237
return f'ISD{self.isd.isd_id}-B{self.base_version}-S{self.serial_version}.trc'
238238

239-
def format_trcfile(self) -> bytes:
240-
return trcs.decode_trc(self.trc)
241-
242239
def check_regular_update_error(self):
243240
"""
244241
Check if this TRC is a proper regular update. For testing.

scionlab/scion/certs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,14 @@ def verify_certificate_valid(cert: str, cert_usage: str):
7575
_run_scion_pki_certificate('validate', '--type', cert_usage, '--check-time', f.name)
7676

7777

78-
def verify_cp_as_chain(cert: str, trc: bytes):
78+
def verify_cp_as_chain(cert: str, trc: str):
7979
"""
8080
Verify that the certificate is valid, using the last TRC as anchor.
8181
The certificate is passed as a PEM string.
8282
The TRC is passed as bytes, basee 64 format.
8383
Raises ScionPkiError if the certificate is not valid.
8484
"""
85-
with NamedTemporaryFile(mode='wb', suffix=".trc") as trc_file,\
85+
with NamedTemporaryFile(mode='wt', suffix=".trc") as trc_file,\
8686
NamedTemporaryFile(mode='wt', suffix=".pem") as cert_file:
8787
files = [trc_file, cert_file]
8888
for f, value in zip(files, [trc, cert]):

scionlab/scion/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def _write_trcs(self, dir):
128128
# As there are no big disadvantages in always including all versions, that's what we do
129129
# for now.
130130
for trc in TRC.objects.all():
131-
self.archive.write_bytes((dir, CERT_DIR, trc.filename()), trc.format_trcfile())
131+
self.archive.write_text((dir, CERT_DIR, trc.filename()), trc.trc)
132132

133133
def _write_certs(self, dir):
134134
for cert in self.AS.certificates_latest().all():

scionlab/scion/trcs.py

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
===========================================
1818
"""
1919

20-
import base64
2120
import toml
2221
import yaml
2322
import contextlib
@@ -33,30 +32,22 @@
3332
from scionlab.scion.pkicommand import run_scion_pki
3433

3534

36-
def encode_trc(trc: bytes) -> str:
37-
return base64.b64encode(trc).decode('ascii')
38-
39-
40-
def decode_trc(trc: str) -> bytes:
41-
return base64.b64decode(trc.encode('ascii'))
42-
43-
44-
def trc_to_dict(trc: bytes) -> dict:
45-
with NamedTemporaryFile('wb') as f:
35+
def trc_to_dict(trc: str) -> dict:
36+
with NamedTemporaryFile('w') as f:
4637
f.write(trc)
4738
f.flush()
4839
ret = _run_scion_pki_trcs('human', '--format', 'yaml', f.name, check=True)
4940
return yaml.safe_load(ret.stdout)
5041

5142

52-
def verify_trcs(*trcs: bytes):
43+
def verify_trcs(*trcs: str):
5344
"""
5445
Verify that the sequence of trcs, using the first TRC as anchor.
5546
TRCs are passed as bytes, base 64 format.
5647
Raises ScionPkiError if the TRCs are not valid.
5748
"""
5849
with contextlib.ExitStack() as stack:
59-
files = [stack.enter_context(NamedTemporaryFile(suffix=".trc")) for _ in range(len(trcs))]
50+
files = [stack.enter_context(NamedTemporaryFile(suffix=".trc", mode="w")) for _ in trcs]
6051
for f, trc in zip(files, trcs):
6152
f.write(trc)
6253
f.flush()
@@ -75,7 +66,7 @@ def generate_trc(prev_trc: bytes,
7566
not_after: datetime,
7667
certificates: List[str],
7768
signers_certs: List[str],
78-
signers_keys: List[str]) -> bytes:
69+
signers_keys: List[str]) -> str:
7970
"""
8071
Generate a new TRC.
8172
This method is the interface between the DB objects and the scion PKI ones.
@@ -157,7 +148,7 @@ def __init__(self,
157148

158149
self._validate()
159150

160-
def generate(self) -> bytes:
151+
def generate(self) -> str:
161152
with TemporaryDirectory() as temp_dir:
162153
self._dump_certificates_to_files(temp_dir)
163154
self._gen_payload(temp_dir)
@@ -172,7 +163,7 @@ def _gen_payload(self, temp_dir: str) -> None:
172163
args = ['payload', '-t', self.CONFIG_FILENAME, '-o', self.PAYLOAD_FILENAME]
173164
if self.predecessor_trc:
174165
pred_filename = Path(temp_dir, self.PRED_TRC_FILENAME)
175-
pred_filename.write_bytes(self.predecessor_trc)
166+
pred_filename.write_text(self.predecessor_trc)
176167
args.extend(['-p', str(pred_filename)])
177168
_run_scion_pki_trcs(*args, cwd=temp_dir)
178169

@@ -186,17 +177,18 @@ def _sign_payload(self, temp_dir: str, cert: str, key: str) -> bytes:
186177
pkcs7.PKCS7Options.NoCapabilities,
187178
pkcs7.PKCS7Options.Binary])
188179

189-
def _combine(self, temp_dir: str, *signed) -> bytes:
190-
""" returns the final TRC by combining the signed blocks and payload """
180+
def _combine(self, temp_dir: str, *signed) -> str:
181+
""" returns the final TRC (in PEM format), by combining the signed blocks and payload """
191182
for i, s in enumerate(signed):
192183
Path(temp_dir, f'signed-{i}.der').write_bytes(s)
193184
_run_scion_pki_trcs(
194185
'combine', '-p', self.PAYLOAD_FILENAME,
195186
*(f'signed-{i}.der' for i in range(len(signed))),
196187
'-o', Path(temp_dir, self.TRC_FILENAME),
188+
'--format', 'pem',
197189
cwd=temp_dir
198190
)
199-
return Path(temp_dir, self.TRC_FILENAME).read_bytes()
191+
return Path(temp_dir, self.TRC_FILENAME).read_text()
200192

201193
def is_update(self):
202194
return self.base_version != self.serial_version

0 commit comments

Comments
 (0)