From 195a1feec400e7159290479c23c0176774a9eccf Mon Sep 17 00:00:00 2001
From: Christian Breunig <christian@breunig.cc>
Date: Sun, 6 Oct 2024 20:13:56 +0200
Subject: pki: T6481: auto import ACME certificate chain into CLI

When using an ACME based certificate with VyOS we provide the necessary PEM
files opaque in the background when using the internal tools. This however will
not properly work with the CA chain portion, as the system is based on the
"pki certificate <name> acme" CLI node of a certificate but CA chains reside
under "pki ca".

This adds support for importing the PEM data of a CA chain issued via ACME into
the "pki ca AUTOCHAIN_<name> certificate" subsystem so it can be queried by
other daemons. Importing the chain only happens, when the chain was not already
added manually by the user.

ACME certificate chains that are automatically added to the CLI are all prefixed
using AUTOCHAIN_certname so they can be consumed by any daemon. This also adds
a safeguard when the intermediate CA changes, the referenced name on the CLI
stays consitent for any pending daemon updates.

(cherry picked from commit 875764b07f937fc599e2e62c667e7b811ddc2ed3)
---
 src/conf_mode/pki.py | 33 ++++++++++++++++++++++++++++++++-
 src/op_mode/pki.py   | 17 +++++++++++++----
 2 files changed, 45 insertions(+), 5 deletions(-)

diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index 4a0e86f32..f08858687 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -27,6 +27,7 @@ from vyos.configdict import node_changed
 from vyos.configdiff import Diff
 from vyos.configdiff import get_config_diff
 from vyos.defaults import directories
+from vyos.pki import encode_certificate
 from vyos.pki import is_ca_certificate
 from vyos.pki import load_certificate
 from vyos.pki import load_public_key
@@ -36,9 +37,11 @@ from vyos.pki import load_private_key
 from vyos.pki import load_crl
 from vyos.pki import load_dh_parameters
 from vyos.utils.boot import boot_configuration_complete
+from vyos.utils.configfs import add_cli_node
 from vyos.utils.dict import dict_search
 from vyos.utils.dict import dict_search_args
 from vyos.utils.dict import dict_search_recursive
+from vyos.utils.file import read_file
 from vyos.utils.process import call
 from vyos.utils.process import cmd
 from vyos.utils.process import is_systemd_service_active
@@ -442,9 +445,37 @@ def generate(pki):
     # Get foldernames under vyos_certbot_dir which each represent a certbot cert
     if os.path.exists(f'{vyos_certbot_dir}/live'):
         for cert in certbot_list_on_disk:
+            # ACME certificate is no longer in use by CLI remove it
             if cert not in certbot_list:
-                # certificate is no longer active on the CLI - remove it
                 certbot_delete(cert)
+                continue
+            # ACME not enabled for individual certificate - bail out early
+            if 'acme' not in pki['certificate'][cert]:
+                continue
+
+            # Read in ACME certificate chain information
+            tmp = read_file(f'{vyos_certbot_dir}/live/{cert}/chain.pem')
+            tmp = load_certificate(tmp, wrap_tags=False)
+            cert_chain_base64 = "".join(encode_certificate(tmp).strip().split("\n")[1:-1])
+
+            # Check if CA chain certificate is already present on CLI to avoid adding
+            # a duplicate. This only checks for manual added CA certificates and not
+            # auto added ones with the AUTOCHAIN_ prefix
+            autochain_prefix = 'AUTOCHAIN_'
+            ca_cert_present = False
+            if 'ca' in pki:
+                for ca_base64, cli_path in dict_search_recursive(pki['ca'], 'certificate'):
+                    # Ignore automatic added CA certificates
+                    if any(item.startswith(autochain_prefix) for item in cli_path):
+                        continue
+                    if cert_chain_base64 == ca_base64:
+                        ca_cert_present = True
+
+            if not ca_cert_present:
+                tmp = dict_search_args(pki, 'ca', f'{autochain_prefix}{cert}', 'certificate')
+                if not bool(tmp) or tmp != cert_chain_base64:
+                    print(f'Adding/replacing automatically imported CA certificate for "{cert}" ...')
+                    add_cli_node(['pki', 'ca', f'{autochain_prefix}{cert}', 'certificate'], value=cert_chain_base64)
 
     return None
 
diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py
index 615a458c9..1b0f61c22 100755
--- a/src/op_mode/pki.py
+++ b/src/op_mode/pki.py
@@ -26,13 +26,22 @@ from cryptography.x509.oid import ExtendedKeyUsageOID
 
 from vyos.config import Config
 from vyos.config import config_dict_mangle_acme
-from vyos.pki import encode_certificate, encode_public_key, encode_private_key, encode_dh_parameters
+from vyos.pki import encode_certificate
+from vyos.pki import encode_public_key
+from vyos.pki import encode_private_key
+from vyos.pki import encode_dh_parameters
 from vyos.pki import get_certificate_fingerprint
-from vyos.pki import create_certificate, create_certificate_request, create_certificate_revocation_list
+from vyos.pki import create_certificate
+from vyos.pki import create_certificate_request
+from vyos.pki import create_certificate_revocation_list
 from vyos.pki import create_private_key
 from vyos.pki import create_dh_parameters
-from vyos.pki import load_certificate, load_certificate_request, load_private_key
-from vyos.pki import load_crl, load_dh_parameters, load_public_key
+from vyos.pki import load_certificate
+from vyos.pki import load_certificate_request
+from vyos.pki import load_private_key
+from vyos.pki import load_crl
+from vyos.pki import load_dh_parameters
+from vyos.pki import load_public_key
 from vyos.pki import verify_certificate
 from vyos.utils.io import ask_input
 from vyos.utils.io import ask_yes_no
-- 
cgit v1.2.3