bcgit / bc-java

Bouncy Castle Java Distribution (Mirror)
https://www.bouncycastle.org/java.html
MIT License
2.23k stars 1.1k forks source link

Feature Request: Private Key Offloading #1715

Open dlakhaws opened 3 weeks ago

dlakhaws commented 3 weeks ago

Is their any guidance or feature for private key offloading in BouncyCastle? (similar to this in s2n-tls)

If not, are there any best practices/guidance for submitting a PR to implement my own signing method (i.e. if I wanted to use KMS sign in the JcaTlsRSASigner class)?

peterdettman commented 3 weeks ago

If you want to use BCJSSE and not just the low-level TLS API, then your signing method needs to integrate with the JCE Provider mechanism, providing suitable PrivateKey, PublicKey and Signature implementations.

So I guess you want to be looking at https://github.com/aws-samples/aws-kms-jce . That will let you get a KeyStore, with which you can create a KeyManagerFactory to init an SSLContext. I think there may still be issues around algorithm names, but we can cross that bridge if you get that far.

If for some reason you have to use the low-level TLS API, then the Provider is still the simplest option, but you would also have the possibility to just implement your own TlsCredentialedSigner (to use instead of JcaDefaultTlsCredentialedSigner or BcDefaultTlsCredentialedSigner).

dlakhaws commented 3 weeks ago

Appreciate the response! I believe this will have to be a low-level TLS API implementation (that also uses the Private and PublicKey you linked). Here is the current version of the implementation that I have working:

Inside of BouncyCastleJsseProvider.java

    // create a new constructor for the provider that takes the kms client and key as it's parameters
    public BouncyCastleJsseProvider(final KmsClient kmsClient, final String mtlsKey)
    {
        this(getPropertyValue(JSSE_CONFIG_PROPERTY, "default"));
        this.KMS_CLIENT = kmsClient;
        this.MTLS_KEY = mtlsKey;
    }

Inside of JcaTlsRSASigner.java

    import software.amazon.awssdk.core.SdkBytes;
    import software.amazon.awssdk.services.kms.KmsClient;
    import software.amazon.awssdk.services.kms.jce.provider.rsa.KmsRSAKeyFactory;
    import software.amazon.awssdk.services.kms.jce.provider.signature.KmsSignature;
    import software.amazon.awssdk.services.kms.jce.provider.signature.KmsSigningAlgorithm;
    import software.amazon.awssdk.services.kms.model.MessageType;
    import software.amazon.awssdk.services.kms.model.SignRequest;
    ...
    public byte[] generateRawSignature(SignatureAndHashAlgorithm algorithm, byte[] hash) throws IOException
    {
        try
        {
            Signature signer = getRawSigner();

            byte[] input;
            byte[] kmsSignature = null;
            if (algorithm != null)
            {
                if (algorithm.getSignature() != SignatureAlgorithm.rsa)
                {
                    throw new IllegalStateException("Invalid algorithm: " + algorithm);
                }

                /*
                 * RFC 5246 4.7. In RSA signing, the opaque vector contains the signature generated
                 * using the RSASSA-PKCS1-v1_5 signature scheme defined in [PKCS1].
                 */
                AlgorithmIdentifier algID = new AlgorithmIdentifier(
                    TlsUtils.getOIDForHashAlgorithm(algorithm.getHash()), DERNull.INSTANCE);
                input = new DigestInfo(algID, hash).getEncoded();

                // Get Signature using KMS
                log.debug("Getting signature using KMS");
                kmsSignature = getKmsSignature(hash, input);
            }
            else
            {
                /*
                 * RFC 5246 4.7. Note that earlier versions of TLS used a different RSA signature
                 * scheme that did not include a DigestInfo encoding.
                 */
                input = hash;
            }

            signer.update(input, 0, input.length);

            return kmsSignature;
        }
    ...
    protected byte[] getKmsSignature(byte[] hash, byte[] input) {
        final KmsClient kmsClient = BouncyCastleJsseProvider.KMS_CLIENT;
        final String mtlsKeyId = BouncyCastleJsseProvider.MTLS_KEY;
        KeyPair keyPair = KmsRSAKeyFactory.getKeyPair(kmsClient, mtlsKeyId);
        KmsSigningAlgorithm signingAlgorithm = <signing algo>;
        KmsSignature sigSpi = new KmsSignature(kmsClient, signingAlgorithm);
        sigSpi.engineInitSign(keyPair.getPrivate());
        sigSpi.engineUpdate(hash, 0, hash.length);

        SignRequest signRequest = SignRequest.builder()
            .keyId(mtlsKeyId)  
            .messageType(MessageType.DIGEST)
            .signingAlgorithm(signingAlgorithm.getSigningAlgorithmSpec())
            .message(SdkBytes.fromByteArray(hash))
            .build();
        SdkBytes signature = kmsClient.sign(signRequest).signature();
        log.debug("Signed using KMS Client");
        return signature.asByteArray();
    }

If I were to move all of this to my own TlsCredentialedSigner how do I tell BouncyCastle to use the new TlsCredentialedSigner class? (and given my implementation is there a better way to do this so I don't have to maintain a fork of BC?)

peterdettman commented 3 weeks ago

So, just to be clear, by "just the low-level TLS API" I meant not using BCJSSE at all (nothing in org.bouncycastle.jsse.* including the Provider). It appears that you are happy to use BCJSSE though.

The standard way to use any HSM or other opaque key manager is to get a KeyStore with your HSM key in it (it appears aws-kms-jce can give you this directly) and use this KeyStore to configure the KeyManagerFactory you initialize your BCJSSE SSLContext with.

Now ideally everything just works. When BCJSSE needs, say, an RSA (or maybe PSS) signature it consults the KeyManager and finds your PrivateKey. Skipping the details, this key ultimately is used to configure a JcaDefaultTlsCredentialedSigner, which will create e.g. a JcaTlsRSASigner and that will do the signing.

There is some JCE magic involved that means the Signature implementation will automatically forward to the Provider that sourced the PrivateKey, so that the HSM will do the signing.

The usual way this can fail is when an HSM Provider doesn't understand the Signature algorithm that BCJSSE is asking for. I called this out as a possible issue because I didn't immediately see "NoneWithRSA" in the list. Also I saw e.g. "RSASSA-PSS/SHA256" but we ask for "SHA256WITHRSAANDMGF1". These sorts of issues would ideally be addressed by expanding the aliases understood by the HSM Provider.

It's not currently possible (short of building your own) to tell BCJSSE to use something other than JcaDefaultTlsCredentialedSigner, but I think it would be nice if that hardcoded new was changed to a factory method call on JcaTlsCrypto - and you can already configure your own JcaTlsCrypto subclass via BouncyCastleJsseProvider(bool, JcaTlsCryptoProvider) constructor. A PR along those lines would be welcome.

dlakhaws commented 3 weeks ago

The standard way to use any HSM or other opaque key manager is to get a KeyStore with your HSM key in it (it appears aws-kms-jce can give you this directly) and use this KeyStore to configure the KeyManagerFactory you initialize your BCJSSE SSLContext with.

Agreed, but what if I cannot store the key in the key store due to certain requirements (aka I don't have access to the asymmetric key after it's stored in KMS HSM)? I need to only use the KMS SIGN call to create a signature using an asymmetric key stored in KMS (which I can't retrieve and store in a keystore).

This is the specific line that I have right now working kmsSignature = getKmsSignature(hash, input); (and my custom getKmsSignature function calls KMS SIGN and returns the signature) but this is in the JcaTlsRSASigner class itself.

peterdettman commented 3 weeks ago

When using an HSM (or KMS), the JCE PrivateKey will only be a handle, basically holding a key identifier of some sort, not the actual key. So it's perfectly fine to have it in a KeyStore and so on.

dlakhaws commented 3 weeks ago

Thanks for the response! Do you have an example of uploading a key identifier (in our case a KMS key UUID) to the jks?

if that hardcoded new was changed to a factory method call on JcaTlsCrypto - and you can already configure your own JcaTlsCrypto subclass via BouncyCastleJsseProvider(bool, JcaTlsCryptoProvider) constructor

Is the recommendation here to implement our own JcaTlsCrypto custom class (any examples for this?) and inside of their, implement createStreamSigner that would do the KMS Sign call and return the signature?

peterdettman commented 3 weeks ago

Do you have an example of uploading a key identifier (in our case a KMS key UUID) to the jks?

You don't need to do that. Please look into using https://github.com/aws-samples/aws-kms-jce . That page gives the following example for getting a KeyStore:

KeyStore keyStore = KeyStore.getInstance("KMS");
keyStore.load(null, null);
...
keyStore.aliases();
keyStore.containsAlias(...);
keyStore.size();
keyStore.getKey(...);

(You of course need to setup the KMS Provider first as explained earlier on the page).


Is the recommendation here to implement our own JcaTlsCrypto custom class (any examples for this?) and inside of their, implement createStreamSigner that would do the KMS Sign call and return the signature?

Well, I recommend the approach above be tried first, because it's the standard way to use managed keys with Java applications within the Provider framework.

This second option would be:

Then you are free to implement the signing in your TlsCredentialedSigner implementation however you want.