I have a .crt certificate and a .key private key file on a Linux machine. The private key is in encrypted PKCS#8 format (BEGIN ENCRYPTED PRIVATE KEY...). I would like to import these into an X509Certificate2 object for further use. Since we're on Linux, we're using .NET Core 2.2 (we cannot migrate to 3.0 yet).
I have explored a few possible solutions, detailed below:
Use openssl to convert the files to a .pfx and import that using X509Certificate2
I do not want to use this option since I don't want to execute shell code from within C#. I would like the solution to be completely programmatically achieved in C#.
Use the C# BouncyCastle libraries to do either:
A conversion of both the certificate and the key to .pfx (as above), or
Importing the certificate and private key separately and using X509Certificate2.CopyWithPrivateKey() to combine them.
However, I cannot find an API for the C# version of BouncyCastle, so I'm not sure what methods I could possibly use to do this.
Some other programmatic method in C# that I'm missing here
Essentially, the end goal is to obtain an X509Certificate2 object from the .crt and .key files. Any help/insight into what approach to use, or even a pointer to helpful BouncyCastle documentation, would be much appreciated. Thanks!
This is possible, though not as friendly as it could be, in .NET Core 3.0:
private static byte[] UnPem(string pem)
{
// This is a shortcut that assumes valid PEM
// -----BEGIN words-----\nbase64\n-----END words-----
const string Dashes = "-----";
int index0 = pem.IndexOf(Dashes);
int index1 = pem.IndexOf('\n', index0 + Dashes.Length);
int index2 = pem.IndexOf(Dashes, index1 + 1);
return Convert.FromBase64String(pem.Substring(index1, index2 - index1));
}
...
string keyPem = File.ReadAllText("private.key");
byte[] keyDer = UnPem(keyPem);
X509Certificate2 certWithKey;
using (X509Certificate2 certOnly = new X509Certificate2("certificate.cer"))
using (RSA rsa = RSA.Create())
{
// For "BEGIN PRIVATE KEY"
rsa.ImportPkcs8PrivateKey(keyDer, out _);
certWithKey = certOnly.CopyWithPrivateKey(rsa);
}
using (certWithKey)
{
Console.WriteLine(certWithKey.HasPrivateKey);
}
RSA private keys can be in three different formats, and you need to call the correct import for each one:
"BEGIN PRIVATE KEY": ImportPkcs8PrivateKey
"BEGIN ENCRYPTED PRIVATE KEY": ImportEncryptedPkcs8PrivateKey
"BEGIN RSA PRIVATE KEY": ImportRSAPrivateKey
In .NET 5 this got as easy as can be:
var certificate =
X509Certificate2.CreateFromPemFile(
crtFile,
Path.ChangeExtension(crtFile, "key"));
Related
I'm currently using this link to store my RSA key in windows Key Container (machine-level) and it works fine, but I'm looking for a way that works for both Linux and windows because I will definitely deploy this project on Linux.
public static void StoreRSAKey(string containerName, string xmlKey)
{
#pragma warning disable CA1416 // Validate platform compatibility
var parameters = new CspParameters
{
KeyContainerName = containerName
};
#pragma warning restore CA1416 // Validate platform compatibility
parameters.Flags = CspProviderFlags.UseMachineKeyStore;
using RSACryptoServiceProvider? rsa = new RSACryptoServiceProvider(parameters);
rsa.FromXmlString(xmlKey);
}
I have found several recommendations on the web but I need a more precise solution.
I'd be glad if anyone can help me through this.
Best way to store RSA key and use it in .Net core(looking for a cross-platform solution)
IMO, the question should be, is it possible to use the RSA key in .net core for cross-platform?
I have recently built open-source encryption and decryption library, spending hours investigating the same question you asked. The short answer it is not possible to use CspParameters with Linux, it is for Windows OS (as this answer mentions). And because the answer is not possible, there is no best way.
So to start with, let's see if we can answer the question of using the RSA key in the .net core for cross-platform.
To do that it is very simple, you need to do following:
Rsa = RSA.Create();
Rsa.KeySize = 2048;
This part does not require installing the library, it is part of netstandard2.0.
That is it, now to export and import a key that you generate, you can do the following.
When you RSA.Create() first you can export the key and store it anywhere safe for later usage.
To export private key and it should be kept safe
Rsa.ToXmlString(true);
To export public key, to encrypt with
Rsa.ToXmlString(false);
When you need to import the key from a local store, you can do the following:
Rsa.FromXmlString(asymmetricKey);
This is the cross-platform compatible solution for windows, Linux, or Mac computers.
It is also possible to import certificates from a local computer using X509Certificate2 and use its public key for encryption and private key for decryption.
It is also possible to import private key parameters to RSAParameters, which requires a helper method to translate XML tags from private key file:
<RSAKeyValue>
<Modulus>xxxx...</Modulus>
<Exponent>xxxx</Exponent>
<P>xxxx...</P>
<Q>xxxx...</Q>
<DP>xxxx...</DP>
<DQ>xxxx...</DQ>
<InverseQ>xxxx...</InverseQ>
<D>xxxx...</D>
</RSAKeyValue>
But I find it easier to use FromXmlString and it is part of RSA class when creating RSA.Create(), so no need for a helper method, that said if performance means a lot for your project, you need to make a performance test to compare the results.
So finally I provide a simple example of how to store and load keys:
public static void Main(string[] args)
{
var rsa = RSA.Create();
rsa.KeySize = 2048;
// public key for decrypting
var privateKey = rsa.ToXmlString(true);
SaveKey(#"privateKey", privateKey);
// public key for encrypting
var publicKey = rsa.ToXmlString(false);
SaveKey(#"publicKey", publicKey);
// initialize the private for use on another instance
var rsaAnotherPlace = RSA.Create();
rsaAnotherPlace.KeySize = 2048;
rsaAnotherPlace.FromXmlString(LoadKey(#"privateKey"));
}
// store my keys
public static void SaveKey(string filename, string content)
{
var bytes = Encoding.ASCII.GetBytes(content);
using var fs = new FileStream(filename, FileMode.Create, FileAccess.Write);
fs.Write(bytes, 0, bytes.Length);
}
// load key
public static string LoadKey(string filename)
{
var bytes = File.ReadAllBytes(filename);
return Encoding.ASCII.GetString(bytes);
}
I have tested the solution on Windows and Linux OS and it passes the macOS test on GitHub actions, but I have not tested it yet on macOS.
Disclaimer: this is the open-source library I am working on.
We are generating some self-signed certificates for testing using BouncyCastle, but the code throws an exception when we try to add a private key to the certificate. Here's the code in question:
private static X509Certificate2 CreateCertificate(string subject, DateTimeOffset notBefore, DataTimeOffset notAfter, string issuer, AsymmetricKeyParamter issuerPrivateKey)
{
// Setup
X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator();
SecureRandom random = new SecureRandom(new CryptoApiRandomGenerator());
RsaKeyPairGenerator keyPairGenerator = new RsaKeyPairGenerator();
keyPairGenerator.Init(new KeyGenerationParameters(random, KeyStrength));
// Randomly generate a serial number
BigInteger serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
certGenerator.SetSerialNumber(serialNumber);
// Set the issuer and subject names
X509Name issuerName = new X509Name(issuer);
X509Name subjectName = new X509Name(subject);
certGenerator.SetIssuerDN(issuerName);
certGenerator.SetSubjectDN(subjectName);
// Set the validity period
certGenerator.SetNotBefore(notBefore.UtcDateTime);
certGenerator.SetNotAfter(notAfter.UtcDateTime);
// Randomly generate the public key
AsymmetricCipherKeyPair subjectKeyPair = keyPairGenerator.GenerateKeyPair();
certGenerator.SetPublicKey(subjectKeyPair.Public);
// Generate the signed certificate
ISignatureFactory signatureFactory = new Asn1SignatureFactory(SHA256RSASignatureAlgorithm, issuerPrivateKey ?? subjectKeyPair.Private, random);
X509Certificate2 certificate = new X509Certificate2(certGenerator.Generate(signatureFactory).GetEncoded());
// Include the private key with the response
// ERROR HERE!
certificate.PrivateKey = DotNetUtilities.ToRSA(subjectKeyPair.Private as RsaPrivateCrtKeyParameters);
return certificate;
}
This code is in a library that targets .NET Standard 2.0, and the library is a dependency of two different applications: one targeting .NET Core 2.1 and the other targeting .NET Framework 4.7.2. I believe this works fine in the .NET Framework app, but in the .NET Core app I'm getting an exception with this message on the indicated line above:
Operation is not supported on this platform.
Apparently this is expected behavior in .NET Core. I am aware of the CopyWithPrivateKey method as mentioned in this question, which in theory is what I should be using. However, this method is not supported in .NET Standard 2.0 (note the error at the top of the page indicating the redirect). Furthermore, the .NET Framework app cannot be converted to .NET Core at the moment because of some other dependencies which are .NET Framework. According to this matrix, .NET Standard 2.1 is not supported by .NET Framework at all, which means I cannot upgrade to .NET Standard 2.1 and use CopyWithPrivateKey!
How can I create an X509Certificate2 with a private key in .NET Standard 2.0 in a way that's compatible with .NET Core?
After much digging, the only solution I found was to convert the certificate to a PKCS12-formatted byte array, append the private key, and then read it back into an X509Certificate2 object. It sucks, but from what I can tell in .NET Core the only way you can get one with the private key is to either call CopyWithPrivateKey (unavailable in .NET Standard 2.0 as mentioned in the question) or to load PKCS12 data which contains the private key. The following code can do that:
private static X509Certificate2 AddPrivateKeyPlatformIndependent(Org.BouncyCastle.X509.X509Certificate bouncyCastleCert, AsymmetricKeyParameter privateKey)
{
string alias = bouncyCastleCert.SubjectDN.ToString();
Pkcs12Store store = new Pkcs12StoreBuilder().Build();
X509CertificateEntry certEntry = new X509CertificateEntry(bouncyCastleCert);
store.SetCertificateEntry(alias, certEntry);
// TODO: This needs extra logic to support a certificate chain
AsymmetricKeyEntry keyEntry = new AsymmetricKeyEntry(privateKey);
store.SetKeyEntry(alias, keyEntry, new X509CertificateEntry[] { certEntry });
byte[] certificateData;
string password = GenerateRandomString();
using (MemoryStream memoryStream = new MemoryStream())
{
store.Save(memoryStream, password.ToCharArray(), new SecureRandom());
memoryStream.Flush();
certificateData = memoryStream.ToArray();
}
return new X509Certificate2(certificateData, password, X509KeyStorageFlags.Exportable);
}
Use this in place of the last two lines in the question code, and instead of creating an X509Certificate2 with certGenerator, use this to create the equivalent Bouncy Castle Type to pass to it: Org.BouncyCastle.X509.X509Certificate bouncyCastleCert = certGenerator.Generate(signatureFactory);
Since documentation on Bouncy Castle and .NET certificates is sparse at best, here are some quirks I found during this process:
The aliases for the certificate entry and the key entry in the PKCS12 container must be the same. Otherwise, you will get a "Keyset does not exist" exception when trying to use the private key.
You must use X509KeyStorageFlags.Exportable when loading the byte array, otherwise you may get a "Key not valid for use in specified state" exception depending on how you use the cert. This also means you have to supply a password because there's no overload without it, but you can use any old string since it's a temporary password.
For the communication between a c# dotnet-core application (windows and linux) and Chef.io API I need to be able to create the same signature within c# dotnet core as this call of openssl executable:
openssl rsautl -in encryption.txt -sign -inkey chefprivatekey.pem | openssl enc -base64
The first implementation (dotnet framework) was based on this
https://github.com/mattberther/dotnet-chef-api:
byte[] input = Encoding.UTF8.GetBytes(canonicalHeader);
var pemReader = new PemReader(new StringReader(privateKey));
AsymmetricKeyParameter key = ((AsymmetricCipherKeyPair)pemReader.ReadObject()).Private;
ISigner signer = new RsaDigestSigner(new NullDigest());
signer.Init(true, key);
signer.BlockUpdate(input, 0, input.Length);
_signature = Convert.ToBase64String(signer.GenerateSignature());
The nuget package dotnet-chef-api contains also an old version of BouncyCastle.Crypto.dll. As long as I use this dll everyting works fine but if I do an update of the dll to a newer verion by using a nuget-package for this, nothing is working any more. As a workaround I have Implemented a system-call of openssl executable to get the correct signature for the API. This works fine. But I need to get rid of the system-calls.
What I have done now is to compare the system call result of openssl exe with the results I get from any signing algorythms like BouncyCastle lib. I have not been able to get the same result as the above mentioned call of openssl-exe. The implementation needs to be dotnet-core for Windows and Linux.
As long as the the results between openssl exe and any c# implementation are not the same I don't neet to test the chef-api.
I was able to create System.Security.Cryptography.RSACryptoServiceProvider from the pem file folloging this example: How to load the RSA public key from file in C#.
with this Code I am alble to get an object of type RSACryptoServiceProvider from the pem file.
RSACryptoServiceProvider provider = PemKeyUtils.GetRSAProviderFromPemFile(pemFile);
var trial1 = Convert.ToBase64String(provider.SignData(Convert.FromBase64String("test"), new SHA1CryptoServiceProvider()));
The content of "trial1" is the same as "_signature" but not the same as from openssl.exe
What I am doing wrong?
"openssl rsautl" by default uses PKCS#1 v1.5 could this be a problem?
Found a solution on my own. I had a look into the Unittests of BouncyCastle and got now this as a working solition:
public string Sign(byte[] input, string privateKeyPath)
{
asymmetricKeyParameter AsymmetricKeyParameterFromPrivateKeyInPemFile
// Sign the hash
IBufferedCipher c = CipherUtilities.GetCipher("RSA//PKCS1Padding");
c.Init(true, asymmetricKeyParameter);
var outBytes = c.DoFinal(input);
return Convert.ToBase64String(outBytes);
}
public AsymmetricKeyParameter AsymmetricKeyParameterFromPrivateKeyInPemFile(string privateKeyPath)
{
using (TextReader privateKeyTextReader = new StringReader(File.ReadAllText(privateKeyPath)))
{
PemReader pr = new PemReader(privateKeyTextReader);
AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair)pr.ReadObject();
return keyPair.Private;
}
}
I am using:
Windows 10 (Version 1709, OS Build 17025.1000)
.net framework 4.7
VS 2017 (version: 15.3.5)
Here is what I did:
Got a self signed ECC certificate using OpenSSL and steps outlined in the script at https://gist.github.com/sidshetye/4759690 with modifications:
a) Used NIST/P-256 curve over a 256 bit prime field
b) Used SHA-256
Load the certificate from file (generated in previous step) into X509Certificate2 object
Imported the PFX file into windows trust store (for testing). This is successful.
Inspection of the imported certificate shows Public Key field as 'ECC (256 Bits)' and Public key parameters as 'ECDSA_P256'.
Next tried to figure out how to encrypt with this certificate.
I am stuck at the last step because all the examples that use X509Certificate2 object predominantly use only RSA and I am using ECC certificate. For RSA certificate, there is a GetRSAPublicKey extention method on X509Certificate2 and RSA class has Encrypt method. However there is no such method for ECC certificates.
Next, I stumbled on this post (Load a Certificate Using X509Certificate2 with ECC Public Key) and tried following (even though it appeared bizarre as to why ECC cert public key is being coerced into RSA type):
RSACryptoServiceProvider csp = (RSACryptoServiceProvider)cert.PublicKey.Key
I got following exception: The certificate key algorithm is not supported.
Next I stumbled on this post (Importing ECC-based certificate from the Windows Certificate Store into CngKey) which basically tried to create CNGKey type and instantiate ECDsaCng with it. However even if I can do it with ECDiffieHellmanCng, there is no Encrypt method on it.
So I am not really sure how can I proceed further to use ECC X509 certificate's public key to encrypt data.
###Background
Asymmetric algorithms have three different purposes (that I know of)
Encryption
RSA is the only "standard" algorithm that can do this directly.
Signature
RSA
DSA
ECDSA
ElGamal Signature
Key Agreement
Diffie-Hellman (DH)
ECDH
ElGamal encryption (the asymmetric startup phase)
MQV
ECMQV
Because RSA encryption is space limited, and was hard for computers in the '90s, RSA encryption's primary use was in "Key Transfer", which is to say that the "encrypted message" was just the symmetric encryption key for DES/3DES (AES not yet having been invented) - https://www.rfc-editor.org/rfc/rfc2313#section-8.
Key agreement (or transfer) schemes always have to be combined with a protocol/scheme to result in an encryption operation. Such schemes include
TLS (nee SSL)
CMS or S/MIME encrypted-data
IES (Integrated Encryption Scheme)
ECIES (Elliptic Curve Integrated Encryption Scheme)
ElGamal encryption (holistically)
PGP encryption
So what you probably want is ECIES.
ECIES.Net
Currently (.NET Framework 4.7.1, .NET Core 2.0) there's no support to get an ECDiffieHellman object from a certificate in .NET.
Game over, right? Well, probably not. Unless a certificate carrying an ECDH key explicitly uses the id-ecDH algorithm identifier (vs the more standard id-ecc one) it can be opened as ECDSA. Then, you can coerce that object into being ECDH:
using (ECDsa ecdsa = cert.GetECDsaPublicKey())
{
return ECDiffieHellman.Create(ecdsa.ExportParameters(false));
}
(a similar thing can be done for a private key, if the key is exportable, otherwise complex things are required, but you shouldn't need it)
Let's go ahead and carve off the recipient public object:
ECDiffieHellmanPublicKey recipientPublic = GetECDHFromCertificate(cert).PublicKey;
ECCurve curve = recipientPublic.ExportParameters().Curve;
So now we turn to http://www.secg.org/sec1-v2.pdf section 5.1 (Elliptic Curve Integrated Encryption Scheme)
###Setup
Choose ANSI-X9.63-KDF with SHA-2-256 as the hash function.
Choose HMAC–SHA-256–256.
Choose AES–256 in CBC mode.
Choose Elliptic Curve Diffie-Hellman Primitive.
You already chose secp256r1.
Hard-coded. Done.
Point compression's annoying, choose not to use it.
I'm omitting SharedInfo. That probably makes me a bad person.
Not using XOR, N/A.
###Encrypt
Make an ephemeral key on the right curve.
ECDiffieHellman ephem = ECDiffieHellman.Create(curve);
We decided no.
ECParameters ephemPublicParams = ephem.ExportParameters(false);
int pointLen = ephemPublicParams.Q.X.Length;
byte[] rBar = new byte[pointLen * 2 + 1];
rBar[0] = 0x04;
Buffer.BlockCopy(ephemPublicParams.Q.X, 0, rBar, 1, pointLen);
Buffer.BlockCopy(ephemPublicParams.Q.Y, 0, rBar, 1 + pointLen, pointLen);
Can't directly do this, moving on.
Can't directly do this, moving on.
Since we're in control here, we'll just do 3, 4, 5, and 6 as one thing.
KDF time.
// This is why we picked AES 256, HMAC-SHA-2-256(-256) and SHA-2-256,
// the KDF is dead simple.
byte[] ek = ephem.DeriveKeyFromHash(
recipientPublic,
HashAlgorithmName.SHA256,
null,
new byte[] { 0, 0, 0, 1 });
byte[] mk = ephem.DeriveKeyFromHash(
recipientPublic,
HashAlgorithmName.SHA256,
null,
new byte[] { 0, 0, 0, 2 });
Encrypt stuff.
byte[] em;
// ECIES uses AES with the all zero IV. Since the key is never reused,
// there's not risk in that.
using (Aes aes = Aes.Create())
using (ICryptoTransform encryptor = aes.CreateEncryptor(ek, new byte[16]))
{
if (!encryptor.CanTransformMultipleBlocks)
{
throw new InvalidOperationException();
}
em = encryptor.TransformFinalBlock(message, 0, message.Length);
}
MAC it
byte[] d;
using (HMAC hmac = new HMACSHA256(mk))
{
d = hmac.ComputeHash(em);
}
Finish
// Either
return Tuple.Create(rBar, em, d);
// Or
return rBar.Concat(em).Concat(d).ToArray();
###Decrypt
Left as an exercise to the reader.
For getting ECDiffieHellman private key from certificate, use the following method:
Install NuGet package Security.Cryptography (CLR Security). (The package is under MIT license.)
Use the following extension method to get the CngKey instance:
CngKey cngKey = certificate.GetCngPrivateKey();
(Note: The extension method certificate.GetECDsaPrivateKey(), natively supported in .NET, returns an ECDsaCng instance; there is no extension method to return ECDiffieHellmanCng.)
The cngKey instance can be used to create either an ECDsaCng or an ECDiffieHellmanCng instance:
var sa = new ECDsaCng(cngKey);
var sa = new ECDiffieHellmanCng(cngKey);
We have this business case where our c# web app will generate a public/private key pair for a corresponding iOS mobile device. The mobile device will use the public key to encrypt data and send it back up to the server.
I am using the following c# code.
const int PROVIDER_RSA_FULL = 1;
const string CONTAINER_NAME = "KeyContainer";
CspParameters cspParams;
cspParams = new CspParameters(PROVIDER_RSA_FULL);
cspParams.KeyContainerName = CONTAINER_NAME;
cspParams.Flags = CspProviderFlags.UseMachineKeyStore;
cspParams.ProviderName = "Microsoft Strong Cryptographic Provider";
var rsa = new RSACryptoServiceProvider(2048,cspParams);
rsa.PersistKeyInCsp = false;
string publicPrivateKeyXML = rsa.ToXmlString(true);
After that, I don't know how to get the rsa.ToXmlString to the iOS app in a format it can support with the objective-c Crypto Libraries. How can I covert that public key (modulus +exponent) WITHOUT using OpenSSL to something iOS will understand and encrypt data with. This key generation will happen a lot (not once), and any new library has to approved before use. So answers that don't require additional libraries are favored.
Please use this C# from this ref, this class will generate public and private RSA key in SSh pem style:
Sample class