Blame SOURCES/pki-core-Add-Subject-Key-ID-to-CSR.patch

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