huysentruitw / pem-utils

Managed .NET (C#) utility library for working with PEM files with DER/ASN.1 encoding

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

PemWriter creates an invalid PEM file when byte begin with a zero

anupkattel opened this issue · comments

Initially I found an intermittent issue in my integration tests with RSAParameters to PEM conversion using PemWriter and reading it back using PemReader.

Now I have narrowed it down to PemWriter and I can replicate it.

When any of the RSAParameters such as Modulus, D, P, etc start with zero as the first byte, PemWriter generates an invalid PEM file. It does not include the first zero in the PEM file.

I am looking further into this. I will post if I find anything interesting.

All initial zeros in the byte arrays of RSAParameters are lost when we convert them to big integers.

        public void WritePrivateKey(RSAParameters parameters)
        {
            var sequence = new DerAsnSequence(new DerAsnType[]
            {
                new DerAsnInteger(ToBigInteger(new byte[] { 0x00 })),   // Version
                new DerAsnInteger(ToBigInteger(parameters.Modulus)),
                new DerAsnInteger(ToBigInteger(parameters.Exponent)),
                new DerAsnInteger(ToBigInteger(parameters.D)),
                new DerAsnInteger(ToBigInteger(parameters.P)),
                new DerAsnInteger(ToBigInteger(parameters.Q)),
                new DerAsnInteger(ToBigInteger(parameters.DP)),
                new DerAsnInteger(ToBigInteger(parameters.DQ)),
                new DerAsnInteger(ToBigInteger(parameters.InverseQ))
            });

Unless we know the length of the array by some means such as knowing RSA Key size, I'm not sure how DerAsn can tell the real length.

Turns out that the problem is not in the export, it's the import that doesn't work well.

I used .net core's inbuilt PKS#1 exporter and tried exporting a certificate that doesn't get exported back by PemUtils properly. The exported PEM file are exactly the same from both methods. So, it must be the import that is not taking care of the leading zero bytes.

Thanks for reaching out. Can you post the RSA parameters you're testing with? Just to make sure we'll do the same testing...

Sure. I basically ran a loop 100 times and kept generating new RSAParameters and trying to export to PEM and import back until it failed. I noticed that every time it fails, one of the RSA parts starts with zero byte. For example, D or Private Exponent is like byte[] {0, .......}

Here's a valid PEM file that works on this website: https://8gwifi.org/PemParserFunctions.jsp

-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAM/tOjXpAjH/9npX23HbImf9ve8JWR4GH7MQze+RdtYYsSY++zQa
OAOzI8qlXIwsZ37dPLHmA4DmDQ8UmEHWTMECAwEAAQJAAKNAn3O0fwTC88MbODEs
3NbJY1dK/62TIVB24To3/BYb4ZwSGSPCmiqJm+rjpQT3SBS4O+sv5xuTDHvFUyOW
5QIhAPk05e5fFsUXKvAusue/6uTsyDVt1tn+fr+hUdgXMuC3AiEA1ZhBOSQotoUw
m3emmJNwQEg1vdsVwfg+WNFVdmkn1kcCIQDCf+TNbO3KmdBrcSc09XcRgTSpbrb3
oUevrOrB6ylMdQIgeCbXuc8PX+z4dNwPyRBXOrHkGVKeoKiGWewXyS5KDI0CIEZ9
Zx+Hyty+TgFVgqXkXCEBW6wj0IlojSeesHMw1AKj
-----END RSA PRIVATE KEY-----

If you try importing this certificate, you will see that the Private Exponent or D will be of 63 bytes instead of being 64 as usual (64 bytes because I generated a 512 bit key for simplicity)

Here's another one where InverseQ or the coefficient starts with a zero:

-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBALaKjfztjNBmquEGlPNLNG5fhoARa7D3vZ+lGHD7Yx7GdOjqPCw2
gmMg/2LrZ3mSqRVNMe8TdjOq5ymUAn7a0Y0CAwEAAQJBAK1xDORr2cYSJv7UwCZD
KLhOr390Df0CB/xuY8DGOzH7Bx6UTlb0wSIfNQT/i+zqStynBYDB9esCnXb3aRoV
xkkCIQDIq/fFd91n1HyFqLbJAXVyvR+WSV6va83hKVYElldvAwIhAOje4O4YPnaX
8tyfs+/XzclDm0adFU2SijLdAZUmidAvAiBgX0ZFDYXFQaTzw8zUx+CR1AYBdQcG
FC3xvppS5ajj5QIhALQ+Ps9LSJ22gq4tlrKf0JJll7wSPbjrIbi4w07Uo3rVAiAA
uDFv77OpPej7Jgi0xfYnJAtkrPG8PKghMe706MhUAw==
-----END RSA PRIVATE KEY-----

In short, the 0x00 is used as padding in case the second byte is > 0x80 (sign-bit) so the number isn't being treated as a negative integer. However, sometimes the 0x00 should be stripped before putting the byte array into RSAParameters and in other cases the 0x00 must be kept, and I don't see when or why.

For reference, here is the openssl code that manages the ASN INTEGER type: https://github.com/openssl/openssl/blob/e7fb44e7c3f7a37ff83a6b69ba51a738e549bf5c/crypto/asn1/a_int.c

We're hitting the same problem, we've been wondering why some keys were not loading properly, and it turns out it's because of leading zeros being omitted. The quickest workaround is to generate and load keys until one of them loads properly, but we should really fix the encoding to encode leading zeros.