Search code examples
c#cryptographybouncycastlepkcs#12

Creating a PKCS#1 v2.1 signature with C#


I'm trying to port a reference implementation that signs a challenge using a PKCS#12 certificate from Java to C#. As far as I understand the reference, the signature should be a PKCS#1 v2.1 signature (there are few comments and no other documentation). The Java implementation uses Bouncy Castle, which would be an option for me, although if I can solve this with System libraries that would be even better.

Despite trying many things I have not managed to generate a signature that is accepted by the targeted API.

Java reference code:

private byte[] signChallenge(final byte[] challenge) {
    X509Certificate sigCert = ...;
    PrivateKey sigKey = ...;

    ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSAandMGF1").setProvider("BC").build(sigKey);
    CMSTypedData cmsData = new CMSProcessableByteArray(challenge);

    CMSSignedDataGenerator cmsGenerator = new CMSSignedDataGenerator();
    cmsGenerator.addSignerInfoGenerator(
        new JcaSignerInfoGeneratorBuilder(
            new JcaDigestCalculatorProviderBuilder().build()
        ).build(signer, sigCert)
    );
    Store certs = new JcaCertStore(Collections.singletonList(sigCert));
    cmsGenerator.addCertificates(certs);

    CMSSignedData cms = cmsGenerator.generate(cmsData, false);
    byte[] signedMessage = cms.getEncoded();
    return signedMessage;
}

First attempt to port using Bouncy Castle:

private byte[] SignChallenge(byte[] challenge)
{
    var sigCert = DotNetUtilities.FromX509Certificate(Cert);
    var sigKey = DotNetUtilities.GetRsaKeyPair(Cert.GetRSAPrivateKey()).Private;
    
    var signer = SignerUtilities.InitSigner("SHA256withRSAandMGF1", true, sigKey, new());
    var cmsData = new CmsProcessableByteArray(challenge);

    var cmsGenerator = new CmsSignedDataGenerator();
    var factory = new Asn1SignatureFactory("SHA256withRSAandMGF1", sigKey);
    cmsGenerator.AddSignerInfoGenerator(new SignerInfoGeneratorBuilder().Build(factory, sigCert));
    cmsGenerator.AddCertificate(sigCert);

    var cms = cmsGenerator.Generate(cmsData, false);
    var signedMessage = cms.GetEncoded();
    return signedMessage;
}

As you can see, this is mostly a 1:1 copy except for two lines which use Jca classes, which are Java specific. This produces a signature with the first ~30 bytes matching, but beyond that it's not matching anymore and has a different overall length with the same input (2324 bytes in Java vs 2231 in C#).

My second attempt was trying to use System.Security.Cryptography.Pkcs:

private byte[] SignChallenge(byte[] challenge)
{
    var contentInfo = new System.Security.Cryptography.Pkcs.ContentInfo(challenge);

    var signedCms = new SignedCms(contentInfo, true);
    var signer = new CmsSigner(Cert);
    signer.DigestAlgorithm = Oid.FromFriendlyName("SHA256", OidGroup.HashAlgorithm);
    signer.SignaturePadding = RSASignaturePadding.Pss;

    signedCms.ComputeSignature(signer);
    var signedMessage = signedCms.Encode();
    return signedMessage;
}

This is a lot more concise, but no luck either.


Solution

  • I was able to solve this by writing the signatures to a file and inspecting them with OpenSSL:

    openssl cms -in <signature file> -noout -cmsout -print -inform DER
    

    This showed a couple differences, the main one being my generated signatures having no signed attributes. The reference signature has contentType (1.2.840.113549.1.9.3), signingTime (1.2.840.113549.1.9.5), messageDigest (1.2.840.113549.1.9.4) and CMSAlgorithmProtection (1.2.840.113549.1.9.52).

    The first three attributes are easily added both in Bouncy Castle and System.Security.Cryptography.Pkcs, but CMSAlgorithmProtection requires special treatment. In Bouncy Castle, the attribute should be automatically generated when using a DefaultSignedAttributeTableGenerator, but the relevant code is currently commented out. I was however able to build the attribute myself:

    var signatureAlgorithm = DefaultSignatureAlgorithmFinder.Instance.Find("SHA256withRSAandMGF1");
    var algorithmProtection = new CmsAlgorithmProtection(new(new("2.16.840.1.101.3.4.2.1")), 1, signatureAlgorithm);
    var attributeTable = new Org.BouncyCastle.Asn1.Cms.AttributeTable(new Dictionary<DerObjectIdentifier, object>
    {
        [CmsAttributes.CmsAlgorithmProtect] = new Org.BouncyCastle.Asn1.Cms.Attribute(CmsAttributes.CmsAlgorithmProtect, new DerSet(algorithmProtection))
    });
    
    cmsGenerator.AddSignerInfoGenerator(
        new SignerInfoGeneratorBuilder()
            .WithSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(attributeTable))
            .Build(new Asn1SignatureFactory(algorithm, sigKey), sigCert));
    

    This code finally produced a working signature.

    I failed to produce a working signature with just System.Security.Cryptography.Pkcs. I was able to leverage the Bouncy Castle CmsAlgorithmProtection class to generate a working signed attribute, but the encoding of the signature algorithm (CMS_ContentInfo -> d.signedData -> signerInfos -> signatureAlgorithm) seems slightly different and lacks some NULL values. The Bouncy Castle solution is good enough for now though.