Search code examples
c#x509certificatebouncycastle

C# generate intermediate certificate from self signed Root CA


I'm using Visual Studio 2019 with c# and Bouncy Castle in version 1.8.5. I was successfully able to generate a Certificate Authority (CA) and now want to generate an Intermediate Certificate. In my current workflow, the CA certificate is returned as a X509Certificate2-object that I pass over to generate the intermediate certificate. From there I want to read the PrivateKey but I have troubles doing so.

For CA generation (the CACertificateDetails is a simple class for storing strings to pass over):

public static X509Certificate2 GenerateCA(CACertificateDetails details)
{
    // generate a random number
    var random = GetSecureRandom();

    // init the certificate generator
    X509V3CertificateGenerator certificateGenerator = new X509V3CertificateGenerator();

    // serial number
    certificateGenerator.SetSerialNumber(SerialNumber(random));

    // set issuer and subject name
    var subjectDN = new X509Name(details.SubjectName);
    var issuerDN = new X509Name(details.IssuerName);
    certificateGenerator.SetIssuerDN(issuerDN);
    certificateGenerator.SetSubjectDN(subjectDN);

    // validation time
    var notBefore = DateTime.UtcNow.Date;
    var notAfter = notBefore.AddYears(details.ValidYears);
    certificateGenerator.SetNotBefore(notBefore);
    certificateGenerator.SetNotAfter(notAfter);

    // subject public Key
    AsymmetricCipherKeyPair subjectKeyPair;
    KeyGenerationParameters keyGenerationParameters = new KeyGenerationParameters(random, details.KeyStrength);
    RsaKeyPairGenerator keyPairGenerator = new RsaKeyPairGenerator();
    keyPairGenerator.Init(keyGenerationParameters);
    subjectKeyPair = keyPairGenerator.GenerateKeyPair();

    certificateGenerator.SetPublicKey(subjectKeyPair.Public);

    // set the hash algorithm
    AsymmetricCipherKeyPair issuerKeyPair = subjectKeyPair;
    ISignatureFactory signatureFactory = new Asn1SignatureFactory(PkcsObjectIdentifiers.Sha512WithRsaEncryption.ToString(), issuerKeyPair.Private, random);

    // generate the certificate
    var certificate = certificateGenerator.Generate(signatureFactory);
    var x509Certificate = new X509Certificate2(certificate.GetEncoded());

    PrivateKeyInfo info = PrivateKeyInfoFactory.CreatePrivateKeyInfo(subjectKeyPair.Private);
    Asn1Sequence seq = (Asn1Sequence)Asn1Object.FromByteArray(info.ParsePrivateKey().GetDerEncoded());
    if (seq.Count != 9)
    {
        throw new PemException("malformed sequence in RSA private key");
    }

    RsaPrivateKeyStructure rsa = RsaPrivateKeyStructure.GetInstance(seq);
    RsaPrivateCrtKeyParameters rsaparams = new RsaPrivateCrtKeyParameters(
        rsa.Modulus, rsa.PublicExponent, rsa.PrivateExponent, rsa.Prime1, rsa.Prime2, rsa.Exponent1, rsa.Exponent2, rsa.Coefficient);

    var convertedRsa = DotNetUtilities.ToRSA(rsaparams);
    var x509CertificateRet = x509Certificate.CopyWithPrivateKey(convertedRsa);
    x509CertificateRet.FriendlyName = details.FriendlyName;

    return x509CertificateRet;
}

The method to generate the intermediate certificate is as follows (the just generated CA is passed as a object now):

public static X509Certificate2 GenerateCertificate(CertificateDetails details, X509Certificate2 issuer)
{
    // generate a random number
    var random = GetSecureRandom();

    // init the certificate generator
    X509V3CertificateGenerator certificateGenerator = new X509V3CertificateGenerator();

    // serial number
    certificateGenerator.SetSerialNumber(SerialNumber(random));

    // set issuer and subject name
    var issuerDN = new X509Name(issuer.Issuer);
    certificateGenerator.SetIssuerDN(issuerDN);

    var distinguishedNames = DistinguishedName(details);
    var nameOids = new ArrayList(distinguishedNames.Select(x => x.Item1).ToArray());
    var nameValues = new ArrayList(distinguishedNames.Select(x => x.Item2).ToArray());
    var subjectDN = new X509Name(nameOids, nameValues);
    certificateGenerator.SetSubjectDN(subjectDN);

    // authority key identifier
    var authorityKeyIdentifier = new AuthorityKeyIdentifierStructure(DotNetUtilities.FromX509Certificate(issuer));
    certificateGenerator.AddExtension(X509Extensions.AuthorityKeyIdentifier.Id, false, authorityKeyIdentifier);

    // Basic Constraints - certificate is allowed to be used as intermediate.
    certificateGenerator.AddExtension(X509Extensions.BasicConstraints.Id, true, new BasicConstraints(details.IsIntermediateCertificate));

    // key usage
    if (!details.IsIntermediateCertificate)
    {
        certificateGenerator.AddExtension(X509Extensions.KeyUsage, true, new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.DataEncipherment | KeyUsage.KeyAgreement));
        certificateGenerator.AddExtension(X509Extensions.ExtendedKeyUsage, true, new ExtendedKeyUsage(new[] { KeyPurposeID.IdKPServerAuth, KeyPurposeID.IdKPServerAuth }));
    }
    else
    {
        certificateGenerator.AddExtension(X509Extensions.KeyUsage, true, new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.DataEncipherment | KeyUsage.KeyAgreement | KeyUsage.KeyCertSign));
        certificateGenerator.AddExtension(X509Extensions.ExtendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeID.AnyExtendedKeyUsage));
    }

    // validation time
    var notBefore = DateTime.UtcNow.Date;
    var notAfter = notBefore.AddYears(details.ValidYears);
    certificateGenerator.SetNotBefore(notBefore);
    certificateGenerator.SetNotAfter(notAfter);

    // subject public Key
    AsymmetricCipherKeyPair subjectKeyPair;
    KeyGenerationParameters keyGenerationParameters = new KeyGenerationParameters(random, details.KeyStrength);
    RsaKeyPairGenerator keyPairGenerator = new RsaKeyPairGenerator();
    keyPairGenerator.Init(keyGenerationParameters);
    subjectKeyPair = keyPairGenerator.GenerateKeyPair();

    certificateGenerator.SetPublicKey(subjectKeyPair.Public);

    // set the hash algorithm
    var issuerPrivateKey = TransformRSAPrivateKey(issuer.PrivateKey);
    ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA3-512withRSA", issuerPrivateKey, random);

    // generate the certificate
    var certificate = certificateGenerator.Generate(signatureFactory);

    PrivateKeyInfo info = PrivateKeyInfoFactory.CreatePrivateKeyInfo(subjectKeyPair.Private);
    var x509Certificate = new X509Certificate2(certificate.GetEncoded());

    Asn1Sequence seq = (Asn1Sequence)Asn1Object.FromByteArray(info.ParsePrivateKey().GetDerEncoded());
    if (seq.Count != 9)
    {
        throw new PemException("Malformed sequence in RSA private key");
    }

    RsaPrivateKeyStructure rsa = RsaPrivateKeyStructure.GetInstance(seq);
    RsaPrivateCrtKeyParameters rsaparams = new RsaPrivateCrtKeyParameters(
        rsa.Modulus, rsa.PublicExponent, rsa.PrivateExponent, rsa.Prime1, rsa.Prime2, rsa.Exponent1, rsa.Exponent2, rsa.Coefficient);

    x509Certificate.PrivateKey = DotNetUtilities.ToRSA(rsaparams);
    x509Certificate.FriendlyName = details.FriendlyName;

    return x509Certificate;
}

I now fail at function TransformRSAPrivateKey in line prov.ExportParameters(true) probably because there is no private key data found, which I find strange due to the fact that I copied it with x509Certificate.CopyWithPrivateKey(convertedRsa) in the CA generation.

private static AsymmetricKeyParameter TransformRSAPrivateKey(AsymmetricAlgorithm privateKey)
{
    RSACryptoServiceProvider prov = privateKey as RSACryptoServiceProvider;
    RSAParameters parameters = prov.ExportParameters(true);

    return new RsaPrivateCrtKeyParameters(
        new BigInteger(1, parameters.Modulus),
        new BigInteger(1, parameters.Exponent),
        new BigInteger(1, parameters.D),
        new BigInteger(1, parameters.P),
        new BigInteger(1, parameters.Q),
        new BigInteger(1, parameters.DP),
        new BigInteger(1, parameters.DQ),
        new BigInteger(1, parameters.InverseQ));
}

How can I proceed to use the CA to generate (and sign) my Intermediate Certificate? I do not want to pass the AsymmetricKeyParameter as seen in multiple examples here on stackoverflow. At some point I want to maybe save the CA to the disk and read it at another time to generate a certificate with the same CA. At that time I won't have the CA generation workflow and therefore not the AsymmetricKeyParameter object.


Solution

  • Okay the issue was in these lines:

    // set the hash algorithm
    var issuerPrivateKey = TransformRSAPrivateKey(issuer.PrivateKey);
    ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA3-512withRSA", issuerPrivateKey, random);
    

    One can pass the X509Certificate2 issuer certificate to the GenerateCertificate function as in the original post. The trick is to load that certificate with the flag X509KeyStorageFlags.Exportable, like X509Certificate2(certificatePath, password, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);.

    Further I changed my code to

    // set the hash algorithm
    var issuerPrivateKey = DotNetUtilities.GetKeyPair(issuerCA.PrivateKey).Private;
    ISignatureFactory signatureFactory = new Asn1SignatureFactory(PkcsObjectIdentifiers.Sha512WithRsaEncryption.ToString(), issuerPrivateKey, random);