20

I am looking for a cross platform way to share public keys for ECDSA signing. I had a great thing going from a performance perspective with CngKey and the standard .NET crypto libraries, but then I couldn't figure out how a 33 (or 65) byte public key (using secp256r1/P256) was getting turned into 104 bytes by MS.. Ergo, I couldn't support cross platform signing and verifying..

I'm using BouncyCastle now, but holy handgranade is it SLOW!

So, looking for suggestions for the following requirements:

  1. Cross platform/Languages (server is .NET, but this is served up via a JSON/Web.API interface)
    • JavaScript, Ruby, Python, C++ etc..
  2. Not crazy as slow on the server
  3. Not so painfully slow people can't use it on the client.

The client has to be able to sign the message, the server has to be able to validate the signature with a public key that was exchanged at registration to the service.

Anyways, Ideas would be awesome... Thanks

samoz
  • 56,849
  • 55
  • 141
  • 195
Josh Handel
  • 1,690
  • 2
  • 13
  • 21
  • Use OpenSSL library. It's the most cross-platform you can get. – prq Jun 16 '14 at 20:09
  • Given recent repeated OpenSSL issues, I am a little leary. but if wrapping openSSL is the only way to get performance and support I'll give it another look (or 3).. – Josh Handel Jun 16 '14 at 20:10
  • There shouldn't be any problems with curves in OpenSSL. It's been used for few years in applications like Bitcoin. You should be OK. – prq Jun 16 '14 at 20:12
  • they've been using it in HTTPS for a few (many many many) years there too with "no issues"... hence my concern :-P.. but yes, given the use case I can probably safely use it... just a touch gun shy. – Josh Handel Jun 16 '14 at 20:17

4 Answers4

40

So I have figured out the format of a CngKey exported in ECCPublicKeyBlob and ECCPrivateKeyBlob. This should allow others to interop between other key formats and CngKey for Elliptcal Curve signing and such.

ECCPrivateKeyBlob is formatted (for P256) as follows

  • [KEY TYPE (4 bytes)][KEY LENGTH (4 bytes)][PUBLIC KEY (64 bytes)][PRIVATE KEY (32 Bytes)]
  • KEY TYPE in HEX is 45-43-53-32
  • KEY LENGTH in HEX is 20-00-00-00
  • PUBLIC KEY is the uncompressed format minus the leading byte (which is always 04 to signify an uncompressed key in other libraries)

ECCPublicKeyBlob is formatted (for P256) as follows

  • [KEY TYPE (4 bytes)][KEY LENGTH (4 bytes)][PUBLIC KEY (64 bytes)]
  • KEY TYPE in HEX is 45-43-53-31
  • KEY LENGTH in HEX is 20-00-00-00
  • PUBLIC KEY is the uncompressed format minus the leading byte (which is always 04 to signify an uncompressed key in other libraries)

So given a uncompressed Public key in Hex from another language, you can trim the first byte, add those 8 bytes to the front and import it using

CngKey.Import(key,CngKeyBlobFormat.EccPrivateBlob);

Note: The key blob format is documented by Microsoft.

The KEY TYPE and KEY LENGTH are defined in BCRYPT_ECCKEY_BLOB struct as:

{ ulong Magic; ulong cbKey; }

ECC public key memory format:

BCRYPT_ECCKEY_BLOB
BYTE X[cbKey] // Big-endian.
BYTE Y[cbKey] // Big-endian.

ECC private key memory format:

BCRYPT_ECCKEY_BLOB
BYTE X[cbKey] // Big-endian.
BYTE Y[cbKey] // Big-endian.
BYTE d[cbKey] // Big-endian.

The MAGIC values available in .NET are in Microsoft's official GitHub dotnet/corefx BCrypt/Interop.Blobs.

internal enum KeyBlobMagicNumber : int
{
    BCRYPT_ECDH_PUBLIC_P256_MAGIC = 0x314B4345,
    BCRYPT_ECDH_PRIVATE_P256_MAGIC = 0x324B4345,
    BCRYPT_ECDH_PUBLIC_P384_MAGIC = 0x334B4345,
    BCRYPT_ECDH_PRIVATE_P384_MAGIC = 0x344B4345,
    BCRYPT_ECDH_PUBLIC_P521_MAGIC = 0x354B4345,
    BCRYPT_ECDH_PRIVATE_P521_MAGIC = 0x364B4345,
    BCRYPT_ECDSA_PUBLIC_P256_MAGIC = 0x31534345,
    BCRYPT_ECDSA_PRIVATE_P256_MAGIC = 0x32534345,
    BCRYPT_ECDSA_PUBLIC_P384_MAGIC = 0x33534345,
    BCRYPT_ECDSA_PRIVATE_P384_MAGIC = 0x34534345
    BCRYPT_ECDSA_PUBLIC_P521_MAGIC = 0x35534345,
    BCRYPT_ECDSA_PRIVATE_P521_MAGIC = 0x36534345,
    ...
    ...
}
agilejoshua
  • 2,848
  • 1
  • 21
  • 30
Josh Handel
  • 1,690
  • 2
  • 13
  • 21
  • Do you have any citable reference for this? Is the byte signature the same for P384 and P521? i.e. would the actual bytes of the public key always be derivable as `len = int32_le(blob[4..7]); pub = blob[8..7+len*2]; priv = blob[8+len*2...]`? – jimbobmcgee May 29 '15 at 17:34
  • 1
    I can't site anything, just unit tests on an internal project and the successful use of this algorithm of storing a few hundred thousand pieces of PII.. – Josh Handel Jun 03 '15 at 14:26
  • 2
    https://msdn.microsoft.com/en-us/library/windows/desktop/aa375520(v=vs.85).aspx. The actual magic values come from bcrypt.h, e.g. #define BCRYPT_ECDSA_PRIVATE_P256_MAGIC 0x32534345L – fsimonazzi Jul 03 '15 at 20:08
  • Thanks, this saved my day. – larsw Oct 01 '15 at 14:19
  • 2
    So. Much. Digging. to find this answer. Thankyou! I hope it can be to as much value for somebody else as it was to me. – Dan Drews Feb 24 '16 at 01:16
  • Same as @DanDrews, thank you very much, and thank you windows... – glihm Mar 31 '20 at 01:14
6

Thanks to you I was able to import a ECDSA_P256 public key from a certificate with this code:

    private static CngKey ImportCngKeyFromCertificate(X509Certificate2 cert)
    {
        var keyType = new byte[] {0x45, 0x43, 0x53, 0x31};
        var keyLength = new byte[] {0x20, 0x00, 0x00, 0x00};

        var key = cert.PublicKey.EncodedKeyValue.RawData.Skip(1);

        var keyImport = keyType.Concat(keyLength).Concat(key).ToArray();

        var cngKey = CngKey.Import(keyImport, CngKeyBlobFormat.EccPublicBlob);
        return cngKey;
    }

The 65 byte keys (public key only) start with 0x04 which needs to be removed. Then the header you described is added.

then I was able to verify a signature like that:

var crypto = ECDsaCng(cngKey);
var verify = crypto.VerifyHash(hash, sig);
andreas
  • 301
  • 3
  • 6
1

I just thought I would say thanks to both above posts as it helped me out tremendously. I had to verify a signature using RSA public key using the RSACng object. I was using the RSACryptoServiceProvider before, but that is not FIPS compliant, so I had some problems switching to RSACng. It also requires .NET 4.6. Here is how I got it to work using the above posters as an example:

                    // This structure is as the header for the CngKey
                    // all should be byte arrays in Big-Endian order
                    //typedef struct _BCRYPT_RSAKEY_BLOB {
                    //  ULONG Magic; 
                    //  ULONG BitLength; 
                    //  ULONG cbPublicExp;
                    //  ULONG cbModulus;
                    //  ULONG cbPrime1;  private key only
                    //  ULONG cbPrime2;  private key only
                    //} BCRYPT_RSAKEY_BLOB;

                    // This is the actual Key Data that is attached to the header
                    //BCRYPT_RSAKEY_BLOB
                    //  PublicExponent[cbPublicExp] 
                    //  Modulus[cbModulus]

                    //first get the public key from the cert (modulus and exponent)
                    // not shown
                    byte[] publicExponent = <your public key exponent>; //Typically equal to from what I've found: {0x01, 0x00, 0x01}
                    byte[] btMod = <your public key modulus>;  //for 128 bytes for 1024 bit key, and 256 bytes for 2048 keys

                    //BCRYPT_RSAPUBLIC_MAGIC = 0x31415352,
                    // flip to big-endian
                    byte[] Magic = new byte[] { 0x52, 0x53, 0x41, 0x31}; 

                    // for BitLendth: convert the length of the key's Modulus as a byte array into bits,
                    // so the size of the key, in bits should be btMod.Length * 8. Convert to a DWord, then flip for Big-Endian 
                    // example 128 bytes = 1024 bits = 0x00000400 = {0x00, 0x00, 0x04, 0x00} = flipped {0x00, 0x04, 0x00, 0x00}
                    // example 256 bytes = 2048 bits = 0x00000800 = {0x00, 0x00, 0x08, 0x00} = flipped {0x00, 0x08, 0x00, 0x00}
                    string sHex = (btMod.Length * 8).ToString("X8");
                    byte[] BitLength = Util.ConvertHexStringToByteArray(sHex);
                    Array.Reverse(BitLength); //flip to Big-Endian

                    // same thing for exponent length (in bytes)
                    sHex = (publicExponent.Length).ToString("X8");
                    byte[] cbPublicExp = Util.ConvertHexStringToByteArray(sHex);
                    Array.Reverse(cbPublicExp);

                    // same thing for modulus length (in bytes)
                    sHex = (btMod.Length).ToString("X8");
                    byte[] cbModulus = Util.ConvertHexStringToByteArray(sHex);
                    Array.Reverse(cbModulus);                      

                    // add the 0 bytes for cbPrime1 and cbPrime2 (always zeros for public keys, these are used for private keys, but need to be zero here)
                    // just make one array with both 4 byte primes as zeros
                    byte[] cbPrimes = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

                    //combine all the parts together into the one big byte array in the order the structure
                    var keyImport = Magic.Concat(BitLength).Concat(cbPublicExp).Concat(cbModulus).Concat(cbPrimes).Concat(publicExponent).Concat(btMod).ToArray();

                    var cngKey = CngKey.Import(keyImport, CngKeyBlobFormat.GenericPublicBlob);

                    // pass the key to the class constructor
                    RSACng rsa = new RSACng(cngKey);

                    //verify: our randomly generated M (message) used to create the signature (not shown), the signature, enum for SHA256, padding
                    verified = rsa.VerifyData(M, signature, HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1);

Note: The sign byte for the modulus (0x00) can either be included in the modulus or not, so the length will be one bigger if it is included. CNGkey seems to handle it ok either way.

jrussID
  • 21
  • 1
  • 6
0

You convert EC key to BCRYPT_ECCKEY_BLOB by like this. We should ignore the first byte from EC key because it just represent compressed/uncompressed format.

BCRYPT_ECCKEY_BLOB eccBlobHeader;
PCHAR bycrtptKey;
eccBlobHeader.dwMagic = BCRYPT_ECDH_PUBLIC_P384_MAGIC;  
eccBlobHeader.cbKey = 48;//size of EC key(without 1st byte)
memcpy(bycrtptKey, &eccBlobHeader, 8);//copying 8bytes header blob
memcpy(bycrtptKey+ 8,publicKeyFromOtherParty+1,publicKeyFromOtherPartySize- 1);

now use bycrtptKey for importing.