How to load a certificate with private key from PEM files in .NET standard - x509certificate

I'm trying to load an X509Certificate2 from PEM files in a .NET standard library.
I created a self-signed certificate using openssl like so:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj /CN=localhost -days 365
I loaded the resulting PEM files into embedded string resources within the project and am trying to load them with the following code:
private X509Certificate2 GetCertificate()
{
try
{
byte[] pubPem = System.Text.Encoding.UTF8.GetBytes(Properties.Resources.DefaultPublicPem.Trim());
var cert = new X509Certificate2(pubPem);
var rsa = GetRSAFromPem(Properties.Resources.DefaultPrivatePem.Trim());
cert.PrivateKey = rsa;
return cert;
}
catch (Exception ex)
{
// ignore errors
return null;
}
}
public static RSA GetRSAFromPem(String pemstr)
{
RSA rsaKey = RSA.Create();
Func<RSA, RsaKeyParameters, RSA> MakePublicRCSP = (RSA rcsp, RsaKeyParameters rkp) =>
{
RSAParameters rsaParameters = DotNetUtilities.ToRSAParameters(rkp);
rcsp.ImportParameters(rsaParameters);
return rsaKey;
};
Func<RSA, RsaPrivateCrtKeyParameters, RSA> MakePrivateRCSP = (RSA rcsp, RsaPrivateCrtKeyParameters rkp) =>
{
RSAParameters rsaParameters = DotNetUtilities.ToRSAParameters(rkp);
rcsp.ImportParameters(rsaParameters);
return rsaKey;
};
PemReader reader = new PemReader(new StringReader(pemstr));
object kp = reader.ReadObject();
// If object has Private/Public property, we have a Private PEM
var hasPrivate = kp.GetType().GetProperty("Private") != null;
var isPrivate = kp is RsaPrivateCrtKeyParameters;
return isPrivate ? MakePrivateRCSP(rsaKey, (RsaPrivateCrtKeyParameters)kp) : hasPrivate ? MakePrivateRCSP(rsaKey, (RsaPrivateCrtKeyParameters)(((AsymmetricCipherKeyPair)kp).Private)) : MakePublicRCSP(rsaKey, (RsaKeyParameters)kp);
}
I tested it on Android and it works great.
On iOS I haven't tested yet, but on UWP it fails and I get a PlatformNotSupported Exception while trying to set the PrivateKey on the certificate.
So I'm wondering what's not supported, and whether I'm not doing it right.

According to this, setting the private key on an existing certificate is not supported in .net core:
The PrivateKey property will be back in netstandard2.0 (#12295), but
it will throw on set for .NET Core.
set_PrivateKey has a large amount of nuance in .NET Framework
(depending on how you use it you can end up with side effects that
persist across machine reboots), and mirroring that level of nuance to
platforms other than Windows is awfully tricky, which is why we don't
support it.
The only supported way to have a cert with a private key on .NET Core
is through a PFX/PKCS12 file (or the cert+key pair to already be
associated via X509Store).
So one way to solve this was to merge public-private pair to a PFX file, embed it as a resource, and initialize the X509Certificate2 from that PFX.
Another way, which I ended up using, is to use the method RSACertificateExtensions.CopyWithPrivateKey on UWP.
So basically I ended up building a platform specific interface for loading the certificates. On UWP it was implemented like this:
public class UWPCertificateBuilder : ICertificateBuilder
{
public X509Certificate2 GetCertificate(X509Certificate2 cert, RSA key)
{
return cert.CopyWithPrivateKey(key);
}
}
On Android it was implemented like this:
public class DroidCertificateBuilder : ICertificateBuilder
{
public X509Certificate2 GetCertificate(X509Certificate2 cert, RSA key)
{
cert.PrivateKey = key;
return cert;
}
}

Instead of RSACryptoServiceProvider you should use base RSA class. In .NET Standard and .NET Core on Windows, RSA private key is resolved to RSACng, instead of legacy RSACryptoServiceProvider.
See this thread for more details: X509AsymmetricSecurityKey.GetAsymmetricAlgorithm returns null after .Net 4.7.2 upgrade

Related

How to link KSP DLL to Certificate

I want to sign HLKX files using a certificate for which the private key is not available on the local system.
I created a custom Key Storage Provider (basically a shell for testing purposes) based on the code sample for a KSP DLL in "Cryptographic Provider Development Kit" and I'm able to register it and it is shown in the enumeration of KSPs available on the system.
I'm using the sign function that is shown as an example at:
https://learn.microsoft.com/en-us/windows-hardware/test/hlk/user/hlk-signing-with-an-hsm
in a C# application.
The custom KSP dll is supposed to handle all the sign commands and connect to a backend that allows using the private key which is stored in a HSM behind an additional software layer that is limiting key access to certain users.
When I'm running the application the signing fails due to the missing private key. So, I need to somehow link the certificate (being it the certificate in a file or imported to the system's certificate store) to the KSP causing the calls for signing hashes etc. to end up in the KSP's API, but I couldn't find any suitable information on how to either:
a) Add the reference to the KSP to the C# signing call
or
b) Import the certificate to the certificate store with it referencing the KSP so that it will be used automatically if the certificate is used for signing.
So, how can I do either a) or b) or what other way is there to manually handle this? The signing application is just using C# because that's the only sort-of sample for this use case that I could find from Microsoft. If there would be a sample in C/C++ that would be fine, too. I guess the problem would be the same in case of using a CSP instead of KSP but unfortunately many posts are massively mixing the two.
I found a way to create the link between the certificate in the store and the provider dll (via the name under which the provider dll is registered in the system). The relevant system API functions are CertSetCertificateContextProperty and CertGetCertificateContextProperty in Crypt32.dll. I was able to verify that this works for signing hlkx files (from with Hardware Lab Kit software or via C# code using PackageDigitalSignatureManager) but I still have problems using this way to sign e.g. executables using Microsoft's signtool.exe which complains about the private key not be available for the certificate.
I was using the system API functions from within C# so I have extracted the relevant code fragments from my project on how to link the certificate with the provider and how to read information on the linked provider from a certificate.
class Program
{
private const UInt32 CERT_SET_KEY_CONTEXT_PROP_ID = 0x00000001;
private const UInt32 CERT_SET_KEY_PROV_HANDLE_PROP_ID = 0x00000001;
private const UInt32 CERT_KEY_PROV_INFO_PROP_ID = 2;
static void Main(string[] args)
{
// Reading certificate from file
X509Certificate2 certificate = new X509Certificate2("C:\\MyCert.crt");
// Adding certificate to store
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
store.Add(certificate);
store.Close();
// Linking certificate with provider
// ProviderName is the name under which the provider is registered in the system
// ContainerName is a string that will be passed to the DLL when calls are made it can be used to
// additional information to the DLL that can be set when linking the certificate with the provider
SetCertificateProviderInformation("My Provider Name", "MyContainerName", certificate);
// Read provider information
GetCertificateProviderInformation(certificate);
}
private static void SetCertificateProviderInformation(string providerName, string containerName, X509Certificate2 certificate)
{
Crypt32Dll.CRYPT_KEY_PROV_INFO cryptKeyProvInfo = new Crypt32Dll.CRYPT_KEY_PROV_INFO
{
pwszProvName = providerName,
pwszContainerName = containerName,
dwProvType = 24,
dwFlags = CERT_SET_KEY_CONTEXT_PROP_ID | CERT_SET_KEY_PROV_HANDLE_PROP_ID,
cProvParam = 0,
rgProvParam = IntPtr.Zero,
dwKeySpec = 2
};
IntPtr pvData = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Crypt32Dll.CRYPT_KEY_PROV_INFO)));
Marshal.StructureToPtr(cryptKeyProvInfo, pvData, false);
if (Crypt32Dll.CertSetCertificateContextProperty(certificate.Handle, CERT_KEY_PROV_INFO_PROP_ID, 0, pvData))
{
// succeeded
}
else
{
Int32 lastError = Marshal.GetLastWin32Error();
// failed
}
if (pvData != IntPtr.Zero)
{
Marshal.FreeHGlobal(pvData);
}
}
private static void GetCertificateProviderInformation(X509Certificate2 certificate)
{
UInt32 dataSize = 0;
// Get required size for struct
if (Crypt32Dll.CertGetCertificateContextProperty(certificate.Handle, CERT_KEY_PROV_INFO_PROP_ID, IntPtr.Zero, ref dataSize))
{
// Allocate unmanaged struct memory of required size and query the information
IntPtr pvData = Marshal.AllocHGlobal((int)dataSize);
if (Crypt32Dll.CertGetCertificateContextProperty(certificate.Handle, CERT_KEY_PROV_INFO_PROP_ID, pvData, ref dataSize))
{
// succeeded
Crypt32Dll.CRYPT_KEY_PROV_INFO keyProviderInformation = (Crypt32Dll.CRYPT_KEY_PROV_INFO)Marshal.PtrToStructure(pvData, typeof(Crypt32Dll.CRYPT_KEY_PROV_INFO));
Console.Out.WriteLine("Provider Name: " + keyProviderInformation.pwszProvName);
Console.Out.WriteLine("Container Name: " + keyProviderInformation.pwszContainerName);
}
else
{
int lastError = Marshal.GetLastWin32Error();
// failed
}
// Free unmanaged struct memory
Marshal.FreeHGlobal(pvData);
}
else
{
// failed
}
}
}
With the code for using the Crypt32.dll being:
class Crypt32Dll
{
private const string DLL_NAME = "Crypt32.dll";
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal struct CRYPT_KEY_PROV_INFO
{
[MarshalAs(UnmanagedType.LPWStr)]
internal string pwszContainerName;
[MarshalAs(UnmanagedType.LPWStr)]
internal string pwszProvName;
internal UInt32 dwProvType;
internal UInt32 dwFlags;
internal UInt32 cProvParam;
internal IntPtr rgProvParam;
internal UInt32 dwKeySpec;
}
[DllImport(DLL_NAME, EntryPoint = "CertSetCertificateContextProperty", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern bool CertSetCertificateContextProperty(
IntPtr pCertContext,
UInt32 dwPropId,
UInt32 dwFlags,
IntPtr pvData
);
[DllImport(DLL_NAME, EntryPoint = "CertGetCertificateContextProperty", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern bool CertGetCertificateContextProperty(
IntPtr pCertContext,
UInt32 dwPropId,
IntPtr pvData,
ref UInt32 pcbData
);
}

.NetCore 2.2: How to get a private key of a certificate?

for the implementation of an API I use, I need to provide a certificate, which consists of 2 byte arrays one for the public key and the other one for private key.
My initial idea was to do this with X509Certificate object of .Net. But I am struggling to get the private key bytes.
var certificate = new X509Certificate2("testCert.pfx", password, X509KeyStorageFlags.Exportable);
byte[] myPublicKey = certificate.GetRawCertData();
byte[] privateKey = ???
I've tried to export the key, but I can't export the private key standalone.
And:
certificate.PrivateKey.ToXmlString(true);
is not available on a Ubuntu System :-(
Do you have any ideas, how to get the private bytes from certificates?
May be X509Certificate2 is not the best solution for this...
Use an approrpiate method of these X509Certificate2 extension methods:
GetRSAPrivateKey(X509Certificate2) -- for RSA keys
GetDSAPrivateKey(X509Certificate2) -- for DSA keys
GetECDsaPrivateKey(X509Certificate2) -- for EC keys
Extension method you need to use depends on asymmtric key algorithm.

Encryption a message which was generated in .NET Core 2.1 under linux adds leading bytes

If have the follwoing situation:
A Client app runs on Raspian with .NETCore 2.1 (Linux)
It encrypts a message with a public cert (RSA)
Sends this encrypted message to a windows server
This server decrypts the message
Now the data has 2 more bytes at the beginning of the text!
Example: Encrypting "Hallo welt" under linux leads to "\u0004\nHallo welt" in windows.
For me it seems that there is a bug in the linux implementation of the .NETCore encryption, or (maybe padding-problem)?
Encryption is done with
public static byte[] Encrypt(byte[] plainData, X509Certificate2 certificate) {
var message = new EnvelopedCms(new ContentInfo(plainData));
message.Encrypt(new CmsRecipient(SubjectIdentifierType.IssuerAndSerialNumber, certificate));
return message.Encode();
}
Decryptiong with:
public static byte[] Decrypt(byte[] encryptedData, X509Certificate2 certificate) {
var message = new EnvelopedCms();
message.Decode(encryptedData);
message.Decrypt(new X509Certificate2Collection {certificate} );
return message.ContentInfo.Content;
}
I can also provide an very simple example project if needed...
This is a known bug in .NET Core and will be fixed in 3.0...
See: https://github.com/dotnet/corefx/issues/32978

OAEPwithMD5andMGF1Padding in node-rsa

I am trying to figure out how to do RSA encryption with OAEPwithMD5andMGF1Padding in node-rsa.
Below is my code in node.js.
var NodeRSA = require('node-rsa');
var fs = require('fs');
var publicKey = '-----BEGIN PUBLIC KEY-----\n*****\n-----END PUBLIC KEY-----';
var privateKey = '-----BEGIN RSA PRIVATE KEY-----\n*****\n-----END RSA PRIVATE KEY-----'
const constants = require('constants');
var options1 = {
environment: 'node',
encryptionScheme: {
scheme: 'pkcs1_oaep',
hash: 'md5', //hash using for scheme
}
}
var text = 'This is the string to be encrypted using RSA!';
var encryptKey = new NodeRSA(publicKey, 'pkcs8-public', options1);
encryptKey.setOptions(options1)
var encrypted = encryptKey.encrypt(text, 'base64');
console.log(encrypted);
console.log(encryptKey.isPublic(true))
var options2 = {
environment: 'node',
encryptionScheme: {
scheme: 'pkcs1_oaep', //scheme
hash: 'md5', //hash using for scheme
}
}
var decryptKey = new NodeRSA(privateKey, 'pkcs1', options2);
decryptKey.setOptions(options2)
var decrypted = decryptKey.decrypt(encrypted, 'utf8');
console.log('decrypted: ', decrypted);
Result of running the above code.
f1zi49yKJSqkWW2J3Jt2lf1fe79JgqufFawYESOJRqhM4YEcGQBcaP39yptn7vShhsJBCTUOsbiV1YcW/YUzoaSQzX9YU0iTMara7h+LNLUrq4FZ2twy5X3uyAP1sUD1SnvQvlRJqrAh23UAwnx31rv6ySC+XgpLPR7wHYaDbSgyQKiF3qhGRj2SIAZ6weziNPfEm9FifBVjnWMvGDQYbjLbanbnSriN+bWpRtXKH9pQqMoskkiMwCviJdKtKzz/vVr0littPLnw0ojbsGSPKQPS3U3xCH3QiBmxEegc0uy3sJdk6aH/2SMuoPzGu7VS+PsLQctxnvKNnC9qsLFWyA==
true
decrypted: This is the string to be encrypted using RSA!
Below is my code in JAVA
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import javax.crypto.Cipher;
public class DecryptATT {
public static void main(String[] args) throws Exception {
String encryptedData = "f1zi49yKJSqkWW2J3Jt2lf1fe79JgqufFawYESOJRqhM4YEcGQBcaP39yptn7vShhsJBCTUOsbiV1YcW/YUzoaSQzX9YU0iTMara7h+LNLUrq4FZ2twy5X3uyAP1sUD1SnvQvlRJqrAh23UAwnx31rv6ySC+XgpLPR7wHYaDbSgyQKiF3qhGRj2SIAZ6weziNPfEm9FifBVjnWMvGDQYbjLbanbnSriN+bWpRtXKH9pQqMoskkiMwCviJdKtKzz/vVr0littPLnw0ojbsGSPKQPS3U3xCH3QiBmxEegc0uy3sJdk6aH/2SMuoPzGu7VS+PsLQctxnvKNnC9qsLFWyA==";
// Cipher decrypt = Cipher.getInstance("RSA/ECB/OAEPwithMD5andMGF1Padding");
Cipher decrypt = Cipher.getInstance("RSA/ECB/OAEPwithSHA1andMGF1Padding");
RSAPrivateKey privateKey = getPrivateKey();
System.out.println("test");
decrypt.init(Cipher.DECRYPT_MODE, privateKey);
byte[] original = decrypt.doFinal(Base64.getDecoder().decode(encryptedData));
System.out.println(new String(original));
}
public static RSAPrivateKey getPrivateKey() throws Exception {
String keyPath = "/Users/C.SubbiahVeluAngamuthu/Desktop/Samsung/Docs/att/Keys/3_my_testing/pkcs8_key";
File privKeyFile = new File(keyPath);
BufferedInputStream bis = null;
try {
bis = new BufferedInputStream(new FileInputStream(privKeyFile));
} catch (FileNotFoundException e) {
throw new Exception("Could not locate keyfile at '" + keyPath + "'", e);
}
byte[] privKeyBytes = new byte[(int) privKeyFile.length()];
bis.read(privKeyBytes);
bis.close();
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
KeySpec ks = new PKCS8EncodedKeySpec(privKeyBytes);
RSAPrivateKey privKey = (RSAPrivateKey) keyFactory.generatePrivate(ks);
return privKey;
}
}
Below is the result of running the JAVA code
test
This is the string to be encrypted using RSA!
But when I change the cipher instance from RSA/ECB/OAEPwithSHA1andMGF1Padding to "RSA/ECB/OAEPwithMD5andMGF1Padding"(which I am assuming is the one that I mentioned in encryptionScheme of node.js program) it throws the below error
test
Exception in thread "main" javax.crypto.BadPaddingException: Decryption error
at sun.security.rsa.RSAPadding.unpadOAEP(RSAPadding.java:499)
at sun.security.rsa.RSAPadding.unpad(RSAPadding.java:293)
at com.sun.crypto.provider.RSACipher.doFinal(RSACipher.java:363)
at com.sun.crypto.provider.RSACipher.engineDoFinal(RSACipher.java:389)
at javax.crypto.Cipher.doFinal(Cipher.java:2165)
at DecryptATT.main(DecryptATT.java:26)
Could some one help me where it is going wrong ?
RSAES-OAEP is parametrized by
the hash Hash used by OAEP, and its width in octet noted hLen
the size of the public key, k octets
the Mask Generation Function (MGF) used by OAEP
Almost invariably, the MGF is MFG1, which itself is parametrized by the hash Hash' used by MFG1, and its width in octet noted hLen' (the ' is not in the standard, I'm making up this notation).
You guessed it, there's noting stating that Hash and Hash' are the same, or even that hLen= hLen'.
And, believe me, unless something special is done about it, under a typical Java environement "RSA/ECB/OAEPwithMD5andMGF1Padding" (if supported) will use MD5 for Hash but default to SHA-1 for Hash'; when perhaps node.js uses MD5 for both.
Facing a similar problem with SHA-256 rather than MD5, we can coerce the nice Java runtime to do the Right Thing with
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.DECRYPT_MODE, privKey, new OAEPParameterSpec(
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT
));
I fear you won't be so lucky, since MGF1ParameterSpec seems to never have had an MD5 specifier; but perhaps give a try to new MGF1ParameterSpec("MD5") to get one before giving up.
If one really needs to get the job done under Java, one option is to roll one's RSAES-OAEP with MD5 on top of Cipher invoked with "RSA/ECB/NoPadding", which will perform textbook RSA, by far the most complex building block (at least, all the key management, modular arithmetic, and ability to offload to an HSM is taken care of). That's few dozens lines of code, including MFG1.
Another option might be BouncyCastle.
It's a bad idea to keep using MD5, even in MFG1. And it is an unmitigated disaster to use it as the main hash if adversaries can choose a part of the message at a time when they know what's before that part. If in doubt, don't use MD5.

Could not create SSL/TLS secure channel when connecting through WSS, only on Azure

I am building a Web Api (using ASP.NET Web API), that connects via Secure WebSockets to an endpoint that our client exposed (wss://client-domain:4747/app/engineData). They gave me their certificates all in .pem format (root.pem and client.pem), and a private key (client_key.pem).
In order to get this done I did the following:
1) Converted client.pem and client_key.pem to a single .pfx file (used this here: Convert a CERT/PEM certificate to a PFX certificate)
2) I used the library System.Net.WebSockets, and wrote the following code:
private void InitWebSockesClient()
{
client = new ClientWebSocket();
client.Options.SetRequestHeader(HEADER_KEY, HEADER_VALUE); //Some headers I need
AddCertificatesSecurity();
}
private void AddCertificatesSecurity()
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
| SecurityProtocolType.Tls11
| SecurityProtocolType.Tls12;
// I KNOW THIS SHOULDNT BE USED ON PROD, had to use it to make it
// work locally.
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
X509Certificate2 x509 = new X509Certificate2();
// this is the pfx I converted from client.pem and client_key
byte[] rawData = ReadFile(certificatesPath + #"\cert.pfx");
x509.Import(rawData, "123456", X509KeyStorageFlags.UserKeySet);
X509Certificate2Collection certificateCollection = new X509Certificate2Collection(x509);
client.Options.ClientCertificates = certificateCollection;
}
And when I want to connect I call:
public async Task<bool> Connect()
{
Uri uriToConnect = new Uri(URL);
await client.ConnectAsync(uriToConnect, CancellationToken.None);
return client.State == WebSocketState.Open;
}
This works fine locally. But whenever I deploy my Web Api on Azure (App Service) and make an HTTP request to it, it throws:
System.Net.WebSockets.WebSocketException - Unable to connect to the remote server.
And the inner exception:
System.Net.WebException - The request was aborted: Could not create SSL/TLS secure channel.
I enabled WebSockets on the AppService instance.
If I delete the line that always return true for the certificate validation, it doesn't work even locally, and the message says something like:
The remote certificate is invalid according to the validation procedure.
So definitely I got something wrong with the certificates, those three .pem files are being used right now in a similar [![enter image description here][1]][1]app in a node.js and work fine, the WSS connection is established properly. I don't really know what usage give to each one, so I am kind of lost here.
These are the cipher suites of the domain I want to connect: https://i.stack.imgur.com/ZFbo3.png
Inspired by Tom's comment, I finally made it work by just adding the certificate to the Web App in Azure App Service, instead of trying to use it from the filesystem. First I uploaded the .pfx file in the SSL Certificates section in Azure. Then, in the App settings, I added a setting called WEBSITE_LOAD_CERTIFICATES, with the thumbprint of the certificate I wanted (the .pfx).
After that, I modified my code to do work like this:
private void InitWebSockesClient()
{
client = new ClientWebSocket();
client.Options.SetRequestHeader(HEADER_KEY, HEADER_VALUE); //Some headers I need
AddCertificateToWebSocketsClient();
}
private void AddCertificateToWebSocketsClient()
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11
| SecurityProtocolType.Tls12;
// this should really validate the cert
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
// reading cert from store
X509Store certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
certStore.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certCollection =
certStore.Certificates.Find(X509FindType.FindByThumbprint,
CERTIFICATES_THUMBPRINT,
false);
if (certCollection.Count > 0)
{
client.Options.ClientCertificates = certCollection;
}
else
{
// handle error
}
certStore.Close();
}
Where CERTIFICATES_THUMBPRINT is a string (thumbsprint of your certificate, the one you saw on Azure).
In case you want to make it work locally, you just need to install the certificate on your computer, as otherwise it won't obviously find it on the store.
Reference for all this in Azure docs: https://learn.microsoft.com/en-us/azure/app-service/app-service-web-ssl-cert-load.

Resources