From 5c459c1861e4132904fe88e99341738f1c3555e8 Mon Sep 17 00:00:00 2001 From: Fraser Tweedale Date: Thu, 11 Jan 2018 19:02:09 +1100 Subject: [PATCH 1/2] install: support adding Subject Key ID to CSR For externally-signed CA installation, some users want to be able to generate a CSR with a Subject Key Identifier extension - either user-specified or a generated default. This commit adds support to NSSDatabase.create_request for generating a CSR with an SKI extension. The process to achieve this is: 1. Generate the key. This behaviour has been extracted to a separate method (NSSDatabase.generate_key). 2. If a "default" SKI is requested, generate a throw-away CSR and compute an SKI value from the public key contained therein. This is a "minimal" CSR whose only purpose is to get the public key in a convenient format. 3. Generate the CSR and write it to the caller-specified file. This CSR contains all the extensions the caller asked for. This commit relies on an enhancement to the certutil(1) program that allows create a CSR for a private key specified by CKA_ID. Part-of: https://pagure.io/dogtagpki/issue/2854 (cherry picked from commit f1f32c31d51dffb93e7874d8c4dd0325136c4db7) --- base/common/python/pki/nssdb.py | 177 ++++++++++++++++++++++++++++++++++------ 1 file changed, 152 insertions(+), 25 deletions(-) diff --git a/base/common/python/pki/nssdb.py b/base/common/python/pki/nssdb.py index bbcb261..11509f0 100644 --- a/base/common/python/pki/nssdb.py +++ b/base/common/python/pki/nssdb.py @@ -22,14 +22,18 @@ from __future__ import absolute_import import base64 +import binascii import logging import os +import re import shutil import stat import subprocess import tempfile import datetime +import six + from cryptography import x509 from cryptography.hazmat.backends import default_backend @@ -444,20 +448,78 @@ class NSSDatabase(object): basic_constraints_ext=None, key_usage_ext=None, extended_key_usage_ext=None, + cka_id=None, + subject_key_id=None, generic_exts=None): + """ + Generate a CSR. + + ``cka_id`` + PKCS #11 CKA_ID of key in the NSSDB to use, as text. + If ``None`` a new key will be generated (this is + the typical use case). + + ``subject_key_id`` + If ``None``, no Subject Key ID will be included in the + request. If ``"DEFAULT"``, the Subject Key ID will be + derived from the generated key, using the default + digest. Otherwise the value must be a hex-encoded + string, without leading ``0x``, containing the desired + Subject Key ID. + + ``generic_exts`` + List of generic extensions, each being a mapping with + the following keys: + + ``oid`` + Extension OID (``str``) + ``critical`` + ``bool`` + ``data`` + Raw extension data (``bytes``) + + """ + if not cka_id: + cka_id = self.generate_key( + key_type=key_type, key_size=key_size, + curve=curve, noise_file=noise_file) + if not isinstance(cka_id, six.text_type): + raise TypeError('cka_id must be a text string') tmpdir = tempfile.mkdtemp() try: - if not noise_file: - noise_file = os.path.join(tmpdir, 'noise.bin') - if key_size: - size = key_size + if subject_key_id is not None: + if subject_key_id == 'DEFAULT': + # Caller wants a default subject key ID included + # in CSR. To do this we must first generate a + # temporary CSR for the key, then compute an SKI + # from the public key data. + tmp_csr = os.path.join(tmpdir, 'tmp_csr.pem') + self.create_request( + subject_dn, tmp_csr, + cka_id=cka_id, subject_key_id=None) + with open(tmp_csr, 'rb') as f: + data = f.read() + csr = x509.load_pem_x509_csr(data, default_backend()) + pub = csr.public_key() + ski = x509.SubjectKeyIdentifier.from_public_key(pub) + ski_bytes = ski.digest else: - size = 2048 - self.create_noise( - noise_file=noise_file, - size=size) + # Explicit subject_key_id provided; decode it + ski_bytes = binascii.unhexlify(subject_key_id) + + if generic_exts is None: + generic_exts = [] + generic_exts.append({ + 'oid': x509.SubjectKeyIdentifier.oid.dotted_string, + 'critical': False, + 'data': bytearray([0x04, len(ski_bytes)]) + ski_bytes, + # OCTET STRING ^tag ^length ^data + # + # This structure is incorrect if len > 127 bytes, but this + # will be fine for a CKA_ID or SKID of sensible length. + }) binary_request_file = os.path.join(tmpdir, 'request.bin') @@ -478,25 +540,9 @@ class NSSDatabase(object): '-f', self.password_file, '-s', subject_dn, '-o', binary_request_file, - '-z', noise_file + '-k', cka_id, ]) - if key_type: - cmd.extend(['-k', key_type]) - - if key_type.lower() == 'ec': - # This is fix for Bugzilla 1544843 - cmd.extend([ - '--keyOpFlagsOn', 'sign', - '--keyOpFlagsOff', 'derive' - ]) - - if key_size: - cmd.extend(['-g', str(key_size)]) - - if curve: - cmd.extend(['-q', curve]) - if hash_alg: cmd.extend(['-Z', hash_alg]) @@ -603,6 +649,87 @@ class NSSDatabase(object): finally: shutil.rmtree(tmpdir) + def generate_key( + self, + key_type=None, key_size=None, curve=None, + noise_file=None): + """ + Generate a key of the given type and size. + Returns the CKA_ID of the generated key, as a text string. + + ``noise_file`` + Path to a noise file, or ``None`` to automatically + generate a noise file. + + """ + ids_pre = set(self.list_private_keys()) + + cmd = [ + 'certutil', + '-d', self.directory, + '-f', self.password_file, + '-G', + ] + if self.token: + cmd.extend(['-h', self.token]) + if key_type: + cmd.extend(['-k', key_type]) + if key_type.lower() == 'ec': + # This is fix for Bugzilla 1544843 + cmd.extend([ + '--keyOpFlagsOn', 'sign', + '--keyOpFlagsOff', 'derive', + ]) + if key_size: + cmd.extend(['-g', str(key_size)]) + if curve: + cmd.extend(['-q', curve]) + + temp_noise_file = noise_file is None + if temp_noise_file: + fd, noise_file = tempfile.mkstemp() + os.close(fd) + size = key_size if key_size else 2048 + self.create_noise(noise_file=noise_file, size=size) + cmd.extend(['-z', noise_file]) + + try: + subprocess.check_call(cmd) + finally: + if temp_noise_file: + os.unlink(noise_file) + + ids_post = set(self.list_private_keys()) + return list(ids_post - ids_pre)[0].decode('ascii') + + def list_private_keys(self): + """ + Return list of hex-encoded private key CKA_IDs in the token. + + """ + cmd = [ + 'certutil', + '-d', self.directory, + '-f', self.password_file, + '-K', + ] + if self.token: + cmd.extend(['-h', self.token]) + try: + out = subprocess.check_output(cmd) + except subprocess.CalledProcessError as e: + if e.returncode == 255: + return [] # no keys were found + else: + raise e # other error; re-raise + + # output contains list that looks like: + # < 0> rsa b995381610fb58e8b45d3c2401dfd30d6efdd595 (orphan) + # < 1> rsa dcd6cbc1226ede02a961488553b01639ff981cdd someNickame + # + # The hex string is the hex-encoded CKA_ID + return re.findall(br'^<\s*\d+>\s+\w+\s+(\w+)', out, re.MULTILINE) + def create_cert(self, request_file, cert_file, serial, issuer=None, key_usage_ext=None, basic_constraints_ext=None, aki_ext=None, ski_ext=None, aia_ext=None, -- 1.8.3.1 From 8f638afeec1527d581a8dd9eefc84cde69c1c6b6 Mon Sep 17 00:00:00 2001 From: Fraser Tweedale Date: Thu, 11 Jan 2018 19:46:40 +1100 Subject: [PATCH 2/2] install: add pkispawn option for adding SKI to CSR For externally-signed CA installation, some users want to be able to generate a CSR with a Subject Key Identifier extension - either user-specified or a generated default. This commit adds the 'pki_req_ski' pkispwan option for specifying that the CSR should bear the SKI extension. It can either be a hex-encoded SKI value or the string "DEFAULT" which asks that the value be derived from the public key. Update the pki_default.cfg.5 man page to document the new option. Fixes: https://pagure.io/dogtagpki/issue/2854 (cherry picked from commit 4f9327b85eab58463adcece81269b823e9def2b4) --- base/server/man/man5/pki_default.cfg.5 | 6 ++++++ base/server/python/pki/server/deployment/pkihelper.py | 2 ++ .../server/python/pki/server/deployment/scriptlets/configuration.py | 6 +++++- .../python/pki/server/deployment/scriptlets/security_databases.py | 4 ++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/base/server/man/man5/pki_default.cfg.5 b/base/server/man/man5/pki_default.cfg.5 index afdcbfb..4d83fcc 100644 --- a/base/server/man/man5/pki_default.cfg.5 +++ b/base/server/man/man5/pki_default.cfg.5 @@ -352,6 +352,12 @@ Sets whether the new CA will have a signing certificate that will be issued by a .IP Required in the first step of the external CA signing process. The CSR will be printed to the screen and stored in this location. .PP +.B pki_req_ski +.IP +Include a Subject Key Identifier extension in the CSR. The value is either a +hex-encoded byte string (\fBwithout\fR leading "0x"), or the string "DEFAULT" +which will derive a value from the public key. +.PP .B pki_external_step_two .IP Specifies that this is the second step of the external CA process. Defaults to False. diff --git a/base/server/python/pki/server/deployment/pkihelper.py b/base/server/python/pki/server/deployment/pkihelper.py index 740caff..48446b0 100644 --- a/base/server/python/pki/server/deployment/pkihelper.py +++ b/base/server/python/pki/server/deployment/pkihelper.py @@ -429,6 +429,8 @@ class ConfigurationFile: # generic extension support in CSR - for external CA self.add_req_ext = config.str2bool( self.mdict['pki_req_ext_add']) + # include SKI extension in CSR - for external CA + self.req_ski = self.mdict.get('pki_req_ski') self.existing = config.str2bool(self.mdict['pki_existing']) self.external = config.str2bool(self.mdict['pki_external']) diff --git a/base/server/python/pki/server/deployment/scriptlets/configuration.py b/base/server/python/pki/server/deployment/scriptlets/configuration.py index b4f3141..3f153ec 100644 --- a/base/server/python/pki/server/deployment/scriptlets/configuration.py +++ b/base/server/python/pki/server/deployment/scriptlets/configuration.py @@ -94,6 +94,7 @@ class PkiScriptlet(pkiscriptlet.AbstractBasePkiScriptlet): basic_constraints_ext=None, key_usage_ext=None, extended_key_usage_ext=None, + subject_key_id=None, generic_exts=None): cert_id = self.get_cert_id(subsystem, tag) @@ -121,6 +122,7 @@ class PkiScriptlet(pkiscriptlet.AbstractBasePkiScriptlet): basic_constraints_ext=basic_constraints_ext, key_usage_ext=key_usage_ext, extended_key_usage_ext=extended_key_usage_ext, + subject_key_id=subject_key_id, generic_exts=generic_exts) with open(csr_path) as f: @@ -174,7 +176,9 @@ class PkiScriptlet(pkiscriptlet.AbstractBasePkiScriptlet): csr_path, basic_constraints_ext=basic_constraints_ext, key_usage_ext=key_usage_ext, - generic_exts=generic_exts + generic_exts=generic_exts, + subject_key_id=subsystem.config.get( + 'preop.cert.signing.subject_key_id'), ) def generate_sslserver_csr(self, deployer, nssdb, subsystem): diff --git a/base/server/python/pki/server/deployment/scriptlets/security_databases.py b/base/server/python/pki/server/deployment/scriptlets/security_databases.py index 7ce32a8..82dd85c 100644 --- a/base/server/python/pki/server/deployment/scriptlets/security_databases.py +++ b/base/server/python/pki/server/deployment/scriptlets/security_databases.py @@ -240,6 +240,10 @@ class PkiScriptlet(pkiscriptlet.AbstractBasePkiScriptlet): subsystem.config['preop.cert.signing.ext.critical'] = \ deployer.configuration_file.req_ext_critical.lower() + if deployer.configuration_file.req_ski: + subsystem.config['preop.cert.signing.subject_key_id'] = \ + deployer.configuration_file.req_ski + subsystem.save() def update_external_certs_conf(self, external_path, deployer): -- 1.8.3.1