weierstrass: prehash option in sign/verify. Remove _normalizePublicKey

This commit is contained in:
Paul Miller 2023-01-25 04:45:49 +00:00
parent 849dc38f3c
commit 5fc38fc0e7
No known key found for this signature in database
GPG Key ID: 697079DA6878B89B
4 changed files with 49 additions and 99 deletions

@ -85,7 +85,8 @@ const DER = {
}; };
type Entropy = Hex | true; type Entropy = Hex | true;
export type SignOpts = { lowS?: boolean; extraEntropy?: Entropy }; export type SignOpts = { lowS?: boolean; extraEntropy?: Entropy; prehash?: boolean };
export type VerOpts = { lowS?: boolean; prehash?: boolean };
/** /**
* ### Design rationale for types * ### Design rationale for types
@ -214,8 +215,7 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
return typeof num === 'bigint' && _0n < num && num < CURVE.n; return typeof num === 'bigint' && _0n < num && num < CURVE.n;
} }
function assertGE(num: bigint) { function assertGE(num: bigint) {
if (!isWithinCurveOrder(num)) if (!isWithinCurveOrder(num)) throw new Error('Expected valid bigint: 0 < bigint < curve.n');
throw new TypeError('Expected valid bigint: 0 < bigint < curve.n');
} }
/** /**
* Validates if a private key is valid and converts it to bigint form. * Validates if a private key is valid and converts it to bigint form.
@ -240,7 +240,7 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
if (key.length !== groupLen) throw new Error(`Private key must be ${groupLen} bytes`); if (key.length !== groupLen) throw new Error(`Private key must be ${groupLen} bytes`);
num = ut.bytesToNumberBE(key); num = ut.bytesToNumberBE(key);
} else { } else {
throw new TypeError('Private key was invalid'); throw new Error('Private key was invalid');
} }
// Useful for curves with cofactor != 1 // Useful for curves with cofactor != 1
if (wrapPrivateKey) num = mod.mod(num, order); if (wrapPrivateKey) num = mod.mod(num, order);
@ -588,7 +588,7 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
const wnaf = wNAF(ProjectivePoint, CURVE.endo ? Math.ceil(_bits / 2) : _bits); const wnaf = wNAF(ProjectivePoint, CURVE.endo ? Math.ceil(_bits / 2) : _bits);
function assertPrjPoint(other: unknown) { function assertPrjPoint(other: unknown) {
if (!(other instanceof ProjectivePoint)) throw new TypeError('ProjectivePoint expected'); if (!(other instanceof ProjectivePoint)) throw new Error('ProjectivePoint expected');
} }
return { return {
ProjectivePoint: ProjectivePoint as ProjectiveConstructor<T>, ProjectivePoint: ProjectivePoint as ProjectiveConstructor<T>,
@ -648,24 +648,15 @@ function validateOpts(curve: CurveType) {
export type CurveFn = { export type CurveFn = {
CURVE: ReturnType<typeof validateOpts>; CURVE: ReturnType<typeof validateOpts>;
getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array; getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array;
getSharedSecret: (privateA: PrivKey, publicB: PubKey, isCompressed?: boolean) => Uint8Array; getSharedSecret: (privateA: PrivKey, publicB: Hex, isCompressed?: boolean) => Uint8Array;
sign: (msgHash: Hex, privKey: PrivKey, opts?: SignOpts) => SignatureType; sign: (msgHash: Hex, privKey: PrivKey, opts?: SignOpts) => SignatureType;
signUnhashed: (msg: Uint8Array, privKey: PrivKey, opts?: SignOpts) => SignatureType; verify: (signature: Hex | SignatureType, msgHash: Hex, publicKey: Hex, opts?: VerOpts) => boolean;
verify: (
signature: Hex | SignatureType,
msgHash: Hex,
publicKey: PubKey,
opts?: {
lowS?: boolean;
}
) => boolean;
ProjectivePoint: ProjectiveConstructor<bigint>; ProjectivePoint: ProjectiveConstructor<bigint>;
Signature: SignatureConstructor; Signature: SignatureConstructor;
utils: { utils: {
_bigintToBytes: (num: bigint) => Uint8Array; _bigintToBytes: (num: bigint) => Uint8Array;
_bigintToString: (num: bigint) => string; _bigintToString: (num: bigint) => string;
_normalizePrivateKey: (key: PrivKey) => bigint; _normalizePrivateKey: (key: PrivKey) => bigint;
_normalizePublicKey: (publicKey: PubKey) => ProjectivePointType<bigint>;
_isWithinCurveOrder: (num: bigint) => boolean; _isWithinCurveOrder: (num: bigint) => boolean;
_isValidFieldElement: (num: bigint) => boolean; _isValidFieldElement: (num: bigint) => boolean;
_weierstrassEquation: (x: bigint) => bigint; _weierstrassEquation: (x: bigint) => bigint;
@ -793,19 +784,6 @@ export function weierstrass(curveDef: CurveType): CurveFn {
} }
const numToFieldStr = (num: bigint): string => bytesToHex(numToField(num)); const numToFieldStr = (num: bigint): string => bytesToHex(numToField(num));
/**
* Normalizes hex, bytes, Point to Point. Checks for curve equation.
*/
function normalizePublicKey(publicKey: PubKey): ProjectivePointType<bigint> {
if (publicKey instanceof Point) {
publicKey.assertValidity();
return publicKey;
} else if (publicKey instanceof Uint8Array || typeof publicKey === 'string') {
return Point.fromHex(publicKey);
// This can happen because PointType can be instance of different class
} else throw new Error(`Unknown type of public key: ${publicKey}`);
}
function isBiggerThanHalfOrder(number: bigint) { function isBiggerThanHalfOrder(number: bigint) {
const HALF = CURVE_ORDER >> _1n; const HALF = CURVE_ORDER >> _1n;
return number > HALF; return number > HALF;
@ -837,7 +815,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
static fromDER(hex: Hex) { static fromDER(hex: Hex) {
const arr = hex instanceof Uint8Array; const arr = hex instanceof Uint8Array;
if (typeof hex !== 'string' && !arr) if (typeof hex !== 'string' && !arr)
throw new TypeError(`Signature.fromDER: Expected string or Uint8Array`); throw new Error(`Signature.fromDER: Expected string or Uint8Array`);
const { r, s } = DER.parseSig(arr ? hex : ut.hexToBytes(hex)); const { r, s } = DER.parseSig(arr ? hex : ut.hexToBytes(hex));
return new Signature(r, s); return new Signature(r, s);
} }
@ -852,35 +830,19 @@ export function weierstrass(curveDef: CurveType): CurveFn {
return new Signature(this.r, this.s, recovery); return new Signature(this.r, this.s, recovery);
} }
/**
* Recovers public key from signature with recovery bit. Throws on invalid hash.
* https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Public_key_recovery
* It's also possible to recover key without bit: try all 4 bit values and check for sig match.
*
* ```
* recover(r, s, h) where
* u1 = hs^-1 mod n
* u2 = sr^-1 mod n
* Q = u1G + u2R
* ```
*
* @param msgHash message hash
* @returns Point corresponding to public key
*/
recoverPublicKey(msgHash: Hex): typeof Point.BASE { recoverPublicKey(msgHash: Hex): typeof Point.BASE {
const { n: N } = CURVE; const { n: N } = CURVE; // ECDSA public key recovery secg.org/sec1-v2.pdf 4.1.6
const { r, s, recovery: rec } = this; const { r, s, recovery: rec } = this;
const h = bits2int_modN(ut.ensureBytes(msgHash)); const h = bits2int_modN(ut.ensureBytes(msgHash)); // Truncate hash
if (rec == null || ![0, 1, 2, 3].includes(rec)) throw new Error('recovery id invalid'); if (rec == null || ![0, 1, 2, 3].includes(rec)) throw new Error('recovery id invalid');
const radj = rec === 2 || rec === 3 ? r + N : r; const radj = rec === 2 || rec === 3 ? r + N : r;
if (radj >= Fp.ORDER) throw new Error('recovery id 2 or 3 currently invalid'); if (radj >= Fp.ORDER) throw new Error('recovery id 2 or 3 invalid');
const prefix = (rec & 1) === 0 ? '02' : '03'; const prefix = (rec & 1) === 0 ? '02' : '03';
const R = Point.fromHex(prefix + numToFieldStr(radj)); const R = Point.fromHex(prefix + numToFieldStr(radj));
const ir = mod.invert(radj, N); // r^-1 const ir = mod.invert(radj, N); // r^-1
const u1 = mod.mod(-h * ir, N); // -hr^-1 const u1 = mod.mod(-h * ir, N); // -hr^-1
const u2 = mod.mod(s * ir, N); // sr^-1 const u2 = mod.mod(s * ir, N); // sr^-1
// (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1) const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2); // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1)
const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2);
if (!Q) throw new Error('point at infinify'); // unsafe is fine: no priv data leaked if (!Q) throw new Error('point at infinify'); // unsafe is fine: no priv data leaked
Q.assertValidity(); Q.assertValidity();
return Q; return Q;
@ -938,7 +900,6 @@ export function weierstrass(curveDef: CurveType): CurveFn {
_bigintToBytes: numToField, _bigintToBytes: numToField,
_bigintToString: numToFieldStr, _bigintToString: numToFieldStr,
_normalizePrivateKey: normalizePrivateKey, _normalizePrivateKey: normalizePrivateKey,
_normalizePublicKey: normalizePublicKey,
_isWithinCurveOrder: isWithinCurveOrder, _isWithinCurveOrder: isWithinCurveOrder,
_isValidFieldElement: isValidFieldElement, _isValidFieldElement: isValidFieldElement,
_weierstrassEquation: weierstrassEquation, _weierstrassEquation: weierstrassEquation,
@ -1002,11 +963,10 @@ export function weierstrass(curveDef: CurveType): CurveFn {
* @param isCompressed whether to return compact (default), or full key * @param isCompressed whether to return compact (default), or full key
* @returns shared public key * @returns shared public key
*/ */
function getSharedSecret(privateA: PrivKey, publicB: PubKey, isCompressed = true): Uint8Array { function getSharedSecret(privateA: PrivKey, publicB: Hex, isCompressed = true): Uint8Array {
if (isProbPub(privateA)) throw new TypeError('getSharedSecret: first arg must be private key'); if (isProbPub(privateA)) throw new Error('first arg must be private key');
if (!isProbPub(publicB)) throw new TypeError('getSharedSecret: second arg must be public key'); if (!isProbPub(publicB)) throw new Error('second arg must be public key');
const b = normalizePublicKey(publicB); const b = Point.fromHex(publicB); // check for being on-curve
b.assertValidity();
return b.multiply(normalizePrivateKey(privateA)).toRawBytes(isCompressed); return b.multiply(normalizePrivateKey(privateA)).toRawBytes(isCompressed);
} }
@ -1047,7 +1007,8 @@ export function weierstrass(curveDef: CurveType): CurveFn {
if (['recovered', 'canonical'].some((k) => k in opts)) if (['recovered', 'canonical'].some((k) => k in opts))
// Ban legacy options // Ban legacy options
throw new Error('sign() legacy options not supported'); throw new Error('sign() legacy options not supported');
let { lowS } = opts; // generates low-s sigs by default let { lowS, prehash, extraEntropy: ent } = opts; // generates low-s sigs by default
if (prehash) msgHash = CURVE.hash(ut.ensureBytes(msgHash));
if (lowS == null) lowS = true; // RFC6979 3.2: we skip step A, because if (lowS == null) lowS = true; // RFC6979 3.2: we skip step A, because
// Step A is ignored, since we already provide hash instead of msg // Step A is ignored, since we already provide hash instead of msg
@ -1062,8 +1023,8 @@ export function weierstrass(curveDef: CurveType): CurveFn {
const d = normalizePrivateKey(privateKey); const d = normalizePrivateKey(privateKey);
// K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k') // K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k')
const seedArgs = [int2octets(d), h1octets]; const seedArgs = [int2octets(d), h1octets];
let ent = opts.extraEntropy; // RFC6979 3.6: additional k' (optional)
if (ent != null) { if (ent != null) {
// RFC6979 3.6: additional k' (optional)
if (ent === true) ent = CURVE.randomBytes(Fp.BYTES); if (ent === true) ent = CURVE.randomBytes(Fp.BYTES);
const e = ut.ensureBytes(ent); const e = ut.ensureBytes(ent);
if (e.length !== Fp.BYTES) throw new Error(`sign: Expected ${Fp.BYTES} bytes of extra data`); if (e.length !== Fp.BYTES) throw new Error(`sign: Expected ${Fp.BYTES} bytes of extra data`);
@ -1100,7 +1061,8 @@ export function weierstrass(curveDef: CurveType): CurveFn {
} }
return { seed, k2sig }; return { seed, k2sig };
} }
const defaultSigOpts: SignOpts = { lowS: CURVE.lowS }; const defaultSigOpts: SignOpts = { lowS: CURVE.lowS, prehash: false };
const defaultVerOpts: VerOpts = { lowS: CURVE.lowS, prehash: false };
/** /**
* Signs message hash (not message: you need to hash it by yourself). * Signs message hash (not message: you need to hash it by yourself).
@ -1119,13 +1081,6 @@ export function weierstrass(curveDef: CurveType): CurveFn {
return genUntil(seed, k2sig); // Steps B, C, D, E, F, G return genUntil(seed, k2sig); // Steps B, C, D, E, F, G
} }
/**
* Signs a message (not message hash).
*/
function signUnhashed(msg: Uint8Array, privKey: PrivKey, opts = defaultSigOpts): Signature {
return sign(CURVE.hash(ut.ensureBytes(msg)), privKey, opts);
}
// Enable precomputes. Slows down first publicKey computation by 20ms. // Enable precomputes. Slows down first publicKey computation by 20ms.
Point.BASE._setWindowSize(8); Point.BASE._setWindowSize(8);
// utils.precompute(8, ProjectivePoint.BASE) // utils.precompute(8, ProjectivePoint.BASE)
@ -1146,10 +1101,12 @@ export function weierstrass(curveDef: CurveType): CurveFn {
function verify( function verify(
signature: Hex | SignatureType, signature: Hex | SignatureType,
msgHash: Hex, msgHash: Hex,
publicKey: PubKey, publicKey: Hex,
opts: { lowS?: boolean } = { lowS: CURVE.lowS } opts = defaultVerOpts
): boolean { ): boolean {
let P: ProjectivePointType<bigint>;
let _sig: Signature | undefined = undefined; let _sig: Signature | undefined = undefined;
if (publicKey instanceof Point) throw new Error('publicKey must be hex');
try { try {
if (signature instanceof Signature) { if (signature instanceof Signature) {
signature.assertValidity(); signature.assertValidity();
@ -1165,28 +1122,21 @@ export function weierstrass(curveDef: CurveType): CurveFn {
} }
} }
msgHash = ut.ensureBytes(msgHash); msgHash = ut.ensureBytes(msgHash);
P = Point.fromHex(publicKey);
} catch (error) { } catch (error) {
return false; return false;
} }
if (opts.lowS && _sig.hasHighS()) return false; if (opts.lowS && _sig.hasHighS()) return false;
let P; if (opts.prehash) msgHash = CURVE.hash(msgHash);
try { const { n: N } = CURVE;
P = normalizePublicKey(publicKey);
} catch (error) {
return false;
}
const { n } = CURVE;
const { r, s } = _sig; const { r, s } = _sig;
const h = bits2int_modN(msgHash); // Cannot use fields methods, since it is group element const h = bits2int_modN(msgHash); // Cannot use fields methods, since it is group element
const sinv = mod.invert(s, n); // s^-1 const is = mod.invert(s, N); // s^-1
// R = u1⋅G - u2⋅P const u1 = mod.mod(h * is, N); // u1 = hs^-1 mod n
const u1 = mod.mod(h * sinv, n); const u2 = mod.mod(r * is, N); // u2 = rs^-1 mod n
const u2 = mod.mod(r * sinv, n); const R = Point.BASE.multiplyAndAddUnsafe(P, u1, u2)?.toAffine(); // R = u1⋅G + u2⋅P
const R = Point.BASE.multiplyAndAddUnsafe(P, u1, u2)?.toAffine();
if (!R) return false; if (!R) return false;
const v = mod.mod(R.x, n); const v = mod.mod(R.x, N);
return v === r; return v === r;
} }
return { return {
@ -1194,7 +1144,6 @@ export function weierstrass(curveDef: CurveType): CurveFn {
getPublicKey, getPublicKey,
getSharedSecret, getSharedSecret,
sign, sign,
signUnhashed,
verify, verify,
// Point, // Point,
ProjectivePoint: Point, ProjectivePoint: Point,

@ -230,9 +230,7 @@ class SchnorrSignature {
const bytes = ensureBytes(hex); const bytes = ensureBytes(hex);
const len = 32; // group length const len = 32; // group length
if (bytes.length !== 2 * len) if (bytes.length !== 2 * len)
throw new Error( throw new Error(`SchnorrSignature.fromHex: expected ${2 * len} bytes, not ${bytes.length}`);
`SchnorrSignature.fromHex: expected ${2 * len} bytes, not ${bytes.length}`
);
const r = bytesToNumberBE(bytes.subarray(0, len)); const r = bytesToNumberBE(bytes.subarray(0, len));
const s = bytesToNumberBE(bytes.subarray(len, 2 * len)); const s = bytesToNumberBE(bytes.subarray(len, 2 * len));
return new SchnorrSignature(r, s); return new SchnorrSignature(r, s);
@ -301,7 +299,7 @@ function schnorrVerify(signature: Hex, message: Hex, publicKey: Hex): boolean {
// Finalize // Finalize
// R = s⋅G - e⋅P // R = s⋅G - e⋅P
// -eP == (n-e)P // -eP == (n-e)P
const R = secp256k1.ProjectivePoint.BASE.mulAddQUnsafe( const R = secp256k1.ProjectivePoint.BASE.multiplyAndAddUnsafe(
P, P,
normalizePrivateKey(s), normalizePrivateKey(s),
mod(-e, secp256k1.CURVE.n) mod(-e, secp256k1.CURVE.n)

@ -62,12 +62,12 @@ should('wychenproof ECDSA vectors', () => {
if (e.message.includes('Invalid signature: incorrect length')) continue; if (e.message.includes('Invalid signature: incorrect length')) continue;
throw e; throw e;
} }
const verified = CURVE.verify(test.sig, m, pubKey); const verified = CURVE.verify(test.sig, m, pubKey.toHex());
deepStrictEqual(verified, true, 'valid'); deepStrictEqual(verified, true, 'valid');
} else if (test.result === 'invalid') { } else if (test.result === 'invalid') {
let failed = false; let failed = false;
try { try {
failed = !CURVE.verify(test.sig, m, pubKey); failed = !CURVE.verify(test.sig, m, pubKey.toHex());
} catch (error) { } catch (error) {
failed = true; failed = true;
} }
@ -312,28 +312,30 @@ function runWycheproof(name, CURVE, group, index) {
const pubKey = CURVE.ProjectivePoint.fromHex(group.key.uncompressed); const pubKey = CURVE.ProjectivePoint.fromHex(group.key.uncompressed);
deepStrictEqual(pubKey.x, BigInt(`0x${group.key.wx}`)); deepStrictEqual(pubKey.x, BigInt(`0x${group.key.wx}`));
deepStrictEqual(pubKey.y, BigInt(`0x${group.key.wy}`)); deepStrictEqual(pubKey.y, BigInt(`0x${group.key.wy}`));
const pubR = pubKey.toRawBytes();
for (const test of group.tests) { for (const test of group.tests) {
const m = CURVE.CURVE.hash(hexToBytes(test.msg)); const m = CURVE.CURVE.hash(hexToBytes(test.msg));
const { sig } = test;
if (test.result === 'valid' || test.result === 'acceptable') { if (test.result === 'valid' || test.result === 'acceptable') {
try { try {
CURVE.Signature.fromDER(test.sig); CURVE.Signature.fromDER(sig);
} catch (e) { } catch (e) {
// Some tests has invalid signature which we don't accept // Some tests has invalid signature which we don't accept
if (e.message.includes('Invalid signature: incorrect length')) continue; if (e.message.includes('Invalid signature: incorrect length')) continue;
throw e; throw e;
} }
const verified = CURVE.verify(test.sig, m, pubKey); const verified = CURVE.verify(sig, m, pubR);
if (name === 'secp256k1') { if (name === 'secp256k1') {
// lowS: true for secp256k1 // lowS: true for secp256k1
deepStrictEqual(verified, !CURVE.Signature.fromDER(test.sig).hasHighS(), `${index}: valid`); deepStrictEqual(verified, !CURVE.Signature.fromDER(sig).hasHighS(), `${index}: valid`);
} else { } else {
deepStrictEqual(verified, true, `${index}: valid`); deepStrictEqual(verified, true, `${index}: valid`);
} }
} else if (test.result === 'invalid') { } else if (test.result === 'invalid') {
let failed = false; let failed = false;
try { try {
failed = !CURVE.verify(test.sig, m, pubKey); failed = !CURVE.verify(sig, m, pubR);
} catch (error) { } catch (error) {
failed = true; failed = true;
} }

@ -314,7 +314,7 @@ describe('secp256k1', () => {
const r = 1n; const r = 1n;
const s = 115792089237316195423570985008687907852837564279074904382605163141518162728904n; const s = 115792089237316195423570985008687907852837564279074904382605163141518162728904n;
const pub = new Point(x, y, 1n); const pub = new Point(x, y, 1n).toRawBytes();
const signature = new secp.Signature(2n, 2n); const signature = new secp.Signature(2n, 2n);
signature.r = r; signature.r = r;
signature.s = s; signature.s = s;
@ -329,7 +329,7 @@ describe('secp256k1', () => {
const y = 32670510020758816978083085130507043184471273380659243275938904335757337482424n; const y = 32670510020758816978083085130507043184471273380659243275938904335757337482424n;
const r = 104546003225722045112039007203142344920046999340768276760147352389092131869133n; const r = 104546003225722045112039007203142344920046999340768276760147352389092131869133n;
const s = 96900796730960181123786672629079577025401317267213807243199432755332205217369n; const s = 96900796730960181123786672629079577025401317267213807243199432755332205217369n;
const pub = new Point(x, y, 1n); const pub = new Point(x, y, 1n).toRawBytes();
const sig = new secp.Signature(r, s); const sig = new secp.Signature(r, s);
deepStrictEqual(secp.verify(sig, msg, pub), false); deepStrictEqual(secp.verify(sig, msg, pub), false);
}); });
@ -339,7 +339,7 @@ describe('secp256k1', () => {
const y = 17482644437196207387910659778872952193236850502325156318830589868678978890912n; const y = 17482644437196207387910659778872952193236850502325156318830589868678978890912n;
const r = 432420386565659656852420866390673177323n; const r = 432420386565659656852420866390673177323n;
const s = 115792089237316195423570985008687907852837564279074904382605163141518161494334n; const s = 115792089237316195423570985008687907852837564279074904382605163141518161494334n;
const pub = new Point(x, y, 1n); const pub = new Point(x, y, 1n).toRawBytes();
const sig = new secp.Signature(r, s); const sig = new secp.Signature(r, s);
deepStrictEqual(secp.verify(sig, msg, pub, { strict: false }), true); deepStrictEqual(secp.verify(sig, msg, pub, { strict: false }), true);
}); });
@ -527,9 +527,10 @@ describe('secp256k1', () => {
}); });
}); });
should('wychenproof vectors', () => { should('wycheproof vectors', () => {
for (let group of wp.testGroups) { for (let group of wp.testGroups) {
const pubKey = Point.fromHex(group.key.uncompressed); // const pubKey = Point.fromHex().toRawBytes();
const pubKey = group.key.uncompressed;
for (let test of group.tests) { for (let test of group.tests) {
const m = secp.CURVE.hash(hexToBytes(test.msg)); const m = secp.CURVE.hash(hexToBytes(test.msg));
if (test.result === 'valid' || test.result === 'acceptable') { if (test.result === 'valid' || test.result === 'acceptable') {