Como criar certificado autoassinado programaticamente para o serviço WCF?
Eu tenho um servidor WCF auto-hospedado executando como um serviço do Windows na conta Sistema Local. Estou tentando criar um certificado autoassinado programaticamente em c # para uso com um ponto de extremidade net.tcp usando a segurança no nível de mensagem.
Estou usando o código a seguir, que é muito próximo da resposta aceita emComo criar um certificado autoassinado usando C #? com algumas pequenas mudanças tentando resolver meu 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 eu guardo com este código:
X509Store store = new X509Store(storeName, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
store.Add(newCert);
store.Close();
Isso cria o certificado e o coloca no armazenamento de certificados LocalMachine. O problema é que, quando tento iniciar o serviço WCF, recebo a seguinte exceção:
Provavelmente, o certificado 'CN = myCertificate' pode não ter uma chave privada capaz de troca de chaves ou o processo pode não ter direitos de acesso para a chave privada. Consulte a exceção interna para obter detalhes. Exceção interna: o conjunto de chaves não existe
A saída da amostra FindPrivateKey (http://msdn.microsoft.com/en-us/library/aa717039%28v=vs.100%29.aspx) para o meu certificado é:
Private key directory:
C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys
Private key file name:
f0d47c7826b8ef5148b6d412f1c40024_4a8a026f-58e4-40f7-b779-3ae9b6aae1a7
Eu posso ver esse arquivo de 1,43 KB no explorer. Se eu olhar as propriedades | Segurança, vejo SISTEMA e Administradores, ambos com Controle total.
Ao pesquisar esse erro, vi muitas respostas sobre a chave privada ausente ou com permissões incorretas. Não consigo ver qual é o problema.
O mais estranho é que, se eu usar o plug-in Certificado mmc, vá para o certificado e escolha Todas as tarefas | Gerenciar chaves privadas ... Eu vejo as mesmas configurações de segurança. Depois de ver isso, mesmo que eu apareça na caixa de diálogo e pressione o botão Cancelar, o certificado agora funciona corretamente no WCF. Posso simplesmente reiniciar o serviço e tudo funciona perfeitamente.
Se eu criar um certificado usando MakeCert, ele funcionará bem desde o início. Não sei o que faz de diferente.
Uma outra informação que pode não ser relevante é que o certificado não só é colocado na minha loja onde eu disse para ele, mas também na loja "Autoridades de certificação intermediárias". Não sei por que ou se importa.
Então ... alguma idéia do que estou fazendo de errado?
ATUALIZAÇÃO: Bem, isso não é apenas um problema do WCF. Essencialmente, obtenho o mesmo problema quando tento usar o certificado para vincular a um terminal com http.sys usandoHttpSetServiceConfiguration. O método retorna 1312 - "Uma sessão de logon especificada não existe. Ela já pode ter sido encerrada". Este não é realmente o erro real. Vi no log de eventos de segurança uma falha de auditoria que diz o seguinte:
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 é Objeto não encontrado. Portanto, este parece ser o mesmo problema. Novamente, depois de abrir a caixa de diálogo Gerenciar chaves privadas do certificado, isso também funciona perfeitamente.
Ainda estou procurando a causa do problema.
ATUALIZAÇÃO # 2: Consegui fazer isso funcionar usando a resposta aceita abaixo. Curiosamente, esse código agora parece colocar o certificado na loja Machine sem chamar o código X509Store. Eu ainda chamo o código porque não tenho certeza e não faz mal a nada. Aqui está o código final que estou usando para criar o certificado.
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_PLA,INTEXT_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
);
}