From 0284f31bb99bbe2fad9fda9cf99d73e4fa3d2dfd Mon Sep 17 00:00:00 2001 From: Nataliia Solomko Date: Wed, 13 May 2026 17:10:46 +0300 Subject: T8492: CRL generated by VyOS PKI lacks X.509 extensions required for strongSwan validation Previously generated CRLs were missing the Authority Key Identifier and CRL Number extensions required by strongSwan for certificate revocation validation. Without these extensions, strongSwan silently ignores the CRL, allowing revoked certificates to authenticate successfully. The migration regenerates existing CRLs for all CAs that have a private key available. CAs with passphrase-protected keys are skipped with a warning, as the passphrase cannot be provided non-interactively --- .../include/version/pki-version.xml.i | 3 + python/vyos/pki.py | 11 ++- src/migration-scripts/pki/0-to-1 | 109 +++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 interface-definitions/include/version/pki-version.xml.i create mode 100644 src/migration-scripts/pki/0-to-1 diff --git a/interface-definitions/include/version/pki-version.xml.i b/interface-definitions/include/version/pki-version.xml.i new file mode 100644 index 000000000..874561368 --- /dev/null +++ b/interface-definitions/include/version/pki-version.xml.i @@ -0,0 +1,3 @@ + + + diff --git a/python/vyos/pki.py b/python/vyos/pki.py index 4598c5daa..17fa97223 100644 --- a/python/vyos/pki.py +++ b/python/vyos/pki.py @@ -207,7 +207,16 @@ def create_certificate_revocation_list(ca_cert, ca_private_key, serial_numbers=[ builder = x509.CertificateRevocationListBuilder() \ .issuer_name(ca_cert.subject) \ .last_update(datetime.datetime.today()) \ - .next_update(datetime.datetime.today() + datetime.timedelta(1, 0, 0)) + .next_update(datetime.datetime.today() + datetime.timedelta(1, 0, 0)) \ + .add_extension(add_key_identifier(ca_cert), critical=False) \ + .add_extension( + x509.CRLNumber( + int( + datetime.datetime.now(datetime.timezone.utc).timestamp() * 1_000_000 + ) + ), + critical=False, + ) for serial_number in serial_numbers: revoked_cert = x509.RevokedCertificateBuilder() \ diff --git a/src/migration-scripts/pki/0-to-1 b/src/migration-scripts/pki/0-to-1 new file mode 100644 index 000000000..9b73a7a66 --- /dev/null +++ b/src/migration-scripts/pki/0-to-1 @@ -0,0 +1,109 @@ +# Copyright (C) VyOS Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . + +# T8492: Regenerate CRLs to add required X.509 extensions (Authority Key +# Identifier and CRL Number) that were missing from previously generated +# CRLs. Without these extensions strongSwan silently ignores the CRL, +# allowing revoked certificates to authenticate. + +from cryptography import x509 + +from vyos.base import Warning +from vyos.configtree import ConfigTree +from vyos.pki import create_certificate_revocation_list +from vyos.pki import encode_certificate +from vyos.pki import load_certificate +from vyos.pki import load_crl +from vyos.pki import load_private_key + +base = ['pki'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base + ['ca']): + return + + for ca_name in config.list_nodes(base + ['ca']): + ca_base = base + ['ca', ca_name] + + if not config.exists(ca_base + ['crl']): + continue + if not config.exists(ca_base + ['private', 'key']): + continue + if not config.exists(ca_base + ['certificate']): + continue + + ca_cert_raw = config.return_value(ca_base + ['certificate']) + ca_key_raw = config.return_value(ca_base + ['private', 'key']) + + ca_cert = load_certificate(ca_cert_raw) + ca_key = load_private_key(ca_key_raw) + + if not ca_cert: + continue + if not ca_key: + Warning( + f'PKI CA "{ca_name}" has a passphrase-protected private key — ' + f'CRL regeneration skipped. To update the CRL manually, run: ' + f'"generate pki crl {ca_name} install"' + ) + continue + + # Check all existing CRLs — if every one already has Authority Key + # Identifier, no migration is needed for this CA. + existing_crls_raw = config.return_values(ca_base + ['crl']) + needs_update = False + for crl_raw in existing_crls_raw: + existing_crl = load_crl(crl_raw) + if not existing_crl: + needs_update = True # corrupt/unreadable CRL — regenerate + break + try: + existing_crl.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) + existing_crl.extensions.get_extension_for_class(x509.CRLNumber) + except x509.extensions.ExtensionNotFound: + needs_update = True # required extension missing — regenerate + break + + if not needs_update: + continue # all CRLs already have AKI + + # Collect serial numbers of all revoked certificates and CAs under + # this CA — mirrors the logic in op_mode get_config_revoked_certificates() + to_revoke = [] + for section in ['certificate', 'ca']: + if not config.exists(base + [section]): + continue + for entry_name in config.list_nodes(base + [section]): + entry_base = base + [section, entry_name] + if not config.exists(entry_base + ['revoke']): + continue + if not config.exists(entry_base + ['certificate']): + continue + cert_raw = config.return_value(entry_base + ['certificate']) + cert = load_certificate(cert_raw) + if cert and cert.issuer == ca_cert.subject: + to_revoke.append(cert.serial_number) + + if not to_revoke: + continue + + new_crl = create_certificate_revocation_list(ca_cert, ca_key, to_revoke) + if not new_crl: + continue + + new_crl_pem = encode_certificate(new_crl) + new_crl_b64 = ''.join(new_crl_pem.strip().split('\n')[1:-1]) + + config.set(ca_base + ['crl'], value=new_crl_b64, replace=True) -- cgit v1.2.3