edwards: change strict option to zip215
This commit is contained in:
parent
113d906233
commit
fe380da8c9
18
README.md
18
README.md
@ -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', () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user