Подписание CMS in.NET с цепочкой сертификатов не в локальном доверенном хранилище сертификатов

У меня есть сертификаты X509, которые хранятся в сети. Я могу прочитать цепочку из удаленного хранилища сертификатов windows. Мне нужно подписать некоторые данные и включить цепочку в подпись, чтобы можно было проверить ее позже.

проблема в том, что я не могу найти способ поместить цепочку сертификатов в CsmSigner. Я прочитал, что он берет сертификат из параметра конструктора и пытается построить цепочку с X509Chain.Строить. Он игнорирует значения списка сертификатов и терпит неудачу (очевидно), потому что сертификат не может быть найден в локальном хранилище сертификатов Windows.

пожалуйста, найдите ниже мой тестовый код (который работает только в том случае, если сертификаты были сохранены локально в хранилище сертификатов windows)

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;
}

есть ли способ заставить его работать без загрузки сертификатов в локальное хранилище? Должен ли я использовать альтернативный signer?

Я могу попытаться загрузить сертификаты в магазин, но проблемы в том, что

  • Я должен добавить и удалить сертификаты (разрешения должны быть предоставлены)

  • существует несколько процессов, которые применяют подпись, поэтому необходимо добавить межпроцессную синхронизацию.

  • Это не то, что я хочу сделать.

3 ответов


пример подписания CMS с BouncyCastle для .NET

можно использовать после установки BouncyCastle crypto library для .NET, которая содержит свой собственный сертификат X509 и подписывающее оборудование CMS. Многие примеры и документация в интернете предназначены для Java, так как BouncyCastle была библиотекой Java в первую очередь. Я использовал ответ на этот вопрос Stackoverflow в качестве отправной точки для загрузки сертификата и ключа, а также добавлена подпись CMS. Вы можете настройка параметров для получения результатов, которые вы хотите для вашего случая использования.

я сделал функцию подписи похожей на вашу, но обратите внимание, что закрытый ключ теперь является отдельным параметром.

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 (быстрое исправление с остальной частью цепочки, опущенной из подписи)

я могу воспроизвести вашу проблему с сертификатом, корень которого не находится в доверенном хранилище сертификатов, и подтвердить, что добавление цепочки сертификатов в cmsSigner/signedCms Certificates коллекция не избегает A certificate chain could not be built to a trusted root authority ошибка.

вы можете успешно подписать, установив cmsSigner.IncludeOption = X509IncludeOption.EndCertOnly;

однако, если вы это сделаете, вы не получите остальную часть цепочки в подписи. Возможно, это не то, чего ты хочешь.

кроме того, в вашем примере вы используете X509Certificate для массива сертификатов в цепочке, но передача их в X509Certificate2Collection (обратите внимание на "2" нет). X509Certificate2 происходит от X509Certificate, но если это не на самом деле X509Certificate2 что вы поместите в одну из этих коллекций, вы получите ошибку приведения, если что-то повторяется над коллекцией (вы не получите ошибку при добавлении сертификата неправильного типа, К сожалению, потому что X509Certificate2Collection происходит от X509CertificateCollection и наследует его методы add).


добавление примера кода, который создает отдельную подпись PKCS7 с помощью BouncyCastle (благодаря softwariness) без хранилища сертификатов.

он использует экземпляры .net X509Certificate2 в качестве входного параметра. Первый сертификат в коллекции должен быть связан с закрытым ключом для подписи данных.

также я хотел бы отметить, что невозможно прочитать закрытый ключ, связанный с сертификатом из удаленного хранилища сертификатов Windows, используя .net X509Certificate2.Собственность с закрытым ключом. От закрытый ключ по умолчанию не загружается с сертификатом с помощью X509Store (@"\remotemachine\MY", StoreLocation.LocalMachine) и когда X509Certificate2.Свойство PrivateKey доступно на локальном компьютере, он терпит неудачу с ошибкой "Keyset не существует".

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);
}

чтобы быть ясным, я нет эксперт по безопасности или криптографии.. но, насколько мне известно, для того, чтобы получатель мог проверить подпись, корневой сертификат в цепочке сертификатов, которую вы использовали для подписи, должен уже быть Доверенные корневые для приемника.

Если получатель не имеет корневого сертификата уже в своем хранилище и отмечен как доверенный корень... тогда не имеет значения, как вы подписываете данные.. он не пройдет проверку на конец приемника. И это по замыслу.

на доверия

следовательно, единственное реальное решение вашей проблемы, которое я вижу, - это убедиться, что корневой сертификат подготовлен как доверенный корень с обоих концов... Обычно выполняется Сертификат.

сценарий корпоративного приложения - обычно на предприятии какая-то группа в ИТ-отделе (у кого есть доступ ко всем машинам в доменном домене администраторы) включили бы этот сценарий, гарантируя, что каждый компьютер в домене имеет корневой сертификат, принадлежащий этой группе, присутствующий на каждом компьютере как доверенный корень, и разработчик приложений на предприятии обычно запрашивает новый сертификат для использования с их приложением, которое имеет цепочку доверия, возвращающуюся к корневому сертификату, уже распространенному на все машины в домене.

выяснил Контактное лицо для этой группы в вашей компании, и они выдают сертификат можно использовать для подписи.

сценарий интернет-приложения - есть установленные центры сертификации, которые владеют своими корневыми сертификатами и работают с поставщиками ОС, чтобы гарантировать, что их корневые сертификаты находятся в доверенном хранилище, поскольку поставщик ОС отправляет ОС своим клиентам. (Одна из причин, почему использование пиратской ОС может быть вредным. Это не только вирусы и вредоносные программы..). И именно поэтому, когда вы используете сертификат, выданный VeriSign для подписания данных, подпись может быть проверена большинством других машин в мире.