Response Format
All API responses include asignature field at the root level:
{
"result": { ... },
"signature": "abcdef123..."
}
How to Verify
Sort all object keys alphabetically (recursively), serialize to JSON, then compute the Blake3 hash.
Implementation
- C#
- Python
- JavaScript (Browser)
- Node.js
- Go
- Java (Maven)
# 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
};
}
}
# Requirements: Python 3.6+
pip install blake3 ecdsa
import json
import blake3
from ecdsa import VerifyingKey, SECP256k1, util, ellipticcurve
def verify_trust(payload: dict, public_key_hex: str) -> bool:
if payload is None:
raise ValueError("Payload must not be None.")
if not public_key_hex:
raise ValueError("Public key must not be empty.")
if 'signature' not in payload:
raise ValueError("Missing 'signature' field in payload.")
signature_hex = payload.pop('signature')
if not signature_hex:
raise ValueError("Signature field is empty.")
canonical_json = _canonicalize_json(payload)
payload_bytes = canonical_json.encode('utf-8')
payload_hash = blake3.blake3(payload_bytes).digest()
public_key_bytes = bytes.fromhex(public_key_hex)
uncompressed_pubkey = _decompress_public_key(public_key_bytes)
signature_bytes = bytes.fromhex(signature_hex)
verifying_key = VerifyingKey.from_string(uncompressed_pubkey, curve=SECP256k1)
return verifying_key.verify_digest(
signature_bytes, payload_hash, sigdecode=util.sigdecode_string
)
def _canonicalize_json(obj: dict) -> str:
return json.dumps(obj, separators=(',', ':'), sort_keys=True)
def _decompress_public_key(public_key: bytes) -> bytes:
if not public_key:
raise ValueError("Public key is empty.")
prefix = public_key[0]
if prefix == 0x04:
if len(public_key) != 65:
raise ValueError("Invalid uncompressed public key length.")
return public_key[1:]
if prefix in (0x02, 0x03):
if len(public_key) != 33:
raise ValueError("Invalid compressed public key length.")
x = int.from_bytes(public_key[1:], 'big')
curve = SECP256k1.curve
p = curve.p()
a = curve.a()
b = curve.b()
y_squared = (x ** 3 + a * x + b) % p
y = pow(y_squared, (p + 1) // 4, p)
if (y % 2) != (prefix % 2):
y = p - y
return x.to_bytes(32, 'big') + y.to_bytes(32, 'big')
raise ValueError("Invalid public key prefix.")
# Requirements: Node.js v14.x+
npm install blake3@2.1.7 secp256k1
import blake3 from 'blake3';
import secp from 'secp256k1';
function canonicalize(obj) {
if (Array.isArray(obj)) {
return obj.map(canonicalize);
}
if (obj !== null && typeof obj === 'object') {
return Object.keys(obj)
.sort()
.reduce((result, key) => {
result[key] = canonicalize(obj[key]);
return result;
}, {});
}
return obj;
}
async function hashPayload(payload) {
const canonicalJson = JSON.stringify(canonicalize(payload));
const encoder = new TextEncoder();
const input = encoder.encode(canonicalJson);
const hasher = await createHash();
hasher.update(input);
return hasher.digest();
}
export async function verifyTrustPackage(payload, publicKeyHex) {
if (!payload || typeof payload !== 'object') {
throw new Error('Payload must be a valid JSON object');
}
if (!payload.signature) {
throw new Error('Missing "signature" field in payload');
}
const signatureHex = payload.signature;
const { signature, ...payloadWithoutSignature } = payload;
const hash = await hashPayload(payloadWithoutSignature);
const signatureObj = secp.Signature.fromHex(signatureHex);
return secp.verify(signatureObj, hash, publicKeyHex);
}
# Requirements: Node.js v14.x+
npm install blake3@2.1.7 secp256k1
const blake3 = require('blake3');
const secp = require('secp256k1');
const { TextEncoder } = require('util');
function canonicalize(obj) {
if (Array.isArray(obj)) {
return obj.map(canonicalize);
}
if (obj !== null && typeof obj === 'object') {
return Object.keys(obj)
.sort()
.reduce((result, key) => {
result[key] = canonicalize(obj[key]);
return result;
}, {});
}
return obj;
}
async function hashPayload(payload) {
if (!payload || typeof payload !== 'object') {
throw new TypeError('Payload must be a non-null object.');
}
const canonicalJson = JSON.stringify(canonicalize(payload));
const encoded = new TextEncoder().encode(canonicalJson);
return blake3.hash(encoded);
}
async function verifyTrustPackage(payload, publicKeyHex) {
if (!payload || typeof payload !== 'object') {
throw new Error('Payload must be a valid JSON object.');
}
if (!payload.signature) {
throw new Error('Missing "signature" field in payload.');
}
const signatureHex = payload.signature;
const { signature, ...payloadData } = payload;
const hash = await hashPayload(payloadData);
const signatureBuffer = Buffer.from(signatureHex, 'hex');
const publicKeyBuffer = Buffer.from(publicKeyHex, 'hex');
if (signatureBuffer.length !== 64) {
throw new Error('Signature must be exactly 64 bytes (r + s).');
}
if (!secp.publicKeyVerify(publicKeyBuffer)) {
throw new Error('Invalid public key format.');
}
return secp.ecdsaVerify(signatureBuffer, hash, publicKeyBuffer);
}
module.exports = { verifyTrustPackage, hashPayload, canonicalize };
# Requirements: Go 1.13+
go get github.com/zeebo/blake3
go get github.com/decred/dcrd/dcrec/secp256k1/v4
package trust
import (
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"errors"
"math/big"
"sort"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/zeebo/blake3"
)
// CanonicalizeJSON unmarshals and remarshals JSON for consistent formatting.
func CanonicalizeJSON(input []byte) ([]byte, error) {
var obj interface{}
if err := json.Unmarshal(input, &obj); err != nil {
return nil, err
}
return json.Marshal(canonicalizeValue(obj))
}
func canonicalizeValue(v interface{}) interface{} {
if m, ok := v.(map[string]interface{}); ok {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
sorted := make(map[string]interface{})
for _, k := range keys {
sorted[k] = canonicalizeValue(m[k])
}
return sorted
}
if a, ok := v.([]interface{}); ok {
out := make([]interface{}, len(a))
for i, x := range a {
out[i] = canonicalizeValue(x)
}
return out
}
return v
}
func VerifyTrustPackage(payloadJson []byte, publicKeyHex string) (bool, error) {
var raw map[string]interface{}
if err := json.Unmarshal(payloadJson, &raw); err != nil {
return false, errors.New("invalid JSON payload: " + err.Error())
}
sigVal, ok := raw["signature"]
if !ok {
return false, errors.New("missing signature field")
}
signatureHex, ok := sigVal.(string)
if !ok {
return false, errors.New("signature must be a string")
}
delete(raw, "signature")
rawBytes, err := json.Marshal(raw)
if err != nil {
return false, errors.New("failed to marshal payload: " + err.Error())
}
canonical, err := CanonicalizeJSON(rawBytes)
if err != nil {
return false, errors.New("failed to canonicalize JSON: " + err.Error())
}
hash := blake3.Sum256(canonical)
pubKeyBytes, err := hex.DecodeString(publicKeyHex)
if err != nil {
return false, errors.New("invalid public key hex: " + err.Error())
}
pubKey, err := secp256k1.ParsePubKey(pubKeyBytes)
if err != nil {
return false, errors.New("failed to parse public key: " + err.Error())
}
sigBytes, err := hex.DecodeString(signatureHex)
if err != nil {
return false, errors.New("invalid signature hex: " + err.Error())
}
if len(sigBytes) != 64 {
return false, errors.New("signature must be exactly 64 bytes (r || s)")
}
r := new(big.Int).SetBytes(sigBytes[:32])
s := new(big.Int).SetBytes(sigBytes[32:])
valid := ecdsa.Verify(pubKey.ToECDSA(), hash[:], r, s)
return valid, nil
}
<!-- Requirements: Java 18+ -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.80</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
package org.example;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.*;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.bouncycastle.crypto.digests.Blake3Digest;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;
import org.bouncycastle.asn1.sec.SECNamedCurves;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.TreeMap;
public class TrustVerifier {
private static final ObjectMapper mapper = JsonMapper.builder().build();
static {
Security.addProvider(new BouncyCastleProvider());
}
public static boolean verify(String payloadJson, String publicKeyHex) throws Exception {
if (payloadJson == null || publicKeyHex == null) {
throw new IllegalArgumentException("Payload and public key must not be null.");
}
JsonNode inputNode = mapper.readTree(payloadJson);
JsonNode signatureNode = inputNode.get("signature");
if (signatureNode == null || !signatureNode.isTextual()) {
throw new IllegalArgumentException("Missing or invalid signature field in payload.");
}
String signatureHex = signatureNode.textValue();
((ObjectNode) inputNode).remove("signature");
String canonicalJson = canonicalizeJsonString(inputNode);
byte[] hash = blake3Hash(canonicalJson.getBytes(StandardCharsets.UTF_8));
byte[] pubKeyBytes = Hex.decode(publicKeyHex);
ECDomainParameters domainParams = getECDomainParameters();
ECPoint q = domainParams.getCurve().decodePoint(pubKeyBytes);
ECPublicKeyParameters publicKey = new ECPublicKeyParameters(q, domainParams);
byte[] sigBytes = Hex.decode(signatureHex);
if (sigBytes.length != 64) {
throw new IllegalArgumentException("Signature must be 64 bytes (r || s)");
}
BigInteger r = new BigInteger(1, slice(sigBytes, 0, 32));
BigInteger s = new BigInteger(1, slice(sigBytes, 32, 64));
ECDSASigner signer = new ECDSASigner();
signer.init(false, publicKey);
return signer.verifySignature(hash, r, s);
}
private static ECDomainParameters getECDomainParameters() {
var params = SECNamedCurves.getByName("secp256k1");
return new ECDomainParameters(
params.getCurve(), params.getG(), params.getN(), params.getH(), params.getSeed()
);
}
private static byte[] blake3Hash(byte[] input) {
Blake3Digest digest = new Blake3Digest();
digest.update(input, 0, input.length);
byte[] output = new byte[32];
digest.doFinal(output, 0);
return output;
}
private static byte[] slice(byte[] array, int start, int end) {
byte[] result = new byte[end - start];
System.arraycopy(array, start, result, 0, result.length);
return result;
}
public static String canonicalizeJsonString(JsonNode node) throws Exception {
JsonNode canonicalNode = canonicalize(node);
return mapper.writeValueAsString(canonicalNode);
}
private static JsonNode canonicalize(JsonNode node) {
if (node.isObject()) {
ObjectNode sorted = mapper.createObjectNode();
TreeMap<String, JsonNode> map = new TreeMap<>();
node.fieldNames().forEachRemaining(
field -> map.put(field, canonicalize(node.get(field)))
);
map.forEach(sorted::set);
return sorted;
} else if (node.isArray()) {
ArrayNode arr = mapper.createArrayNode();
for (JsonNode item : node) {
arr.add(canonicalize(item));
}
return arr;
} else {
return node;
}
}
}
