Search code examples
c#xamarin.androidcryptographymonobouncycastle

C# Generating PublicKey/IPublicKey object from EC Public key bytes?


When porting a snippet of code from Java to C#, I have come across a specific function which I am struggling to find a solution to. Basically when decoding, an array of bytes from an EC PublicKey needs to be converted to a PublicKey object and everything I have found on the internet doesn't seem to help.

I am developing this on Xamarin.Android using Java.Security libraries and BouncyCastle on Mono 6.12.0.

This is the code I am using in Java:

static PublicKey getPublicKeyFromBytes(byte[] pubKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
    ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256r1");
    KeyFactory kf = KeyFactory.getInstance("EC", new BouncyCastleProvider());
    ECNamedCurveSpec params = new ECNamedCurveSpec("secp256r1", spec.getCurve(), spec.getG(), spec.getN());
    ECPoint point = ECPointUtil.decodePoint(params.getCurve(), pubKey);
    ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params);
    return (ECPublicKey) kf.generatePublic(pubKeySpec);
}

This was the best solution I could come up with which didn't throw any errors in VS. Sadly, it throws an exception and tells me that the spec is wrong:

X9ECParameters curve = CustomNamedCurves.GetByName("secp256r1");
ECDomainParameters domain = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H);
ECPoint point = curve.Curve.DecodePoint(pubKey);
ECPublicKeyParameters pubKeySpec = new ECPublicKeyParameters(point, domain);

// Get the encoded representation of the public key
byte[] encodedKey = pubKeySpec.Q.GetEncoded();

// Create a KeyFactory object for EC keys
KeyFactory keyFactory = KeyFactory.GetInstance("EC");

// Generate a PublicKey object from the encoded key data
var pbKey = keyFactory.GeneratePublic(new X509EncodedKeySpec(encodedKey));

I have previously created a PrivateKey in a similar way where I generate a PrivateKey and then export the key in PKCS#8 format, then generating the object from this format. However I couldn't get this to work from an already set array of bytes.


Solution

  • Importing a raw public EC key (e.g. for secp256r1) is possible with pure Xamarin classes, BouncyCastle is not needed for this. The returned key can be used directly when generating the KeyAgreement:

    using Java.Security.Spec;
    using Java.Security;
    using Java.Math;
    using Java.Lang;
    ...
    private IPublicKey GetPublicKeyFromBytes(byte[] rawXY) // assuming a valid raw key
    {
        int size = rawXY.Length / 2;
        ECPoint q = new ECPoint(new BigInteger(1, rawXY[0..size]), new BigInteger(1, rawXY[size..]));
        AlgorithmParameters algParams = AlgorithmParameters.GetInstance("EC"); 
        algParams.Init(new ECGenParameterSpec("secp256r1"));
        ECParameterSpec ecParamSpec = (ECParameterSpec)algParams.GetParameterSpec(Class.FromType(typeof(ECParameterSpec)));
        KeyFactory keyFactory = KeyFactory.GetInstance("EC");
        return keyFactory.GeneratePublic(new ECPublicKeySpec(q, ecParamSpec));
    }
    

    In the above example rawXY is the concatenation of the x and y coordinates of the public key. For secp256r1, both coordinates are 32 bytes each, so the total raw key is 64 bytes.

    However, the Java reference code does not import raw keys, but an uncompressed or compressed EC key. The uncompressed key corresponds to the concatenation of x and y coordinate (i.e. the raw key) plus an additional leading 0x04 byte, the compressed key consists of the x coordinate plus a leading 0x02 (for even y) or 0x03 (for odd y) byte.

    For secp256r1 the uncompressed key is 65 bytes, the compressed key 33 bytes. A compressed key can be converted to an uncompressed key using BouncyCastle. An uncompressed key is converted to a raw key by removing the leading 0x04 byte.

    To apply the above import in the case of an uncompressed or compressed key, it is necessary to convert it to a raw key, which can be done with BouncyCastle, e.g. as follows:

    using Org.BouncyCastle.Asn1.X9;
    using Org.BouncyCastle.Crypto.EC;
    ...
    private byte[] ConvertToRaw(byte[] data) // assuming a valid uncompressed (leading 0x04) or compressed (leading 0x02 or 0x03) key 
    {
        if (data[0] != 4) 
        {
            X9ECParameters curve = CustomNamedCurves.GetByName("secp256r1");
            Org.BouncyCastle.Math.EC.ECPoint point = curve.Curve.DecodePoint(data).Normalize();
            data = point.GetEncoded(false);
        }
        return data[1..];
    }
    

    Test: Import of a compressed key:

    using Java.Util;
    using Hex = Org.BouncyCastle.Utilities.Encoders.Hex;
    ...
    byte[] compressed = Hex.Decode("023291D3F8734A33BCE3871D236431F2CD09646CB574C64D07FD3168EA07D3DB78");
    pubKey = GetPublicKeyFromBytes(ConvertToRaw(compressed));
    Console.WriteLine(Base64.GetEncoder().EncodeToString(pubKey.GetEncoded())); // MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMpHT+HNKM7zjhx0jZDHyzQlkbLV0xk0H/TFo6gfT23ish58blPNhYrFI51Q/czvkAwCtLZz/6s1n/M8aA9L1Vg==
    

    As can be easily verified with an ASN.1 parser (e.g. https://lapo.it/asn1js/), the exported X.509/SPKI key MFkw... contains the raw key, i.e. the compressed key was imported correctly.