From c4d0dd61547f509b5db8e52c8d59a0ed0f88246d Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Mon, 9 Sep 2024 20:09:44 +0200 Subject: [PATCH] core / trust - add support for certificate bundles in our certctl.py script. Since we deploy ca_root_nss in a directory with less priority, we can exclude the certificates already part of the base install. When ca_root_nss contains a certificate not yet known in base, it will copy the certificate from the bundle in the "hashed" file, which also gurantees our packed bundles contain the same content as openssl would use by default. While here, also include /usr/share/certs/untrusted, which contains the base blacklisted certs. --- src/opnsense/scripts/system/certctl.py | 67 ++++++++++++++++---------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/src/opnsense/scripts/system/certctl.py b/src/opnsense/scripts/system/certctl.py index cc1ea4854..4f312f092 100755 --- a/src/opnsense/scripts/system/certctl.py +++ b/src/opnsense/scripts/system/certctl.py @@ -31,36 +31,42 @@ import sys import os import OpenSSL.crypto from cryptography import x509 +from cryptography.hazmat.primitives import serialization TRUSTPATH = ['/usr/share/certs/trusted', '/usr/local/share/certs', '/usr/local/etc/ssl/certs'] -BLACKLISTPATH = ['/usr/share/certs/blacklisted', '/usr/local/etc/ssl/blacklisted'] +BLACKLISTPATH = ['/usr/share/certs/untrusted', '/usr/share/certs/blacklisted', '/usr/local/etc/ssl/blacklisted'] CERTDESTDIR = '/etc/ssl/certs' BLACKLISTDESTDIR = '/etc/ssl/blacklisted' -def get_name_hash_file_pattern(filename): +def certificate_iterator(filename): fext = os.path.splitext(filename)[1][1:].lower() try: if fext == 'crl': - x509_item = x509.load_pem_x509_crl(open(filename, 'rb').read()) + x509_items = [x509.load_pem_x509_crl(open(filename, 'rb').read())] elif fext in ['pem', 'cer', 'crt']: - tmp = x509.load_pem_x509_certificates(open(filename, 'rb').read()) - # XXX: should be enabled after investigating the ca_root_nss situation - # if len(tmp) > 1: - # print('Skipping %s as it does not contain exactly one certificate' % filename) - # return None - x509_item = tmp[0] + x509_items = x509.load_pem_x509_certificates(open(filename, 'rb').read()) else: # not supported return None except (ValueError, TypeError): return None - tmp = OpenSSL.crypto.X509().get_issuer() - for item in x509_item.issuer: - setattr(tmp, item.rfc4514_attribute_name, item.value) + needs_copy = len(x509_items) > 1 + for x509_item in x509_items: + data = x509_item.public_bytes(serialization.Encoding.PEM) if needs_copy else filename + tmp = OpenSSL.crypto.X509().get_issuer() + for item in x509_item.issuer: + setattr(tmp, item.rfc4514_attribute_name, item.value) + hashval = hex(tmp.hash()).lstrip('0x').zfill(8) + yield { + 'hash': hashval, + 'target_pattern': '%s.%s%%d' % (hashval, 'r' if fext == 'crl' else ''), + 'type': 'copy' if needs_copy else 'link', + 'data': data, + 'filename': filename + } - return '%s.%s%%d' % (hex(tmp.hash()).lstrip('0x').zfill(8), 'r' if fext == 'crl' else '') def get_cert_common_name(filename): try: @@ -101,29 +107,40 @@ def cmd_rehash(): targetname = 'trusted' if path in TRUSTPATH else 'blacklisted' print("Scanning %s for certificates..." % path) for filename in glob.glob('%s/*' % path): - pattern = get_name_hash_file_pattern(filename) - if pattern: + for record in certificate_iterator(filename): + pattern = record['target_pattern'] if pattern not in targets[targetname]: targets[targetname][pattern] = [] - targets[targetname][pattern].append(filename) + if record['type'] == 'copy' and len(targets[targetname][pattern]) > 0: + # skip hardcopies when a link or hardcopy already exists + continue + targets[targetname][pattern].append(record) for path in [BLACKLISTDESTDIR, CERTDESTDIR]: for filename in glob.glob('%s/*.[0-9]' % path) + glob.glob('%s/*.r[0-9]' % path): if os.path.islink(filename): os.unlink(filename) + else: + os.remove(filename) for target_name in targets: for pattern in targets[target_name]: - for seq, filename in enumerate(targets[target_name][pattern]): - if target_name == 'blacklisted': - os.symlink(os.path.relpath(filename, BLACKLISTDESTDIR), "%s/%s" % (BLACKLISTDESTDIR, pattern % seq)) + for seq, record in enumerate(targets[target_name][pattern]): + is_bl = target_name == 'blacklisted' + src_filename = os.path.relpath(record['filename'], BLACKLISTDESTDIR if is_bl else CERTDESTDIR) + dst_filename = "%s/%s" % (BLACKLISTDESTDIR if is_bl else CERTDESTDIR, pattern % seq) + if not is_bl and hash in targets['blacklisted']: + print( + "Skipping blacklisted certificate %s (%s/%s)" % (filename, BLACKLISTDESTDIR, pattern % seq) + ) + continue + + if record['type'] == 'copy': + with open(dst_filename, 'wb') as f_out: + f_out.write(record['data']) + os.chmod(dst_filename, 0o644) else: - if hash in targets['blacklisted']: - print( - "Skipping blacklisted certificate %s (%s/%s)" % (filename, BLACKLISTDESTDIR, pattern % seq) - ) - else: - os.symlink(os.path.relpath(filename, CERTDESTDIR), "%s/%s" % (CERTDESTDIR, pattern % seq)) + os.symlink(src_filename, dst_filename) if __name__ == '__main__':