edwards: change strict option to zip215

This commit is contained in:
Paul Miller 2023-04-02 16:35:34 +00:00
parent 113d906233
commit fe380da8c9
No known key found for this signature in database
GPG Key ID: 697079DA6878B89B
3 changed files with 61 additions and 34 deletions

@ -53,10 +53,12 @@ Package consists of two parts:
providing ready-to-use: providing ready-to-use:
- NIST curves secp256r1/P256, secp384r1/P384, secp521r1/P521 - NIST curves secp256r1/P256, secp384r1/P384, secp521r1/P521
- SECG curve secp256k1 - SECG curve secp256k1
- ed25519/curve25519/x25519/ristretto255, edwards448/curve448/x448 - ed25519 / curve25519 / x25519 / ristretto255,
edwards448 / curve448 / x448
implementing implementing
[RFC7748](https://www.rfc-editor.org/rfc/rfc7748) / [RFC7748](https://www.rfc-editor.org/rfc/rfc7748) /
[RFC8032](https://www.rfc-editor.org/rfc/rfc8032) / [RFC8032](https://www.rfc-editor.org/rfc/rfc8032) /
[FIPS 186-5](https://csrc.nist.gov/publications/detail/fips/186/5/final) /
[ZIP215](https://zips.z.cash/zip-0215) standards [ZIP215](https://zips.z.cash/zip-0215) standards
- pairing-friendly curves bls12-381, bn254 - pairing-friendly curves bls12-381, bn254
2. [Abstract](#abstract-api), zero-dependency EC algorithms 2. [Abstract](#abstract-api), zero-dependency EC algorithms
@ -118,11 +120,23 @@ const isValid = schnorr.verify(sig, msg, pub);
ed25519 module has ed25519ctx / ed25519ph variants, ed25519 module has ed25519ctx / ed25519ph variants,
x25519 ECDH and [ristretto255](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448). x25519 ECDH and [ristretto255](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448).
It follows [ZIP215](https://zips.z.cash/zip-0215) and [can be used in consensus-critical applications](https://hdevalence.ca/blog/2020-10-04-its-25519am):
Default `verify` behavior follows [ZIP215](https://zips.z.cash/zip-0215) and
[can be used in consensus-critical applications](https://hdevalence.ca/blog/2020-10-04-its-25519am).
It does not affect security.
There is `zip215: false` option that switches verification criteria to RFC8032 / FIPS 186-5.
```ts ```ts
import { ed25519 } from '@noble/curves/ed25519'; import { ed25519 } from '@noble/curves/ed25519';
const priv = ed25519.utils.randomPrivateKey();
const pub = ed25519.getPublicKey(priv);
const msg = new TextEncoder().encode('hello');
const sig = ed25519.sign(msg, priv);
ed25519.verify(sig, msg, pub); // Default mode: follows ZIP215
ed25519.verify(sig, msg, pub, { zip215: false }); // RFC8032 / FIPS 186-5
// Variants from RFC8032: with context, prehashed // Variants from RFC8032: with context, prehashed
import { ed25519ctx, ed25519ph } from '@noble/curves/ed25519'; import { ed25519ctx, ed25519ph } from '@noble/curves/ed25519';

@ -22,6 +22,9 @@ export type CurveType = BasicCurve<bigint> & {
mapToCurve?: (scalar: bigint[]) => AffinePoint<bigint>; // for hash-to-curve standard mapToCurve?: (scalar: bigint[]) => AffinePoint<bigint>; // for hash-to-curve standard
}; };
// verification rule is either zip215 or rfc8032 / nist186-5. Consult fromHex:
const VERIFY_DEFAULT = { zip215: true };
function validateOpts(curve: CurveType) { function validateOpts(curve: CurveType) {
const opts = validateBasic(curve); const opts = validateBasic(curve);
ut.validateObject( ut.validateObject(
@ -93,7 +96,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
const { const {
Fp, Fp,
n: CURVE_ORDER, n: CURVE_ORDER,
prehash: preHash, prehash: prehash,
hash: cHash, hash: cHash,
randomBytes, randomBytes,
nByteLength, nByteLength,
@ -352,7 +355,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
// Converts hash string or Uint8Array to Point. // Converts hash string or Uint8Array to Point.
// Uses algo from RFC8032 5.1.3. // Uses algo from RFC8032 5.1.3.
static fromHex(hex: Hex, strict = true): Point { static fromHex(hex: Hex, zip215 = false): Point {
const { d, a } = CURVE; const { d, a } = CURVE;
const len = Fp.BYTES; const len = Fp.BYTES;
hex = ensureBytes('pointHex', hex, len); // copy hex to a new array hex = ensureBytes('pointHex', hex, len); // copy hex to a new array
@ -364,8 +367,8 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
// y=0 is allowed // y=0 is allowed
} else { } else {
// RFC8032 prohibits >= p, but ZIP215 doesn't // RFC8032 prohibits >= p, but ZIP215 doesn't
if (strict) assertInRange(y, Fp.ORDER); // strict=true [1..P-1] (2^255-19-1 for ed25519) if (zip215) assertInRange(y, MASK); // zip215=true [1..P-1] (2^255-19-1 for ed25519)
else assertInRange(y, MASK); // strict=false [1..MASK-1] (2^256-1 for ed25519) else assertInRange(y, Fp.ORDER); // zip215=false [1..MASK-1] (2^256-1 for ed25519)
} }
// Ed25519: x² = (y²-1)/(dy²+1) mod p. Ed448: x² = (y²-1)/(dy²-1) mod p. Generic case: // Ed25519: x² = (y²-1)/(dy²+1) mod p. Ed448: x² = (y²-1)/(dy²-1) mod p. Generic case:
@ -427,13 +430,13 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
// int('LE', SHA512(dom2(F, C) || msgs)) mod N // int('LE', SHA512(dom2(F, C) || msgs)) mod N
function hashDomainToScalar(context: Hex = new Uint8Array(), ...msgs: Uint8Array[]) { function hashDomainToScalar(context: Hex = new Uint8Array(), ...msgs: Uint8Array[]) {
const msg = ut.concatBytes(...msgs); const msg = ut.concatBytes(...msgs);
return modN_LE(cHash(domain(msg, ensureBytes('context', context), !!preHash))); return modN_LE(cHash(domain(msg, ensureBytes('context', context), !!prehash)));
} }
/** Signs message with privateKey. RFC8032 5.1.6 */ /** Signs message with privateKey. RFC8032 5.1.6 */
function sign(msg: Hex, privKey: Hex, options: { context?: Hex } = {}): Uint8Array { function sign(msg: Hex, privKey: Hex, options: { context?: Hex } = {}): Uint8Array {
msg = ensureBytes('message', msg); msg = ensureBytes('message', msg);
if (preHash) msg = preHash(msg); // for ed25519ph etc. if (prehash) msg = prehash(msg); // for ed25519ph etc.
const { prefix, scalar, pointBytes } = getExtendedPublicKey(privKey); const { prefix, scalar, pointBytes } = getExtendedPublicKey(privKey);
const r = hashDomainToScalar(options.context, prefix, msg); // r = dom2(F, C) || prefix || PH(M) const r = hashDomainToScalar(options.context, prefix, msg); // r = dom2(F, C) || prefix || PH(M)
const R = G.multiply(r).toRawBytes(); // R = rG const R = G.multiply(r).toRawBytes(); // R = rG
@ -444,17 +447,27 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
return ensureBytes('result', res, nByteLength * 2); // 64-byte signature return ensureBytes('result', res, nByteLength * 2); // 64-byte signature
} }
const verifyOpts: { context?: Hex; strict?: boolean } = { strict: false }; const verifyOpts: { context?: Hex; zip215?: boolean } = VERIFY_DEFAULT;
function verify(sig: Hex, msg: Hex, publicKey: Hex, options = verifyOpts): boolean { function verify(sig: Hex, msg: Hex, publicKey: Hex, options = verifyOpts): boolean {
const { context, zip215 } = options;
const len = Fp.BYTES; // Verifies EdDSA signature against message and public key. RFC8032 5.1.7. const len = Fp.BYTES; // Verifies EdDSA signature against message and public key. RFC8032 5.1.7.
sig = ensureBytes('signature', sig, 2 * len); // An extended group equation is checked. sig = ensureBytes('signature', sig, 2 * len); // An extended group equation is checked.
msg = ensureBytes('message', msg); // ZIP215 compliant, which means not fully RFC8032 compliant. msg = ensureBytes('message', msg);
if (preHash) msg = preHash(msg); // for ed25519ph, etc if (prehash) msg = prehash(msg); // for ed25519ph, etc
const A = Point.fromHex(publicKey, false); // Check for s bounds, hex validity
const R = Point.fromHex(sig.slice(0, len), options.strict); // R <P (RFC8032) or <2^256 (ZIP215)
const s = ut.bytesToNumberLE(sig.slice(len, 2 * len)); const s = ut.bytesToNumberLE(sig.slice(len, 2 * len));
const SB = G.multiplyUnsafe(s); // 0 <= s < l is done inside // zip215: true is good for consensus-critical apps and allows points < 2^256
const k = hashDomainToScalar(options.context, R.toRawBytes(), A.toRawBytes(), msg); // zip215: false follows RFC8032 / NIST186-5 and restricts points to CURVE.p
let A, R, SB;
try {
A = Point.fromHex(publicKey, zip215);
R = Point.fromHex(sig.slice(0, len), zip215);
SB = G.multiplyUnsafe(s); // 0 <= s < l is done inside
} catch (error) {
return false;
}
const k = hashDomainToScalar(context, R.toRawBytes(), A.toRawBytes(), msg);
const RkA = R.add(A.multiplyUnsafe(k)); const RkA = R.add(A.multiplyUnsafe(k));
// [8][S]B = [8]R + [8][k]A' // [8][S]B = [8]R + [8][k]A'
return RkA.subtract(SB).clearCofactor().equals(Point.ZERO); return RkA.subtract(SB).clearCofactor().equals(Point.ZERO);

@ -305,23 +305,25 @@ describe('ed25519', () => {
// https://zips.z.cash/zip-0215 // https://zips.z.cash/zip-0215
// Vectors from https://gist.github.com/hdevalence/93ed42d17ecab8e42138b213812c8cc7 // Vectors from https://gist.github.com/hdevalence/93ed42d17ecab8e42138b213812c8cc7
should('ZIP-215 compliance tests/should pass all of them', () => { describe('ZIP215', () => {
const str = utf8ToBytes('Zcash'); should('pass all compliance tests', () => {
for (let v of zip215) { const str = utf8ToBytes('Zcash');
let noble = false; for (let v of zip215) {
try { let noble = false;
noble = ed.verify(v.sig_bytes, str, v.vk_bytes); try {
} catch (e) { noble = ed.verify(v.sig_bytes, str, v.vk_bytes);
noble = false; } catch (e) {
noble = false;
}
deepStrictEqual(noble, v.valid_zip215, JSON.stringify(v));
} }
deepStrictEqual(noble, v.valid_zip215, JSON.stringify(v)); });
} should('disallow sig.s >= CURVE.n', () => {
}); // sig.R = BASE, sig.s = N+1
should('ZIP-215 compliance tests/disallows sig.s >= CURVE.n', () => { const sig =
// sig.R = BASE, sig.s = N+1 '5866666666666666666666666666666666666666666666666666666666666666eed3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010';
const sig = deepStrictEqual(ed.verify(sig, 'deadbeef', Point.BASE), false);
'5866666666666666666666666666666666666666666666666666666666666666eed3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010'; });
throws(() => ed.verify(sig, 'deadbeef', Point.BASE));
}); });
// should('X25519/getSharedSecret() should be commutative', () => { // should('X25519/getSharedSecret() should be commutative', () => {
@ -403,9 +405,7 @@ describe('ed25519', () => {
s = numberToBytesLE(s, 32); s = numberToBytesLE(s, 32);
const sig_invalid = concatBytes(R, s); const sig_invalid = concatBytes(R, s);
throws(() => { deepStrictEqual(ed25519.verify(sig_invalid, message, publicKey), false);
ed25519.verify(sig_invalid, message, publicKey);
});
}); });
should('not accept point without z, t', () => { should('not accept point without z, t', () => {