Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Invalid signature when creating a certificate using BouncyCastle with an external Azure KeyVault (HSM) Key

I'm trying to generate a certificate self-signed by a KeyPair stored in Azure KeyVault.

My end result is a certificate with an invalid signature:

enter image description here

Generating the certificate parameters:

     DateTime startDate = DateTime.Now.AddDays(-30);
     DateTime expiryDate = startDate.AddYears(100);

     BigInteger serialNumber = new BigInteger(32, new Random());
     X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();

     X509Name selfSignedCA = new X509Name("CN=Test Root CA");

     certGen.SetSerialNumber(serialNumber);
     certGen.SetIssuerDN(selfSignedCA); //Self Signed
     certGen.SetNotBefore(startDate);
     certGen.SetNotAfter(expiryDate);
     certGen.SetSubjectDN(selfSignedCA);
      

Fetching a reference to the Azure KeyVault stored key (HSM like service):

    //Create a client connector to Azure KeyVault
    var keyClient = new Azure.Security.KeyVault.Keys.KeyClient(
         vaultUri: new Uri("https://xxxx.vault.azure.net/"),
         credential: new ClientSecretCredential(
             tenantId: "xxxx", //Active Directory
             clientId: "xxxx", //Application id?
             clientSecret: "xxxx"
             )
         );

        var x = keyClient.GetKey("key-new-ec"); //Fetch the reference to the key

The key is successfully retrieved. I then try to generate a ECPublicKeyParameters object with the key's public data:

    X9ECParameters x9 = ECNamedCurveTable.GetByName("P-256");
    Org.BouncyCastle.Math.EC.ECCurve curve = x9.Curve;

    var ecPoint = curve.CreatePoint(new Org.BouncyCastle.Math.BigInteger(1, x.Value.Key.X), new Org.BouncyCastle.Math.BigInteger(1, x.Value.Key.Y));
    ECDomainParameters dParams = new ECDomainParameters(curve, ecPoint, x9.N);
    ECPublicKeyParameters pubKey = new ECPublicKeyParameters(ecPoint, dParams);

    certGen.SetPublicKey(pubKey); //Setting the certificate's public key with the fetched one

Next step is generating a certificate signed with the key. I implemented a new ISignatureFactory object that should sign with an external signature function of KeyVault:

      AzureKeyVaultSignatureFactory customSignatureFactory = new AzureKeyVaultSignatureFactory(1);
      Org.BouncyCastle.X509.X509Certificate cert = certGen.Generate(customSignatureFactory);

This is my custom AzureKeyVaultSignatureFactory:

public class AzureKeyVaultSignatureFactory : ISignatureFactory
{
    private readonly int _keyHandle;

    public AzureKeyVaultSignatureFactory(int keyHandle)
    {
        this._keyHandle = keyHandle;
    }

    public IStreamCalculator CreateCalculator()
    {
        var sig = new CustomAzureKeyVaultDigestSigner(this._keyHandle);

        sig.Init(true, null);

        return new DefaultSignatureCalculator(sig);
    }

    internal class CustomAzureKeyVaultDigestSigner : ISigner
    {
        private readonly int _keyHandle;
        private byte[] _input;

        public CustomAzureKeyVaultDigestSigner(int keyHandle)
        {
            this._keyHandle = keyHandle;
        }

        public void Init(bool forSigning, ICipherParameters parameters)
        {
            this.Reset();
        }

        public void Update(byte input)
        {
            return;
        }

        public void BlockUpdate(byte[] input, int inOff, int length)
        {
            this._input = input.Skip(inOff).Take(length).ToArray();
        }

        public byte[] GenerateSignature()
        {
            //Crypto Client (Specific Key)
            try
            {

                //Crypto Client (Specific Key)
                CryptographyClient identitiesCAKey_cryptoClient = new CryptographyClient(
                    keyId: new Uri("https://xxxx.vault.azure.net/keys/key-new-ec/xxxx"),
                    credential: new ClientSecretCredential(

                          tenantId: "xxxx", //Active Directory
                          clientId: "xxxx", //Application id?
                          clientSecret: "xxxx"
                          )
                );

                SignResult signResult = identitiesCAKey_cryptoClient.SignData(SignatureAlgorithm.ES256, this._input);
                return signResult.Signature;


            }
            catch (Exception ex)
            {

                throw ex;
            }

            return null;
        }

        public bool VerifySignature(byte[] signature)
        {
            return false;
        }

        public void Reset() { }

        public string AlgorithmName => "SHA-256withECDSA";
    }

    public object AlgorithmDetails => new AlgorithmIdentifier(X9ObjectIdentifiers.ECDsaWithSha256, DerNull.Instance);
}

Then I convert and write the certificate to a file:

 //convert to windows type 2 and get Base64 
 X509Certificate2 cert2 = new X509Certificate2(DotNetUtilities.ToX509Certificate(cert));
 byte[] encoded = cert2.GetRawCertData();
 string certOutString = Convert.ToBase64String(encoded);
 System.IO.File.WriteAllBytes(@"test-signed2.cer", encoded); //-this is good!

What am I doing wrong? Maybe constructing the ECCurve from X/Y is not enough?

Thanks!

like image 448
NOP-MOV Avatar asked Aug 05 '20 15:08

NOP-MOV


1 Answers

The problem is that the signature being returned by key vault is in a "raw" (64-byte) format, where the first 32 are R and the last 32 are S. For this to work in bouncycastle, your GenerateSignature method needs to return this in an ASN.1 formatted byte array, which in the end will be somewhere between 70 and 72 bytes.

You can look around online on what this practically means, but you will want to:

  1. Create a new byte array for your result
  2. split the output from key vault into two initially 32-bit arrays, R and S
  3. If the 0th element of either of the R or S arrays has a high MSB, you need to insert a 0 before the start of the respective array (otherwise do nothing and the array stays 32 bytes long).
  4. Build the necessary ASN.1 headers (either manually like I showed below, or maybe bouncycastle has some library features to create an ASN.1 message). So at the end, the output byte array should contain
0x30
one byte containing the length of the rest of the array*
0x02
a byte containing the length of the R array (either 32 or 33 depending on if + or -)
0x02
a byte containing the length of the S array (either 32 or 33 depending on if + or -)
the entire S array

  1. Return this array as the output of GenerateSignature

* so the entire length will be length of R + length of S + 4 header bytes (R length, R header, S length, S header)

I have tested this approach with a key of my own as returned by a cloud service which also returns the 64 byte R+S response and it works.

like image 105
WutIsWrongWithMyCode Avatar answered Nov 19 '22 03:11

WutIsWrongWithMyCode