Search code examples
javacryptographybouncycastlehardware-security-module

Wrap-unwrap of private key using EC master key and Bouncy Castle


I want to wrap a private key out of a HSM, using an external EC key pair (master key) and then verify that I can recover it.
The wrapping occurs as follow:

  1. Generate a secret AES key in the HSM, using the public part of the EC master key, the private part of the internal key pair and a derivation mechanism CKM_ECDH1_DERIVE. The derivation parameters for this mechanism are: a derivation function CKD_SHA256_KDF, shared data and public data (public data are taken from the public EC master key).
  2. Wrap the private key, using the secret AES key and a mechanims such as CKM_AES_GCM, CKM_AES_KEY_WRAP_PAD or CKM_AES_CBC_PAD.
  3. The HSM returns a byte array.

Then I would like to verify if the wrapped private is the expected one.

I know how to decrypt the private key once I have recovered the secret key used ot protect it. Because it is not like RSA, I have to derive the same secret key using some elements I have, but I don't know how to do this with BC.
I'm trying to use something like this, trying to find an concrete implementation of AlgorithmParamSpec:

KeyAgreement agreement = KeyAgreement.getInstance("ECCDHwithSHA256CKDF", "BC");
agreement.init(externalEcMasterKey.getPrivate(), someAlgorithmParamSpec);
agreement.doPhase(internalEcKeyPair.asJavaPublic(), true);
SecretKey agreedKey = agreement.generateSecret("AES[256]");

Unfortunately, with UserKeyingMaterialSpec for example, it returns a different key at each time, which is not what I want :)

Thanks in advance


Solution

  • In the HSM response, I have:

    • A byte array which is the public key of the ephemeral EC transport key generated from my long-term EC master key: transportPublicKey;
    • A byte array which is the private key wrapped by the session key: privateKeyWrappedBySessionKey;

    To decrypt the private key:

    1. Generate the same session key, using the derivation mechanism CKM_ECDH1_DERIVE and the derivation function CKD_SHA256_KDF, the private part of the EC master key, shared data (which can be null) and the public key of the ephemeral EC transport key:
    KeyAgreement recipientAgreement = KeyAgreement.getInstance(
                        "ECDHWithSHA256KDF",
                        BouncyCastleProvider.PROVIDER_NAME);
    recipientAgreement.init(recipientPrivateKey,
                            new UserKeyingMaterialSpec(sharedData));
    recipientAgreement.doPhase(transportPublicKey), true);            
    final byte[] secret = recipientAgreement.generateSecret();
    SecretKeySpec sharedSecret = new SecretKeySpec(secret, "AES");         
    
    1. Decrypt the private key with the session key and the appropriate symmetric algorithm:
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7PADDING");
    final InitializationVectorParameters initializationVectorParameters = new InitializationVectorParameters(new byte[16]);
    byte[] ivb = initializationVectorParameters.getInitializationVector();
    cipher.init(Cipher.DECRYPT_MODE, sharedSecret, new IvParameterSpec(ivb));
    byte[] decryptedPrivateKey = cipher.doFinal(privateKeyWrappedBySessionKey);         
    
    PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(decryptedPrivateKey);            
    KeyFactory keyFactory = KeyHelper.keyFactory(wrappedPrivateKeyType);
    PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
    

    Additional information:

    • In case of CKD_NULL derivation function, AlgorithmParameterSpec MUST NOT be provided, as stated in PKCS#11

    (§ 2.3.20) With the key derivation function CKD_NULL, pSharedData must be NULL and ulSharedDataLen must be zero.

    recipientAgreement.init(recipientPrivateKey);
    
    • In case of a session key with size shorter than 256 bits, only the keySize / 8 first bits of the generated secret have to be used:

    (§ 2.3.20) ... and gets the first ulAESKeyBits bits of the derived key to be the temporary AES key.

    • In the case of CKM_AES_KEY_WRAP or CKM_AES_KEY_WRAP_PAD mechanisms, no initialization vector is required:
    cipher.init(Cipher.DECRYPT_MODE, sharedSecret);
    
    • Note that, at the moment, this does not work with CKD_NULL and session key size other than 256 bits.