Skip to main content
UAE KYC API responses are signed with a private key. Verify signatures using the secp256k1 elliptic curve and Blake3 hashing algorithm to ensure responses haven’t been tampered with.

Response Format

All API responses include a signature field at the root level:
{
    "result": { ... },
    "signature": "abcdef123..."
}

How to Verify

1
Get the public key
2
Contact the UAEKYC Infra team for the public key (HEX format), shared through official channels.
3
Remove the signature field from the response
4
Extract and remove the signature value before hashing.
5
Canonicalize and hash the remaining JSON
6
Sort all object keys alphabetically (recursively), serialize to JSON, then compute the Blake3 hash.
7
Verify the ECDSA signature
8
Use the public key and the hash to verify the signature using ECDSA on the secp256k1 curve.

Implementation

# Requirements: .NET 6.0+, C# 9.0+
Install-Package Blake3
Install-Package Portable.BouncyCastle
Install-Package Newtonsoft.Json
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Org.BouncyCastle.Asn1.Sec;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Math.EC;
using Blake3;

public static class SignatureVerifier
{
    public static bool Verify(JToken fullPayloadJson, string publicKeyHex)
{
    if (fullPayloadJson == null)
        throw new ArgumentNullException(nameof(fullPayloadJson), "Input JSON is null.");

    if (fullPayloadJson is not JObject jsonObject)
        throw new ArgumentException("Expected a JSON object.");

    string signatureHex = jsonObject["signature"]?.ToString();
    if (string.IsNullOrEmpty(signatureHex))
        throw new ArgumentException("Missing or empty 'signature' field in payload.");

    jsonObject.Remove("signature");

    var canonicalJson = JsonConvert.SerializeObject(Canonicalize(jsonObject), Formatting.None);

    var hasher = Hasher.New();
    hasher.Update(Encoding.UTF8.GetBytes(canonicalJson));
    var payloadHash = hasher.Finalize().AsSpan().ToArray();

    var publicKeyBytes = HexToBytes(publicKeyHex);
    var ecParams = SecNamedCurves.GetByName("secp256k1");
    var domainParams = new ECDomainParameters(ecParams.Curve, ecParams.G, ecParams.N, ecParams.H);
    var ecPoint = ecParams.Curve.DecodePoint(publicKeyBytes);
    var publicKey = new ECPublicKeyParameters(ecPoint, domainParams);

    var signatureBytes = HexToBytes(signatureHex);
    if (signatureBytes.Length != 64)
        throw new ArgumentException("Signature must be exactly 64 bytes (32 bytes for r and 32 for s).");

    var r = new BigInteger(1, signatureBytes.AsSpan(0, 32).ToArray());
    var s = new BigInteger(1, signatureBytes.AsSpan(32, 32).ToArray());

    var signer = new ECDsaSigner();
    signer.Init(false, publicKey);
    return signer.VerifySignature(payloadHash, r, s);
}

    private static byte[] HexToBytes(string hex)
    {
        if (string.IsNullOrWhiteSpace(hex))
            throw new ArgumentException("Hex string is null or empty.");
        if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
            hex = hex[2..];
        if (hex.Length % 2 != 0)
            throw new FormatException("Hex string must have an even number of characters.");
        var bytes = new byte[hex.Length / 2];
        for (int i = 0; i < bytes.Length; i++)
            bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
        return bytes;
    }

    private static JToken Canonicalize(JToken token)
    {
        return token switch
        {
            JObject obj => new JObject(
                obj.Properties()
                    .OrderBy(p => p.Name, StringComparer.Ordinal)
                    .Select(p => new JProperty(p.Name, Canonicalize(p.Value)))
            ),
            JArray array => new JArray(array.Select(Canonicalize)),
            _ => token
        };
    }
}