9

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)

enter image description here

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

Martijn Kooij
  • 1,360
  • 13
  • 23
  • You should be able to read p8 key file using BouncyCastle's `PemParser` – Codeguard Oct 01 '18 at 08:15
  • I am looking for the inverse of that. I have the p7 data and p8 key and I need to create a PEM from that. PemParser is for reading PEM data if I'm not mistaking. – Martijn Kooij Oct 01 '18 at 08:29
  • According to docs, `PemParser`: Class for parsing OpenSSL PEM encoded streams containing X509 certificates, PKCS8 encoded keys and PKCS7 objects. – Codeguard Oct 01 '18 at 12:29
  • PemParser appears to have been renamed to PemReader in C#. That comment is indeed above that PemReader class, but all that it can do is read in a PEM file... At least, that's all that I can find in it. There's also a PEMWriter but I can find no relation with p7 and p8 data... – Martijn Kooij Oct 01 '18 at 18:35
  • You are getting this bad padding exception right? Could it be that the c# implementation contains a bug? One strategy I am considering is to fetch the Bouncy Castle c# source code with its unit tests and define a unit test using the same encryption algorithm as your pkcs#7 file is using. Other strategy is to try out the Microsoft libraries, but then we need to set up an x509 certificate collection. Is an x509 certificate easily producible from the resources you have got? – Carl in 't Veld Oct 03 '18 at 06:36
  • Good tip of testing from the BC source code! Unfortunately the actual error is way too deep into the crypto part for me to make sense of. I suspect the cause is more obvious, probably has to do with me just calling GetContent with the private key (because it fits) instead of the JceKeyTransEnvelopedRecipient which is not available in C# I will look into creating an X509 collection, but to what end would that be? I have looked at ChilKat but could not find any obvious route there... – Martijn Kooij Oct 03 '18 at 08:16
  • Well if we can devise a unit test that just works, then we know you are right and we need some transformation on the private key. Can't find any documentation on the meaning of `JceKeyTransEnvelopedRecipient ` though. The x509 collection is required because Microsoft does not provide a method for just a private key, i.e. `envelopedCms.Decrypt(firstRecipient, coll);` (an instance of `System.Security.Cryptography.Pkcs.EnvelopedCms`) – Carl in 't Veld Oct 03 '18 at 15:36
  • Burned through another day. No luck with the unittest, the code executed is just not relatable to the Java version of bouncy castle, so I can’t reverse engineer what’s what. I also have no clue as to what the JceKeyTransEnvelopedRecipient does... I tried the x509 route but ran into a dead end there as well. Tried https://stackoverflow.com/questions/9143036/create-a-x509certificate2-from-rsacryptoserviceprovider-fails-with-cannot-find-t and https://social.msdn.microsoft.com/Forums/vstudio/en-US/d7e2ccea-4bea-4f22-890b-7e48c267657f/creating-a-x509-certificate-from-a-rsa-private-key-in-pem-file – Martijn Kooij Oct 03 '18 at 20:13
  • Have you raised a ticket with Bouncy Castle https://github.com/bcgit/bc-csharp? Were you able to process a CMS enveloped structure yourself with the same encryption/decryption applied (aes256-CBC)? – Carl in 't Veld Oct 05 '18 at 07:11
  • I have updated the question as I discovered that trying to convert using openssl also fails??? Once that is resolved and I still have the issue in c# I will open an issue with BC. What did you mean with the second part of your suggestion: "Were you able to process a CMS enveloped structure yourself with the same..." ? – Martijn Kooij Oct 08 '18 at 18:35
  • With your help I was able to decrypt the PKCS#7 message, although I had to convert the PKCS#8 binary key to PEM first: `openssl pkcs8 -inform der -outform pem -in private.p8 -out private-p8.pem -topk8 -nocrypt` and then `openssl cms -decrypt -in p7\id-4.p7 -inkey private-p8.pem -out id.pem`. What can I do with the resulting private key? – Carl in 't Veld Oct 08 '18 at 19:46
  • With regards to processing an arbitrary CMS structure; can you encrypt and decrypt one yourself with the Bouncy Castle library using the same encryption? – Carl in 't Veld Oct 08 '18 at 19:49
  • A OK, so the .p8 file is the binary DER format, which you could then convert to base64 and add the BEGIN/END PRIVATE KEY headers around it to make it a PEM using C# as well. Got it! That does at least solve the mystery of the "corrupt" file, just a misunderstanding. I have updated the project repo and readme so you can now see how you could use it using PEM data. But I would still like to be able to convert the p7+p8 inside the library so clients would not have to depend on openssl. Not sure if this brings me closer? – Martijn Kooij Oct 09 '18 at 21:17

2 Answers2

1

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;
}
Carl in 't Veld
  • 1,363
  • 2
  • 14
  • 29
  • Looks good Carl, great work. I will test you solution and mark the answer as valid asap. Would you also like to work on the PR for this issue or is stack overflow fame enough for you? – Martijn Kooij Oct 14 '18 at 06:25
  • 2 comments: 1. You should probably mention (or include the source of) that the StringToByteArray is for converting a HEX string to a byte array. 2. Instead of PaddingMode.Zeros on the RijndaelManaged I had to use PaddingMode.PKCS7 to make the outcome the same as the provided PEM. This is all just about white spaces after the PEM footer. – Martijn Kooij Oct 14 '18 at 07:05
  • Hi @MartijnKooij it will be a pleasure to submit a pull request with this solution. Although we first need to add an ASN.1 parser to get the AES IV from the CMS file. In the meanwhile I will amend the proposed solution with your feedback. – Carl in 't Veld Oct 14 '18 at 09:09
0

You should be able to achieve your goals with .NET 4.7.2:

using (CngKey key = CngKey.Import(p8Data, CngKeyBlobFormat.Pkcs8PrivateBlob))
{
    // The export policy needs to be redefined because CopyWithPrivateKey
    // needs to export/re-import ephemeral keys
    key.SetProperty(
        new CngProperty(
            "Export Policy",
            BitConverter.GetBytes((int)CngExportPolicies.AllowPlaintextExport),
            CngPropertyOptions.Persist));

    using (RSA rsa = new RSACng(key))
    using (X509Certificate2 cert = new X509Certificate2(certData))
    using (X509Certificate2 certWithKey = cert.CopyWithPrivateKey(rsa))
    {
        EnvelopedCms cms = new EnvelopedCms();
        cms.Decode(p7Data);
        cms.Decrypt(new X509Certificate2Collection(certWithKey));

        // I get here reliably with your reference documents
    }
}
bartonjs
  • 30,352
  • 2
  • 71
  • 111
  • Trying this out right now. In the 3rd using statement you use an undefined variable `certData`, is that a typo or is something missing? Still searching for CopyWithPrivateKey in .Net Core, it should be there I think but I cannot find it yet... – Martijn Kooij Oct 20 '18 at 12:19
  • `CopyWithPrivateKey` is not in net standard 2.0... It's in Core 2.0 in full 4.7, but not in the standard... Not sure if this part is worth creating 2 implementations. Any ideas? – Martijn Kooij Oct 20 '18 at 12:50
  • `certData` is an "I assume you have a copy of the recipient cert somewhere". As for CopyWithPrivateKey, if it's the only reason you have to dual-compile you could make your own extension method to reflect-invoke it off of the RSACertificateExtensions type. (A little fragile) – bartonjs Oct 20 '18 at 17:32
  • I only have the p7 and p8, nothing more, nothing less. They contain the EC private key PEM which I need for the rest of the algorithm. The RSA extensions depend on CertificatePAL which contains platform specific code, so that unfortunately would be a pain to extract and maintain. – Martijn Kooij Oct 20 '18 at 19:06
  • Reflection isn't the same as copying the code... as you see copying it really won't work. Your reference link has the cert next to the p8, so I assumed it was fair game. If you really don't have the cert, you could make one up with CertificateRequest so that it matches the recipient, but that's tricky. (Or you want API from .NET Core 3.0). But if it's really an EC key then the .NET Core EnvelopedCms won't work for you, it only supports RSA / KeyTransfer. – bartonjs Oct 20 '18 at 19:29