[Pki-devel] [PATCH] 0084..0086 Lightweight CA replication support

Fraser Tweedale ftweedal at redhat.com
Wed Apr 20 06:15:16 UTC 2016


New version of 0097 attached (0097-4).  The only change is some
minor improvements to the pki-ipa-retrieve-key Python program.

Cheers,
Fraser

On Tue, Apr 19, 2016 at 07:32:16PM +1000, Fraser Tweedale wrote:
> Both issues addressed in latest patchset.  Two new patches in the
> mix; the order is:
> 
>     0095-4, 0098, 0099, 0096-4, 0097-3 (tip)
> 
> I also added another attribute to schema for the authority
> certificate serial number.  It is not used in current code but I
> have a hunch it may be needed for renewal, so I'm adding it now.
> 
> Thanks,
> Fraser
> 
> On Thu, Apr 14, 2016 at 05:34:45PM -0400, Ade Lee wrote:
> > Couple of points on 96/97.
> > 
> > 1. First off, I'm not sure you followed my concern about being able to
> > distinguish between CA instances.
> > 
> > On an IPA system, this is not an issue because there is only one CA on
> > the server.  In this case, I imagine there will be a well known
> > directory which custodia would work with.
> > 
> > In general though, we have to imagine that someone could end up
> > installing two different dogtag ca instances on the same server. 
> >  CMS.getEEHost() would result in the same value (the hostname) for both
> > CAs.  How does your helper program (or custodia) know which key to
> > retrieve?
> > 
> > The way to distinguish Dogtag instances is host AND port.
> > 
> > 2.  So, we're very careful that the signing keys are never in memory in
> > the server.  All accesses to the system certs are through JSS/NSS which
> > essentially provides us handles to the keys.
> > 
> > Now, I see a case where we import PKCS12 data AND the password into
> > memory, so that we can import it into NSS?  Say it ain't so ..
> > 
> > With custodia, we have a secure mechanism of transferring the keys from
> > one server to another. It makes more sense to me to have the server
> > kick off the custodia transfer and then have that process also import
> > into the NSS db.  The server would then need to await status from the
> > custodia/retriever process - and then initialize the signing unit from
> > the NSS DB.  Or am I completely confused?
> > 
> > Ade
> > 
> > 
> > 
> > On Thu, 2016-04-14 at 16:35 -0400, Ade Lee wrote:
> > > Still reviewing .. ACK on 87-95 (inclusive).
> > > 
> > > On Thu, 2016-04-14 at 16:18 +1000, Fraser Tweedale wrote:
> > > > On Thu, Apr 14, 2016 at 09:04:31AM +1000, Fraser Tweedale wrote:
> > > > > On Wed, Apr 13, 2016 at 05:26:44PM -0400, Ade Lee wrote:
> > > > > > Still reviewing ..
> > > > > > 
> > > > > > See comment on 87.  ACK on 88,89,90,91,92,93, 94, 95.
> > > > > > 
> > > > > > Ade
> > > > > > 
> > > > > > On Mon, 2016-04-11 at 12:32 +1000, Fraser Tweedale wrote:
> > > > > > > Thanks for review, Ade.  Comments to specific feedback
> > > > > > > inline.
> > > > > > > Rebased and updated patches attached.  The substantive
> > > > > > > changes
> > > > > > > are:
> > > > > > > 
> > > > > > > - KeyRetriever implementations are now required NOT to import
> > > > > > > the
> > > > > > >   key themselves.  Instead the API is updated with
> > > > > > >   KeyRetriever.retrieveKey returning a Result, which contains
> > > > > > > PKCS
> > > > > > >   #12 data and password for same.
> > > > > > > 
> > > > > > > - KeyRetrieverRunner reads the Result and imports the PKCS
> > > > > > > #12
> > > > > > > into
> > > > > > >   NSSDB.
> > > > > > > 
> > > > > > > - Added new patch 0097 which provides the
> > > > > > > IPACustodiaKeyRetriever
> > > > > > >   and assoicated Python helper script.  It depends on an
> > > > > > > unmerged
> > > > > > >   FreeIPA patch[1] as well as a particular principal and
> > > > > > > associated
> > > > > > >   keytab and Custodia keys existing.  I'm working on FreeIPA
> > > > > > > updates
> > > > > > >   to satisfy these requirements automatically on install or
> > > > > > > upgrade
> > > > > > >   but if you want to test this patch LMK and I'll provide
> > > > > > > detailed
> > > > > > >   instructions.
> > > > > > > 
> > > > > > >   [1] 
> > > > > > > https://www.redhat.com/archives/freeipa-devel/2016-April/msg0
> > > > > > > 00
> > > > > > > 55.html
> > > > > > > 
> > > > > > > Other comments inline.
> > > > > > > 
> > > > > > > Cheers,
> > > > > > > Fraser
> > > > > > > 
> > > > > > > On Fri, Apr 08, 2016 at 11:16:19AM -0400, Ade Lee wrote:
> > > > > > > > 
> > > > > > > > 0087
> > > > > > > > 
> > > > > > > > 1. In SigningUnit.java -- you catch an ObjectNotFound
> > > > > > > > exception and
> > > > > > > > rethrow that as a CAMissingKey exception.  Is that the only
> > > > > > > > way the
> > > > > > > > ObjectNotFound exception can be thrown?  What if the key is
> > > > > > > > present
> > > > > > > > but
> > > > > > > > the cert is not?  Can we refactor here to ensure that the
> > > > > > > > correct
> > > > > > > > exception is thrown?
> > > > > > > > 
> > > > > > > One can't get additional info out of ObjectNotFound without
> > > > > > > inspecting the String message, which I'm not comfortable
> > > > > > > doing.
> > > > > > >   The
> > > > > > > key retrieval system should import key and cert at same time
> > > > > > > so
> > > > > > > I've
> > > > > > > renamed the exception to CAMissingKeyOrCert for clarity.
> > > > > > > 
> > > > > > 
> > > > > > Well, you can always nest exceptions like so :
> > > > > > 
> > > > > > 	    mToken.login(cb); // ONE_TIME by default.
> > > > > > 
> > > > > >             try {
> > > > > >                 mCert = mManager.findCertByNickname(mNickname);
> > > > > >                 CMS.debug("Found cert by nickname: '" +
> > > > > > mNickname
> > > > > > + "' with serial number: " + mCert.getSerialNumber());
> > > > > > 
> > > > > >                 mCertImpl = new
> > > > > > X509CertImpl(mCert.getEncoded());
> > > > > >                 CMS.debug("converted to x509CertImpl");
> > > > > >             } catch (ObjectNotFoundException e) {
> > > > > >                 throw new CAMissingCertException();
> > > > > >             }
> > > > > > 
> > > > > >             try {
> > > > > >                 mPrivk = mManager.findPrivKeyByCert(mCert);
> > > > > >                 CMS.debug("Got private key from cert");
> > > > > >             } catch (ObjectNotFoundException e) {
> > > > > >                throw new CAMissingKeyException();
> > > > > >             }
> > > > > >             ....
> > > > > > 
> > > > > > The only reason that I suggest this is that I could imagine
> > > > > > this
> > > > > > kind
> > > > > > of differentiation being useful in debugging failed custodia
> > > > > > replications.  If you think otherwise, I'm prepare to be
> > > > > > convinced
> > > > > > otherwise.
> > > > > > 
> > > > > I think a scenario where we get key but not cert, or vice versa,
> > > > > is
> > > > > unlikely (Custodia gives us a PKCS #12 file with both).  However,
> > > > > your suggestion should work and it is a relatively small change.
> > > > > I'll cut a new patchset with this change today, along with the
> > > > > rebase.
> > > > > 
> > > > Updated and rebased patches attached.
> > > > 
> > > > The suggested changes were made to 0087.  This also resulted in
> > > > changes to patch 0094 (indicate when CA does not yet have keys).
> > > > 
> > > > No substantive changes to any other patches.
> > > > 
> > > > Cheers,

> From f5322fdf885196275a93696cc31a489c9b4aab1d Mon Sep 17 00:00:00 2001
> From: Fraser Tweedale <ftweedal at redhat.com>
> Date: Wed, 30 Mar 2016 16:06:25 +1100
> Subject: [PATCH] Lightweight CAs: authority schema changes
> 
> Add the 'authorityKeyHost' attribute which will contain names of
> hosts that possess the authority's signing keys.
> 
> Add the 'authoritySerial' attribute which may contain the serial
> number of the certificate most recently issued for the authority.
> 
> Change other attributes to be single-valued.
> 
> Part of: https://fedorahosted.org/pki/ticket/1625
> ---
>  base/server/share/conf/schema-authority.ldif | 16 +++++++++-------
>  base/server/share/conf/schema.ldif           | 15 ++++++++-------
>  2 files changed, 17 insertions(+), 14 deletions(-)
> 
> diff --git a/base/server/share/conf/schema-authority.ldif b/base/server/share/conf/schema-authority.ldif
> index 7d261f18fbc9475983bf93b1cddcc184d7f9d178..fd3c4fa225b036142a9aa4e99c65697365160dfd 100644
> --- a/base/server/share/conf/schema-authority.ldif
> +++ b/base/server/share/conf/schema-authority.ldif
> @@ -1,8 +1,10 @@
>  dn: cn=schema
> -attributeTypes: ( authorityID-oid NAME 'authorityID' DESC 'Authority ID' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'user defined' )
> -attributeTypes: ( authorityKeyNickname-oid NAME 'authorityKeyNickname' DESC 'Authority key nickname' SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 X-ORIGIN 'user-defined' )
> -attributeTypes: ( authorityParentID-oid NAME 'authorityParentID' DESC 'Authority Parent ID' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'user defined' )
> -attributeTypes: ( authorityEnabled-oid NAME 'authorityEnabled' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 X-ORIGIN 'user defined' )
> -attributeTypes: ( authorityDN-oid NAME 'authorityDN' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'user defined' )
> -attributeTypes: ( authorityParentDN-oid NAME 'authorityParentDN' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'user defined' )
> -objectClasses: ( authority-oid NAME 'authority' DESC 'Certificate Authority' SUP top STRUCTURAL MUST ( cn $ authorityID $ authorityKeyNickname $ authorityEnabled $ authorityDN ) MAY ( authorityParentID $ authorityParentDN $ description ) X-ORIGIN 'user defined' )
> +attributeTypes: ( authorityID-oid NAME 'authorityID' DESC 'Authority ID' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE X-ORIGIN 'user defined' )
> +attributeTypes: ( authorityKeyNickname-oid NAME 'authorityKeyNickname' DESC 'Authority key nickname' SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 SINGLE-VALUE X-ORIGIN 'user-defined' )
> +attributeTypes: ( authorityParentID-oid NAME 'authorityParentID' DESC 'Authority Parent ID' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE X-ORIGIN 'user defined' )
> +attributeTypes: ( authorityEnabled-oid NAME 'authorityEnabled' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'user defined' )
> +attributeTypes: ( authorityDN-oid NAME 'authorityDN' DESC 'Authority DN' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'user defined' )
> +attributeTypes: ( authoritySerial-oid NAME 'authoritySerial' DESC 'Authority certificate serial number' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'user defined' )
> +attributeTypes: ( authorityParentDN-oid NAME 'authorityParentDN' DESC 'Authority Parent DN' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'user defined' )
> +attributeTypes: ( authorityKeyHost-oid NAME 'authorityKeyHost' DESC 'Authority Key Hosts' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'user defined' )
> +objectClasses: ( authority-oid NAME 'authority' DESC 'Certificate Authority' SUP top STRUCTURAL MUST ( cn $ authorityID $ authorityKeyNickname $ authorityEnabled $ authorityDN ) MAY ( authoritySerial $ authorityParentID $ authorityParentDN $ authorityKeyHost $ description ) X-ORIGIN 'user defined' )
> diff --git a/base/server/share/conf/schema.ldif b/base/server/share/conf/schema.ldif
> index a15601ae7a362635bc398b92b9bfda1c72f0dfc8..5e4118d328ebe1fcac2743b3f51fb5ca9d57f9eb 100644
> --- a/base/server/share/conf/schema.ldif
> +++ b/base/server/share/conf/schema.ldif
> @@ -671,12 +671,13 @@ objectClasses: ( certProfile-oid NAME 'certProfile' DESC 'Certificate profile' S
>  dn: cn=schema
>  changetype: modify
>  add: attributeTypes
> -attributeTypes: ( authorityID-oid NAME 'authorityID' DESC 'Authority ID' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'user defined' )
> -attributeTypes: ( authorityKeyNickname-oid NAME 'authorityKeyNickname' DESC 'Authority key nickname' SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 X-ORIGIN 'user-defined' )
> -attributeTypes: ( authorityParentID-oid NAME 'authorityParentID' DESC 'Authority Parent ID' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'user defined' )
> -attributeTypes: ( authorityEnabled-oid NAME 'authorityEnabled' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 X-ORIGIN 'user defined' )
> -attributeTypes: ( authorityDN-oid NAME 'authorityDN' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'user defined' )
> -attributeTypes: ( authorityParentDN-oid NAME 'authorityParentDN' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'user defined' )
> +attributeTypes: ( authorityID-oid NAME 'authorityID' DESC 'Authority ID' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE X-ORIGIN 'user defined' )
> +attributeTypes: ( authorityKeyNickname-oid NAME 'authorityKeyNickname' DESC 'Authority key nickname' SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 SINGLE-VALUE X-ORIGIN 'user-defined' )
> +attributeTypes: ( authorityParentID-oid NAME 'authorityParentID' DESC 'Authority Parent ID' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE X-ORIGIN 'user defined' )
> +attributeTypes: ( authorityEnabled-oid NAME 'authorityEnabled' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'user defined' )
> +attributeTypes: ( authorityDN-oid NAME 'authorityDN' DESC 'Authority DN' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'user defined' )
> +attributeTypes: ( authorityParentDN-oid NAME 'authorityParentDN' DESC 'Authority Parent DN' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'user defined' )
> +attributeTypes: ( authorityKeyHost-oid NAME 'authorityKeyHost' DESC 'Authority Key Hosts' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'user defined' )
>  -
>  add: objectClasses
> -objectClasses: ( authority-oid NAME 'authority' DESC 'Certificate Authority' SUP top STRUCTURAL MUST ( cn $ authorityID $ authorityKeyNickname $ authorityEnabled $ authorityDN ) MAY ( authorityParentID $ authorityParentDN $ description ) X-ORIGIN 'user defined' )
> +objectClasses: ( authority-oid NAME 'authority' DESC 'Certificate Authority' SUP top STRUCTURAL MUST ( cn $ authorityID $ authorityKeyNickname $ authorityEnabled $ authorityDN ) MAY ( authorityParentID $ authorityParentDN $ authorityKeyHost $ description ) X-ORIGIN 'user defined' )
> -- 
> 2.5.5
> 

> From 3d42d650abf5b4862570620ef7b130bb511bb17d Mon Sep 17 00:00:00 2001
> From: Fraser Tweedale <ftweedal at redhat.com>
> Date: Tue, 19 Apr 2016 13:25:12 +1000
> Subject: [PATCH 98/99] Add method CryptoUtil.importPKIArchiveOptions
> 
> Add the method CryptoUtil.importPKIArchiveOptions for importing a
> wrapped key from a PKIArchiveOptions object.  Also add another
> variant of the createPKIArchiveOptions method, with a narrower API.
> 
> Part of: https://fedorahosted.org/pki/ticket/1625
> ---
>  .../com/netscape/cmsutil/crypto/CryptoUtil.java    | 58 ++++++++++++++++++++++
>  1 file changed, 58 insertions(+)
> 
> diff --git a/base/util/src/com/netscape/cmsutil/crypto/CryptoUtil.java b/base/util/src/com/netscape/cmsutil/crypto/CryptoUtil.java
> index 06caa0242ab192c5bbc14845dd7abc772601bd58..7ac3e4e9ad2702e09c704638b6ab773bb2dccb49 100644
> --- a/base/util/src/com/netscape/cmsutil/crypto/CryptoUtil.java
> +++ b/base/util/src/com/netscape/cmsutil/crypto/CryptoUtil.java
> @@ -80,7 +80,9 @@ import org.mozilla.jss.CryptoManager;
>  import org.mozilla.jss.CryptoManager.NotInitializedException;
>  import org.mozilla.jss.NoSuchTokenException;
>  import org.mozilla.jss.SecretDecoderRing.KeyManager;
> +import org.mozilla.jss.asn1.ANY;
>  import org.mozilla.jss.asn1.ASN1Util;
> +import org.mozilla.jss.asn1.ASN1Value;
>  import org.mozilla.jss.asn1.BIT_STRING;
>  import org.mozilla.jss.asn1.InvalidBERException;
>  import org.mozilla.jss.asn1.OBJECT_IDENTIFIER;
> @@ -1971,6 +1973,31 @@ public class CryptoUtil {
>          // wrap session key using transport key
>          byte[] session_data = wrapSymmetricKey(manager, token, transportCert, sk);
>  
> +        return createPKIArchiveOptions(IV, session_data, key_data);
> +    }
> +
> +    public static byte[] createPKIArchiveOptions(
> +            CryptoToken token, PublicKey wrappingKey, PrivateKey toBeWrapped,
> +            KeyGenAlgorithm keyGenAlg, int symKeySize, IVParameterSpec IV)
> +            throws TokenException, NoSuchAlgorithmException,
> +            InvalidAlgorithmParameterException, InvalidKeyException,
> +            IOException, InvalidBERException {
> +        SymmetricKey sessionKey = CryptoUtil.generateKey(token, keyGenAlg, symKeySize);
> +
> +        KeyWrapper wrapper = token.getKeyWrapper(KeyWrapAlgorithm.DES3_CBC_PAD);
> +        wrapper.initWrap(sessionKey, IV);
> +        byte[] key_data = wrapper.wrap(toBeWrapped);
> +
> +        wrapper = token.getKeyWrapper(KeyWrapAlgorithm.RSA);
> +        wrapper.initWrap(wrappingKey, null);
> +        byte session_data[] = wrapper.wrap(sessionKey);
> +
> +        return createPKIArchiveOptions(IV, session_data, key_data);
> +    }
> +
> +    private static byte[] createPKIArchiveOptions(
> +            IVParameterSpec IV, byte[] session_data, byte[] key_data)
> +            throws IOException, InvalidBERException {
>          // create PKIArchiveOptions structure
>          AlgorithmIdentifier algS = new AlgorithmIdentifier(new OBJECT_IDENTIFIER("1.2.840.113549.3.7"),
>                  new OCTET_STRING(IV.getIV()));
> @@ -1996,6 +2023,37 @@ public class CryptoUtil {
>          return encoded;
>      }
>  
> +    public static PrivateKey importPKIArchiveOptions(
> +            CryptoToken token, PrivateKey unwrappingKey,
> +            PublicKey pubkey, byte[] data)
> +            throws InvalidBERException, Exception {
> +        ByteArrayInputStream in = new ByteArrayInputStream(data);
> +        PKIArchiveOptions options = (PKIArchiveOptions)
> +            (new PKIArchiveOptions.Template()).decode(in);
> +        EncryptedKey encKey = options.getEncryptedKey();
> +        EncryptedValue encVal = encKey.getEncryptedValue();
> +        AlgorithmIdentifier algId = encVal.getSymmAlg();
> +        BIT_STRING encSymKey = encVal.getEncSymmKey();
> +        BIT_STRING encPrivKey = encVal.getEncValue();
> +
> +        KeyWrapper wrapper = token.getKeyWrapper(KeyWrapAlgorithm.RSA);
> +        wrapper.initUnwrap(unwrappingKey, null);
> +        SymmetricKey sk = wrapper.unwrapSymmetric(
> +            encSymKey.getBits(), SymmetricKey.Type.DES3, 0);
> +
> +        ASN1Value v = algId.getParameters();
> +        v = ((ANY) v).decodeWith(new OCTET_STRING.Template());
> +        byte iv[] = ((OCTET_STRING) v).toByteArray();
> +        IVParameterSpec ivps = new IVParameterSpec(iv);
> +
> +        wrapper = token.getKeyWrapper(KeyWrapAlgorithm.DES3_CBC_PAD);
> +        wrapper.initUnwrap(sk, ivps);
> +        PrivateKey.Type keyType = pubkey.getAlgorithm().equals("EC")
> +            ? PrivateKey.Type.EC
> +            : PrivateKey.Type.RSA;
> +        return wrapper.unwrapPrivate(encPrivKey.getBits(), keyType, pubkey);
> +    }
> +
>      public static boolean sharedSecretExists(String nickname) throws NotInitializedException, TokenException {
>          CryptoManager cm = CryptoManager.getInstance();
>          CryptoToken token = cm.getInternalKeyStorageToken();
> -- 
> 2.5.5
> 

> From ba32660ee26fc00132882809ce9365690f375c6c Mon Sep 17 00:00:00 2001
> From: Fraser Tweedale <ftweedal at redhat.com>
> Date: Tue, 19 Apr 2016 13:28:56 +1000
> Subject: [PATCH 99/99] Add ca-authority-key-export command
> 
> Add the 'pki ca-authority-key-export' CLI command for exporting a
> PKIArchiveOptions object containing a nominated target key, wrapped
> by a nominated wrapping key.  This command is to be used by Custodia
> to export key data for transmission to a requesting clone.
> 
> Part of: https://fedorahosted.org/pki/ticket/1625
> ---
>  .../netscape/cmstools/authority/AuthorityCLI.java  |   1 +
>  .../cmstools/authority/AuthorityKeyExportCLI.java  | 109 +++++++++++++++++++++
>  2 files changed, 110 insertions(+)
>  create mode 100644 base/java-tools/src/com/netscape/cmstools/authority/AuthorityKeyExportCLI.java
> 
> diff --git a/base/java-tools/src/com/netscape/cmstools/authority/AuthorityCLI.java b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityCLI.java
> index ac06ea24ce824ad1b4be29a4176658caa9302e89..f42660d6727059bc76ab7ccd0bd0b22a87bc5f9a 100644
> --- a/base/java-tools/src/com/netscape/cmstools/authority/AuthorityCLI.java
> +++ b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityCLI.java
> @@ -18,6 +18,7 @@ public class AuthorityCLI extends CLI {
>          addModule(new AuthorityDisableCLI(this));
>          addModule(new AuthorityEnableCLI(this));
>          addModule(new AuthorityRemoveCLI(this));
> +        addModule(new AuthorityKeyExportCLI(this));
>      }
>  
>      public String getFullName() {
> diff --git a/base/java-tools/src/com/netscape/cmstools/authority/AuthorityKeyExportCLI.java b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityKeyExportCLI.java
> new file mode 100644
> index 0000000000000000000000000000000000000000..a3dee82c8d7ef3ad923aa53635b0825f3d272998
> --- /dev/null
> +++ b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityKeyExportCLI.java
> @@ -0,0 +1,109 @@
> +package com.netscape.cmstools.authority;
> +
> +import java.nio.file.Files;
> +import java.nio.file.Paths;
> +import java.security.PublicKey;
> +
> +import org.apache.commons.cli.CommandLine;
> +import org.apache.commons.cli.Option;
> +import org.apache.commons.cli.ParseException;
> +
> +import org.mozilla.jss.CryptoManager;
> +import org.mozilla.jss.crypto.CryptoToken;
> +import org.mozilla.jss.crypto.IVParameterSpec;
> +import org.mozilla.jss.crypto.KeyGenAlgorithm;
> +import org.mozilla.jss.crypto.PrivateKey;
> +import org.mozilla.jss.crypto.X509Certificate;
> +
> +import com.netscape.cmstools.cli.CLI;
> +import com.netscape.cmsutil.crypto.CryptoUtil;
> +
> +public class AuthorityKeyExportCLI extends CLI {
> +
> +    public AuthorityCLI authorityCLI;
> +
> +    public AuthorityKeyExportCLI(AuthorityCLI authorityCLI) {
> +        super("key-export", "Export wrapped CA signing key", authorityCLI);
> +        this.authorityCLI = authorityCLI;
> +
> +        options.addOption(null, "help", false, "Show usage");
> +
> +        Option option = new Option("o", "output", true, "Output file");
> +        option.setArgName("filename");
> +        options.addOption(option);
> +
> +        option = new Option(null, "wrap-nickname", true, "Nickname of wrapping key");
> +        option.setArgName("nickname");
> +        options.addOption(option);
> +
> +        option = new Option(null, "target-nickname", true, "Nickname of target key");
> +        option.setArgName("nickname");
> +        options.addOption(option);
> +    }
> +
> +    public void printHelp() {
> +        formatter.printHelp(getFullName() + "--wrap-nickname NICKNAME --target-nickname NICKNAME -o FILENAME", options);
> +    }
> +
> +    public void execute(String[] args) throws Exception {
> +        CommandLine cmd = null;
> +
> +        try {
> +            cmd = parser.parse(options, args);
> +        } catch (ParseException e) {
> +            System.err.println("Error: " + e.getMessage());
> +            printHelp();
> +            System.exit(-1);
> +        }
> +
> +        if (cmd.hasOption("help")) {
> +            // Display usage
> +            printHelp();
> +            System.exit(0);
> +        }
> +
> +        String filename = cmd.getOptionValue("output");
> +        if (filename == null) {
> +            System.err.println("Error: No output file specified.");
> +            printHelp();
> +            System.exit(-1);
> +        }
> +
> +        String wrapNick = cmd.getOptionValue("wrap-nickname");
> +        if (wrapNick == null) {
> +            System.err.println("Error: no wrapping key nickname specified.");
> +            printHelp();
> +            System.exit(-1);
> +        }
> +
> +        String targetNick = cmd.getOptionValue("target-nickname");
> +        if (targetNick == null) {
> +            System.err.println("Error: no target key nickname specified.");
> +            printHelp();
> +            System.exit(-1);
> +        }
> +
> +        try {
> +            CryptoManager cm = CryptoManager.getInstance();
> +            X509Certificate wrapCert = cm.findCertByNickname(wrapNick);
> +            X509Certificate targetCert = cm.findCertByNickname(targetNick);
> +
> +            PublicKey wrappingKey = wrapCert.getPublicKey();
> +            PrivateKey toBeWrapped = cm.findPrivKeyByCert(targetCert);
> +            CryptoToken token = cm.getInternalKeyStorageToken();
> +
> +            byte iv[] = { 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1 };
> +            IVParameterSpec ivps = new IVParameterSpec(iv);
> +
> +            byte[] data = CryptoUtil.createPKIArchiveOptions(
> +                token, wrappingKey, toBeWrapped,
> +                KeyGenAlgorithm.DES3, 0, ivps);
> +
> +            Files.newOutputStream(Paths.get(filename)).write(data);
> +        } catch (Throwable e) {
> +            e.printStackTrace();
> +            System.exit(-1);
> +        }
> +
> +    }
> +}
> -- 
> 2.5.5
> 

> From 4c8fdc1b11e052d469aee944e491cd8725b7a1e9 Mon Sep 17 00:00:00 2001
> From: Fraser Tweedale <ftweedal at redhat.com>
> 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<String> 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<String> 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<String> keyHosts;
> +        if (keyHostsAttr == null) {
> +            keyHosts = Collections.emptyList();
> +        } else {
> +            @SuppressWarnings("unchecked")
> +            Enumeration<String> 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<String> 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
> 

> From d2e57290b2d3392082335955be72dfaf3111afd0 Mon Sep 17 00:00:00 2001
> From: Fraser Tweedale <ftweedal at redhat.com>
> Date: Fri, 8 Apr 2016 22:23:42 +1000
> Subject: [PATCH] Lightweight CAs: add IPACustodiaKeyRetriever
> 
> Add 'IPACustodiaKeyRetriever', a 'KeyRetriever' implementation for
> use when Dogtag is deployed as a FreeIPA CA.  The Java class invokes
> 'pki-ipa-key-retriever', a Python script that retrieves lightweight
> CA keys from the Custodia server on a replica that possesses the
> keys.  'pki-ipa-key-retriever' depends on FreeIPA libraries, FreeIPA
> server configuration, and Kerberos and Custodia keys owned by
> 'pkiuser'.
> 
> Part of: https://fedorahosted.org/pki/ticket/1625
> ---
>  base/ca/src/CMakeLists.txt                         |  9 ++-
>  .../com/netscape/ca/IPACustodiaKeyRetriever.java   | 75 ++++++++++++++++++++++
>  base/server/CMakeLists.txt                         | 11 ++++
>  base/server/libexec/pki-ipa-retrieve-key           | 42 ++++++++++++
>  specs/pki-core.spec                                |  1 +
>  5 files changed, 137 insertions(+), 1 deletion(-)
>  create mode 100644 base/ca/src/com/netscape/ca/IPACustodiaKeyRetriever.java
>  create mode 100755 base/server/libexec/pki-ipa-retrieve-key
> 
> diff --git a/base/ca/src/CMakeLists.txt b/base/ca/src/CMakeLists.txt
> index 5b805e1b3a5eddb46d17178ca2ac204c36ae5680..1817dacfbacaeb2635db2550e32ff62c26d628ef 100644
> --- a/base/ca/src/CMakeLists.txt
> +++ b/base/ca/src/CMakeLists.txt
> @@ -24,6 +24,13 @@ find_file(COMMONS_CODEC_JAR
>          /usr/share/java
>  )
>  
> +find_file(COMMONS_IO_JAR
> +    NAMES
> +        commons-io.jar
> +    PATHS
> +        /usr/share/java
> +)
> +
>  find_file(COMMONS_LANG_JAR
>      NAMES
>          commons-lang.jar
> @@ -73,7 +80,7 @@ javac(pki-ca-classes
>          com/netscape/ca/*.java
>          org/dogtagpki/server/ca/*.java
>      CLASSPATH
> -        ${COMMONS_CODEC_JAR} ${COMMONS_LANG_JAR}
> +        ${COMMONS_CODEC_JAR} ${COMMONS_IO_JAR} ${COMMONS_LANG_JAR}
>          ${JSS_JAR} ${SYMKEY_JAR}
>          ${LDAPJDK_JAR}
>          ${SERVLET_JAR} ${TOMCAT_CATALINA_JAR}
> diff --git a/base/ca/src/com/netscape/ca/IPACustodiaKeyRetriever.java b/base/ca/src/com/netscape/ca/IPACustodiaKeyRetriever.java
> new file mode 100644
> index 0000000000000000000000000000000000000000..4a162d3702fccc19dfef792a5213c653286930f3
> --- /dev/null
> +++ b/base/ca/src/com/netscape/ca/IPACustodiaKeyRetriever.java
> @@ -0,0 +1,75 @@
> +// --- 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.lang.Process;
> +import java.lang.ProcessBuilder;
> +import java.util.Collection;
> +import java.util.Stack;
> +
> +import org.apache.commons.io.IOUtils;
> +import org.apache.commons.lang.ArrayUtils;
> +
> +import com.netscape.certsrv.apps.CMS;
> +
> +public class IPACustodiaKeyRetriever implements KeyRetriever {
> +    public Result retrieveKey(String nickname, Collection<String> hostPorts) {
> +        CMS.debug("Running IPACustodiaKeyRetriever");
> +
> +        Stack<String> command = new Stack<>();
> +        command.push("/usr/libexec/pki-ipa-retrieve-key");
> +        command.push(nickname);
> +
> +        for (String hostPort : hostPorts) {
> +            String host = hostPort.split(":")[0];
> +            command.push(host);
> +            CMS.debug("About to execute command: " + command);
> +            ProcessBuilder pb = new ProcessBuilder(command);
> +            try {
> +                Process p = pb.start();
> +                int exitValue = p.waitFor();
> +                if (exitValue != 0)
> +                    continue;
> +
> +                /* Custodia returns a PEM-encoded certificate and a
> +                 * base64-encoded PKIArchiveOptions containing the
> +                 * wrapped private key.  These values are output by
> +                 * the Python 'pki-ipa-retrieve-key' program,
> +                 * separated by a null byte (password first)
> +                 */
> +                byte[] output = IOUtils.toByteArray(p.getInputStream());
> +                int splitIndex = ArrayUtils.indexOf(output, (byte) 0);
> +                if (splitIndex == ArrayUtils.INDEX_NOT_FOUND) {
> +                    CMS.debug("Invalid output: null byte not found");
> +                    continue;
> +                }
> +                return new Result(
> +                    ArrayUtils.subarray(output, 0, splitIndex),
> +                    ArrayUtils.subarray(output, splitIndex + 1, output.length)
> +                );
> +            } catch (Throwable e) {
> +                CMS.debug("Caught exception while executing command: " + e);
> +            } finally {
> +                command.pop();
> +            }
> +        }
> +        CMS.debug("Failed to retrieve key from any host.");
> +        return null;
> +    }
> +}
> diff --git a/base/server/CMakeLists.txt b/base/server/CMakeLists.txt
> index 5a6aea96a2317655fb454967f9f218020443bcb8..9e5b27833c8d023e63320c43d64ad64b0055c254 100644
> --- a/base/server/CMakeLists.txt
> +++ b/base/server/CMakeLists.txt
> @@ -81,6 +81,17 @@ install(
>  
>  install(
>      DIRECTORY
> +        libexec/
> +    DESTINATION
> +        ${LIBEXEC_INSTALL_DIR}
> +    FILE_PERMISSIONS
> +        OWNER_EXECUTE OWNER_WRITE OWNER_READ
> +        GROUP_EXECUTE GROUP_READ
> +        WORLD_EXECUTE WORLD_READ
> +)
> +
> +install(
> +    DIRECTORY
>          upgrade
>      DESTINATION
>          ${DATA_INSTALL_DIR}/server/
> diff --git a/base/server/libexec/pki-ipa-retrieve-key b/base/server/libexec/pki-ipa-retrieve-key
> new file mode 100755
> index 0000000000000000000000000000000000000000..9305150a446bb4deed339b89daad821a19b9e7df
> --- /dev/null
> +++ b/base/server/libexec/pki-ipa-retrieve-key
> @@ -0,0 +1,42 @@
> +#!/usr/bin/python
> +
> +from __future__ import print_function
> +
> +import ConfigParser
> +import base64
> +import sys
> +
> +from jwcrypto.common import json_decode
> +
> +from ipaplatform.paths import paths
> +from ipapython.secrets.client import CustodiaClient
> +
> +conf = ConfigParser.ConfigParser()
> +conf.read(paths.IPA_DEFAULT_CONF)
> +hostname = conf.get('global', 'host')
> +realm = conf.get('global', 'realm')
> +
> +keyname = "ca_wrapped/" + sys.argv[1]
> +servername = sys.argv[2]
> +
> +client_keyfile = "/etc/pki/pki-tomcat/dogtag-ipa-custodia.keys"
> +client_keytab = "/etc/pki/pki-tomcat/dogtag-ipa-custodia.keytab"
> +
> +client = CustodiaClient(
> +    client=hostname, server=servername, realm=realm,
> +    ldap_uri="ldaps://" + hostname,
> +    client_servicename='dogtag-ipa-custodia',
> +    keyfile=client_keyfile, keytab=client_keytab,
> +    )
> +
> +result_json = client.fetch_key(keyname, store=False)
> +result = json_decode(result_json)
> +certificate = result["certificate"]
> +wrapped_key = base64.b64decode(result["wrapped_key"])
> +
> +# Custodia returns a PEM-encoded certificate and a base64-encoded
> +# DER PKIArchiveOptions object.  Output these values, separated by a
> +# null byte (certificate first), to be read by the Java
> +# IPACustodiaKeyRetriever that invoked this program.
> +
> +print(certificate, wrapped_key, sep='\0', end='')
> diff --git a/specs/pki-core.spec b/specs/pki-core.spec
> index bce6bd2d298bc8e1ad5cb40618f982b5ba23b27d..509ecdafa4e5d0e651ce22497db06420abcdf259 100644
> --- a/specs/pki-core.spec
> +++ b/specs/pki-core.spec
> @@ -1007,6 +1007,7 @@ systemctl daemon-reload
>  %{_sbindir}/pki-server
>  %{_sbindir}/pki-server-nuxwdog
>  %{_sbindir}/pki-server-upgrade
> +%{_libexecdir}/pki-ipa-retrieve-key
>  %{python2_sitelib}/pki/server/
>  %dir %{_datadir}/pki/deployment
>  %{_datadir}/pki/deployment/config/
> -- 
> 2.5.5
> 

> _______________________________________________
> Pki-devel mailing list
> Pki-devel at redhat.com
> https://www.redhat.com/mailman/listinfo/pki-devel

-------------- next part --------------
From 66e0b84fabe4175be8077a5f587f84ba999dd074 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 8 Apr 2016 22:23:42 +1000
Subject: [PATCH] Lightweight CAs: add IPACustodiaKeyRetriever

Add 'IPACustodiaKeyRetriever', a 'KeyRetriever' implementation for
use when Dogtag is deployed as a FreeIPA CA.  The Java class invokes
'pki-ipa-retrieve-key', a Python script that retrieves lightweight
CA keys from the Custodia server on a replica that possesses the
keys.  'pki-ipa-retrieve-key' depends on FreeIPA libraries, FreeIPA
server configuration, and Kerberos and Custodia keys owned by
'pkiuser'.

Part of: https://fedorahosted.org/pki/ticket/1625
---
 base/ca/src/CMakeLists.txt                         |  9 ++-
 .../com/netscape/ca/IPACustodiaKeyRetriever.java   | 75 ++++++++++++++++++++++
 base/server/CMakeLists.txt                         | 11 ++++
 base/server/libexec/pki-ipa-retrieve-key           | 45 +++++++++++++
 specs/pki-core.spec                                |  1 +
 5 files changed, 140 insertions(+), 1 deletion(-)
 create mode 100644 base/ca/src/com/netscape/ca/IPACustodiaKeyRetriever.java
 create mode 100755 base/server/libexec/pki-ipa-retrieve-key

diff --git a/base/ca/src/CMakeLists.txt b/base/ca/src/CMakeLists.txt
index 5b805e1b3a5eddb46d17178ca2ac204c36ae5680..1817dacfbacaeb2635db2550e32ff62c26d628ef 100644
--- a/base/ca/src/CMakeLists.txt
+++ b/base/ca/src/CMakeLists.txt
@@ -24,6 +24,13 @@ find_file(COMMONS_CODEC_JAR
         /usr/share/java
 )
 
+find_file(COMMONS_IO_JAR
+    NAMES
+        commons-io.jar
+    PATHS
+        /usr/share/java
+)
+
 find_file(COMMONS_LANG_JAR
     NAMES
         commons-lang.jar
@@ -73,7 +80,7 @@ javac(pki-ca-classes
         com/netscape/ca/*.java
         org/dogtagpki/server/ca/*.java
     CLASSPATH
-        ${COMMONS_CODEC_JAR} ${COMMONS_LANG_JAR}
+        ${COMMONS_CODEC_JAR} ${COMMONS_IO_JAR} ${COMMONS_LANG_JAR}
         ${JSS_JAR} ${SYMKEY_JAR}
         ${LDAPJDK_JAR}
         ${SERVLET_JAR} ${TOMCAT_CATALINA_JAR}
diff --git a/base/ca/src/com/netscape/ca/IPACustodiaKeyRetriever.java b/base/ca/src/com/netscape/ca/IPACustodiaKeyRetriever.java
new file mode 100644
index 0000000000000000000000000000000000000000..4a162d3702fccc19dfef792a5213c653286930f3
--- /dev/null
+++ b/base/ca/src/com/netscape/ca/IPACustodiaKeyRetriever.java
@@ -0,0 +1,75 @@
+// --- 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.lang.Process;
+import java.lang.ProcessBuilder;
+import java.util.Collection;
+import java.util.Stack;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.ArrayUtils;
+
+import com.netscape.certsrv.apps.CMS;
+
+public class IPACustodiaKeyRetriever implements KeyRetriever {
+    public Result retrieveKey(String nickname, Collection<String> hostPorts) {
+        CMS.debug("Running IPACustodiaKeyRetriever");
+
+        Stack<String> command = new Stack<>();
+        command.push("/usr/libexec/pki-ipa-retrieve-key");
+        command.push(nickname);
+
+        for (String hostPort : hostPorts) {
+            String host = hostPort.split(":")[0];
+            command.push(host);
+            CMS.debug("About to execute command: " + command);
+            ProcessBuilder pb = new ProcessBuilder(command);
+            try {
+                Process p = pb.start();
+                int exitValue = p.waitFor();
+                if (exitValue != 0)
+                    continue;
+
+                /* Custodia returns a PEM-encoded certificate and a
+                 * base64-encoded PKIArchiveOptions containing the
+                 * wrapped private key.  These values are output by
+                 * the Python 'pki-ipa-retrieve-key' program,
+                 * separated by a null byte (password first)
+                 */
+                byte[] output = IOUtils.toByteArray(p.getInputStream());
+                int splitIndex = ArrayUtils.indexOf(output, (byte) 0);
+                if (splitIndex == ArrayUtils.INDEX_NOT_FOUND) {
+                    CMS.debug("Invalid output: null byte not found");
+                    continue;
+                }
+                return new Result(
+                    ArrayUtils.subarray(output, 0, splitIndex),
+                    ArrayUtils.subarray(output, splitIndex + 1, output.length)
+                );
+            } catch (Throwable e) {
+                CMS.debug("Caught exception while executing command: " + e);
+            } finally {
+                command.pop();
+            }
+        }
+        CMS.debug("Failed to retrieve key from any host.");
+        return null;
+    }
+}
diff --git a/base/server/CMakeLists.txt b/base/server/CMakeLists.txt
index 5a6aea96a2317655fb454967f9f218020443bcb8..9e5b27833c8d023e63320c43d64ad64b0055c254 100644
--- a/base/server/CMakeLists.txt
+++ b/base/server/CMakeLists.txt
@@ -81,6 +81,17 @@ install(
 
 install(
     DIRECTORY
+        libexec/
+    DESTINATION
+        ${LIBEXEC_INSTALL_DIR}
+    FILE_PERMISSIONS
+        OWNER_EXECUTE OWNER_WRITE OWNER_READ
+        GROUP_EXECUTE GROUP_READ
+        WORLD_EXECUTE WORLD_READ
+)
+
+install(
+    DIRECTORY
         upgrade
     DESTINATION
         ${DATA_INSTALL_DIR}/server/
diff --git a/base/server/libexec/pki-ipa-retrieve-key b/base/server/libexec/pki-ipa-retrieve-key
new file mode 100755
index 0000000000000000000000000000000000000000..a7c1396a2fce6b3572a897cdf760d5264eefaab4
--- /dev/null
+++ b/base/server/libexec/pki-ipa-retrieve-key
@@ -0,0 +1,45 @@
+#!/usr/bin/python
+
+from __future__ import print_function
+
+import ConfigParser
+import base64
+import os
+import sys
+
+from jwcrypto.common import json_decode
+
+from ipaplatform.constants import constants
+from ipaplatform.paths import paths
+from ipapython.secrets.client import CustodiaClient
+
+conf = ConfigParser.ConfigParser()
+conf.read(paths.IPA_DEFAULT_CONF)
+hostname = conf.get('global', 'host')
+realm = conf.get('global', 'realm')
+
+keyname = "ca_wrapped/" + sys.argv[1]
+servername = sys.argv[2]
+
+service = constants.PKI_GSSAPI_SERVICE_NAME
+client_keyfile = os.path.join(paths.PKI_TOMCAT, service + '.keys')
+client_keytab = os.path.join(paths.PKI_TOMCAT, service + '.keytab')
+
+client = CustodiaClient(
+    client=hostname, server=servername, realm=realm,
+    ldap_uri="ldaps://" + hostname,
+    client_servicename=service,
+    keyfile=client_keyfile, keytab=client_keytab,
+    )
+
+result_json = client.fetch_key(keyname, store=False)
+result = json_decode(result_json)
+certificate = result["certificate"]
+wrapped_key = base64.b64decode(result["wrapped_key"])
+
+# Custodia returns a PEM-encoded certificate and a base64-encoded
+# DER PKIArchiveOptions object.  Output these values, separated by a
+# null byte (certificate first), to be read by the Java
+# IPACustodiaKeyRetriever that invoked this program.
+
+print(certificate, wrapped_key, sep='\0', end='')
diff --git a/specs/pki-core.spec b/specs/pki-core.spec
index bce6bd2d298bc8e1ad5cb40618f982b5ba23b27d..509ecdafa4e5d0e651ce22497db06420abcdf259 100644
--- a/specs/pki-core.spec
+++ b/specs/pki-core.spec
@@ -1007,6 +1007,7 @@ systemctl daemon-reload
 %{_sbindir}/pki-server
 %{_sbindir}/pki-server-nuxwdog
 %{_sbindir}/pki-server-upgrade
+%{_libexecdir}/pki-ipa-retrieve-key
 %{python2_sitelib}/pki/server/
 %dir %{_datadir}/pki/deployment
 %{_datadir}/pki/deployment/config/
-- 
2.5.5



More information about the Pki-devel mailing list