edwards: change API. Add options.strict, context. Add edwardsToMontgomery

This commit is contained in:
Paul Miller 2023-04-02 15:35:03 +00:00
parent 65c0dc6c59
commit 113d906233
No known key found for this signature in database
GPG Key ID: 697079DA6878B89B
5 changed files with 108 additions and 73 deletions

@ -18,7 +18,7 @@ export type CurveType = BasicCurve<bigint> & {
adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array; // clears bits to get valid field elemtn
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array; // Used for hashing
uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint }; // Ratio √(u/v)
preHash?: FHash; // RFC 8032 pre-hashing of messages to sign() / verify()
prehash?: FHash; // RFC 8032 pre-hashing of messages to sign() / verify()
mapToCurve?: (scalar: bigint[]) => AffinePoint<bigint>; // for hash-to-curve standard
};
@ -90,7 +90,15 @@ export type CurveFn = {
// It is not generic twisted curve for now, but ed25519/ed448 generic implementation
export function twistedEdwards(curveDef: CurveType): CurveFn {
const CURVE = validateOpts(curveDef) as ReturnType<typeof validateOpts>;
const { Fp, n: CURVE_ORDER, preHash, hash: cHash, randomBytes, nByteLength, h: cofactor } = CURVE;
const {
Fp,
n: CURVE_ORDER,
prehash: preHash,
hash: cHash,
randomBytes,
nByteLength,
h: cofactor,
} = CURVE;
const MASK = _2n ** BigInt(nByteLength * 8);
const modP = Fp.create; // Function overrides
@ -423,29 +431,30 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
}
/** Signs message with privateKey. RFC8032 5.1.6 */
function sign(msg: Hex, privKey: Hex, context?: Hex): Uint8Array {
function sign(msg: Hex, privKey: Hex, options: { context?: Hex } = {}): Uint8Array {
msg = ensureBytes('message', msg);
if (preHash) msg = preHash(msg); // for ed25519ph etc.
const { prefix, scalar, pointBytes } = getExtendedPublicKey(privKey);
const r = hashDomainToScalar(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 k = hashDomainToScalar(context, R, pointBytes, msg); // R || A || PH(M)
const k = hashDomainToScalar(options.context, R, pointBytes, msg); // R || A || PH(M)
const s = modN(r + k * scalar); // S = (r + k * s) mod L
assertGE0(s); // 0 <= s < l
const res = ut.concatBytes(R, ut.numberToBytesLE(s, Fp.BYTES));
return ensureBytes('result', res, nByteLength * 2); // 64-byte signature
}
function verify(sig: Hex, msg: Hex, publicKey: Hex, context?: Hex): boolean {
const verifyOpts: { context?: Hex; strict?: boolean } = { strict: false };
function verify(sig: Hex, msg: Hex, publicKey: Hex, options = verifyOpts): boolean {
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), false); // 0 <= R < 2^256: ZIP215 R can be >= P
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 SB = G.multiplyUnsafe(s); // 0 <= s < l is done inside
const k = hashDomainToScalar(context, R.toRawBytes(), A.toRawBytes(), msg);
const k = hashDomainToScalar(options.context, R.toRawBytes(), A.toRawBytes(), msg);
const RkA = R.add(A.multiplyUnsafe(k));
// [8][S]B = [8]R + [8][k]A'
return RkA.subtract(SB).clearCofactor().equals(Point.ZERO);

@ -95,7 +95,7 @@ export const ED25519_TORSION_SUBGROUP = [
const Fp = Field(ED25519_P, undefined, true);
const ED25519_DEF = {
const ed25519Defaults = {
// Param: a
a: BigInt(-1),
// Equal to -121665/121666 over finite field.
@ -120,7 +120,7 @@ const ED25519_DEF = {
uvRatio,
} as const;
export const ed25519 = twistedEdwards(ED25519_DEF);
export const ed25519 = twistedEdwards(ed25519Defaults);
function ed25519_domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) {
if (ctx.length > 255) throw new Error('Context is too big');
return concatBytes(
@ -130,11 +130,11 @@ function ed25519_domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) {
data
);
}
export const ed25519ctx = twistedEdwards({ ...ED25519_DEF, domain: ed25519_domain });
export const ed25519ctx = twistedEdwards({ ...ed25519Defaults, domain: ed25519_domain });
export const ed25519ph = twistedEdwards({
...ED25519_DEF,
...ed25519Defaults,
domain: ed25519_domain,
preHash: sha512,
prehash: sha512,
});
export const x25519 = montgomery({
@ -153,6 +153,20 @@ export const x25519 = montgomery({
randomBytes,
});
/**
* Converts ed25519 public key to x25519 public key. Uses formula:
* * `(u, v) = ((1+y)/(1-y), sqrt(-486664)*u/x)`
* * `(x, y) = (sqrt(-486664)*u/v, (u-1)/(u+1))`
* @example
* const aPub = ed25519.getPublicKey(utils.randomPrivateKey());
* x25519.getSharedSecret(edwardsToMontgomery(aPub), edwardsToMontgomery(someonesPub))
*/
export function edwardsToMontgomery(edwardsPub: Hex): Uint8Array {
const { y } = ed25519.ExtendedPoint.fromHex(edwardsPub);
const _1n = BigInt(1);
return Fp.toBytes(Fp.create((y - _1n) * Fp.inv(y + _1n)));
}
// Hash To Curve Elligator2 Map (NOTE: different from ristretto255 elligator)
// NOTE: very important part is usage of FpSqrtEven for ELL2_C1_EDWARDS, since
// SageMath returns different root first and everything falls apart

@ -120,7 +120,7 @@ const ED448_DEF = {
export const ed448 = twistedEdwards(ED448_DEF);
// NOTE: there is no ed448ctx, since ed448 supports ctx by default
export const ed448ph = twistedEdwards({ ...ED448_DEF, preHash: shake256_64 });
export const ed448ph = twistedEdwards({ ...ED448_DEF, prehash: shake256_64 });
export const x448 = montgomery({
a: BigInt(156326),
@ -136,22 +136,22 @@ export const x448 = montgomery({
},
adjustScalarBytes,
randomBytes,
// The 4-isogeny maps between the Montgomery curve and this Edwards
// curve are:
// (u, v) = (y^2/x^2, (2 - x^2 - y^2)*y/x^3)
// (x, y) = (4*v*(u^2 - 1)/(u^4 - 2*u^2 + 4*v^2 + 1),
// -(u^5 - 2*u^3 - 4*u*v^2 + u)/
// (u^5 - 2*u^2*v^2 - 2*u^3 - 2*v^2 + u))
// xyToU: (p: PointType) => {
// const P = ed448P;
// const { x, y } = p;
// if (x === _0n) throw new Error(`Point with x=0 doesn't have mapping`);
// const invX = invert(x * x, P); // x^2
// const u = mod(y * y * invX, P); // (y^2/x^2)
// return numberToBytesLE(u, 56);
// },
});
/**
* Converts edwards448 public key to x448 public key. Uses formula:
* * `(u, v) = ((y-1)/(y+1), sqrt(156324)*u/x)`
* * `(x, y) = (sqrt(156324)*u/v, (1+u)/(1-u))`
* @example
* const aPub = ed448.getPublicKey(utils.randomPrivateKey());
* x448.getSharedSecret(edwardsToMontgomery(aPub), edwardsToMontgomery(someonesPub))
*/
export function edwardsToMontgomery(edwardsPub: string | Uint8Array): Uint8Array {
const { y } = ed448.ExtendedPoint.fromHex(edwardsPub);
const _1n = BigInt(1);
return Fp.toBytes(Fp.create((y - _1n) * Fp.inv(y + _1n)));
}
// Hash To Curve Elligator2 Map
const ELL2_C1 = (Fp.ORDER - BigInt(3)) / BigInt(4); // 1. c1 = (q - 3) / 4 # Integer arithmetic
const ELL2_J = BigInt(156326);

@ -62,8 +62,14 @@ describe('RFC8032ctx', () => {
const v = VECTORS_RFC8032_CTX[i];
should(`${i}`, () => {
deepStrictEqual(hex(ed25519ctx.getPublicKey(v.secretKey)), v.publicKey);
deepStrictEqual(hex(ed25519ctx.sign(v.message, v.secretKey, v.context)), v.signature);
deepStrictEqual(ed25519ctx.verify(v.signature, v.message, v.publicKey, v.context), true);
deepStrictEqual(
hex(ed25519ctx.sign(v.message, v.secretKey, { context: v.context })),
v.signature
);
deepStrictEqual(
ed25519ctx.verify(v.signature, v.message, v.publicKey, { context: v.context }),
true
);
});
}
});
@ -93,14 +99,7 @@ describe('RFC8032ph', () => {
});
// x25519
should('X25519 base point', () => {
const { y } = ed25519ph.ExtendedPoint.BASE;
const { Fp } = ed25519ph.CURVE;
const u = Fp.create((y + 1n) * Fp.inv(1n - y));
deepStrictEqual(numberToBytesLE(u, 32), x25519.GuBytes);
});
describe('RFC7748', () => {
describe('RFC7748 X25519 ECDH', () => {
const rfc7748Mul = [
{
scalar: 'a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4',
@ -127,7 +126,7 @@ describe('RFC7748', () => {
];
for (let i = 0; i < rfc7748Iter.length; i++) {
const { scalar, iters } = rfc7748Iter[i];
should(`scalarMult iteration (${i})`, () => {
should(`scalarMult iteration x${iters}`, () => {
let k = x25519.GuBytes;
for (let i = 0, u = k; i < iters; i++) [k, u] = [x25519.scalarMult(k, u), k];
deepStrictEqual(hex(k), scalar);
@ -145,10 +144,16 @@ describe('RFC7748', () => {
deepStrictEqual(hex(x25519.scalarMult(alicePrivate, bobPublic)), shared);
deepStrictEqual(hex(x25519.scalarMult(bobPrivate, alicePublic)), shared);
});
});
describe('Wycheproof', () => {
should('base point', () => {
const { y } = ed25519ph.ExtendedPoint.BASE;
const { Fp } = ed25519ph.CURVE;
const u = Fp.create((y + 1n) * Fp.inv(1n - y));
deepStrictEqual(numberToBytesLE(u, 32), x25519.GuBytes);
});
const group = x25519vectors.testGroups[0];
should(`X25519`, () => {
should('wycheproof', () => {
for (let i = 0; i < group.tests.length; i++) {
const v = group.tests[i];
const comment = `(${i}, ${v.result}) ${v.comment}`;

@ -513,8 +513,11 @@ describe('ed448', () => {
const v = VECTORS_RFC8032_CTX[i];
should(`${i}`, () => {
deepStrictEqual(hex(ed.getPublicKey(v.secretKey)), v.publicKey);
deepStrictEqual(hex(ed.sign(v.message, v.secretKey, v.context)), v.signature);
deepStrictEqual(ed.verify(v.signature, v.message, v.publicKey, v.context), true);
deepStrictEqual(hex(ed.sign(v.message, v.secretKey, { context: v.context })), v.signature);
deepStrictEqual(
ed.verify(v.signature, v.message, v.publicKey, { context: v.context }),
true
);
});
}
});
@ -559,8 +562,14 @@ describe('ed448', () => {
const v = VECTORS_RFC8032_PH[i];
should(`${i}`, () => {
deepStrictEqual(hex(ed448ph.getPublicKey(v.secretKey)), v.publicKey);
deepStrictEqual(hex(ed448ph.sign(v.message, v.secretKey, v.context)), v.signature);
deepStrictEqual(ed448ph.verify(v.signature, v.message, v.publicKey, v.context), true);
deepStrictEqual(
hex(ed448ph.sign(v.message, v.secretKey, { context: v.context })),
v.signature
);
deepStrictEqual(
ed448ph.verify(v.signature, v.message, v.publicKey, { context: v.context }),
true
);
});
}
});
@ -661,34 +670,32 @@ describe('ed448', () => {
deepStrictEqual(hex(x448.scalarMult(bobPrivate, alicePublic)), shared);
});
describe('wycheproof', () => {
should('wycheproof', () => {
const group = x448vectors.testGroups[0];
should(`X448`, () => {
for (let i = 0; i < group.tests.length; i++) {
const v = group.tests[i];
const index = `(${i}, ${v.result}) ${v.comment}`;
if (v.result === 'valid' || v.result === 'acceptable') {
try {
const shared = hex(x448.scalarMult(v.private, v.public));
deepStrictEqual(shared, v.shared, index);
} catch (e) {
// We are more strict
if (e.message.includes('Expected valid scalar')) return;
if (e.message.includes('Invalid private or public key received')) return;
if (e.message.includes('Expected 56 bytes')) return;
throw e;
}
} else if (v.result === 'invalid') {
let failed = false;
try {
x448.scalarMult(v.private, v.public);
} catch (error) {
failed = true;
}
deepStrictEqual(failed, true, index);
} else throw new Error('unknown test result');
}
});
for (let i = 0; i < group.tests.length; i++) {
const v = group.tests[i];
const index = `(${i}, ${v.result}) ${v.comment}`;
if (v.result === 'valid' || v.result === 'acceptable') {
try {
const shared = hex(x448.scalarMult(v.private, v.public));
deepStrictEqual(shared, v.shared, index);
} catch (e) {
// We are more strict
if (e.message.includes('Expected valid scalar')) return;
if (e.message.includes('Invalid private or public key received')) return;
if (e.message.includes('Expected 56 bytes')) return;
throw e;
}
} else if (v.result === 'invalid') {
let failed = false;
try {
x448.scalarMult(v.private, v.public);
} catch (error) {
failed = true;
}
deepStrictEqual(failed, true, index);
} else throw new Error('unknown test result');
}
});
should('have proper base point', () => {