2013-08-20 33 views
13

我有一个在本地系统帐户下作为Windows服务运行的自托管WCF服务器。我正在尝试在c#中以编程方式创建自签名证书,以便与使用消息级安全性的net.tcp端点一起使用。如何以编程方式为WCF服务创建自签名证书?

我正在使用以下代码,它非常接近于How to create a self-signed certificate using C#?中接受的答案,并尝试解决我的问题。

public static X509Certificate2 CreateSelfSignedCertificate(string subjectName, TimeSpan expirationLength) 
{ 
    // create DN for subject and issuer 
    var dn = new CX500DistinguishedName(); 
    dn.Encode("CN=" + subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE); 

    CX509PrivateKey privateKey = new CX509PrivateKey(); 
    privateKey.ProviderName = "Microsoft Strong Cryptographic Provider"; 
    privateKey.Length = 1024; 
    privateKey.KeySpec = X509KeySpec.XCN_AT_KEYEXCHANGE; 
    privateKey.KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_DECRYPT_FLAG | X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_KEY_AGREEMENT_FLAG; 
    privateKey.MachineContext = true; 
    privateKey.ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_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, "SHA1"); 

    // Create the self signing request 
    var cert = new CX509CertificateRequestCertificate(); 
    cert.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, privateKey, ""); 
    cert.Subject = dn; 
    cert.Issuer = dn; // the issuer and the subject are the same 
    cert.NotBefore = DateTime.Now.Date; 
    // this cert expires immediately. Change to whatever makes sense for you 
    cert.NotAfter = cert.NotBefore + expirationLength; 
    //cert.X509Extensions.Add((CX509Extension)eku); // add the EKU 
    cert.HashAlgorithm = hashobj; // Specify the hashing algorithm 
    cert.Encode(); // encode the certificate 

    // Do the final enrollment process 
    var enroll = new CX509Enrollment(); 
    enroll.InitializeFromRequest(cert); // load the certificate 
    enroll.CertificateFriendlyName = subjectName; // Optional: add a friendly name 
    string 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 System.Security.Cryptography.X509Certificates.X509Certificate2(
     System.Convert.FromBase64String(base64encoded), "", 
     // mark the private key as exportable (this is usually what you want to do) 
     // mark private key to go into the Machine store instead of the current users store 
     X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet 
    ); 
} 

,我又把它存储与此代码:

X509Store store = new X509Store(storeName, StoreLocation.LocalMachine); 
store.Open(OpenFlags.ReadWrite); 
store.Add(newCert); 
store.Close(); 

这将创建证书,并把它在LOCALMACHINE证书存储区。问题是,当我尝试启动WCF服务时,出现以下异常:

证书'CN = myCertificate'可能没有可进行密钥交换的私钥,或者该进程可能没有私钥的访问权限。详情请参阅内部例外。 内部异常:键集不存在

我的证书FindPrivateKey样品(http://msdn.microsoft.com/en-us/library/aa717039%28v=vs.100%29.aspx)的输出是:

Private key directory: 
C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys 
Private key file name: 
f0d47c7826b8ef5148b6d412f1c40024_4a8a026f-58e4-40f7-b779-3ae9b6aae1a7 

我可以看到在资源管理器这个1.43KB文件。如果我查看属性|安全性,我将看到SYSTEM和Administrators都完全控制。

在研究这个错误我见过许多关于私钥丢失或权限不正确的答案。我看不出有什么问题。

真奇怪的是,如果我使用mmc证书插件,请转到证书并选择所有任务|管理私钥......我看到相同的安全设置。即使我弹出对话框并点击“取消”按钮,在查看完这些后,证书现在可以在WCF中正常工作。我可以简单地重新启动服务,一切都运行完美。

如果我使用MakeCert创建证书,它从一开始就工作得很好。我不知道它有什么不同。

另一个可能不相关的信息是,该证书不仅被放到My Store中,我告诉它放进去,但它也被放入“中级认证机构”商店。我不知道为什么或者是否重要。

所以......任何想法我做错了什么?

更新:好吧,这不仅仅是一个WCF问题。当我尝试使用HttpSetServiceConfiguration使用证书绑定到使用http.sys的端点时,我基本上遇到同样的问题。该方法返回1312 - “指定的登录会话不存在,它可能已被终止”。这实际上不是真正的错误。我在安全事件日志看到的审计失败是这样说:

Cryptographic Parameters: 
    Provider Name: Microsoft Software Key Storage Provider 
    Algorithm Name: Not Available. 
    Key Name: {A23712D0-9A7B-4377-89DB-B1B39E3DA8B5} 
    Key Type: Machine key. 

Cryptographic Operation: 
    Operation: Open Key. 
    Return Code: 0x80090011 

0x80090011是未找到对象。所以这似乎是同样的问题。再次,我打开证书的管理私钥对话框后,这也很完美。

我仍在寻找问题的原因。

更新#2:我能够使用下面接受的答案得到这个工作。有趣的是,这段代码现在似乎将证书放入机器存储中,而不调用X509Store代码。我仍然称这些代码是因为我不确定,它不会伤害任何东西。这是我用来创建证书的最终代码。

static public X509Certificate2 CreateSelfSignedCertificate(string subjectName, TimeSpan expirationLength) 
    { 
     // create DN for subject and issuer 
     var dn = new CX500DistinguishedName(); 
     dn.Encode("CN=" + subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE); 

     CX509PrivateKey privateKey = new CX509PrivateKey(); 
     privateKey.ProviderName = "Microsoft Strong Cryptographic Provider"; 
     privateKey.Length = 2048; 
     privateKey.KeySpec = X509KeySpec.XCN_AT_KEYEXCHANGE; 
     privateKey.KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_DECRYPT_FLAG | X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_KEY_AGREEMENT_FLAG; 
     privateKey.MachineContext = true; 
     privateKey.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"); 

     // Create the self signing request 
     var cert = new CX509CertificateRequestCertificate(); 
     cert.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, privateKey, ""); 
     cert.Subject = dn; 
     cert.Issuer = dn; // the issuer and the subject are the same 
     cert.NotBefore = DateTime.Now.Date; 
     // this cert expires immediately. Change to whatever makes sense for you 
     cert.NotAfter = cert.NotBefore + expirationLength; 
     cert.HashAlgorithm = hashobj; // Specify the hashing algorithm 
     cert.Encode(); // encode the certificate 

     // Do the final enrollment process 
     var enroll = new CX509Enrollment(); 
     enroll.InitializeFromRequest(cert); // load the certificate 
     enroll.CertificateFriendlyName = subjectName; // Optional: add a friendly name 
     string 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 System.Security.Cryptography.X509Certificates.X509Certificate2(
      System.Convert.FromBase64String(base64encoded), "", 
      // mark the private key as exportable (this is usually what you want to do) 
      // mark private key to go into the Machine store instead of the current users store 
      X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet 
     ); 
    } 
+0

有谁知道如何通过友好名称加载现有证书? –

+0

感谢您提供更新..我有一个类似的问题,无法确定我做错了什么,你帮助我出了很多这篇文章! :) – Spyral

回答

5

我在PowerShell中使用等效代码时遇到了同样的问题。看来私人密钥有时会消失。我使用了进程监视器,您可以看到要删除的密钥文件。

我解决这个问题的方法是将X509KeyStorageFlags.PersistKeySet添加到X509Certificate2构造函数中。

+0

谢谢你的回答。我有一个工作解决方案,我不喜欢改变它。我现在没有时间来测试这个答案,但是如果我这样做,我会回来并将其标记为公认的答案。 – MarkR

+0

一年后,我终于检查出这个答案,它的工作原理!我将PersistKeySet标志添加到构造函数中,并且键不会消失。我更新了我的原始帖子以显示工作代码。 – MarkR

5

我无法做这项工作,但我找到了一个替代解决方案。 (2014年12月更新:我现在已经使用已接受的答案开始工作。)

我能够使用PluralSight.Crypto library来实现我所需的功能。我不得不稍微修改源代码以获取私钥存储在LocalMachine存储中。我所做的更改是CryptContext.cs文件。我改变了CreateSelfSignedCertificate方法。以下是一段代码,包括我所做的更改。实质上,如果CryptContext对象在其标志中包含此值,我将CryptKeyProviderInformation结构的标志成员设置为0x20(CRYPT_MACHINE_KEYSET)。

 byte[] asnName = properties.Name.RawData; 
     GCHandle asnNameHandle = GCHandle.Alloc(asnName, GCHandleType.Pinned); 

     int flags = 0;     // New code 
     if ((this.Flags & 0x20) == 0x20) // New code 
      flags = 0x20;     // New code 

     var kpi = new Win32Native.CryptKeyProviderInformation 
     { 
      ContainerName = this.ContainerName, 
      KeySpec = (int)KeyType.Exchange, 
      ProviderType = 1, // default RSA Full provider 
      Flags = flags     // New code 
     }; 

然后,我使用的功能在我自己的代码是这样的:

 using (Pluralsight.Crypto.CryptContext ctx = new Pluralsight.Crypto.CryptContext()) { 

      ctx.Flags = 0x8 | 0x20; 
      ctx.Open(); 

      X509Certificate2 cert = ctx.CreateSelfSignedCertificate(
       new Pluralsight.Crypto.SelfSignedCertProperties 
       { 
        IsPrivateKeyExportable = true, 
        KeyBitLength = 4096, 
        Name = new X500DistinguishedName("CN=" + subjectName), 
        ValidFrom = DateTime.Today, 
        ValidTo = DateTime.Today + expirationLength, 
       }); 

      return cert; 
     } 

请注意,我设置的标志为CryptContext对象是0x8中| 0x20(CRYPT_NEWKEYSET | CRYPT_MACHINE_KEYSET)。

我希望我能弄清楚我原来的解决方案出了什么问题。但我需要一些工作,并在测试中这个解决方案做我需要的。我希望它能帮助别人。

+0

我在哪里可以找到这些CryptContext标志?我找不到任何文档。 –

+0

您可以在这里找到Flags的文档:http://msdn.microsoft.com/en-us/library/windows/desktop/aa379886%28v=vs.85%29.aspx和数字标志值在这里:http ://www.pinvoke.net/default.aspx/advapi32.cryptacquirecontext – MarkR

3

您还可以使用CodePlex上的CLR安全库(https://clrsecurity.codeplex.com/)。以下是创建自签名证书的示例代码,并使用SSLStream进行测试。

 var machineName = Environment.MachineName; 
     var keyCreationParameters = new CngKeyCreationParameters(); 
     keyCreationParameters.KeyUsage = CngKeyUsages.AllUsages; 
     keyCreationParameters.KeyCreationOptions = CngKeyCreationOptions.OverwriteExistingKey; 
     keyCreationParameters.Parameters.Add(new CngProperty("Length", BitConverter.GetBytes(4096), CngPropertyOptions.None)); 
     var cngKey = CngKey.Create(CngAlgorithm2.Rsa, "Test", keyCreationParameters); 

     var x500DistinguishedName = new X500DistinguishedName("CN=" + machineName); 
     x500DistinguishedName.Oid.Value = "1.3.6.1.5.5.7.3.1"; 
     var certificateCreationParameters = new X509CertificateCreationParameters(x500DistinguishedName); 
     certificateCreationParameters.SignatureAlgorithm = X509CertificateSignatureAlgorithm.RsaSha512; 
     certificateCreationParameters.TakeOwnershipOfKey = true; 
     certificateCreationParameters.CertificateCreationOptions = X509CertificateCreationOptions.None; 
     certificateCreationParameters.EndTime = new DateTime(9999, 12,31, 23, 59, 59, 999, DateTimeKind.Utc); 
     var certificate = cngKey.CreateSelfSignedCertificate(certificateCreationParameters); 

     var certificateStore = new X509Store(StoreName.Root, StoreLocation.CurrentUser); 
     certificateStore.Open(OpenFlags.ReadWrite); 
     certificateStore.Add(certificate); 
     certificateStore.Close(); 


     var tcpListener = TcpListener.Create(6666); 
     tcpListener.Start(); 
     var client = new TcpClient("localhost", 6666); 
     var acceptedClient = tcpListener.AcceptTcpClient(); 
     var acceptedClinetSslStream = new SslStream(
      acceptedClient.GetStream(), false); 
     var serverAuthTask = acceptedClinetSslStream.AuthenticateAsServerAsync(certificate, 
          false, SslProtocols.Tls, true); 

     SslStream clientSslStream = new SslStream(
      client.GetStream(), 
      false, 
      delegate(object o, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors errors) 
       { 
        if (errors == SslPolicyErrors.None) 
         return true; 

        Console.WriteLine("Certificate error: {0}", errors); 

        // Do not allow this client to communicate with unauthenticated servers. 
        return false; 
       }, 
      null); 
     var clientAuthTask = clientSslStream.AuthenticateAsClientAsync(machineName); 

     Task.WaitAll(serverAuthTask, clientAuthTask);