2013-08-20 14 views
13

Ho un server WCF auto-ospitato che funziona come un servizio di Windows con l'account Sistema locale. Sto cercando di creare un certificato autofirmato a livello di codice in C# per l'utilizzo con un endpoint net.tcp utilizzando la sicurezza a livello di messaggio.Come creare un certificato autofirmato a livello di codice per il servizio WCF?

Sto usando il seguente codice che è strettamente basato sulla risposta accettata in How to create a self-signed certificate using C#? con alcune piccole modifiche che cercano di risolvere il mio problema.

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

E io conservarlo con questo codice:

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

questo crea il certificato e lo mette nell'archivio certificati LocalMachine. Il problema è che quando provo ad avviare il servizio WCF ottengo la seguente eccezione:

È probabile che il certificato "CN = myCertificate" non disponga di una chiave privata che sia in grado di scambiare le chiavi o che il processo non possa avere diritti di accesso per la chiave privata. Si prega di vedere l'eccezione interna per i dettagli. Eccezione interna: Keyset non esiste

L'uscita del campione FindPrivateKey (http://msdn.microsoft.com/en-us/library/aa717039%28v=vs.100%29.aspx) per il mio certificato è:

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

posso vedere questo file 1.43KB in Esplora risorse. Se guardo le proprietà | Sicurezza vedo SISTEMA e amministratori entrambi con controllo completo.

Nella ricerca di questo errore ho visto molte risposte sulla chiave privata mancante o autorizzazioni errate. Non riesco a vedere quale sia il problema.

La cosa davvero strana è che se utilizzo il plugin Certificato mmc, vai al certificato e scegli Tutte le attività | Gestisci chiavi private ... Vedo le stesse impostazioni di sicurezza. Dopo aver visto questo anche se ho appena visualizzato la finestra di dialogo e premuto il pulsante Annulla, il certificato ora funziona correttamente in WCF. Posso semplicemente riavviare il servizio e tutto funziona perfettamente.

Se creo un certificato utilizzando MakeCert, funziona perfettamente dall'inizio. Non so cosa faccia in modo diverso.

Un'altra informazione che potrebbe non essere rilevante è che il certificato non viene inserito solo nel mio negozio dove gli ho detto di essere inserito, ma viene anche inserito nell'archivio "Autorità di certificazione intermedie". Non so perché o se è importante.

Quindi ... qualche idea su cosa sto facendo male?

AGGIORNAMENTO: Beh, questo non è solo un problema WCF. In sostanza, ho lo stesso problema quando provo a usare il certificato per collegarmi a un endpoint con http.sys usando HttpSetServiceConfiguration. Il metodo restituisce 1312 - "Una sessione di accesso specificata non esiste. Potrebbe essere già stata terminata". Questo non è in realtà il vero errore. Ho visto in caso di sicurezza registrare un errore di controllo che dice questo:

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 è oggetto non è stato trovato. Quindi questo sembra essere lo stesso problema. Ancora una volta, dopo aver aperto la finestra di dialogo Gestisci chiavi private per il certificato, questo funziona perfettamente anche.

Sto ancora cercando la causa del problema.

AGGIORNAMENTO # 2: sono riuscito a farlo funzionare utilizzando la risposta accettata di seguito. È interessante notare che questo codice ora sembra mettere il certificato nell'archivio della macchina senza chiamare il codice X509Store. Io chiamo ancora il codice perché non ne sono sicuro e non danneggia nulla. Ecco il codice finale che sto usando per creare il certificato.

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

Qualcuno sa come caricare un certificato esistente per nome descrittivo? –

+0

Grazie per avermi fornito un aggiornamento .. Avevo un problema simile e non riuscivo a individuare cosa stavo facendo male, mi hai aiutato molto con questo post! :) – Spyral

risposta

5

Ho avuto lo stesso problema utilizzando il codice equivalente in PowerShell. Sembra che a volte la chiave privata scompaia. Ho usato Process Monitor e puoi vedere il file chiave che viene eliminato.

Il modo in cui ho risolto questo problema era aggiungere X509KeyStorageFlags.PersistKeySet al costruttore X509Certificate2.

+0

Grazie per la risposta. Ho una soluzione funzionante e sono dispiaciuto di cambiarlo. Non ho tempo per testare questa risposta ora, ma se lo farò, tornerò indietro e segnerò questa come risposta accettata. – MarkR

+0

Un anno dopo, ho finalmente controllato questa risposta e funziona! Ho aggiunto il flag PersistKeySet al costruttore e la chiave non scompare. Ho aggiornato il mio post originale per mostrare il codice funzionante. – MarkR

5

Non ho potuto fare questo lavoro, ma ho trovato una soluzione alternativa. (Aggiornamento dicembre 2014: ora ho funzionato usando la risposta accettata.)

Sono stato in grado di utilizzare lo PluralSight.Crypto library per ottenere ciò di cui ho bisogno. Ho dovuto modificare leggermente il codice sorgente per ottenere la chiave privata da memorizzare nel negozio LocalMachine. Le modifiche apportate riguardavano il file CryptContext.cs. Ho modificato il metodo CreateSelfSignedCertificate. Di seguito è riportato un frammento di codice che include la modifica che ho apportato. In sostanza, ho impostato il membro Flags della struttura CryptKeyProviderInformation per impostarlo su 0x20 (CRYPT_MACHINE_KEYSET) se l'oggetto CryptContext contiene questo valore nei suoi Flag.

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

Poi io uso la funzione nel mio codice come questo:

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

Si noti che ho impostato le bandiere per l'oggetto CryptContext di essere 0x8 | 0x20 (CRYPT_NEWKEYSET | CRYPT_MACHINE_KEYSET).

Vorrei poter capire cosa c'era di sbagliato nella mia soluzione originale. Ma ho bisogno di qualcosa su cui lavorare e nei miei test questa soluzione fa quello che mi serve. Spero che aiuti qualcun altro lungo la strada.

+0

Dove posso trovare questi flag CryptContext per favore? Non riesco a trovare alcuna documentazione. –

+0

Puoi trovare la documentazione per i flag qui: http://msdn.microsoft.com/en-us/library/windows/desktop/aa379886%28v=vs.85%29.aspx e i valori dei flag numerici qui: http : //www.pinvoke.net/default.aspx/advapi32.cryptacquirecontext – MarkR

3

È inoltre possibile utilizzare la libreria di sicurezza CLR su CodePlex (https://clrsecurity.codeplex.com/). Ecco un codice di esempio che crea un certificato autofirmato e lo verifica con 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);