Исключение при попытке создать самозаверяющий сертификат

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

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

Вот как я создаю pfx для первого, корневого сертификата CA.

makecert -n "CN=Root CA" -cy authority -r -a sha256 -len 2048 -sv root.pvk root.cer
pvk2pfx -pvk root.pvk -spc root.cer -pfx root.pfx -pi 123 -po 123

Чтобы создать сертификат из кода C#, я сослался на вопросы Как программно создать самозаверяющий сертификат для службы WCF? и C# Генерирует не самоподписанный клиентский запрос CX509Certificate без CA, используя certenroll.dll.

Пока у меня есть следующий код. Способ генерации сертификата:

/// <summary>
/// Generates self-signed certificate with specified subject, which will expire after specified timespan.
/// </summary>
public X509Certificate2 CreateCertificate(string subjectName, TimeSpan expirationLength, X509Certificate2 issuer = null)
{
    // create DN for subject and issuer
    var dn = new CX500DistinguishedName();
    dn.Encode("CN=" + subjectName);

    var issuerName = new CX500DistinguishedName();
    if(issuer != null)
    {
        issuerName.Encode(issuer.Subject);
    }

    var privateKey = new CX509PrivateKey
    {
        ProviderName = "Microsoft Strong Cryptographic Provider",
        Length = 2048,
        KeySpec = X509KeySpec.XCN_AT_KEYEXCHANGE,
        KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_DECRYPT_FLAG |
                       X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_KEY_AGREEMENT_FLAG,
        MachineContext = true,
        ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG
    };

    privateKey.Create();

    // Use the stronger SHA512 hashing algorithm
    var hashobj = new CObjectId();
    hashobj.InitializeFromAlgorithmName(ObjectIdGroupId.XCN_CRYPT_HASH_ALG_OID_GROUP_ID,
            ObjectIdPublicKeyFlags.XCN_CRYPT_OID_INFO_PUBKEY_ANY,
            AlgorithmFlags.AlgorithmFlagsNone, "SHA512");

    var cert = new CX509CertificateRequestCertificate();
    cert.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, privateKey, "");
    cert.Subject = dn;
    if (issuer != null)
        cert.Issuer = issuerName;
    else
        cert.Issuer = dn;
    cert.NotBefore = DateTime.Now.Date;
    cert.NotAfter = cert.NotBefore + expirationLength;
    cert.HashAlgorithm = hashobj; // Specify the hashing algorithm

    if(issuer != null)
    {
        var signerCertificate = new CSignerCertificate();
        signerCertificate.Initialize(true, X509PrivateKeyVerify.VerifyAllowUI, EncodingType.XCN_CRYPT_STRING_HEX, issuer.GetRawCertDataString());
        cert.SignerCertificate = signerCertificate; 
    }
    cert.Encode();

    // Do the final enrollment process
    var enroll = new CX509Enrollment();
    enroll.InitializeFromRequest(cert); // load the certificate
    enroll.CertificateFriendlyName = subjectName; // Optional: add a friendly name

    var csr = enroll.CreateRequest(); // Output the request in base64
    // and install it back as the response
    enroll.InstallResponse(InstallResponseRestrictionFlags.AllowUntrustedCertificate,
            csr, EncodingType.XCN_CRYPT_STRING_BASE64, ""); // no password
    // output a base64 encoded PKCS#12 so we can import it back to the .Net security classes
    var base64encoded = enroll.CreatePFX("", // no password, this is for internal consumption
            PFXExportOptions.PFXExportChainWithRoot);

    // instantiate the target class with the PKCS#12 data (and the empty password)
    return new X509Certificate2(Convert.FromBase64String(base64encoded), "",
        X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
}

Простое приложение для поиска существующего сертификата и создания нового на его основе:

static void Main(string[] args)
{
    var certificateGenerator = new CertificateGenerator();

    X509Certificate2 rootCA;

    using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
    {
        store.Open(OpenFlags.ReadWrite);

        rootCA = store.Certificates.OfType<X509Certificate2>()
            .FirstOrDefault(c => c.Subject.StartsWith("CN=Root CA", StringComparison.Ordinal));

        store.Close();
    }

    if (rootCA == null)
        throw new Exception("Can't find root CA certificate");

    var testCert = certificateGenerator.CreateCertificate("Test", new TimeSpan(3650, 0, 0, 0), rootCA);
    using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
    {
        store.Open(OpenFlags.ReadWrite);
        store.Add(testCert);
        store.Close();
    }
}

Дело в том, что он прекрасно работает, если я пытаюсь ссылаться на сертификат не в доверенных корневых центрах сертификации, а в личном (даже если у меня есть пароль в сертификате). Но если я пытаюсь создать сертификат на основе сертификата CA из доверенных корневых центров сертификации, я получаю исключение по signerCertificate.Initializeговоря

Cannot find object or property. 0x80092004 (-2146885628 CRYPT_E_NOT_FOUND)

Итак, что мне не хватает?

1 ответ

Решение

ISignerCertificate::Initialize требует, чтобы закрытый ключ был связан через Запросы или Мой магазин:

https://msdn.microsoft.com/en-us/library/windows/desktop/aa376832(v=vs.85).aspx:

Если требуется закрытый ключ, поиск выполняется только в личных хранилищах и хранилищах запросов.

Если закрытый ключ не нужен, поиск корневого и промежуточного хранилищ также выполняется.

Windows ожидает, что вы только поместите открытую часть CA в хранилища CA (промежуточный) или Root/ThirdPartyRoot, и что если вы являетесь эмитентом, он также будет установлен (с закрытым ключом) в CurrentUser\My или LocalMachine\My.

Другие вопросы по тегам