diff --git a/lib/beacon-verification.ts b/lib/beacon-verification.ts index a747c90..2bb91ba 100644 --- a/lib/beacon-verification.ts +++ b/lib/beacon-verification.ts @@ -10,7 +10,8 @@ import { RandomnessBeacon, G2UnchainedBeacon, isG1G2SwappedBeacon, - G1UnchainedBeacon + G1UnchainedBeacon, + isG1Rfc9380 } from './index' async function verifyBeacon(chainInfo: ChainInfo, beacon: RandomnessBeacon): Promise { @@ -32,6 +33,10 @@ async function verifyBeacon(chainInfo: ChainInfo, beacon: RandomnessBeacon): Pro return verifySigOnG1(beacon.signature, await unchainedBeaconMessage(beacon), publicKey) } + if (isG1Rfc9380(beacon, chainInfo)) { + return verifySigOnG1(beacon.signature, await unchainedBeaconMessage(beacon), publicKey, 'BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_') + } + console.error(`Beacon type ${chainInfo.schemeID} was not supported`) return false @@ -49,13 +54,19 @@ function normP2(point: G2Hex): PointG2 { return point instanceof PointG2 ? point : PointG2.fromHex(point); } -async function normP1Hash(point: G1Hex): Promise { - return point instanceof PointG1 ? point : PointG1.hashToCurve(point); +async function normP1Hash(point: G1Hex, domainSeparationTag: string): Promise { + return point instanceof PointG1 ? point : PointG1.hashToCurve(point, {DST: domainSeparationTag}); } -export async function verifySigOnG1(signature: G1Hex, message: G1Hex, publicKey: G2Hex): Promise { +export async function verifySigOnG1( + signature: G1Hex, + message: G1Hex, + publicKey: G2Hex, + // default DST is the invalid one used for 'bls-unchained-on-g1' for backwards compat + domainSeparationTag= 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_' +): Promise { const P = normP2(publicKey); - const Hm = await normP1Hash(message); + const Hm = await normP1Hash(message, domainSeparationTag); const G = PointG2.BASE; const S = normP1(signature); const ePHm = pairing(Hm, P.negate(), false); diff --git a/lib/index.ts b/lib/index.ts index 28900a1..9cd61df 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -144,7 +144,7 @@ export type ChainInfo = { } // currently drand supports chained and unchained randomness - read more here: https://drand.love/docs/cryptography/#randomness -export type RandomnessBeacon = G2ChainedBeacon | G2UnchainedBeacon | G1UnchainedBeacon +export type RandomnessBeacon = G2ChainedBeacon | G2UnchainedBeacon | G1UnchainedBeacon | G1RFC9380Beacon export type G2ChainedBeacon = { round: number @@ -157,7 +157,7 @@ export type G2UnchainedBeacon = { round: number randomness: string signature: string - // this is needed to distinguish it from the `G1UnchainedBeacon` so the type guard works correctly + // this is needed to distinguish it from the other unchained beacons so the type guard works correctly _phantomg2?: never } @@ -165,10 +165,18 @@ export type G1UnchainedBeacon = { round: number randomness: string signature: string - // this distinguishes it from the `G2UnchainedBeacon` so the type guard works correctly + // this distinguishes it from the other unchained beacons so the type guard works correctly _phantomg1?: never } +export type G1RFC9380Beacon = { + round: number + randomness: string + signature: string + // this distinguishes it from the other unchained beacons so the type guard works correctly + _phantomg19380?: never +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isChainedBeacon(value: any, info: ChainInfo): value is G2ChainedBeacon { return info.schemeID === 'pedersen-bls-chained' && @@ -197,5 +205,14 @@ export function isG1G2SwappedBeacon(value: any, info: ChainInfo): value is G1Unc value.round > 0 } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isG1Rfc9380(value: any, info: ChainInfo): value is G1RFC9380Beacon { + return info.schemeID === 'bls-unchained-g1-rfc9380' && + !!value.randomness && + !!value.signature && + value.previous_signature === undefined && + value.round > 0 +} + // exports some default implementations of the above interfaces and other utility functions that could be used with them export {HttpChain, HttpChainClient, HttpCachingChain, MultiBeaconNode, FastestNodeClient, roundAt, roundTime} diff --git a/package.json b/package.json index a873f0e..f5aeb94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "drand-client", - "version": "1.1.1", + "version": "1.2.0", "description": "A client to the drand randomness beacon network.", "main": "index.js", "types": "index.d.ts", diff --git a/test/beacon-verification.test.ts b/test/beacon-verification.test.ts index fdc70fb..f914eb3 100644 --- a/test/beacon-verification.test.ts +++ b/test/beacon-verification.test.ts @@ -177,4 +177,29 @@ describe('verifyBeacon', () => { await expect(verifyBeacon(chainInfo, invalidBeacon)).resolves.toEqual(false) }) }) + describe('signatures on G1 with DST', () => { + const validBeacon = { + round: 38, + randomness: 'b2fc21325a24904a6a9e81a6c63f65f6cec3f0b2400df3f4a56b214770e9ccca', + signature: '95c93585c513ebbcb4777ff15599b3140e5ec0295faa0e483f3deadd88fa6d43f0d3703e3a4ce106e8fd6c6987f32126' + } + + const chainInfo = { + public_key: '81d320f220ee9c79e60e19dedc838c31e3ab919b15481e9feb52f643628c4f6a13fdc52129493875a818109d767272ca0541cbcdcea9335f2870d781b39b845ba8cbd44fdfe4967781cf72ca5917fc9398bcf97ca0548ed5a709016c4b1ff0f3', + period: 3, + genesis_time: 1687506816, + hash: 'af8b6fc95693b058a3a59efe586eb31c2c352fe00cf40c62a427d87c34f7a235', + groupHash: '02d678e93908888871b70e3f015b396a8c082bc1493ae5479f8743cb5d972b54', + schemeID: 'bls-unchained-g1-rfc9380', + metadata: {'beaconID': 'walkthrough'} + } + + it('should verify a valid signature', async () => { + await expect(verifyBeacon(chainInfo, validBeacon)).resolves.toEqual(true) + }) + it('should not verify a signature on G1 for the wrong round', async () => { + const invalidBeacon = {...validBeacon, round: 55} + await expect(verifyBeacon(chainInfo, invalidBeacon)).resolves.toEqual(false) + }) + }) }) diff --git a/test/index.test.ts b/test/index.test.ts index 16c0d33..30a4162 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -23,11 +23,10 @@ async function endToEndTest(url: string) { const chains = await node.chains() expect(chains).not.toHaveLength(0) - // currently the drand-client does not support the new G1/G2 swapped scheme let chainToUse for (const chain of chains) { const info = await chain.info() - if (info.schemeID === 'pedersen-bls-chained' || info.schemeID === 'pedersen-bls-unchained') { + if (info.schemeID === 'pedersen-bls-chained' || info.schemeID === 'pedersen-bls-unchained' || info.schemeID === 'bls-unchained-on-g1' || info.schemeID === 'bls-unchained-g1-rfc9380') { chainToUse = chain break }