I want to switch from self-signed certificate per device to pair of certificates, one of which is previously generated, placed in Trusted Root Certificate Authorities store, is same for all devices, and works as root CA for second certificate, which is generated per device, and placed in Personal store.
I would like to not use makecert, since creating signed certificate shows up UI, which I want to avoid. Also, OpenSSL can't be used due to some license-related stuff (although I have working solution with it). So, for now I'm working with small C# tool, based on CertEnroll lib.
This is how I create pfx for first, root CA certificate.
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
To create certificate from C# code, I've referenced questions How to create self-signed certificate programmatically for WCF service? and C# Generate a non self signed client CX509Certificate Request without a CA using the certenroll.dll.
So far, I have following code. Method for certificate generation:
/// <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);
}
Simple application to find existing certificate and create new one based on it:
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();
}
}
The thing is, that it works great, if I try to reference certificate not in Trusted Root Certificate Authorities, but in Personal (even if I have password on certificate). But if I try to create certificate based on CA certificate from Trusted Root Certificate Authorities, I receive exception on signerCertificate.Initialize
, saying
Cannot find object or property. 0x80092004 (-2146885628 CRYPT_E_NOT_FOUND)
So, what am I missing?
ISignerCertificate::Initialize
requires that the private key be bound via the Requests or My store:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa376832(v=vs.85).aspx:
If a private key is needed, only the personal and request stores are searched.
If a private key is not needed, the root and intermediate CA stores are also searched.
Windows expects that you only put the public portion of the CA into the CA (intermediate) or Root/ThirdPartyRoot stores, and that if you're the issuer you'll ALSO have it installed (with the private key now) into CurrentUser\My or LocalMachine\My.
User contributions licensed under CC BY-SA 3.0