CMS signing in .NET with certificate chain not in

2019-04-06 16:54发布

I have X509 certificates that are stored on the network. I can read the chain from remote windows certificate store. I need to sign some data and include chain to the signature to make it possible to validate it later.

The problem is that I can't find a way to put certificate chain to the CsmSigner. I have read that it takes certificate from constructor parameter and tries to build a chain with X509Chain.Build. It ignores Certificates list values and fails (obviously) because no certificate can be found in the local Windows cert store.

Please find below my test code (that works only if certificates were saved locally to the windows cert store)

protected byte[] SignWithSystem(byte[] data, X509Certificate2 cert, X509Certificate[] chain)
{
    ContentInfo contentInfo = new ContentInfo(data);

    SignedCms signedCms = new SignedCms(contentInfo, true);

    CmsSigner cmsSigner = new CmsSigner(cert);
    cmsSigner.DigestAlgorithm = new Oid("2.16.840.1.101.3.4.2.1"); //sha256
    cmsSigner.IncludeOption = X509IncludeOption.WholeChain;

    if (chain != null)
    {
        //adding cert chain to signer
        cmsSigner.Certificates.AddRange(chain);
        signedCms.Certificates.AddRange(chain);
    }

    signedCms.ComputeSignature(cmsSigner); //fails here with System.Security.Cryptography.CryptographicException : A certificate chain could not be built to a trusted root authority.


    byte[] signedPkcs = signedCms.Encode();
    return signedPkcs;
}

Is there any way to make it work without uploading certificates to the local store? Should I use any alternative signer?

I can try to upload certificates to the store but the problems are that

  • I have to add and remove certificates (permissions have to be granted)

  • There are several processes that applies signature so cross-process synchronization have to be added.

  • This is not that I'd like to do.

3条回答
兄弟一词,经得起流年.
2楼-- · 2019-04-06 17:28

To be clear, I am no security or cryptography expert.. but per my knowledge, for receiver to be able to validate the signature, the root certificate in the certificate chain you used for signing, must already be a trusted root for the receiver.

If the receiver does not have the root certificate already in their store, and marked as a trusted root... then doesn't matter how you sign the data.. it will fail validation on receiver end. And this is by design.

See more at Chain of trust

Hence the only real solution to your problem I see is to ensure that the root certificate is provisioned as trusted root on both ends... Typically done by a Certificate Authority.

Enterprise application scenario - Typically in an enterprise some group in IT department (who have access to all machines in the domain - like domain admins) would enable this scenario by ensuring that every computer in the domain has root certificate owned by this group, present on every machine as trusted root, and an application developer in the enterprise typically requests a new certificate for use with their application, which has the chain of trust going back to the root certificate already distributed to all machines in the domain.

Found out contact person for this group in your company, and have them issue a certificate you can use for signature.

Internet application scenario - There are established Certificate Authorities, who own their root certificates, and work with OS vendors to ensure that their root certificates are in trusted store, as the OS vendor ships the OS to it's customers. (One reason why using pirated OS can be harmful. It's not just about viruses / malware..). And that is why when you use a certificate issued by VeriSign to sign the data, the signature can be validated by most other machines in the world.

查看更多
男人必须洒脱
3楼-- · 2019-04-06 17:31

Example CMS Signing with BouncyCastle for .NET

You could use the BouncyCastle crypto library for .NET, which contains its own X509 certificate and CMS signing machinery. A lot of the examples and documentation on the web are for Java, as BouncyCastle was a Java library first. I've used the answer to this Stackoverflow question as a starting point for the certificate and key loading, and added the CMS signing. You may have to tweak parameters to produce the results you want for your use case.

I've made the signing function look approximately like yours, but note the private key is a separate parameter now.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

using Org.BouncyCastle.Cms;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.X509.Store;

class Program
{
  protected static byte[] SignWithSystem(byte[] data, AsymmetricKeyParameter key, X509Certificate cert, X509Certificate[] chain)
  {
    var generator = new CmsSignedDataGenerator();
    // Add signing key
    generator.AddSigner(
      key,
      cert,
      "2.16.840.1.101.3.4.2.1"); // SHA256 digest ID
    var storeCerts = new List<X509Certificate>();
    storeCerts.Add(cert); // NOTE: Adding end certificate too
    storeCerts.AddRange(chain); // I'm assuming the chain collection doesn't contain the end certificate already
    // Construct a store from the collection of certificates and add to generator
    var storeParams = new X509CollectionStoreParameters(storeCerts);
    var certStore = X509StoreFactory.Create("CERTIFICATE/COLLECTION", storeParams);
    generator.AddCertificates(certStore);

    // Generate the signature
    var signedData = generator.Generate(
      new CmsProcessableByteArray(data),
      false); // encapsulate = false for detached signature
    return signedData.GetEncoded();
  }

  static void Main(string[] args)
  {
    try
    {
      // Load end certificate and signing key
      AsymmetricKeyParameter key;
      var signerCert = ReadCertFromFile(@"C:\Temp\David.p12", "pin", out key);

      // Read CA cert
      var caCert = ReadCertFromFile(@"C:\Temp\CA.cer");
      var certChain = new X509Certificate[] { caCert };

      var result = SignWithSystem(
        Guid.NewGuid().ToByteArray(), // Any old data for sake of example
        key,
        signerCert,
        certChain);

      File.WriteAllBytes(@"C:\Temp\Signature.data", result);
    }
    catch (Exception ex)
    {
      Console.WriteLine("Failed : " + ex.ToString());
      Console.ReadKey();
    }
  }

  public static X509Certificate ReadCertFromFile(string strCertificatePath)
  {
    // Create file stream object to read certificate
    using (var keyStream = new FileStream(strCertificatePath, FileMode.Open, FileAccess.Read))
    {
      var parser = new X509CertificateParser();
      return parser.ReadCertificate(keyStream);
    }
  }

  // This reads a certificate from a file.
  // Thanks to: http://blog.softwarecodehelp.com/2009/06/23/CodeForRetrievePublicKeyFromCertificateAndEncryptUsingCertificatePublicKeyForBothJavaC.aspx
  public static X509Certificate ReadCertFromFile(string strCertificatePath, string strCertificatePassword, out AsymmetricKeyParameter key)
  {
    key = null;
    // Create file stream object to read certificate
    using (var keyStream = new FileStream(strCertificatePath, FileMode.Open, FileAccess.Read))
    {
      // Read certificate using BouncyCastle component
      var inputKeyStore = new Pkcs12Store();
      inputKeyStore.Load(keyStream, strCertificatePassword.ToCharArray());

      var keyAlias = inputKeyStore.Aliases.Cast<string>().FirstOrDefault(n => inputKeyStore.IsKeyEntry(n));

      // Read Key from Aliases  
      if (keyAlias == null)
        throw new NotImplementedException("Alias");
      key = inputKeyStore.GetKey(keyAlias).Key;
      //Read certificate into 509 format
      return (X509Certificate)inputKeyStore.GetCertificate(keyAlias).Certificate;
    }
  }
}

.NET CMS (Quick-fix with rest of chain omitted from signature)

I can reproduce your problem with a certificate whose root is not in the trusted certificate store, and confirm that adding the certificate chain to the cmsSigner/signedCms Certificates collection does not avoid the A certificate chain could not be built to a trusted root authority error.

You can sign successfully by setting cmsSigner.IncludeOption = X509IncludeOption.EndCertOnly;

However, if you do this, you will not get the rest of the chain in the signature. This probably isn't what you want.

As an aside, in your example you are using X509Certificate for the array of certificates in the chain, but passing them to an X509Certificate2Collection (note the "2" in there). X509Certificate2 derives from X509Certificate, but if its not actually an X509Certificate2 that you put in one of those collections, you'll get a cast error if something iterates over the collection (you don't get an error when adding a certificate of the wrong type unfortunately, because X509Certificate2Collection also derives from X509CertificateCollection and inherits its add methods).

查看更多
成全新的幸福
4楼-- · 2019-04-06 17:50

Adding sample code that creates detached PKCS7 signature using BouncyCastle (thanks to softwariness) without Certificate store.

It uses .net X509Certificate2 instances as input parameter. First certificate in collection have to be linked with private key to sign data.

Also I'd like to note that it is not possible to read private key associated with certificate from remote Windows cert store using .net X509Certificate2.PrivateKey property. By default private key is not loaded with certificate using X509Store(@"\\remotemachine\MY", StoreLocation.LocalMachine) and when X509Certificate2.PrivateKey property is accessed on local machine it fails with error "Keyset does not exist".

public void SignWithBouncyCastle(Collection<X509Certificate2> netCertificates)
{
    // first cert have to be linked with private key
    var signCert = netCertificates[0];
    var Cert = Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(signCert); 

    var data = Encoding.ASCII.GetBytes(Cert.SubjectDN.ToString());

    var bcCertificates = netCertificates.Select(_ => Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(_)).ToList();
    var x509Certs = X509StoreFactory.Create("Certificate/Collection", new X509CollectionStoreParameters(bcCertificates));

    var msg = new CmsProcessableByteArray(data);
    var gen = new CmsSignedDataGenerator();
    var privateKey = Org.BouncyCastle.Security.DotNetUtilities.GetKeyPair(signCert.PrivateKey).Private;
    gen.AddSigner(privateKey, Cert, CmsSignedDataGenerator.DigestSha256);
    gen.AddCertificates(x509Certs);

    var signature = gen.Generate(msg, false).GetEncoded();
    Trace.TraceInformation("signed");

    CheckSignature(data, signature);
    Trace.TraceInformation("checked");
    try
    {
        CheckSignature(new byte[100], signature);
    }
    catch (CryptographicException cex)
    {
        Trace.TraceInformation("signature was checked for modified data '{0}'", cex.Message);
    }
}

void CheckSignature(byte[] data, byte[] signature)
{
    var ci = new ContentInfo(data);
    SignedCms signedCms = new SignedCms(ci, true);
    signedCms.Decode(signature);
    foreach (X509Certificate cert in signedCms.Certificates)
        Trace.TraceInformation("certificate found {0}", cert.Subject);
    signedCms.CheckSignature(true);
}
查看更多
登录 后发表回答