C# - Generate X509 Certificate based on a given issuer certificate in byte[] - c#

I want to create iothub device certificates from C# code. The root CA is stored in keyvault as a .pfx, fetched as a string, and then converted from base 64 in order to obtain the certificate bytes as it is required for a certificate stored in keyvault: Azure Key Vault Certificates does not have the Private Key when retrieved via IKeyVaultClient.GetCertificateAsync
I want to write a function that will take these bytes, along with a subject name (for the leaf certificate) and will create a x509 certificate (with both public and private keys) that would have the issuer as the root.
Here is what I have sketched so far:
public static X509Certificate2 GenerateCertificateBasedOnIssuer(string subjectName, byte[] issuerByteCert)
{
var issuerCertificate = new X509Certificate2(issuerByteCert);
RSA keyProvider = issuerCertificate.GetRSAPrivateKey();
CertificateRequest certificateRequest = new CertificateRequest($"CN={subjectName}", keyProvider, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
CryptoApiRandomGenerator randomGenerator = new CryptoApiRandomGenerator();
SecureRandom random = new SecureRandom(randomGenerator);
BigInteger serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
var publicOnlyDeviceCertificate = certificateRequest.Create(issuerCertificate, issuerCertificate.NotBefore, issuerCertificate.NotAfter, serialNumber.ToByteArray());
return publicOnlyDeviceCertificate; // oh no ! :(
}
The issue I am having with this solution is that the created certificate only contains a public key.
I found another solution that appears to solve my problem on another Stack Overflow question using BouncyCastle's X509V3CertificateGenerator: Generate a self-signed certificate on the fly
The issue I have with this solution is that I cannot convert my rootCA certificate's private key to an AsymmetricKeyParameter (first parameter of the X509V3CertificateGenerator.Generate method). I tried converting the issuer's key to AsymmetricKeyParameter using this solution: convert PEM encoded RSA public key to AsymmetricKeyParameter, but I got an invalid operation exception.
I was wondering if I was on the right path (as far as understanding goes) and if there is a way to generate a certificate with a private (and public key) based on the code I currently have in place.
UPDATE: I have been able to convert a private key to an AsymmetricKeyParameter by hardcoding the key as follows:
string testKey = #"-----BEGIN PRIVATE KEY-----
<THE KEY>
-----END PRIVATE KEY-----
";
var stringReader = new StringReader(testKey);
var pemReader = new PemReader(stringReader);
var pemObject = pemReader.ReadObject();
var keyParam = ((AsymmetricKeyParameter)pemObject);
Azure keyvault stores certificate in a pfx format. I am thinking of storing the private key as a secret string. I will keep testing with an hardcoded key for now until I get to a working solution.
I am now testing with BouncyCastle and will come back with a working solution if it works!

The key you pass to CertificateRequest is used as the public key in the cert... so you want to pass a new key, not the issuer's key.
Then, once you now have the subject key, you use CopyWithPrivateKey at the end to glue them back together.
public static X509Certificate2 GenerateCertificateBasedOnIssuer(string subjectName, byte[] issuerByteCert)
{
using (var issuerCertificate = new X509Certificate2(issuerByteCert))
using (RSA subjectKey = RSA.Create(2048))
{
CertificateRequest certificateRequest = new CertificateRequest($"CN={subjectName}", subjectKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
CryptoApiRandomGenerator randomGenerator = new CryptoApiRandomGenerator();
SecureRandom random = new SecureRandom(randomGenerator);
BigInteger serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
var publicOnlyDeviceCertificate = certificateRequest.Create(issuerCertificate, issuerCertificate.NotBefore, issuerCertificate.NotAfter, serialNumber.ToByteArray());
using (publicOnlyDeviceCertificate)
{
return publicOnlyDeviceCertificate.CopyWithPrivateKey(subjectKey);
}
}

Related

How can I protect the rsacryptoserviceprovider privatekey with a password and add it in windows store

I have a publickey certificate and a privatekey. Before adding to the x509store I want to protect the privatekey with a password.
My requirement is when I find the x509certificate2 from store using subject name I should get the certiifcate with the privatekey.But privatekey should be password protected without any ui prompt.
When creating rsacryptoservice provider I tried to attach key password to the csp params and I can able to create rsacryptoserviceprovider object.I have attached that to a x509certificate2 and added to the windows store. But after retrieving from store private key is throwing cryptographic exception.
var cspParams = new CspParameters
{
ProviderType = 1,
Flags = CspProviderFlags.UseMachineKeyStore
};
string passphrase = "password";
char[] passPhrase = passphrase.ToCharArray();
SecureString keyPassword = new SecureString();
for (int i = 0; i < passPhrase.Length; i++)
{
keyPassword.AppendChar(passPhrase[i]);
}
cspParams.KeyPassword = keyPassword;
using (RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider(cspParams))
{
rsaProvider.ImportParameters(rsaParam);
rsaProvider.PersistKeyInCsp = true;
X509Certificate2 x509Certificate = new X509Certificate2(Convert.FromBase64String(cryptoCertificate),
"123",
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable |
X509KeyStorageFlags.PersistKeySet)
{PrivateKey = rsaProvider};
store.Add(x509Certificate);
}
The problem is here:
using (RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider(cspParams))
{
rsaProvider.ImportParameters(rsaParam);
rsaProvider.PersistKeyInCsp = true;
X509Certificate2 x509Certificate = new X509Certificate2(<...>)
{PrivateKey = rsaProvider};
<..>
}
You are creating a new RSA asymmetric key pair and attempt to attach it to a certificate. However, this key pair is brand new and doesn't match the public key stored in certificate. Instead, you have to acquire private key handle from X509Certificate2 object and work on it, do not create new keys, it will never work.
And do not use RSACryptoServiceProvider it is dead and obsolete. Instead, you must use X509Certificate.GetRSAPrivateKey() extension method to get an instance of RSA class and extract CSP parameters from that RSA object.

Casting private key to RSACryptoServiceProvider not working

I have a X509Certificate2 variable and I'm trying to cast the private key of the variable to a RSACryptoServiceProvider
RSACryptoServiceProvider pkey = (RSACryptoServiceProvider)cert.PrivateKey;
However I get this exception.
System.InvalidCastException: 'Unable to cast object of type 'System.Security.Cryptography.RSACng' to type 'System.Security.Cryptography.RSACryptoServiceProvider'.'
It's weird that this happens because other answers in SO suggested the same procedure as mine but I get an exception. Any solutions to this?
So after a few tries and discussions in the comments I came up with the following solution.
RSA rsa = (RSA)cert.PrivateKey;
(cert.PrivateKey as RSACng).Key.SetProperty(
new CngProperty(
"Export Policy",
BitConverter.GetBytes((int)CngExportPolicies.AllowPlaintextExport),
CngPropertyOptions.Persist));
RSAParameters RSAParameters = rsa.ExportParameters(true);
AsymmetricCipherKeyPair keypair = DotNetUtilities.GetRsaKeyPair(RSAParameters);
The problem was that the variable rsa wasn't exportable. To change this I set a new CngProperty for the export policy. Works perfectly now
Just wanted to note that there's also an extension method that can be used:
using System.Security.Cryptography.X509Certificates;
...
//certificate is a X509Certificate2
using (var rsa = certificate.GetRSAPrivateKey())
{
//the var rsa is an RSA object
//...
}
In my case the same problem was happening when trying to convert local store certificate to RSACryptoServiceProvider as below:
RSACryptoServiceProvider encryptProvider =
certificate.PrivateKey as RSACryptoServiceProvider;
Issue fixed by using RSA instead of RSACryptoServiceProvider.
Putting instructions here in case if someone will be curious how to do that )).
To store some certificate into your machine open Visual Studio Developer Command and type following:
makecert -n "CN=JohnDoe" -sr currentuser -ss someCertStore
...where you can specify and values instead of "JohnDoe" and "demoCertStore".
Now you can use the below code to access certificates from the local certificate store:
public class Program
{
static void DumpBytes(string title, byte[] bytes)
{
Console.Write(title);
foreach (byte b in bytes)
{
Console.Write("{0:X} ", b);
}
Console.WriteLine();
}
static void Main(string[] args)
{
// This will convert our input string into bytes and back
var converter = new ASCIIEncoding();
// Get a crypto provider out of the certificate store
// should be wrapped in using for production code
var store = new X509Store("someCertStore", StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
// should be wrapped in using for production code
X509Certificate2 certificate = store.Certificates[0];
RSA rsa = (RSA)certificate.PrivateKey;
(certificate.PrivateKey as RSACng)?.Key.SetProperty(
new CngProperty(
"Export Policy",
BitConverter.GetBytes((int)CngExportPolicies.AllowPlaintextExport),
CngPropertyOptions.Persist));
string messageToSign = "This is the message I want to sign";
Console.WriteLine("Message: {0}", messageToSign);
byte[] messageToSignBytes = converter.GetBytes(messageToSign);
// need to calculate a hash for this message - this will go into the
// signature and be used to verify the message
// Create an implementation of the hashing algorithm we are going to us
// should be wrapped in using for production code
DumpBytes("Message to sign in bytes: ", messageToSignBytes);
HashAlgorithm hasher = new SHA1Managed();
// Use the hasher to hash the message
byte[] hash = hasher.ComputeHash(messageToSignBytes);
DumpBytes("Hash for message: ", hash);
// Now sign the hash to create a signature
byte[] signature = rsa.SignHash(hash, HashAlgorithmName.SHA1, RSASignaturePadding.Pss);
DumpBytes("Signature: ", messageToSignBytes);
// Now use the signature to perform a successful validation of the mess
bool validSignature = rsa.VerifyHash(hash: hash,
signature: signature,
hashAlgorithm: HashAlgorithmName.SHA1,
padding: RSASignaturePadding.Pss);
Console.WriteLine("Correct signature validated OK: {0}", validSignature);
// Change one byte of the signature
signature[0] = 99;
// Now try the using the incorrect signature to validate the message
bool invalidSignature = rsa.VerifyHash(hash: hash,
signature: signature,
hashAlgorithm: HashAlgorithmName.SHA1,
padding: RSASignaturePadding.Pss);
Console.WriteLine("Incorrect signature validated OK: {0}", invalidSignature);
Console.ReadKey();
}
You can avoid the code that is setting the export policy altogether by simply creating the certificate with the export policy already being correct. I used the New-SelfSignedCertificate PowerShell utility to create a certificate that was exportable from inception.
PS C:>New-SelfSignedCertificate -CertStoreLocation "Cert:\CurrentUser\" -Subject "CN=JUSTIN" -KeyExportPolicy Exportable
This negates the need for:
(certificate.PrivateKey as RSACng)?.Key.SetProperty(new CngProperty("Export Policy", BitConverter.GetBytes((int)CngExportPolicies.AllowPlaintextExport),CngPropertyOptions.Persist));

Certificate signing produces different signature when on server

I am trying to sign some data using a certificate private key. The issue I'm finding is that the signature is different depending on if I'm executing it locally or on a server.
I'm using the following code as a test, running under the same user both locally and on the server:
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace TestSignature
{
class Program
{
static void Main(string[] args)
{
var key = SigningKeyFromCertificate(StoreName.My, StoreLocation.LocalMachine, X509FindType.FindByThumbprint, "thumbprint");
var alg = CryptoConfig.MapNameToOID("SHA256");
var data = Encoding.UTF8.GetBytes("test");
var sig = key.SignData(data, alg);
Console.WriteLine(Convert.ToBase64String(sig));
}
private static RSACryptoServiceProvider SigningKeyFromCertificate(StoreName storeName, StoreLocation storeLocation, X509FindType findType, string findValue)
{
X509Store store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
var certs = store.Certificates.Find(findType, findValue, false);
if (certs?.Count > 0)
{
var cert = certs[0];
if (cert.HasPrivateKey)
{
// Force use of Enhanced RSA and AES Cryptographic Provider to allow use of SHA256.
var key = cert.PrivateKey as RSACryptoServiceProvider;
var enhanced = new RSACryptoServiceProvider().CspKeyContainerInfo;
var parameters = new CspParameters(enhanced.ProviderType, enhanced.ProviderName, key.CspKeyContainerInfo.UniqueKeyContainerName);
return new RSACryptoServiceProvider(parameters);
}
else
{
throw new Exception($"No private key access to cert '{findValue}.'");
}
}
else
{
throw new Exception($"Cert '{findValue}' not found!");
}
}
}
}
Locally, I get the following signature:
YUjspKhLl7v3u5VQkh1PfHytMTpEtbAftxOA5v4lmph3B4ssVlZp7KedO5NW9K5L222Kz9Ik9/55NirS0cNCz/cDhEFRtD4daJ9qLRuM8oD5hCj6Jt9Vc6WeS2he+Cqfoylnv4V9plfi1xw8y7EyAf4C77BGkXOdyP5wyz2Xubo=
On the server, I get this one instead:
u1RUDwbBlUpOgNNkAjXhYEWfVLGpMOa0vEfm6PUkB4y9PYBk1lDmCAp+488ta+ipbTdSDLM9btRqsQfZ7JlIn/dIBw9t5K63Y7dcDcc7gDLE1+umLJ7EincMcdwUv3YQ0zCvzc9RrP0jKJManV1ptQNnODpMktGYAq1KmJb9aTY=
Any idea of what could be different? I would think, with the same certificate, the same code, and the same data, the signature should be the same.
(The example is written in C# 4.5.2.)
You have some code to reopen the CAPI key handle under PROV_RSA_AES:
// Force use of Enhanced RSA and AES Cryptographic Provider to allow use of SHA256.
var key = cert.PrivateKey as RSACryptoServiceProvider;
var enhanced = new RSACryptoServiceProvider().CspKeyContainerInfo;
var parameters = new CspParameters(
enhanced.ProviderType,
enhanced.ProviderName,
key.CspKeyContainerInfo.UniqueKeyContainerName);
return new RSACryptoServiceProvider(parameters);
But key.CspKeyContainerInfo.UniqueKeyContainerName isn't the name of the key (it's the name of the file on disk where the key lives), so you're opening a brand new key (you're also generating a new ephemeral key just to ask what the default provider is). Since it's a named key it persists, and subsequent application executions resolve to the same key -- but a different "same" key on each computer.
A more stable way of reopening the key is
var cspParameters = new CspParameters
{
KeyContainerName = foo.CspKeyContainerInfo.KeyContainerName,
Flags = CspProviderFlags.UseExistingKey,
};
(since the provider type and name aren't specified they will use the defaults, and by saying UseExistingKey you get an exception if you reference a key that doesn't exist).
That said, the easiest fix is to stop using RSACryptoServiceProvider. .NET Framework 4.6 (and .NET Core 1.0) have a(n extension) method on X509Certificate2, GetRSAPrivateKey(), it returns an RSA (which you should avoid casting) which is usually RSACng (on Windows), but may be RSACryptoServiceProvider if only CAPI had a driver required for a HSM, and may be some other RSA in the future. Since RSACng handles SHA-2 better there's almost never a need to "reopen" the return object (even if it's RSACryptoServiceProvider, and even if the type isn't PROV_RSA_AES (24), that doesn't mean the HSM will fail to do SHA-2).

Creating of ECDSA certficate signed by other ECDSA certificate

I need to create "Client" ECDSA certificate signed by "Root" certificate (self-signed, ECDSA).
"Root" certificate was created as described in Translating Elliptic Curve parameters (BC to MS).
To create "Client" certificate (signed by "Root") slightly modified algorithm can be used.
The difference is that the private key (used to sign public key from keypair generated for "Client" certificate) must be supplied from the "outside" - it is a private-key of "Root" certificate.
But this is the issue. I cannot find a way how to get and translate private key to type Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters that could be passed to signature-factory.
// 1. get private-key of "Root" certificate from existing certificate:
byte[] msRootCertData = File.ReadAllBytes(#"c:\root_ecdsa_cert.pfx");
X509Certificate2 msRootCert = new X509Certificate2(msRootCertData);
ECDsaCng msRootPrivateKey = msRootCert.GetECDsaPrivateKey() as ECDsaCng;
ECParameters msRootPrivateKeyParameters = msRootPrivateKey.ExportParameters(true);
// here comes the issue:
ECPrivateKeyParameters bcRootPrivateKeysParameters = TranslateMSKeysToBouncy(msRootPrivateKeyParameters);
// 2. generate "Client" key-pair:
AsymmetricCipherKeyPair bcClientKeyPair = bcKeyGen.GenerateKeyPair();
ECPrivateKeyParameters bcClientPrivKey = (ECPrivateKeyParameters)bcClientKeyPair.Private;
ECPrivateKeyParameters bcClientPublKey = (ECPublicKeyParameters)bcClientKeyPair.Public;
// 3. create X509 certificate:
X509V3CertificateGenerator bcCertGen = new X509V3CertificateGenerator();
bcCertGen.SetPublicKey(bcClientPublKey);
// .. set subject, validity period etc
ISignatureFactory sigFac = new Asn1SignatureFactory("Sha256WithECDSA", bcRootPrivateKeysParameters);
Org.BouncyCastle.X509.X509Certificate bcClientX509Cert = bcCertGen.Generate(sigFac);
byte[] x509CertEncoded = bcClientX509Cert.GetEncoded();
// the rest is the same as in the mentioned example.
Any hints?
Or is there other way? (for example: passing instance of X509Certificate2 directly to BouncyCastle library (avoid to translate private-keys to Cng), or generating "Client" certificate without BouncyCastle)
Thanks.
If you can take a dependency on .NET Framework 4.7.2 (or .NET Core 2.0) you can do it without BouncyCastle, via the new CertificateRequest class:
X509Certificate2 publicPrivate;
using (ECDsa clientPrivateKey = ECDsa.Create())
{
var request = new CertificateRequest(
"CN=Et. Cetera",
clientPrivateKey,
HashAlgorithmName.SHA256);
// Assuming this isn't another CA cert:
request.CertificateExtensions.Add(
new X509BasicConstraintsExtension(false, false, 0, false));
// other CertificateExtensions as you desire.
// Assign, or derive, a serial number.
// RFC 3280 recommends that it have no more than 20 bytes encoded.
// 12 random bytes seems long enough.
byte[] serial = new byte[12];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetBytes(serial);
}
DateTimeOffset notBefore = DateTimeOffset.UtcNow;
DateTimeOffset notAfter = notBefore.AddMonths(15);
using (X509Certificate2 publicOnly = request.Create(
msRootCert,
notBefore,
notAfter,
serial))
{
publicPrivate = publicOnly.CopyWithPrivateKey(clientPrivateKey);
}
}
// The original key object was disposed,
// but publicPrivate.GetECDsaPrivateKey() still works.
If you want to add publicPrivate to an X509Store you need to either 1) export it to a PFX and re-import it, or 2) change the key creation to use a named key. Otherwise, only the public portion will be saved (on Windows).

Construct RSACryptoServiceProvider from public key (not certificate)

I am working on a project where I need to use a "public key" to encrypt a message using RSA algorithm. I was provided with a certificate and my first thought was to use Public Key from that certificate and after investigation I learned I need to use RSACryptoServiceProvider for encryption.
I have checked msdn and only method I thought I should use is RSACryptoServiceProvider.ImportCspBlob(byte[] keyBlob).
When I tried to use public key exported from certificate I was getting an error that the header data for certificate is invalid.
I know I can cast X509certificate2.PublicKey.Key to RSACryptoServiceProvider but from what I understood from my client is that going forward I will be given only a public key and not the certificate. This key will have to be saved in .xml configuration file.
So to summarize: Is there a way to generate an RSACryptoServiceProvider given only a certificate's public key?
You can try to look at this example: RSA public key encryption in C#
var publicKey = "<RSAKeyValue><Modulus>21wEnTU+mcD2w0Lfo1Gv4rtcSWsQJQTNa6gio05AOkV/Er9w3Y13Ddo5wGtjJ19402S71HUeN0vbKILLJdRSES5MHSdJPSVrOqdrll/vLXxDxWs/U0UT1c8u6k/Ogx9hTtZxYwoeYqdhDblof3E75d9n2F0Zvf6iTb4cI7j6fMs=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>";
var testData = Encoding.UTF8.GetBytes("testing");
using ( var rsa = new RSACryptoServiceProvider(1024))
{
try
{
// client encrypting data with public key issued by server
//
rsa.FromXmlString(publicKey);
var encryptedData = rsa.Encrypt(testData, true);
var base64Encrypted = Convert.ToBase64String(encryptedData);
}
finally
{
rsa.PersistKeyInCsp = false;
}
}
You are OK and following a good typical pattern. The Sender of the data does not need the private key.
The following may confirm some of the code you already have figured out.
The one line where I set the private key for the receiver/decoder I left out.
I took this from a test case I have in my build deploy stuff.
byte[] certBytAr; // This is the certificate as bianry in a .cer file (no private key in it - public only)
X509Certificate2 cert2 = new X509Certificate2(certBytAr);
string strToEncrypt = "Public To Private Test StackOverFlow PsudeoCode. Surfs Up at Secret Beach.";
byte[] bytArToEncrypt = Encoding.UTF8.GetBytes(strToEncrypt);
RSACryptoServiceProvider rsaEncryptor = (RSACryptoServiceProvider)cert2.PublicKey.Key;
byte[] dataNowEncryptedArray = rsaEncryptor.Encrypt(bytArToEncrypt, true);
// done - you now have encrypted bytes
//
// somewhere elxe ...
// this should decrpyt it - simulate the destination which will decrypt the data with the private key
RSACryptoServiceProvider pk = // how this is set is complicated
// set the private key in the x509 oobject we created way above
cert2.PrivateKey = pk;
RSACryptoServiceProvider rsaDecryptor = (RSACryptoServiceProvider)cert2.PrivateKey;
byte[] dataDecrypted = rsaDecryptor.Decrypt(dataNowEncryptedArray, true);
Console.WriteLine(" encrypt 1 Way Intermediate " + BitConverter.ToString(dataDecrypted));
string strDecodedFinal = Encoding.UTF8.GetString(dataDecrypted);
if (strDecodedFinal == strToEncrypt)
{
}
else
{
Console.WriteLine(" FAILURE OF ENCRYPTION ROUND TRIP IN SIMPLE TEST (Direction: Public to Private). No Surfing For You ");
}

Categories

Resources