Optimized Noir library that evaluates ECDSA (Elliptic Curve Digital Signature Algorithm) signatures for multiple elliptic curves.
This library supports verification of ECDSA signatures on various elliptic curves including NIST standard curves (secp192r1, secp224r1, secp256r1, secp384r1, secp521r1) and Brainpool curves (brainpoolP192r1/t1, brainpoolP224r1/t1, brainpoolP256r1/t1, brainpoolP384r1/t1, brainpoolP512r1/t1).
This library uses a slightly modified version of noir-bignum as a dependency for big number arithmetic operations as well as a slightly modified version of noir-bigcurve for curve operations.
- secp192r1 (P-192): 192-bit prime field curve
- secp224r1 (P-224): 224-bit prime field curve
- secp256r1 (P-256): 256-bit prime field curve
- secp384r1 (P-384): 384-bit prime field curve
- secp521r1 (P-521): 521-bit prime field curve
- brainpoolP192r1: 192-bit Brainpool curve
- brainpoolP192t1: 192-bit Brainpool curve (twisted)
- brainpoolP224r1: 224-bit Brainpool curve
- brainpoolP224t1: 224-bit Brainpool curve (twisted)
- brainpoolP256r1: 256-bit Brainpool curve
- brainpoolP256t1: 256-bit Brainpool curve (twisted)
- brainpoolP384r1: 384-bit Brainpool curve
- brainpoolP384t1: 384-bit Brainpool curve (twisted)
- brainpoolP512r1: 512-bit Brainpool curve
- brainpoolP512t1: 512-bit Brainpool curve (twisted)
In your Nargo.toml file, add the version of this library you would like to install under dependency:
[dependencies]
noir_ecdsa = { tag = "v0.5.0", git = "https://github.com/zkpassport/noir-ecdsa" }
bigcurve = {tag = "v0.14.0-2", git = "https://github.com/zkpassport/noir_bigcurve"}The library provides both a generic verify_ecdsa function and curve-specific convenience functions. Here are examples for each supported curve:
use bigcurve::curves::secp256r1::{Secp256r1_Fq, Secp256r1_Fr};
use noir_ecdsa::ecdsa::verify_secp256r1_ecdsa;
// Your message hash (typically SHA-256)
let message_hash: [u8; 32] = [/* your 32-byte hash */];
// Public key coordinates (on the secp256r1 curve)
let public_key_x: Secp256r1_Fq = /* your public key x coordinate */;
let public_key_y: Secp256r1_Fq = /* your public key y coordinate */;
// Both must be valid field elements: either call `validate_in_field()` on them,
// or obtain them from `from_be_bytes` / `from_le_bytes`, which validate for you.
// ECDSA signature (r, s)
let signature_r: Secp256r1_Fr = /* signature r component */;
let signature_s: Secp256r1_Fr = /* signature s component */;
// Both must be valid field elements: either call `validate_in_field()` on them,
// or obtain them from `from_be_bytes` / `from_le_bytes`, which validate for you.
// Verify the signature
let is_valid = verify_secp256r1_ecdsa(
public_key_x,
public_key_y,
message_hash,
(signature_r, signature_s)
);
assert(is_valid);use bigcurve::curves::secp384r1::{Secp384r1_Fq, Secp384r1_Fr};
use noir_ecdsa::ecdsa::verify_secp384r1_ecdsa;
let message_hash: [u8; 32] = [/* your hash */];
let public_key_x: Secp384r1_Fq = /* public key x */;
let public_key_y: Secp384r1_Fq = /* public key y */;
let signature_r: Secp384r1_Fr = /* signature r */;
let signature_s: Secp384r1_Fr = /* signature s */;
let is_valid = verify_secp384r1_ecdsa(
public_key_x,
public_key_y,
message_hash,
(signature_r, signature_s)
);use bigcurve::curves::secp521r1::{Secp521r1_Fq, Secp521r1_Fr};
use noir_ecdsa::ecdsa::verify_secp521r1_ecdsa;
let message_hash: [u8; 32] = [/* your hash */];
let public_key_x: Secp521r1_Fq = /* public key x */;
let public_key_y: Secp521r1_Fq = /* public key y */;
let signature_r: Secp521r1_Fr = /* signature r */;
let signature_s: Secp521r1_Fr = /* signature s */;
let is_valid = verify_secp521r1_ecdsa(
public_key_x,
public_key_y,
message_hash,
(signature_r, signature_s)
);use bigcurve::curves::brainpoolP256r1::{BrainpoolP256r1_Fq, BrainpoolP256r1_Fr};
use noir_ecdsa::ecdsa::{
verify_brainpoolp192r1_ecdsa,
verify_brainpoolp192t1_ecdsa,
verify_brainpoolp224r1_ecdsa,
verify_brainpoolp224t1_ecdsa,
verify_brainpoolp256r1_ecdsa,
verify_brainpoolp256t1_ecdsa,
verify_brainpoolp384r1_ecdsa,
verify_brainpoolp384t1_ecdsa,
verify_brainpoolp512r1_ecdsa,
verify_brainpoolp512t1_ecdsa
};
// Example with brainpoolP256r1
let message_hash: [u8; 32] = [/* your hash */];
let public_key_x: BrainpoolP256r1_Fq = /* public key x */;
let public_key_y: BrainpoolP256r1_Fq = /* public key y */;
let signature_r: BrainpoolP256r1_Fr = /* signature r */;
let signature_s: BrainpoolP256r1_Fr = /* signature s */;
let is_valid = verify_brainpoolp256r1_ecdsa(
public_key_x,
public_key_y,
message_hash,
(signature_r, signature_s)
);For advanced use cases, you can use the generic verify_ecdsa function:
use noir_ecdsa::ecdsa::verify_ecdsa;
use bigcurve::curves::secp256r1::{Secp256r1, Secp256r1_Fq, Secp256r1_Fr};
let is_valid = verify_ecdsa::<32, 65, Secp256r1_Fq, Secp256r1_Fr, Secp256r1>(
public_key_x,
public_key_y,
message_hash,
(signature_r, signature_s)
);All verification functions support variable hash sizes using generics:
// For a SHA-384 hash (48 bytes)
let sha384_hash: [u8; 48] = [/* your 48-byte hash */];
// Note: you should not need to specify the hash size as it is inferred from the array size
let is_valid = verify_secp384r1_ecdsa::<48>(
public_key_x,
public_key_y,
sha384_hash,
(signature_r, signature_s)
);
// For a SHA-512 hash (64 bytes)
// Note: you should not need to specify the hash size as it is inferred from the array size
let sha512_hash: [u8; 64] = [/* your 64-byte hash */];
let is_valid = verify_secp521r1_ecdsa::<64>(
public_key_x,
public_key_y,
sha512_hash,
(signature_r, signature_s)
);This library automatically protects against signature malleability attacks by enforcing canonical signature form. For any valid signature (r, s), there could potentially be another valid signature (r, n - s) where n is the curve order. To prevent this, the library checks that s ≤ n/2, ensuring only one canonical form is accepted.
This assumes the signature you pass has a s value less than n/2, otherwise you will get this error: Signature s value is not in canonical form.
So make sure to preprocess your ECDSA signatures before passing them to the library to ensure this. You can check how we do this in TypeScript here.
randsare expected to be valid field elements. Either callvalidate_in_fieldon them, or use the safefrom_be_bytes/from_le_bytesversions when importing them from bytes.- The
s <= n/2malleability check is a requirement on the caller. You must pass the low-sform of the signature, otherwise verification fails (see "Signature Malleability Protection" above). public_key_xandpublic_key_ymust be valid field elements. As withrands, either callvalidate_in_fieldon them, or use the safefrom_be_bytes/from_le_bytesversions when importing them from bytes, so an out-of-range limb encoding can't slip through.- The public key must lie on the curve. The library enforces this with
validate_on_curve(); do not remove it. Skipping it allows an invalid-curve attack, where a prover supplies an off-curve point and forges a passing verification. - The public key must NOT be a free private witness. This is a soundness requirement, not a nicety.
validate_on_curve()only checks that the point is a valid curve point, not that it is the expected signer's key. Ifpublic_key_x/public_key_yare supplied as private, prover-controlled inputs, verification is unsound: an attacker can generate their own keypair, sign any message with it, and prove a "valid signature" for arbitrary data. Always bind the public key to something the verifier trusts, e.g. expose it as a public input, hash it into the public inputs, or check it against a trusted key registry. - The final
R.x == rcomparison castsR.xlimb by limb from the base field into the scalar field. This works for the supported curves. The rule isbitlen(Fq) <= bitlen(Fr): if the base field modulusphas more bits than the scalar field modulusn,R.xcould overflow the scalar field's range. Check the bit lengths, do not assume - cofactor > 1 is a red flag (n << p), but even a cofactor-1 curve can havebitlen(Fq) > bitlen(Fr)whenpsits just above a power of two. (The reverse,nwider thanp, is fine.)
See the test functions in src/tests.nr for complete working examples of each curve. Each test demonstrates the following:
- Computing a SHA-256 hash of a message
- Setting up public key coordinates and signature values
- Verifying the signature
- Asserting the result
To run the tests:
nargo testThe ECDSA verification algorithm implemented follows the standard process:
- Input processing: Split the signature into its
randscomponents - Non-zero check: Ensure
r != 0 (mod n), which also rejects the non-canonicalr == n - Canonical form check: Ensure
s ≤ n/2to prevent malleability - Hash processing: Pad the message hash to the appropriate size and convert it to a field element
- Inverse computation: Calculate
w = s^(-1) mod n - Scalar computation: Calculate
u1 = e*w mod nandu2 = r*w mod n - Public key validation: Convert the public key to a point on the curve and ensure it is on the curve
- Point computation: Calculate
R = u1*G + u2*Qusing multi-scalar multiplication (MSM) - Verification: Check if
R.x ≡ r (mod n), the final check to ensure the signature is valid
Where:
eis the message hash as a field elementGis the curve generator pointQis the public key pointnis the curve order
We welcome contributions! Please feel free to submit issues and pull requests.
This project is licensed under the Apache 2.0 License - see the LICENSE file for details.