diff options
| author | Nataliia Solomko <natalirs1985@gmail.com> | 2026-05-13 17:10:46 +0300 |
|---|---|---|
| committer | Nataliia Solomko <natalirs1985@gmail.com> | 2026-05-13 19:52:04 +0300 |
| commit | 0284f31bb99bbe2fad9fda9cf99d73e4fa3d2dfd (patch) | |
| tree | dfffa8e236e36681903bf0402b8d867b1eed99ec | |
| parent | 1dd997d724449e941c6bf42be98529372d9f1530 (diff) | |
| download | vyos-1x-0284f31bb99bbe2fad9fda9cf99d73e4fa3d2dfd.tar.gz vyos-1x-0284f31bb99bbe2fad9fda9cf99d73e4fa3d2dfd.zip | |
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
| -rw-r--r-- | interface-definitions/include/version/pki-version.xml.i | 3 | ||||
| -rw-r--r-- | python/vyos/pki.py | 11 | ||||
| -rw-r--r-- | src/migration-scripts/pki/0-to-1 | 109 |
3 files changed, 122 insertions, 1 deletions
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 @@ +<!-- include start from include/version/pki-version.xml.i --> +<syntaxVersion component='pki' version='1'></syntaxVersion> +<!-- include end --> 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 <http://www.gnu.org/licenses/>. + +# 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) |
