diff options
| -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) |
