diff --git a/benchmark/index.js b/benchmark/index.js index 5b9c0f1..1ef86b4 100644 --- a/benchmark/index.js +++ b/benchmark/index.js @@ -96,6 +96,10 @@ export const CURVES = { old_secp.recoverPublicKey(msg, new old_secp.Signature(sig.r, sig.s), sig.recovery), secp256k1: ({ sig, msg }) => sig.recoverPublicKey(msg), }, + hashToCurve: { + samples: 500, + noble: () => secp256k1.Point.hashToCurve('abcd'), + }, }, ed25519: { data: () => { @@ -124,6 +128,10 @@ export const CURVES = { old: ({ sig, msg, pub }) => noble_ed25519.sync.verify(sig, msg, pub), noble: ({ sig, msg, pub }) => ed25519.verify(sig, msg, pub), }, + hashToCurve: { + samples: 500, + noble: () => ed25519.Point.hashToCurve('abcd'), + }, }, ed448: { data: () => { @@ -145,6 +153,10 @@ export const CURVES = { samples: 500, noble: ({ sig, msg, pub }) => ed448.verify(sig, msg, pub), }, + hashToCurve: { + samples: 500, + noble: () => ed448.Point.hashToCurve('abcd'), + }, }, nist: { data: () => { @@ -168,6 +180,12 @@ export const CURVES = { P384: ({ p384: { sig, msg, pub } }) => P384.verify(sig, msg, pub), P521: ({ p521: { sig, msg, pub } }) => P521.verify(sig, msg, pub), }, + hashToCurve: { + samples: 500, + P256: () => P256.Point.hashToCurve('abcd'), + P384: () => P384.Point.hashToCurve('abcd'), + P521: () => P521.Point.hashToCurve('abcd'), + }, }, stark: { data: () => { diff --git a/src/abstract/hash-to-curve.ts b/src/abstract/hash-to-curve.ts index 6f0ac4c..84ab51c 100644 --- a/src/abstract/hash-to-curve.ts +++ b/src/abstract/hash-to-curve.ts @@ -17,10 +17,11 @@ export type htfOpts = { k: number; // option to use a message that has already been processed by // expand_message_xmd - expand: boolean; + expand?: 'xmd' | 'xof'; // Hash functions for: expand_message_xmd is appropriate for use with a // wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others. // BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247 + // TODO: verify that hash is shake if expand==='xof' via types hash: CHash; }; @@ -29,7 +30,8 @@ export function validateHTFOpts(opts: htfOpts) { if (typeof opts.p !== 'bigint') throw new Error('Invalid htf/p'); if (typeof opts.m !== 'number') throw new Error('Invalid htf/m'); if (typeof opts.k !== 'number') throw new Error('Invalid htf/k'); - if (typeof opts.expand !== 'boolean') throw new Error('Invalid htf/expand'); + if (opts.expand !== 'xmd' && opts.expand !== 'xof' && opts.expand !== undefined) + throw new Error('Invalid htf/expand'); if (typeof opts.hash !== 'function' || !Number.isSafeInteger(opts.hash.outputLen)) throw new Error('Invalid htf/hash function'); } @@ -101,6 +103,32 @@ export function expand_message_xmd( return pseudo_random_bytes.slice(0, lenInBytes); } +export function expand_message_xof( + msg: Uint8Array, + DST: Uint8Array, + lenInBytes: number, + k: number, + H: CHash +) { + // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3 + // DST = H('H2C-OVERSIZE-DST-' || a_very_long_DST, Math.ceil((lenInBytes * k) / 8)); + if (DST.length > 255) { + const dkLen = Math.ceil((2 * k) / 8); + DST = H.create({ dkLen }).update(stringToBytes('H2C-OVERSIZE-DST-')).update(DST).digest(); + } + if (lenInBytes > 65535 || DST.length > 255) + throw new Error('expand_message_xof: invalid lenInBytes'); + return ( + H.create({ dkLen: lenInBytes }) + .update(msg) + .update(i2osp(lenInBytes, 2)) + // 2. DST_prime = DST || I2OSP(len(DST), 1) + .update(DST) + .update(i2osp(DST.length, 1)) + .digest() + ); +} + // hashes arbitrary-length byte strings to a list of one or more elements of a finite field F // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.3 // Inputs: @@ -116,8 +144,10 @@ export function hash_to_field(msg: Uint8Array, count: number, options: htfOpts): const len_in_bytes = count * options.m * L; const DST = stringToBytes(options.DST); let pseudo_random_bytes = msg; - if (options.expand) { + if (options.expand === 'xmd') { pseudo_random_bytes = expand_message_xmd(msg, DST, len_in_bytes, options.hash); + } else if (options.expand === 'xof') { + pseudo_random_bytes = expand_message_xof(msg, DST, len_in_bytes, options.k, options.hash); } const u = new Array(count); for (let i = 0; i < count; i++) { diff --git a/src/abstract/utils.ts b/src/abstract/utils.ts index 3940d77..b6768fc 100644 --- a/src/abstract/utils.ts +++ b/src/abstract/utils.ts @@ -12,7 +12,7 @@ export type CHash = { (message: Uint8Array | string): Uint8Array; blockLen: number; outputLen: number; - create(): any; + create(opts?: { dkLen?: number }): any; // For shake }; // NOTE: these are generic, even if curve is on some polynominal field (bls), it will still have P/n/h diff --git a/src/bls12-381.ts b/src/bls12-381.ts index 68699ab..cd34853 100644 --- a/src/bls12-381.ts +++ b/src/bls12-381.ts @@ -926,12 +926,12 @@ const htfDefaults = { k: 128, // option to use a message that has already been processed by // expand_message_xmd - expand: true, + expand: 'xmd', // Hash functions for: expand_message_xmd is appropriate for use with a // wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others. // BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247 hash: sha256, -}; +} as const; // Encoding utils // Point on G1 curve: (x, y) diff --git a/src/p256.ts b/src/p256.ts index 688d7f1..eb176f6 100644 --- a/src/p256.ts +++ b/src/p256.ts @@ -37,7 +37,7 @@ export const P256 = createCurve( p: Fp.ORDER, m: 1, k: 128, - expand: true, + expand: 'xmd', hash: sha256, }, } as const, diff --git a/src/p384.ts b/src/p384.ts index 41f940e..0cf3ae3 100644 --- a/src/p384.ts +++ b/src/p384.ts @@ -41,7 +41,7 @@ export const P384 = createCurve({ p: Fp.ORDER, m: 1, k: 192, - expand: true, + expand: 'xmd', hash: sha384, }, } as const, diff --git a/src/p521.ts b/src/p521.ts index 5111f48..042f2f7 100644 --- a/src/p521.ts +++ b/src/p521.ts @@ -54,7 +54,7 @@ export const P521 = createCurve({ p: Fp.ORDER, m: 1, k: 256, - expand: true, + expand: 'xmd', hash: sha512, }, } as const, sha512); diff --git a/test/hash-to-curve.test.js b/test/hash-to-curve.test.js index 446a4a4..5bdda88 100644 --- a/test/hash-to-curve.test.js +++ b/test/hash-to-curve.test.js @@ -4,16 +4,27 @@ import { bytesToHex } from '@noble/hashes/utils'; // Generic tests for all curves in package import { sha256 } from '@noble/hashes/sha256'; import { sha512 } from '@noble/hashes/sha512'; +import { shake128, shake256 } from '@noble/hashes/sha3'; import { secp256r1 } from '../lib/esm/p256.js'; import { secp384r1 } from '../lib/esm/p384.js'; import { secp521r1 } from '../lib/esm/p521.js'; +import { ed25519 } from '../lib/esm/ed25519.js'; +import { ed448 } from '../lib/esm/ed448.js'; import { secp256k1 } from '../lib/esm/secp256k1.js'; import { bls12_381 } from '../lib/esm/bls12-381.js'; -import { stringToBytes, expand_message_xmd } from '../lib/esm/abstract/hash-to-curve.js'; - +import { + stringToBytes, + expand_message_xmd, + expand_message_xof, +} from '../lib/esm/abstract/hash-to-curve.js'; +// XMD import { default as xmd_sha256_38 } from './hash-to-curve/expand_message_xmd_SHA256_38.json' assert { type: 'json' }; import { default as xmd_sha256_256 } from './hash-to-curve/expand_message_xmd_SHA256_256.json' assert { type: 'json' }; import { default as xmd_sha512_38 } from './hash-to-curve/expand_message_xmd_SHA512_38.json' assert { type: 'json' }; +// XOF +import { default as xof_shake128_36 } from './hash-to-curve/expand_message_xof_SHAKE128_36.json' assert { type: 'json' }; +import { default as xof_shake128_256 } from './hash-to-curve/expand_message_xof_SHAKE128_256.json' assert { type: 'json' }; +import { default as xof_shake256_36 } from './hash-to-curve/expand_message_xof_SHAKE256_36.json' assert { type: 'json' }; // P256 import { default as p256_ro } from './hash-to-curve/P256_XMD:SHA-256_SSWU_RO_.json' assert { type: 'json' }; import { default as p256_nu } from './hash-to-curve/P256_XMD:SHA-256_SSWU_NU_.json' assert { type: 'json' }; @@ -58,6 +69,26 @@ testExpandXMD(sha256, xmd_sha256_38); testExpandXMD(sha256, xmd_sha256_256); testExpandXMD(sha512, xmd_sha512_38); +function testExpandXOF(hash, vectors) { + for (let i = 0; i < vectors.tests.length; i++) { + const t = vectors.tests[i]; + should(`expand_message_xof/${vectors.hash}/${vectors.DST.length}/${i}`, () => { + const p = expand_message_xof( + stringToBytes(t.msg), + stringToBytes(vectors.DST), + +t.len_in_bytes, + vectors.k, + hash + ); + deepStrictEqual(bytesToHex(p), t.uniform_bytes); + }); + } +} + +testExpandXOF(shake128, xof_shake128_36); +testExpandXOF(shake128, xof_shake128_256); +testExpandXOF(shake256, xof_shake256_36); + function stringToFp(s) { // bls-G2 support if (s.includes(',')) { @@ -97,8 +128,8 @@ testCurve(secp521r1, p521_ro, p521_nu); testCurve(bls12_381.G1, g1_ro, g1_nu); testCurve(bls12_381.G2, g2_ro, g2_nu); testCurve(secp256k1, secp256k1_ro, secp256k1_nu); -//testCurve(ed25519, ed25519_ro, ed25519_nu); -//testCurve(ed448, ed448_ro, ed448_nu); +testCurve(ed25519, ed25519_ro, ed25519_nu); +testCurve(ed448, ed448_ro, ed448_nu); // ESM is broken. import url from 'url';