Including other certificates when signing using SignedCms and CmsSigner on .NET C#

202 Views Asked by At

I'm trying to implement the following signing operation using C# .NET APIs.

$ openssl smime -binary -sign -certfile WWDR.pem -signer signerCertWithKey.pem -in manifest.json -out signature -outform DER -passin pass:<signerKey passphrase> (adapter from the very last step described at https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates).

My code is something like (please see the comments in the code):

    static byte[] GetSignature(X509Certificate2 x509,
        byte[] dataToSign, IList<X509Certificate2>? certs=default)
    {

        ContentInfo contentInfo = new(dataToSign);
        SignedCms cms = new(contentInfo, true); // A detached SignedCms message.
        
        // Sign the message.
        CmsSigner cmsSigner = new(x509);
        
        if (certs != default)
        {
            foreach (var cert in certs)
            {
    /* This part is where I have doubt. What's the correct way to add the 
    additional certificates (that cert doesn't include any private key) 
    corresponding to what's specified by the "-certfile" flag of 
    the openssl command shown above?
    */
                cms.AddCertificate(cert); // <-- Should it be added like this
                // cmsSigner.Certificates.Add(cert); // <--- OR like this

                /* In both the cases I get an error complaining about the
                missing private key in the certificate.*/
            }
        }
        cms.ComputeSignature(cmsSigner);

        /* Produces a CMS/PKCS #7 message containing the signature.*/
        return cms.Encode();

    }

Any pointer to the right direction will be greatly appreciated.

UPDATE: Following is the complete code to help reproduce this issue.

    using System.Security.Cryptography.Pkcs;
    using System.Security.Cryptography.X509Certificates;

    namespace Foo.Common;

    public class FooUtil
    {

        public static byte[] GetSignature(string certPemFile,
            byte[] dataToSign, ReadOnlySpan<char> password,
            string? keyFilePath = default, IList<string>? moreCertPemFiles = default)
        {
            var x509 = X509Certificate2.CreateFromEncryptedPemFile(certPemFile,
            password, keyFilePath);

            List<X509Certificate2> certs = new(); // Holds additional certificates
            if (moreCertPemFiles != default)
            {
                foreach (var item in moreCertPemFiles)
                {
                    certs.Add(X509Certificate2.CreateFromPemFile(item));
                }
            }

            // The dataToSign byte array holds the data to be signed.
            ContentInfo contentInfo = new(dataToSign);
            // Create a new, detached SignedCms message.
            SignedCms signedCms = new(contentInfo, true);
            if (certs != default) // Attach additional certificates
            {
                foreach (var cert in certs)
                {
                    signedCms.AddCertificate(cert);
                }
            }
            // Sign the message.
            CmsSigner cmsSigner = new(x509);
            signedCms.ComputeSignature(cmsSigner);

            // Produces a CMS/PKCS #7 message containing the signature.
            return signedCms.Encode();
        }

    }

Unit test to run the above method:

    namespace Foo.Common.Tests;

    using System.Text;
    using Foo.Common;
    using NUnit.Framework;

    public class FooTests
    {

        [Test]
        public void TestSignatures()
        {
            byte[] data1 = Encoding.ASCII.GetBytes("This is a test!");
            byte[] data2 = Encoding.ASCII.GetBytes("This is a test!");
            byte[] data3 = Encoding.ASCII.GetBytes("Yet another test!");
            string certPem = "/home/xxx/src/mycert.pem";
            List<string> certList = new()
            {
                "/home/xxx/src/wwdr.pem"
            };
            byte[] sign1 = FooUtil.GetSignature(certPem, data1, 
                "0123456789", null, certList);
            byte[] sign2 = FooUtil.GetSignature(certPem, data2, 
                "0123456789", null, certList);
            byte[] sign3 = FooUtil.GetSignature(certPem, data3, 
                "0123456789", null, certList);
            File.WriteAllBytes("/home/xxx/sign3.p7b", sign3);

            Assert.That(sign1.Length > 10);
            Assert.That(sign2.Length > 10);
            Assert.That(sign3.Length > 10);
            Assert.That(sign1, Is.EqualTo(sign2));
            Assert.That(sign2, !Is.EqualTo(sign3));
        }

    }

Exception message:

Starting test run
[Failed] TestSignatures
    Message:
        System.Security.Cryptography.CryptographicException : The key contents do not contain a PEM, the content is malformed, or the key does not match the certificate.
    Stack Trace:
        at System.Security.Cryptography.X509Certificates.X509Certificate2.ExtractKeyFromPem[TAlg](ReadOnlySpan'1 keyPem, String[] labels, Func'1 factory, Func'2 import)
        at System.Security.Cryptography.X509Certificates.X509Certificate2.CreateFromPem(ReadOnlySpan'1 certPem, ReadOnlySpan'1 keyPem)
        at Foo.Common.FooUtil.GetSignature(String certPemFile, Byte[] dataToSign, ReadOnlySpan'1 password, String keyFilePath, IList'1 moreCertPemFiles) in /home/xxx/src/FooUtil.cs:line 21
        at Foo.Common.Tests.FooTests.TestSignatures() in /home/xxx/Tests/FooTest.cs:line 21
        at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
        at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)

==== Summary ====
Failed!  - Failed:    1, Passed:    0, Skipped:    0, Total:    1, Duration: 293ms

Error message when using the following (as suggested by @Charlieface in comments):

cmsSigner.IncludeOption = X509IncludeOption.WholeChain;
            foreach (var cert in certs)
            {
                // signedCms.AddCertificate(cert);
                cmsSigner.Certificates.Add(cert);
            }

[Failed] TestSignatures Message: System.Security.Cryptography.CryptographicException : The key contents do not contain a PEM, the content is malformed, or the key does not match the certificate. Stack Trace: at System.Security.Cryptography.X509Certificates.X509Certificate2.ExtractKeyFromPem[TAlg](ReadOnlySpan'1 keyPem, String[] labels, Func'1 factory, Func'2 import) at System.Security.Cryptography.X509Certificates.X509Certificate2.CreateFromPem(ReadOnlySpan'1 certPem, ReadOnlySpan'1 keyPem) at Foo.Common.FooUtil.GetSignature(String certPemFile, Byte[] dataToSign, ReadOnlySpan'1 password, String keyFilePath, IList'1 moreCertPemFiles)

1

There are 1 best solutions below

2
On

Seems from the source code that SignedCms.Certificates creates a new collection every time, and this does not connect back to the SignedCms object.

Instead use SignedCms.AddCertificate.

That function was added through this GitHub issue, exactly why the original function was not modified instead is not clear.