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)
Seems from the source code that
SignedCms.Certificates
creates a new collection every time, and this does not connect back to theSignedCms
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.