Intro
I am working on converting a Java library to .Net.
The library is an implementation of polymorphic pseudonym decryption and will be used in The Netherlands to decrypt "BSNk"s in the area of European eIDAS electronic identification services.
I have already converted most of the library and worked with the author of the Java version to verify the results. The next step is to make the .Net library actually usable for Dutch companies, and that's where I have been stuck for the past 2 weeks.
The algorithms use an elliptic curve in a PEM file as one of the parts for the calculation. But the clients (the users of the library) will receive this in the form of a p7 and a p8 file which you can convert/extract/decode (?) to the PEM data.
Question
How can I get from te p7+p8 files to a PEM string in C#?
Preferably using just System.Security.Cryptography.Pkcs, but I currently am using BouncyCastle in other parts (because the Java version did). Not listed below but I did also try to do this using SignedCms and EnvelopedCms, but got nothing but (to me) incomprehensible errors from that. I don't have a lot of experience in cryptography, but have learned quite a bit over the past few weeks.
If I understand it correctly than I would explain this as the p7 file being the envelope of the PEM message, and the envelope is signed/encrypted using the private key in the p8 file?
Code
public static string ConvertToPem(string p7File, string p8File)
{
var p7Data = File.ReadAllBytes(p7File);
var p8Data = File.ReadAllBytes(p8File);
// Java version gets the private key like this:
// KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(bytesArray));
var privateKey = PrivateKeyFactory.CreateKey(p8Data);
var parser = new CmsEnvelopedDataParser(p7Data);
var recipients = parser.GetRecipientInfos().GetRecipients().OfType<RecipientInformation>();
var recipientInformation = recipients.First();
//Java version gets the message like this:
//final byte[] message = keyInfo.getContent(new JceKeyTransEnvelopedRecipient(key).setProvider("BC"));
var keyInfo = (KeyTransRecipientInformation)recipientInformation;
var message = keyInfo.GetContent(privateKey);
return Encoding.ASCII.GetString(message);
}
Update 8-10-2018 Following a tip from the author of the Java library I tried to skip the problem of automatically converting to PEM and just using openssl to decrypt it. Unfortunately the openssl command to decrypt the files also fails! Both on Windows as well as on Linux. The strange this is that this is done using the same files that work perfectly fine when used in the Java library. Is the p8 corrupt? Is it somehow only compatible when used in the Java JceKeyTransEnvelopedRecipient???
openssl cms -decrypt -inform DER -in dv_keys_ID_D_oin.p7 -inkey privatep8.key -out id.pem
(I also tried using PEM instead of DER but to no avail. The files are in the GitHub repo)
Update 9-10-2018 Thanks to Carl who figured out the cause of the seemingly corrupt p8 file. Instead of directly decrypting it using openssl cms we had to convert the binary DER p8 to a base64 encoded PEM first.
openssl pkcs8 -inform der -outform pem -in private.p8 -out private-p8.pem -topk8 -nocrypt
We could also do this in c# by reading the bytes from the p8 file, converting them to Base64 and adding the BEGIN/END PRIVATE KEY header/footer around it.
Resources
You can see this code being used and failing as a unit test in my project. The project also includes matching p7, p8 and PEM files to test with.
The Java version can be found here: https://github.com/BramvanPelt/PPDecryption
My work in progress version can be found here: https://github.com/MartijnKooij/PolymorphicPseudonymisation
Finally I was able to successfully decrypt the message; it looks like the BouncyCastle APIs are ignoring the SHA-256 OAEP directive and stick to SHA-1 OAEP which results in the padding exception. Additionally, the Microsoft APIs leverage X509Certificate2
which only support RsaCryptoServiceProvider
with SHA-1 OAEP support as far as I discovered. One needs the newer RsaCng
for SHA-256 OAEP support. I think we need to raise a ticket with corefx (https://github.com/dotnet/corefx) as well as bc-csharp (https://github.com/bcgit/bc-csharp).
The following c# code will decrypt the message; using Microsoft APIs:
// Read the RSA private key:
var p8Data = File.ReadAllBytes(@"resources\private.p8");
CngKey key = CngKey.Import(p8Data, CngKeyBlobFormat.Pkcs8PrivateBlob);
var rsaprovider = new RSACng(key);
// Process the enveloped CMS structure:
var p7Data = File.ReadAllBytes(@"resources\p7\ID-4.p7");
var envelopedCms = new System.Security.Cryptography.Pkcs.EnvelopedCms();
envelopedCms.Decode(p7Data);
var recipients = envelopedCms.RecipientInfos;
var firstRecipient = recipients[0];
// Decrypt the AES-256 CBC session key; take note of enforcing OAEP SHA-256:
var result = rsaprovider.Decrypt(firstRecipient.EncryptedKey, RSAEncryptionPadding.OaepSHA256);
// Build out the AES-256 CBC decryption:
RijndaelManaged alg = new RijndaelManaged();
alg.KeySize = 256;
alg.BlockSize = 128;
alg.Key = result;
// I used an ASN.1 parser (https://lapo.it/asn1js/) to grab the AES IV from the PKCS#7 file.
// I could not find an API call to get this from the enveloped CMS object:
string hexstring = "919D287AAB62B672D6912E72D5DA29CD";
var iv = StringToByteArray(hexstring);
alg.IV = iv;
alg.Mode = CipherMode.CBC;
alg.Padding = PaddingMode.PKCS7;
// Strangely both BouncyCastle as well as the Microsoft API report 406 bytes;
// whereas https://lapo.it/asn1js/ reports only 400 bytes.
// The 406 bytes version results in an System.Security.Cryptography.CryptographicException
// with the message "Length of the data to decrypt is invalid.", so we strip it to 400 bytes:
byte[] content = new byte[400];
Array.Copy(envelopedCms.ContentInfo.Content, content, 400);
string decrypted = null;
ICryptoTransform decryptor = alg.CreateDecryptor(alg.Key, alg.IV);
using (var memoryStream = new MemoryStream(content)) {
using (var cryptoStream = new CryptoStream(memoryStream, alg.CreateDecryptor(alg.Key, alg.IV), CryptoStreamMode.Read)) {
decrypted = new StreamReader(cryptoStream).ReadToEnd();
}
}
The implementation of StringToByteArray
is as follows:
public static byte[] StringToByteArray(String hex) {
NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With