diff --git a/README.md b/README.md index 89c9907..b619bd2 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,12 @@ Package consists of two parts: providing ready-to-use: - NIST curves secp256r1/P256, secp384r1/P384, secp521r1/P521 - SECG curve secp256k1 - - ed25519/curve25519/x25519/ristretto255, edwards448/curve448/x448 + - ed25519 / curve25519 / x25519 / ristretto255, + edwards448 / curve448 / x448 implementing [RFC7748](https://www.rfc-editor.org/rfc/rfc7748) / [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 - pairing-friendly curves bls12-381, bn254 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, 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 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 import { ed25519ctx, ed25519ph } from '@noble/curves/ed25519'; diff --git a/src/abstract/edwards.ts b/src/abstract/edwards.ts index 4feace6..f785ede 100644 --- a/src/abstract/edwards.ts +++ b/src/abstract/edwards.ts @@ -22,6 +22,9 @@ export type CurveType = BasicCurve & { mapToCurve?: (scalar: bigint[]) => AffinePoint; // 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) { const opts = validateBasic(curve); ut.validateObject( @@ -93,7 +96,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { const { Fp, n: CURVE_ORDER, - prehash: preHash, + prehash: prehash, hash: cHash, randomBytes, nByteLength, @@ -352,7 +355,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { // Converts hash string or Uint8Array to Point. // 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 len = Fp.BYTES; 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 } else { // RFC8032 prohibits >= p, but ZIP215 doesn't - if (strict) assertInRange(y, Fp.ORDER); // strict=true [1..P-1] (2^255-19-1 for ed25519) - else assertInRange(y, MASK); // strict=false [1..MASK-1] (2^256-1 for ed25519) + if (zip215) assertInRange(y, MASK); // zip215=true [1..P-1] (2^255-19-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: @@ -427,13 +430,13 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { // int('LE', SHA512(dom2(F, C) || msgs)) mod N function hashDomainToScalar(context: Hex = new Uint8Array(), ...msgs: Uint8Array[]) { 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 */ function sign(msg: Hex, privKey: Hex, options: { context?: Hex } = {}): Uint8Array { 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 r = hashDomainToScalar(options.context, prefix, msg); // r = dom2(F, C) || prefix || PH(M) 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 } - 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 { + const { context, zip215 } = options; 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. - msg = ensureBytes('message', msg); // ZIP215 compliant, which means not fully RFC8032 compliant. - 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

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