Import a Public Key from Somewhere Else to Cngkey

Import a Public key from somewhere else to CngKey?

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,
...
...
}

CngKey import from a ECSsaP192 public key

The posted public key is an X.509/SPKI key for NIST P-192 (aka secp192r1 or prime192v1). The signature is given in ASN.1 format. This can be verified most easily with an ASN.1 parser, e.g. here.

There is a bug in the code. In the line

signer.BlockUpdate(data, 0, dataStr.Length);

dataStr.Length must be replaced by data.Length. Apart from that the code works.

Nevertheless, the verification fails for your data, i.e. data and signature are inconsistent for some reason (possibly public and private keys do not match, or the signature was created for other data, etc.).

The code itself is not the cause. I have successfully tested the (fixed) code using the following private (PKCS8) and public (X.509) keys (for NIST P-192):

-----BEGIN PRIVATE KEY-----
MG8CAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQEEVTBTAgEBBBgN4QXSZ9VyMP0sfb/E
vPObk83EHj2gemmhNAMyAAQESDhrEDN9oOetGgTzf+hN5Wm6xQqjOgjrDIdlXunl
gvQU9HS0dd/wzNuFy2pqD4I=
-----END PRIVATE KEY-----

-----BEGIN PUBLIC KEY-----
MEkwEwYHKoZIzj0CAQYIKoZIzj0DAQEDMgAEBEg4axAzfaDnrRoE83/oTeVpusUK
ozoI6wyHZV7p5YL0FPR0tHXf8Mzbhctqag+C
-----END PUBLIC KEY-----

If the plaintext posted by you is signed with the private key above, you will get, for example, the following signature (Base64 encoded):

MDYCGQDrACykwYbQ6lQppw5PEcu5Bm7BuHjkVHoCGQDZ+RD3KvanoOzYj9bqQP2GHGhyrH6NOwA=

If this signature is verified with your (fixed) code and the public key above, the verification is successful!

By the way, it is easier to import the public key with:

using Org.BouncyCastle.Security;
...
//var param = GetPublicKeyParam(keyBytes); // remove
var param = PublicKeyFactory.CreateKey(keyBytes); // add

C# Get CngKey object from public key in text file

EccPublicBlob maps to BCRYPT_ECCPUBLIC_BLOB format type, not X.509 SubjectPublicKeyInfo.

If all of your keys are on secp256r1/NIST P-256 then there's a pretty straightforward hacky approach.

You may have noticed that all of your keys start with MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE. We'll see why shortly.

Convert

MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaq6djyzkpHdX7kt8DsSt6IuSoXjp
WVlLfnZPoLaGKc/2BSfYQuFIO2hfgueQINJN3ZdujYXfUJ7Who+XkcJqHQ==

to bytes (or, here, hex):

30 59 30 13 06 07 2A 86 48 CE 3D 02 01 06 08 2A 
86 48 CE 3D 03 01 07 03 42 00 04 6A AE 9D 8F 2C
E4 A4 77 57 EE 4B 7C 0E C4 AD E8 8B 92 A1 78 E9
59 59 4B 7E 76 4F A0 B6 86 29 CF F6 05 27 D8 42
E1 48 3B 68 5F 82 E7 90 20 D2 4D DD 97 6E 8D 85
DF 50 9E D6 86 8F 97 91 C2 6A 1D

This is a DER encoded X.509 SubjectPublicKeyInfo blob.

Using our DER-fu we see

// SubjectPublicKeyInfo
30 59 // SEQUENCE, 0x59 == 89 bytes of payload
// AlgorithmIdentifier
30 13 // SEQUENCE, 0x13 == 19 bytes of payload
// AlgorithmIdentifier.algorithm
06 07 2A 86 48 CE 3D 02 01 // OBJECT ID 1.2.840.10045.2.1 (id-ecPublicKey)
// AlgorithmIdentifier.parameters
06 08 2A 86 48 CE 3D 03 01 07 // OBJECT ID 1.2.840.10045.3.1.7 (secp256r1)
// SubjectPublicKeyInfo.publicKey
03 42 00 // BIT STRING, 0x42 == 66 (65) payload bytes, 0 unused bits
// "the public key"
04
92F809EAC73630CD000055096D5383FE2DF860927C8A77AA4CDF83323A48E6AE
DF40D3C4BFDCB6CFF79C3E5F2A3EEB3C7E54B888B38EC5DC06A05D6653D9707C

Since the algorithm identifier is id-ecPublicKey the parameters is an OID identifying the curve (in this case, secp256r1 / NIST P-256). And "the public key" is of a format from SEC 1 v2.0 (2.3.4 Octet-String-to-Elliptic-Curve-Point Conversion).

The most common encoding is type 04, uncompressed key. (0x04 followed by Qx padded to the necessary length followed by Qy padded to the necessary length).

So, for all points encoded with type 04 on secp256r1 the byte pattern starts with

30 59 30 13 06 07 2A 86 48 CE 3D 02 01 06 08 2A 
86 48 CE 3D 03 01 07 03 42 00 04

which happens to align to a common base64 prefix of MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE.

What CNG wants is [32-bit identifier][32-bit little-endian length][padded Qx][padded Qy].

So the super-duper hacky version is:

private static readonly byte[] s_secp256r1Prefix =
Convert.FromBase64String("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE");

// For ECDH instead of ECDSA, change 0x53 to 0x4B.
private static readonly byte[] s_cngBlobPrefix = { 0x45, 0x43, 0x53, 0x31, 0x20, 0, 0, 0 };

private static CngKey ImportECDsa256PublicKey(string base64)
{
byte[] subjectPublicKeyInfo = Convert.FromBase64String(base64);

if (subjectPublicKeyInfo.Length != 91)
throw new InvalidOperationException();

byte[] prefix = s_secp256r1Prefix;

if (!subjectPublicKeyInfo.Take(prefix.Length).SequenceEqual(prefix))
throw new InvalidOperationException();

byte[] cngBlob = new byte[s_cngBlobPrefix.Length + 64];
Buffer.BlockCopy(s_cngBlobPrefix, 0, cngBlob, 0, s_cngBlobPrefix.Length);

Buffer.BlockCopy(
subjectPublicKeyInfo,
s_secp256r1Prefix.Length,
cngBlob,
s_cngBlobPrefix.Length,
64);

return CngKey.Import(cngBlob, CngKeyBlobFormat.EccPublicBlob);
}

To support other curves you need to change the first 4 bytes of the CNG blob to the correct "Magic" value, and change the 5th byte to be the correct length. And, of course, different SubjectPublicKeyInfo prefixes, and 64 won't be the public key coordinate length (64 == 256 / 8 * 2). But all of that is left as an exercise to the reader.

See C# and PHP ECDH not matching for the reverse.

C# - importing a public key blob into ECDiffieHellmanCng

Problem is you are trying to derive shared secret (result of DeriveKeyMaterial) using two public keys. This isn't going to work, because you need private key of one party and public key of the other party (public key of the first party is not needed because it can be derived from private key). Here is an example (I fixed some terms because now they are misleading - CreatePrivateKey does not create private key). Note that you don't usually export private keys like this and store them in container instead, so that is just for example:

public static (byte[] publicKey, byte[] privateKey) CreateKeyPair() {
using (ECDiffieHellmanCng cng = new ECDiffieHellmanCng(
// need to do this to be able to export private key
CngKey.Create(
CngAlgorithm.ECDiffieHellmanP256,
null,
new CngKeyCreationParameters
{ ExportPolicy = CngExportPolicies.AllowPlaintextExport }))) {
cng.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash;
cng.HashAlgorithm = CngAlgorithm.Sha512;
// export both private and public keys and return
var pr = cng.Key.Export(CngKeyBlobFormat.EccPrivateBlob);
var pub = cng.PublicKey.ToByteArray();
return (pub, pr);
}
}

public static byte[] CreateSharedSecret(byte[] privateKey, byte[] publicKey) {
// this returns shared secret, not private key
// initialize algorithm with private key of one party
using (ECDiffieHellmanCng cng = new ECDiffieHellmanCng(CngKey.Import(privateKey, CngKeyBlobFormat.EccPrivateBlob))) {
cng.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash;
cng.HashAlgorithm = CngAlgorithm.Sha512;
// use public key of another party
return cng.DeriveKeyMaterial(CngKey.Import(publicKey, CngKeyBlobFormat.EccPublicBlob));
}
}

Now with two functions above:

var aliceKeyPair = CreateKeyPair();
var bobKeyPair = CreateKeyPair();
byte[] bobSharedSecret = CreateSharedSecret(bobKeyPair.privateKey, aliceKeyPair.publicKey);
byte[] aliceSharedSecret = CreateSharedSecret(aliceKeyPair.privateKey, bobKeyPair.publicKey);
// derived shared secrets are the same - the whole point of this algoritm
Debug.Assert(aliceSharedSecret.SequenceEqual(bobSharedSecret));

what is the CngKeyBlobFormat of EccPrivateBlob generated by CngAlgorithm.ECDiffieHellmanP521/P256/P384?

The binary structure of the blobs is the same for all three curves:

<magic number, 4 bytes><modulus length in bytes, 4 bytes><x-value of public key><y-value of public key><private key>

In detail applies:

  • secp256r1 / NIST P-256

    Private: 45434B32 20000000 <x-value of public key, 32 bytes><y-value of public key, 32 bytes><private key, 32 bytes>   total length: 104 bytes
    Public: 45434B31 20000000 <x-value of public key, 32 bytes><y-value of public key, 32 bytes> total length: 72 bytes
  • secp384r1 / NIST P-384

    Private: 45434B34 30000000 <x-value of public key, 48 bytes><y-value of public key, 48 bytes><private key, 48 bytes>   total length: 152 bytes
    Public: 45434B33 30000000 <x-value of public key, 48 bytes><y-value of public key, 48 bytes> total length: 104 bytes
  • secp521r1 / NIST P-521

    Private: 45434B36 42000000 <x-value of public key, 66 bytes><y-value of public key, 66 bytes><private key, 66 bytes>   total length: 206 bytes
    Public: 45434B35 42000000 <x-value of public key, 66 bytes><y-value of public key, 66 bytes> total length: 140 bytes

The private key and the x- and y-component of the public key are stored in big-endian format. All three components have the length of the modulus. The different lengths of the blobs are thus caused by the different modulus of the curves.

See also: SECG, SEC2, key blob format, magic numbers, format of ECCPublicBlob and ECCPrivateBlob

How to convert a public key in PEM format to a CNG key blob?

for convert PEM public key to CNG - generic steps is next:

  1. CryptStringToBinaryA for convert string to binary
  2. CryptDecodeObjectEx with X509_PUBLIC_KEY_INFO - convert binary
    to CERT_PUBLIC_KEY_INFO
  3. CryptImportPublicKeyInfoEx2 - import CERT_PUBLIC_KEY_INFO to CNG

example of code

inline ULONG BOOL_TO_ERROR(BOOL f)
{
return f ? NOERROR : GetLastError();
}

ULONG PemToCNG(_In_ PCSTR pszString, _Out_ BCRYPT_KEY_HANDLE* phKey)
{
PBYTE pb = 0;
ULONG cb = 0;

ULONG dwError;

while (NOERROR == (dwError = BOOL_TO_ERROR(
CryptStringToBinaryA(pszString, 0, CRYPT_STRING_BASE64_ANY, pb, &cb, 0, 0))))
{
if (pb)
{
PCERT_PUBLIC_KEY_INFO PublicKeyInfo;

if (NOERROR == (dwError = BOOL_TO_ERROR(CryptDecodeObjectEx(
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
X509_PUBLIC_KEY_INFO, pb, cb,
CRYPT_DECODE_ALLOC_FLAG|
CRYPT_DECODE_NOCOPY_FLAG|
CRYPT_DECODE_SHARE_OID_STRING_FLAG,
0, &PublicKeyInfo, &cb))))
{
dwError = BOOL_TO_ERROR(CryptImportPublicKeyInfoEx2(
X509_ASN_ENCODING, PublicKeyInfo, 0, 0, phKey));

LocalFree(PublicKeyInfo);
}
break;
}

if (!(pb = (PBYTE)LocalAlloc(0, cb)))
{
dwError = GetLastError();
break;
}
}

if (pb)
{
LocalFree(pb);
}

return dwError;
}

void TestPem()
{
static const char Pem[] =
"-----BEGIN PUBLIC KEY-----\r\n"
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETHfi8foQF4UtSNVxSFxeu7W+gMxd\r\n"
"SGElhdo7825SD3Lyb+Sqh4G6Kra0ro1BdrM6Qx+hsUx4Qwdby7QY0pzxyA==\r\n"
"-----END PUBLIC KEY-----";

BCRYPT_KEY_HANDLE hKey;
if (NOERROR == PemToCNG(Pem, &hKey))
{
PBYTE pb = 0;
ULONG cb = 0;
while (0 <= BCryptExportKey(hKey, 0, BCRYPT_PUBLIC_KEY_BLOB, pb, cb, &cb, 0))
{
if (pb)
{
PSTR psz = 0;
ULONG cch = 0;
while (CryptBinaryToStringA(pb, cb, CRYPT_STRING_HEXASCII, psz, &cch))
{
if (psz)
{
DbgPrint(psz);
break;
}

psz = (PSTR)alloca(cch * sizeof(char));
}
break;
}

pb = (PBYTE)alloca(cb);
}

BCryptDestroyKey(hKey);
}

}

output is

45 43 53 31 20 00 00 00  4c 77 e2 f1 fa 10 17 85   ECS1 ...Lw......
2d 48 d5 71 48 5c 5e bb b5 be 80 cc 5d 48 61 25 -H.qH\^.....]Ha
85 da 3b f3 6e 52 0f 72 f2 6f e4 aa 87 81 ba 2a ..;.nR.r.o.....*
b6 b4 ae 8d 41 76 b3 3a 43 1f a1 b1 4c 78 43 07 ....Av.:C...LxC.
5b cb b4 18 d2 9c f1 c8 [.......

Can someone explain C# CngKey.Import please?

I believe (based on vague recollection and a similar answer) that you can make Create import at the same time, unless it's an encrypted PKCS#8.

byte[] exported = key.Export(blobType);

Send exported and blobType to somewhere else.

var keyParams = new CngKeyCreationParameters();
// whatever else you want to assign here.

// Add an import to the create step.
keyParams.Properties.Add(new CngProperty(blobType.Format, exported, CngPropertyOptions.None));

CngKey key = CngKey.Create(algorithm, keyName, keyParams);


Related Topics



Leave a reply



Submit