From 4c8fdc1b11e052d469aee944e491cd8725b7a1e9 Mon Sep 17 00:00:00 2001 From: Fraser Tweedale Date: Wed, 30 Mar 2016 12:38:24 +1100 Subject: [PATCH] Lightweight CAs: add key retrieval framework Add the framework for key retrieval when a lightweight CA is missing its signing key. This includes all the bits for loading a KeyRetriever implementation, initiating retrieval in a thread and updating the record of which clones possess the key if retrieval was successful. It does not include a KeyRetriever implementation. A subsequent commit will provide this. Part of: https://fedorahosted.org/pki/ticket/1625 --- .../src/com/netscape/ca/CertificateAuthority.java | 162 ++++++++++++++++++++- base/ca/src/com/netscape/ca/KeyRetriever.java | 56 +++++++ .../src/netscape/security/pkcs/PKCS12Util.java | 3 + 3 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 base/ca/src/com/netscape/ca/KeyRetriever.java diff --git a/base/ca/src/com/netscape/ca/CertificateAuthority.java b/base/ca/src/com/netscape/ca/CertificateAuthority.java index 37f1e95fc97f3d21ec6dc379962e27b42fb5b074..ac45141662aea732389353814e5a7e7b3ba516a7 100644 --- a/base/ca/src/com/netscape/ca/CertificateAuthority.java +++ b/base/ca/src/com/netscape/ca/CertificateAuthority.java @@ -35,6 +35,7 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateParsingException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Enumeration; @@ -62,8 +63,10 @@ import org.mozilla.jss.crypto.CryptoToken; import org.mozilla.jss.crypto.KeyPairAlgorithm; import org.mozilla.jss.crypto.KeyPairGenerator; import org.mozilla.jss.crypto.NoSuchItemOnTokenException; +import org.mozilla.jss.crypto.PrivateKey; import org.mozilla.jss.crypto.SignatureAlgorithm; import org.mozilla.jss.crypto.TokenException; +import org.mozilla.jss.crypto.X509Certificate; import org.mozilla.jss.pkix.cert.Extension; import org.mozilla.jss.pkix.primitive.Name; @@ -205,6 +208,7 @@ public class CertificateAuthority protected AuthorityID authorityID = null; protected AuthorityID authorityParentID = null; protected String authorityDescription = null; + protected Collection authorityKeyHosts = null; protected boolean authorityEnabled = true; private boolean hasKeys = false; private ECAException signingUnitException = null; @@ -340,6 +344,7 @@ public class CertificateAuthority AuthorityID aid, AuthorityID parentAID, String signingKeyNickname, + Collection authorityKeyHosts, String authorityDescription, boolean authorityEnabled ) throws EBaseException { @@ -355,6 +360,7 @@ public class CertificateAuthority this.authorityDescription = authorityDescription; this.authorityEnabled = authorityEnabled; mNickname = signingKeyNickname; + this.authorityKeyHosts = authorityKeyHosts; init(hostCA.mOwner, hostCA.mConfig); } @@ -504,7 +510,7 @@ public class CertificateAuthority // init signing unit & CA cert. try { - initSigUnit(); + initSigUnit(/* retrieveKeys */ true); // init default CA attributes like cert version, validity. initDefCaAttrs(); } catch (EBaseException e) { @@ -1446,7 +1452,7 @@ public class CertificateAuthority /** * init CA signing unit & cert chain. */ - private void initSigUnit() + private boolean initSigUnit(boolean retrieveKeys) throws EBaseException { try { // init signing unit @@ -1476,7 +1482,14 @@ public class CertificateAuthority } catch (CAMissingCertException | CAMissingKeyException e) { CMS.debug("CA signing key and cert not (yet) present in NSSDB"); signingUnitException = e; - return; + if (retrieveKeys == true) { + CMS.debug("Starting KeyRetrieverRunner thread"); + new Thread( + new KeyRetrieverRunner(this), + "KeyRetrieverRunner-" + authorityID + ).start(); + } + return false; } CMS.debug("CA signing unit inited"); @@ -1601,6 +1614,8 @@ public class CertificateAuthority mNickname = mSigningUnit.getNickname(); CMS.debug("in init - got CA name " + mName); + return true; + } catch (CryptoManager.NotInitializedException e) { log(ILogger.LL_FAILURE, CMS.getLogMessage("CMSCORE_CA_CA_OCSP_SIGNING", e.toString())); throw new ECAException(CMS.getUserMessage("CMS_CA_CRYPTO_NOT_INITIALIZED")); @@ -2527,11 +2542,14 @@ public class CertificateAuthority throw new EBaseException("Failed to convert issuer DN to string: " + e); } + String thisClone = CMS.getEEHost() + ":" + CMS.getEESSLPort(); + LDAPAttribute[] attrs = { new LDAPAttribute("objectclass", "authority"), new LDAPAttribute("cn", aidString), new LDAPAttribute("authorityID", aidString), new LDAPAttribute("authorityKeyNickname", nickname), + new LDAPAttribute("authorityKeyHost", thisClone), new LDAPAttribute("authorityEnabled", "TRUE"), new LDAPAttribute("authorityDN", subjectDN), new LDAPAttribute("authorityParentDN", parentDNString) @@ -2612,7 +2630,9 @@ public class CertificateAuthority return new CertificateAuthority( hostCA, subjectX500Name, - aid, this.authorityID, nickname, description, true); + aid, this.authorityID, + nickname, Collections.singleton(thisClone), + description, true); } /** @@ -2785,6 +2805,23 @@ public class CertificateAuthority } } + /** + * Add this instance to the authorityKeyHosts + */ + private void addInstanceToAuthorityKeyHosts() throws ELdapException { + String hostname = CMS.getEEHost(); + if (authorityKeyHosts.contains(hostname)) { + // already there; nothing to do + return; + } + LDAPModificationSet mods = new LDAPModificationSet(); + mods.add( + LDAPModification.ADD, + new LDAPAttribute("authorityKeyHost", hostname)); + modifyAuthorityEntry(mods); + authorityKeyHosts.add(hostname); + } + public synchronized void deleteAuthority() throws EBaseException { if (isHostAuthority()) throw new CATypeException("Cannot delete the host CA"); @@ -2933,7 +2970,6 @@ public class CertificateAuthority case LDAPPersistSearchControl.ADD: CMS.debug("authorityMonitor: ADD"); readAuthority(entry); - // TODO kick off signing key replication via custodia break; case LDAPPersistSearchControl.DELETE: CMS.debug("authorityMonitor: DELETE"); @@ -2990,6 +3026,7 @@ public class CertificateAuthority LDAPAttribute aidAttr = entry.getAttribute("authorityID"); LDAPAttribute nickAttr = entry.getAttribute("authorityKeyNickname"); + LDAPAttribute keyHostsAttr = entry.getAttribute("authorityKeyHost"); LDAPAttribute dnAttr = entry.getAttribute("authorityDN"); LDAPAttribute parentAIDAttr = entry.getAttribute("authorityParentID"); LDAPAttribute parentDNAttr = entry.getAttribute("authorityParentDN"); @@ -3052,6 +3089,16 @@ public class CertificateAuthority } String keyNick = (String) nickAttr.getStringValues().nextElement(); + + Collection keyHosts; + if (keyHostsAttr == null) { + keyHosts = Collections.emptyList(); + } else { + @SuppressWarnings("unchecked") + Enumeration keyHostsEnum = keyHostsAttr.getStringValues(); + keyHosts = Collections.list(keyHostsEnum); + } + AuthorityID parentAID = null; if (parentAIDAttr != null) parentAID = new AuthorityID((String) @@ -3067,7 +3114,7 @@ public class CertificateAuthority try { CertificateAuthority ca = new CertificateAuthority( - hostCA, dn, aid, parentAID, keyNick, desc, enabled); + hostCA, dn, aid, parentAID, keyNick, keyHosts, desc, enabled); caMap.put(aid, ca); entryUSNs.put(aid, newEntryUSN); nsUniqueIds.put(aid, nsUniqueId); @@ -3117,4 +3164,107 @@ public class CertificateAuthority } } + private class KeyRetrieverRunner implements Runnable { + private CertificateAuthority ca; + + public KeyRetrieverRunner(CertificateAuthority ca) { + this.ca = ca; + } + + public void run() { + String KR_CLASS_KEY = "features.authority.keyRetrieverClass"; + String className = null; + try { + className = CMS.getConfigStore().getString(KR_CLASS_KEY); + } catch (EBaseException e) { + CMS.debug("Unable to read key retriever class from CS.cfg: " + e); + return; + } + + KeyRetriever kr = null; + try { + kr = Class.forName(className) + .asSubclass(KeyRetriever.class) + .newInstance(); + } catch (ClassNotFoundException e) { + CMS.debug("Could not find class: " + className); + CMS.debug(e); + return; + } catch (ClassCastException e) { + CMS.debug("Class is not an instance of KeyRetriever: " + className); + CMS.debug(e); + return; + } catch (InstantiationException | IllegalAccessException e) { + CMS.debug("Could not instantiate class: " + className); + CMS.debug(e); + return; + } + + KeyRetriever.Result krr = null; + try { + krr = kr.retrieveKey(ca.mNickname, ca.authorityKeyHosts); + } catch (Throwable e) { + CMS.debug("Caught exception during execution of KeyRetriever.retrieveKey"); + CMS.debug(e); + return; + } + + if (krr == null) { + CMS.debug("KeyRetriever did not return a result."); + return; + } + + CMS.debug("Importing key and cert"); + byte[] certBytes = krr.getCertificate(); + byte[] paoData = krr.getPKIArchiveOptions(); + try { + CryptoManager manager = CryptoManager.getInstance(); + CryptoToken token = manager.getInternalKeyStorageToken(); + + X509Certificate cert = manager.importCACertPackage(certBytes); + PublicKey pubkey = cert.getPublicKey(); + token.getCryptoStore().deleteCert(cert); + + PrivateKey unwrappingKey = hostCA.mSigningUnit.getPrivateKey(); + + CryptoUtil.importPKIArchiveOptions( + token, unwrappingKey, pubkey, paoData); + + cert = manager.importUserCACertPackage(certBytes, ca.mNickname); + } catch (Throwable e) { + CMS.debug("Caught exception during cert/key import"); + CMS.debug(e); + return; + } + + boolean initSigUnitSucceeded = false; + try { + CMS.debug("Reinitialising SigningUnit"); + // re-init signing unit, but avoid triggering + // key replication if initialisation fails again + // for some reason + // + initSigUnitSucceeded = ca.initSigUnit(/* retrieveKeys */ false); + } catch (Throwable e) { + CMS.debug("Caught exception during SigningUnit re-init"); + CMS.debug(e); + return; + } + + if (!initSigUnitSucceeded) { + CMS.debug("Failed to re-init SigningUnit"); + return; + } + + CMS.debug("Adding self to authorityKeyHosts attribute"); + try { + ca.addInstanceToAuthorityKeyHosts(); + } catch (Throwable e) { + CMS.debug("Failed to add self to authorityKeyHosts"); + CMS.debug(e); + return; + } + } + } + } diff --git a/base/ca/src/com/netscape/ca/KeyRetriever.java b/base/ca/src/com/netscape/ca/KeyRetriever.java new file mode 100644 index 0000000000000000000000000000000000000000..7c0df0bf56578b81062de77de47aa516b5c9d949 --- /dev/null +++ b/base/ca/src/com/netscape/ca/KeyRetriever.java @@ -0,0 +1,56 @@ +// --- BEGIN COPYRIGHT BLOCK --- +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; version 2 of the License. +// +// This program 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// +// (C) 2016 Red Hat, Inc. +// All rights reserved. +// --- END COPYRIGHT BLOCK --- + +package com.netscape.ca; + +import java.util.Collection; + +public interface KeyRetriever { + /** + * Retrieve the specified signing key from specified clone and + * return to the KeyRetrieverRunner. + * + * A KeyRetriever MUST NOT import the cert and key to the NSSDB + * itself. It SHALL, if successful in retrieving the key and + * certificate, return a Result which contains a PEM-encoded + * X.509 certificate and a DER-encoded PKIArchiveOptions object + * containing an EncryptedValue of the target private key + * wrapped by the host authority's public key. + * + * Upon failure the KeyRetriever SHALL return null. + */ + Result retrieveKey(String nickname, Collection hostPorts); + + class Result { + private byte[] certificate; + private byte[] pkiArchiveOptions; + + public Result(byte[] certificate, byte[] pkiArchiveOptions) { + this.certificate = certificate; + this.pkiArchiveOptions = pkiArchiveOptions; + } + + public byte[] getCertificate() { + return certificate; + } + + public byte[] getPKIArchiveOptions() { + return pkiArchiveOptions; + } + } +} diff --git a/base/util/src/netscape/security/pkcs/PKCS12Util.java b/base/util/src/netscape/security/pkcs/PKCS12Util.java index 43435c822c9400248fe556bf066cd2659e18ae17..9931027dacfe88e636b694b7c490ffc6804068dd 100644 --- a/base/util/src/netscape/security/pkcs/PKCS12Util.java +++ b/base/util/src/netscape/security/pkcs/PKCS12Util.java @@ -536,7 +536,10 @@ public class PKCS12Util { Path path = Paths.get(filename); byte[] b = Files.readAllBytes(path); + return loadFromByteArray(b, password); + } + public PKCS12 loadFromByteArray(byte[] b, Password password) throws Exception { ByteArrayInputStream bis = new ByteArrayInputStream(b); PFX pfx = (PFX) (new PFX.Template()).decode(bis); -- 2.5.5