From 86579277c1f995664f96428a69dc157428552c21 Mon Sep 17 00:00:00 2001 From: "Endi S. Dewata" Date: Thu, 16 Nov 2023 14:39:00 -0600 Subject: [PATCH] Restore support for cert bundle in pki_cert_chain_path The NSSDatabase.import_cert_chain() has been modified to support importing cert bundle by converting it into a PKCS #7 file, then import it using the existing code. The test for installing KRA on a separate instance has been modified to use a root CA and a sub CA, then use a cert bundle in the pki_cert_chain_path param. The NSSCertImportCLI has been updated to avoid an NPE if the nickname is not specified. https://bugzilla.redhat.com/show_bug.cgi?id=2250162 --- .github/workflows/kra-separate-test.yml | 199 +++++++++++++----- base/common/python/pki/nssdb.py | 127 +++++++++-- .../cmstools/nss/NSSCertImportCLI.java | 16 +- 3 files changed, 264 insertions(+), 78 deletions(-) diff --git a/.github/workflows/kra-separate-test.yml b/.github/workflows/kra-separate-test.yml index 9538fd7af1e..dabffe07222 100644 --- a/.github/workflows/kra-separate-test.yml +++ b/.github/workflows/kra-separate-test.yml @@ -28,38 +28,122 @@ jobs: - name: Create network run: docker network create example - - name: Set up CA DS container + - name: Set up root CA DS container run: | - tests/bin/ds-container-create.sh cads + tests/bin/ds-container-create.sh rootcads env: IMAGE: ${{ env.DB_IMAGE }} - HOSTNAME: cads.example.com + HOSTNAME: rootcads.example.com PASSWORD: Secret.123 - - name: Connect CA DS container to network - run: docker network connect example cads --alias cads.example.com + - name: Connect root CA DS container to network + run: docker network connect example rootcads --alias rootcads.example.com - - name: Set up CA container + - name: Set up root CA container run: | - tests/bin/runner-init.sh ca + tests/bin/runner-init.sh rootca env: - HOSTNAME: ca.example.com + HOSTNAME: rootca.example.com - - name: Connect CA container to network - run: docker network connect example ca --alias ca.example.com + - name: Connect root CA container to network + run: docker network connect example rootca --alias rootca.example.com - - name: Install CA in CA container + - name: Install root CA run: | - docker exec ca pkispawn \ + docker exec rootca pkispawn \ -f /usr/share/pki/server/examples/installation/ca.cfg \ -s CA \ - -D pki_ds_url=ldap://cads.example.com:3389 \ + -D pki_ds_url=ldap://rootcads.example.com:3389 \ -v - docker exec ca pki-server cert-find + - name: Check root CA certs + if: always() + run: | + docker exec rootca pki -d /etc/pki/pki-tomcat/alias nss-cert-find + + docker exec rootca pki-server cert-export \ + --cert-file ${SHARED}/root-ca_signing.crt \ + ca_signing + + - name: Check root CA users + if: always() + run: | + docker exec rootca pki-server ca-user-find + docker exec rootca pki-server ca-user-show caadmin + docker exec rootca pki-server ca-user-role-find caadmin + + - name: Set up sub CA DS container + run: | + tests/bin/ds-container-create.sh subcads + env: + IMAGE: ${{ env.DB_IMAGE }} + HOSTNAME: subcads.example.com + PASSWORD: Secret.123 - - name: Install banner in CA container - run: docker exec ca cp /usr/share/pki/server/examples/banner/banner.txt /etc/pki/pki-tomcat + - name: Connect sub CA DS container to network + run: docker network connect example subcads --alias subcads.example.com + + - name: Set up sub CA container + run: | + tests/bin/runner-init.sh subca + env: + HOSTNAME: subca.example.com + + - name: Connect sub CA container to network + run: docker network connect example subca --alias subca.example.com + + - name: Install sub CA + run: | + docker exec subca pkispawn \ + -f /usr/share/pki/server/examples/installation/subca.cfg \ + -s CA \ + -D pki_cert_chain_path=${SHARED}/root-ca_signing.crt \ + -D pki_ds_url=ldap://subcads.example.com:3389 \ + -D pki_security_domain_uri=https://rootca.example.com:8443 \ + -D pki_subordinate_create_new_security_domain=True \ + -D pki_issuing_ca_uri=https://rootca.example.com:8443 \ + -v + + - name: Check sub CA certs + if: always() + run: | + docker exec subca pki -d /etc/pki/pki-tomcat/alias nss-cert-find + + docker exec subca pki-server cert-export \ + --cert-file ${SHARED}/ca_signing.crt \ + ca_signing + + - name: Check sub CA users + if: always() + run: | + docker exec subca pki-server ca-user-find + docker exec subca pki-server ca-user-show caadmin + docker exec subca pki-server ca-user-role-find caadmin + + - name: Export subordinate CA cert bundle + run: | + cat root-ca_signing.crt > cert_chain.crt + cat ca_signing.crt >> cert_chain.crt + + cat cert_chain.crt + + - name: Install banner in sub CA container + run: docker exec subca cp /usr/share/pki/server/examples/banner/banner.txt /etc/pki/pki-tomcat + + - name: Verify sub CA admin + run: | + docker exec subca pki nss-cert-import \ + --cert ${SHARED}/root-ca_signing.crt \ + --trust CT,C,C + docker exec subca pki nss-cert-import \ + --cert ${SHARED}/ca_signing.crt \ + --trust CT,C,C + + docker exec subca pki pkcs12-import \ + --pkcs12 /root/.dogtag/pki-tomcat/ca_admin_cert.p12 \ + --pkcs12-password Secret.123 + + docker exec subca pki -n caadmin --ignore-banner ca-user-show caadmin - name: Set up KRA DS container run: | @@ -81,21 +165,34 @@ jobs: - name: Connect KRA container to network run: docker network connect example kra --alias kra.example.com - - name: Install KRA in KRA container + - name: Install KRA run: | - docker exec ca pki-server cert-export ca_signing --cert-file ${SHARED}/ca_signing.crt - docker exec ca cp /root/.dogtag/pki-tomcat/ca_admin.cert ${SHARED}/ca_admin.cert + docker exec subca pki-server cert-export \ + --cert-file ${SHARED}/ca_signing.crt \ + ca_signing + docker exec subca cp /root/.dogtag/pki-tomcat/ca_admin.cert ${SHARED}/ca_admin.cert docker exec kra pkispawn \ -f /usr/share/pki/server/examples/installation/kra.cfg \ -s KRA \ - -D pki_security_domain_hostname=ca.example.com \ + -D pki_security_domain_uri=https://subca.example.com:8443 \ + -D pki_issuing_ca_uri=https://subca.example.com:8443 \ -D pki_cert_chain_nickname=ca_signing \ - -D pki_cert_chain_path=${SHARED}/ca_signing.crt \ + -D pki_cert_chain_path=${SHARED}/cert_chain.crt \ -D pki_admin_cert_file=${SHARED}/ca_admin.cert \ -D pki_ds_url=ldap://krads.example.com:3389 \ -v - docker exec kra pki-server cert-find + - name: Check KRA certs + if: always() + run: | + docker exec kra pki -d /etc/pki/pki-tomcat/alias nss-cert-find + + - name: Check KRA users + if: always() + run: | + docker exec kra pki-server kra-user-find + docker exec kra pki-server kra-user-show kraadmin + docker exec kra pki-server kra-user-role-find kraadmin - name: Install banner in KRA container run: docker exec kra cp /usr/share/pki/server/examples/banner/banner.txt /etc/pki/pki-tomcat @@ -106,57 +203,55 @@ jobs: - name: Verify KRA admin run: | - docker exec ca cp /root/.dogtag/pki-tomcat/ca_admin_cert.p12 ${SHARED}/ca_admin_cert.p12 - docker exec kra pki client-cert-import ca_signing --ca-cert ${SHARED}/ca_signing.crt + docker exec kra pki nss-cert-import \ + --cert ${SHARED}/root-ca_signing.crt \ + --trust CT,C,C + docker exec kra pki nss-cert-import \ + --cert ${SHARED}/ca_signing.crt \ + --trust CT,C,C + + docker exec subca cp /root/.dogtag/pki-tomcat/ca_admin_cert.p12 ${SHARED}/ca_admin_cert.p12 docker exec kra pki pkcs12-import \ --pkcs12 ${SHARED}/ca_admin_cert.p12 \ --pkcs12-password Secret.123 docker exec kra pki -n caadmin --ignore-banner kra-user-show kraadmin - - name: Verify KRA connector in CA + - name: Verify KRA connector in sub CA run: | - docker exec ca pki client-cert-import ca_signing --ca-cert ${SHARED}/ca_signing.crt - docker exec ca pki pkcs12-import \ - --pkcs12 /root/.dogtag/pki-tomcat/ca_admin_cert.p12 \ - --pkcs12-password Secret.123 - - docker exec ca pki -n caadmin --ignore-banner ca-kraconnector-show | tee output + docker exec subca pki -n caadmin --ignore-banner ca-kraconnector-show | tee output sed -n 's/\s*Host:\s\+\(\S\+\):.*/\1/p' output > actual echo kra.example.com > expected diff expected actual - - name: Gather artifacts from CA containers + - name: Gather artifacts if: always() run: | - tests/bin/ds-artifacts-save.sh --output=/tmp/artifacts/ca cads - tests/bin/pki-artifacts-save.sh ca - continue-on-error: true - - - name: Gather artifacts from KRA containers - if: always() - run: | - tests/bin/ds-artifacts-save.sh --output=/tmp/artifacts/kra krads + tests/bin/ds-artifacts-save.sh rootcads + tests/bin/pki-artifacts-save.sh rootca + tests/bin/ds-artifacts-save.sh subcads + tests/bin/pki-artifacts-save.sh subca + tests/bin/ds-artifacts-save.sh krads tests/bin/pki-artifacts-save.sh kra continue-on-error: true - - name: Remove KRA from KRA container + - name: Remove KRA run: docker exec kra pkidestroy -i pki-tomcat -s KRA -v - - name: Remove CA from CA container - run: docker exec ca pkidestroy -i pki-tomcat -s CA -v + - name: Remove sub CA + run: docker exec subca pkidestroy -i pki-tomcat -s CA -v - - name: Upload artifacts from CA containers - if: always() - uses: actions/upload-artifact@v3 - with: - name: kra-separate-ca - path: | - /tmp/artifacts/ca + - name: Remove root CA + run: docker exec rootca pkidestroy -i pki-tomcat -s CA -v - - name: Upload artifacts from KRA containers + - name: Upload artifacts if: always() uses: actions/upload-artifact@v3 with: - name: kra-separate-kra + name: kra-separate path: | + /tmp/artifacts/rootca + /tmp/artifacts/rootcads + /tmp/artifacts/subca + /tmp/artifacts/subcads /tmp/artifacts/kra + /tmp/artifacts/krads diff --git a/base/common/python/pki/nssdb.py b/base/common/python/pki/nssdb.py index 8abdc81dcac..f3ea58e030d 100644 --- a/base/common/python/pki/nssdb.py +++ b/base/common/python/pki/nssdb.py @@ -47,6 +47,9 @@ import pki +PRIVATE_KEY_HEADER = '-----BEGIN PRIVATE KEY-----' +PRIVATE_KEY_FOOTER = '-----END PRIVATE KEY-----' + CSR_HEADER = '-----BEGIN CERTIFICATE REQUEST-----' CSR_FOOTER = '-----END CERTIFICATE REQUEST-----' @@ -59,6 +62,22 @@ PKCS7_HEADER = '-----BEGIN PKCS7-----' PKCS7_FOOTER = '-----END PKCS7-----' +PEM_HEADERS = [ + PRIVATE_KEY_HEADER, + CSR_HEADER, + LEGACY_CSR_HEADER, + CERT_HEADER, + PKCS7_HEADER +] + +PEM_FOOTERS = [ + PRIVATE_KEY_FOOTER, + CSR_FOOTER, + LEGACY_CSR_FOOTER, + CERT_FOOTER, + PKCS7_FOOTER +] + INTERNAL_TOKEN_NAME = 'internal' INTERNAL_TOKEN_FULL_NAME = 'Internal Key Storage Token' @@ -137,27 +156,55 @@ def convert_pkcs7(pkcs7_data, input_format, output_format): PKCS7_HEADER, PKCS7_FOOTER) -def get_file_type(filename): +def get_pem_type(data): ''' - This method detects the content of a PEM file. It supports - CSR, certificate, PKCS #7 certificate chain. + Get PEM data type. ''' - with open(filename, 'r', encoding='utf-8') as f: - data = f.read() + if data.startswith(PRIVATE_KEY_HEADER): + return 'PRIVATE KEY' if data.startswith(CSR_HEADER) or data.startswith(LEGACY_CSR_HEADER): - return 'csr' + return 'CERTIFICATE REQUEST' if data.startswith(CERT_HEADER): - return 'cert' + return 'CERTIFICATE' if data.startswith(PKCS7_HEADER): - return 'pkcs7' + return 'PKCS7' return None +def split_pem_data(data): + ''' + Split PEM data which might contain multiple parts: + - private keys + - CSRs + - certs + - PKCS #7 cert chain + + If the input contains Base64 data without headers/footers, + it will be returned as is. + ''' + + parts = [] + part = '' + + for line in data.splitlines(): + + part += line + '\n' + + if line in PEM_FOOTERS: + parts.append(part) + part = '' + + if part: + parts.append(part) + + return parts + + def internal_token(token): ''' Check whether the token is an empty string, 'internal', or @@ -2209,6 +2256,40 @@ def remove_cert( finally: shutil.rmtree(tmpdir) + def __convert_certs_into_pkcs7(self, certs, pkcs7_file): + + tmpdir = self.create_tmpdir() + try: + for cert in certs: + + logger.info('Importing cert into PKCS #7:\n%s', cert) + + # store each cert into a file + cert_pem = os.path.join(tmpdir, 'cert.pem') + with open(cert_pem, 'w', encoding='utf-8') as f: + f.write(cert) + + # import cert into PKCS #7 file + cmd = [ + 'pki', + '-d', self.directory, + 'pkcs7-cert-import', + '--pkcs7', pkcs7_file, + '--input-file', cert_pem, + '--append', + ] + + if logger.isEnabledFor(logging.DEBUG): + cmd.append('--debug') + + elif logger.isEnabledFor(logging.INFO): + cmd.append('--verbose') + + self.run(cmd, check=True) + + finally: + shutil.rmtree(tmpdir) + def import_cert_chain( self, nickname, @@ -2218,16 +2299,29 @@ def import_cert_chain( logger.debug('NSSDatabase.import_cert_chain(%s) begins', nickname) + with open(cert_chain_file, 'r', encoding='utf-8') as f: + data = f.read() + + pem_parts = split_pem_data(data) + tmpdir = self.create_tmpdir() try: - file_type = get_file_type(cert_chain_file) + if len(pem_parts) > 1: # cert bundle + logger.debug('Converting cert bundle into PKCS #7') + input_type = 'PKCS7' + input_file = os.path.join(tmpdir, 'cert_chain.p7b') + self.__convert_certs_into_pkcs7(pem_parts, input_file) - if file_type == 'cert': # import single PEM cert + else: # single cert, PKCS #7, or Base64 data + input_type = get_pem_type(pem_parts[0]) + input_file = cert_chain_file + + if input_type == 'CERTIFICATE': # import single PEM cert logger.debug('Importing a single cert') self.add_cert( nickname=nickname, - cert_file=cert_chain_file, + cert_file=input_file, token=token, trust_attributes=trust_attributes) return ( @@ -2238,15 +2332,15 @@ def import_cert_chain( [nickname] ) - elif file_type == 'pkcs7': # import PKCS #7 cert chain + elif input_type == 'PKCS7': # import PKCS #7 cert chain logger.debug('Importing a PKCS #7 cert chain') self.import_pkcs7( - pkcs7_file=cert_chain_file, + pkcs7_file=input_file, nickname=nickname, token=token, trust_attributes=trust_attributes) - with open(cert_chain_file, 'r', encoding='utf-8') as f: + with open(input_file, 'r', encoding='utf-8') as f: pkcs7_data = f.read() base64_data = convert_pkcs7(pkcs7_data, 'pem', 'base64') @@ -2255,14 +2349,11 @@ def import_cert_chain( else: # import PKCS #7 data without header/footer logger.debug('Importing a PKCS #7 data without header/footer') - with open(cert_chain_file, 'r', encoding='utf-8') as f: - base64_data = f.read() - # TODO: fix ipaserver/install/cainstance.py in IPA # to no longer remove PKCS #7 header/footer # join base-64 data into a single line - base64_data = base64_data.replace('\r', '').replace('\n', '') + base64_data = data.replace('\r', '').replace('\n', '') pkcs7_data = convert_pkcs7(base64_data, 'base64', 'pem') diff --git a/base/tools/src/main/java/com/netscape/cmstools/nss/NSSCertImportCLI.java b/base/tools/src/main/java/com/netscape/cmstools/nss/NSSCertImportCLI.java index 42533fbf608..017ab07a797 100644 --- a/base/tools/src/main/java/com/netscape/cmstools/nss/NSSCertImportCLI.java +++ b/base/tools/src/main/java/com/netscape/cmstools/nss/NSSCertImportCLI.java @@ -94,6 +94,13 @@ public void execute(CommandLine cmd) throws Exception { ClientConfig clientConfig = mainCLI.getConfig(); + NSSDatabase nssdb = mainCLI.getNSSDatabase(); + + if (nickname == null) { + nssdb.addCertificate(cert, trustFlags); + return; + } + String tokenName = null; int i = nickname.indexOf(':'); @@ -106,13 +113,6 @@ public void execute(CommandLine cmd) throws Exception { nickname = nickname.substring(i + 1); } - NSSDatabase nssdb = mainCLI.getNSSDatabase(); - - if (nickname == null) { - nssdb.addCertificate(cert, trustFlags); - - } else { - nssdb.addCertificate(tokenName, nickname, cert, trustFlags); - } + nssdb.addCertificate(tokenName, nickname, cert, trustFlags); } }