ox
Version:
636 lines (603 loc) • 15.5 kB
text/typescript
import type { ProjPointType } from '@noble/curves/abstract/weierstrass'
import { bls12_381 as bls } from '@noble/curves/bls12-381'
import type * as BlsPoint from './BlsPoint.js'
import * as Bytes from './Bytes.js'
import type * as Errors from './Errors.js'
import * as Hex from './Hex.js'
import type { OneOf } from './internal/types.js'
export type Size = 'short-key:long-sig' | 'long-key:short-sig'
/** Re-export of noble/curves BLS12-381 utilities. */
export const noble = bls
/**
* Aggregates a set of BLS points that are either on the G1 or G2 curves (ie. public keys or signatures).
*
* @example
* ### Aggregating Signatures
*
* ```ts twoslash
* import { Bls, Hex } from 'ox'
*
* const payload = Hex.random(32)
*
* const signatures = [
* Bls.sign({ payload, privateKey: '0x...' }),
* Bls.sign({ payload, privateKey: '0x...' }),
* ]
* const signature = Bls.aggregate(signatures)
* ```
*
* @example
* ### Aggregating Public Keys
*
* ```ts twoslash
* import { Bls } from 'ox'
*
* const publicKeys = [
* Bls.getPublicKey({ privateKey: '0x...' }),
* Bls.getPublicKey({ privateKey: '0x...' }),
* ]
* const publicKey = Bls.aggregate(publicKeys)
* ```
*
* @param points - The points to aggregate.
* @returns The aggregated point.
*/
export function aggregate<const points extends readonly BlsPoint.BlsPoint[]>(
points: points,
): points extends readonly BlsPoint.G1[] ? BlsPoint.G1 : BlsPoint.G2
// eslint-disable-next-line jsdoc/require-jsdoc
export function aggregate(
points: readonly BlsPoint.BlsPoint[],
): BlsPoint.BlsPoint {
const group = typeof points[0]?.x === 'bigint' ? bls.G1 : bls.G2
const point = points.reduce(
(acc, point) =>
acc.add(new (group as any).ProjectivePoint(point.x, point.y, point.z)),
group.ProjectivePoint.ZERO,
)
return {
x: point.px,
y: point.py,
z: point.pz,
}
}
export declare namespace aggregate {
type ErrorType = Errors.GlobalErrorType
}
/**
* Creates a new BLS12-381 key pair consisting of a private key and its corresponding public key.
*
* - G1 Point (Default):
* - short (48 bytes)
* - computes longer G2 Signatures (96 bytes)
* - G2 Point:
* - long (96 bytes)
* - computes short G1 Signatures (48 bytes)
*
* @example
* ### Short G1 Public Keys (Default)
*
* ```ts twoslash
* import { Bls } from 'ox'
*
* const { publicKey } = Bls.createKeyPair()
* // ^?
*
*
*
*
*
*
*
* ```
*
* @example
* ### Long G2 Public Keys
*
* A G2 Public Key can be derived as a G2 point (96 bytes) using `size: 'long-key:short-sig'`.
*
* This will allow you to compute G1 Signatures (48 bytes) with {@link ox#Bls.(sign:function)}.
*
* ```ts twoslash
* import { Bls } from 'ox'
*
* const { publicKey } = Bls.createKeyPair({
* size: 'long-key:short-sig',
* })
*
* publicKey
* // ^?
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*
* ### Serializing
*
* Public Keys can be serialized to hex or bytes using {@link ox#BlsPoint.(toHex:function)} or {@link ox#BlsPoint.(toBytes:function)}:
*
* ```ts twoslash
* import { Bls, BlsPoint } from 'ox'
*
* const { publicKey } = Bls.createKeyPair()
*
* const publicKeyHex = BlsPoint.toHex(publicKey)
* // ^?
*
*
* const publicKeyBytes = BlsPoint.toBytes(publicKey)
* // ^?
*
* ```
*
* They can also be deserialized from hex or bytes using {@link ox#BlsPoint.(fromHex:function)} or {@link ox#BlsPoint.(fromBytes:function)}:
*
* ```ts twoslash
* import { Bls, BlsPoint } from 'ox'
*
* const publicKeyHex = '0x...'
*
* const publicKey = BlsPoint.fromHex(publicKeyHex, 'G1')
* // ^?
*
*
*
*
*
*
*
* ```
*
* @param options - The options to generate the key pair.
* @returns The generated key pair containing both private and public keys.
*/
export function createKeyPair<
as extends 'Hex' | 'Bytes' = 'Hex',
size extends Size = 'short-key:long-sig',
>(
options: createKeyPair.Options<as, size> = {},
): createKeyPair.ReturnType<as, size> {
const { as = 'Hex', size = 'short-key:long-sig' } = options
const privateKey = randomPrivateKey({ as })
const publicKey = getPublicKey({ privateKey, size })
return {
privateKey: privateKey as never,
publicKey: publicKey as never,
}
}
export declare namespace createKeyPair {
type Options<
as extends 'Hex' | 'Bytes' = 'Hex',
size extends Size = 'short-key:long-sig',
> = {
/**
* Format of the returned private key.
* @default 'Hex'
*/
as?: as | 'Hex' | 'Bytes' | undefined
/**
* Size of the public key to compute.
*
* - `'short-key:long-sig'`: 48 bytes; computes long signatures (96 bytes)
* - `'long-key:short-sig'`: 96 bytes; computes short signatures (48 bytes)
*
* @default 'short-key:long-sig'
*/
size?: size | Size | undefined
}
type ReturnType<as extends 'Hex' | 'Bytes', size extends Size> = {
privateKey:
| (as extends 'Bytes' ? Bytes.Bytes : never)
| (as extends 'Hex' ? Hex.Hex : never)
publicKey: size extends 'short-key:long-sig' ? BlsPoint.G1 : BlsPoint.G2
}
type ErrorType =
| Hex.fromBytes.ErrorType
| getPublicKey.ErrorType
| Errors.GlobalErrorType
}
/**
* Computes the BLS12-381 public key from a provided private key.
*
* Public Keys can be derived as a point on one of the BLS12-381 groups:
*
* - G1 Point (Default):
* - short (48 bytes)
* - computes longer G2 Signatures (96 bytes)
* - G2 Point:
* - long (96 bytes)
* - computes short G1 Signatures (48 bytes)
*
* @example
* ### Short G1 Public Keys (Default)
*
* ```ts twoslash
* import { Bls } from 'ox'
*
* const publicKey = Bls.getPublicKey({ privateKey: '0x...' })
* // ^?
*
*
*
*
*
*
*
* ```
*
* @example
* ### Long G2 Public Keys
*
* A G2 Public Key can be derived as a G2 point (96 bytes) using `size: 'long-key:short-sig'`.
*
* This will allow you to compute G1 Signatures (48 bytes) with {@link ox#Bls.(sign:function)}.
*
* ```ts twoslash
* import { Bls } from 'ox'
*
* const publicKey = Bls.getPublicKey({
* privateKey: '0x...',
* size: 'long-key:short-sig',
* })
*
* publicKey
* // ^?
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*
* ### Serializing
*
* Public Keys can be serialized to hex or bytes using {@link ox#BlsPoint.(toHex:function)} or {@link ox#BlsPoint.(toBytes:function)}:
*
* ```ts twoslash
* import { Bls, BlsPoint } from 'ox'
*
* const publicKey = Bls.getPublicKey({ privateKey: '0x...' })
*
* const publicKeyHex = BlsPoint.toHex(publicKey)
* // ^?
*
*
* const publicKeyBytes = BlsPoint.toBytes(publicKey)
* // ^?
*
* ```
*
* They can also be deserialized from hex or bytes using {@link ox#BlsPoint.(fromHex:function)} or {@link ox#BlsPoint.(fromBytes:function)}:
*
* ```ts twoslash
* import { Bls, BlsPoint } from 'ox'
*
* const publicKeyHex = '0x...'
*
* const publicKey = BlsPoint.fromHex(publicKeyHex, 'G1')
* // ^?
*
*
*
*
*
*
*
* ```
*
* @param options - The options to compute the public key.
* @returns The computed public key.
*/
export function getPublicKey<size extends Size = 'short-key:long-sig'>(
options: getPublicKey.Options<size>,
): size extends 'short-key:long-sig' ? BlsPoint.G1 : BlsPoint.G2
// eslint-disable-next-line jsdoc/require-jsdoc
export function getPublicKey(options: getPublicKey.Options): BlsPoint.BlsPoint {
const { privateKey, size = 'short-key:long-sig' } = options
const group = size === 'short-key:long-sig' ? bls.G1 : bls.G2
const { px, py, pz } = group.ProjectivePoint.fromPrivateKey(
Hex.from(privateKey).slice(2),
)
return { x: px, y: py, z: pz }
}
export declare namespace getPublicKey {
type Options<size extends Size = 'short-key:long-sig'> = {
/**
* Private key to compute the public key from.
*/
privateKey: Hex.Hex | Bytes.Bytes
/**
* Size of the public key to compute.
*
* - `'short-key:long-sig'`: 48 bytes; computes long signatures (96 bytes)
* - `'long-key:short-sig'`: 96 bytes; computes short signatures (48 bytes)
*
* @default 'short-key:long-sig'
*/
size?: size | Size | undefined
}
type ErrorType = Hex.from.ErrorType | Errors.GlobalErrorType
}
/**
* Generates a random BLS12-381 private key.
*
* @example
* ```ts twoslash
* import { Bls } from 'ox'
*
* const privateKey = Bls.randomPrivateKey()
* ```
*
* @param options - The options to generate the private key.
* @returns The generated private key.
*/
export function randomPrivateKey<as extends 'Hex' | 'Bytes' = 'Hex'>(
options: randomPrivateKey.Options<as> = {},
): randomPrivateKey.ReturnType<as> {
const { as = 'Hex' } = options
const bytes = bls.utils.randomPrivateKey()
if (as === 'Hex') return Hex.fromBytes(bytes) as never
return bytes as never
}
export declare namespace randomPrivateKey {
type Options<as extends 'Hex' | 'Bytes' = 'Hex'> = {
/**
* Format of the returned private key.
* @default 'Hex'
*/
as?: as | 'Hex' | 'Bytes' | undefined
}
type ReturnType<as extends 'Hex' | 'Bytes'> =
| (as extends 'Bytes' ? Bytes.Bytes : never)
| (as extends 'Hex' ? Hex.Hex : never)
type ErrorType = Hex.fromBytes.ErrorType | Errors.GlobalErrorType
}
/**
* Signs the payload with the provided private key.
*
* @example
* ```ts twoslash
* import { Bls, Hex } from 'ox'
*
* const signature = Bls.sign({ // [!code focus]
* payload: Hex.random(32), // [!code focus]
* privateKey: '0x...' // [!code focus]
* }) // [!code focus]
* ```
*
* @example
* ### Serializing
*
* Signatures can be serialized to hex or bytes using {@link ox#BlsPoint.(toHex:function)} or {@link ox#BlsPoint.(toBytes:function)}:
*
* ```ts twoslash
* import { Bls, BlsPoint, Hex } from 'ox'
*
* const signature = Bls.sign({ payload: Hex.random(32), privateKey: '0x...' })
*
* const signatureHex = BlsPoint.toHex(signature)
* // ^?
*
*
*
* const signatureBytes = BlsPoint.toBytes(signature)
* // ^?
*
*
* ```
*
* They can also be deserialized from hex or bytes using {@link ox#BlsPoint.(fromHex:function)} or {@link ox#BlsPoint.(fromBytes:function)}:
*
* ```ts twoslash
* import { Bls, BlsPoint } from 'ox'
*
* const signatureHex = '0x...'
*
* const signature = BlsPoint.fromHex(signatureHex, 'G2')
* // ^?
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*
* @param options - The signing options.
* @returns BLS Point.
*/
export function sign<size extends Size = 'short-key:long-sig'>(
options: sign.Options<size>,
): size extends 'short-key:long-sig' ? BlsPoint.G2 : BlsPoint.G1
// eslint-disable-next-line jsdoc/require-jsdoc
export function sign(options: sign.Options): BlsPoint.BlsPoint {
const { payload, privateKey, suite, size = 'short-key:long-sig' } = options
const payloadGroup = size === 'short-key:long-sig' ? bls.G2 : bls.G1
const payloadPoint = payloadGroup.hashToCurve(
Bytes.from(payload),
suite ? { DST: Bytes.fromString(suite) } : undefined,
)
const privateKeyGroup = size === 'short-key:long-sig' ? bls.G1 : bls.G2
const signature = payloadPoint.multiply(
privateKeyGroup.normPrivateKeyToScalar(privateKey.slice(2)),
) as ProjPointType<any>
return {
x: signature.px,
y: signature.py,
z: signature.pz,
}
}
export declare namespace sign {
type Options<size extends Size = 'short-key:long-sig'> = {
/**
* Payload to sign.
*/
payload: Hex.Hex | Bytes.Bytes
/**
* BLS private key.
*/
privateKey: Hex.Hex | Bytes.Bytes
/**
* Ciphersuite to use for signing. Defaults to "Basic".
*
* @see https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-05#section-4
* @default 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_'
*/
suite?: string | undefined
/**
* Size of the signature to compute.
*
* - `'long-key:short-sig'`: 48 bytes
* - `'short-key:long-sig'`: 96 bytes
*
* @default 'short-key:long-sig'
*/
size?: size | Size | undefined
}
type ErrorType = Bytes.from.ErrorType | Errors.GlobalErrorType
}
/**
* Verifies a payload was signed by the provided public key(s).
*
* @example
*
* ```ts twoslash
* import { Bls, Hex } from 'ox'
*
* const payload = Hex.random(32)
* const privateKey = Bls.randomPrivateKey()
*
* const publicKey = Bls.getPublicKey({ privateKey })
* const signature = Bls.sign({ payload, privateKey })
*
* const verified = Bls.verify({ // [!code focus]
* payload, // [!code focus]
* publicKey, // [!code focus]
* signature, // [!code focus]
* }) // [!code focus]
* ```
*
* @example
* ### Verify Aggregated Signatures
*
* We can also pass a public key and signature that was aggregated with {@link ox#Bls.(aggregate:function)} to `Bls.verify`.
*
* ```ts twoslash
* import { Bls, Hex } from 'ox'
*
* const payload = Hex.random(32)
* const privateKeys = Array.from({ length: 100 }, () => Bls.randomPrivateKey())
*
* const publicKeys = privateKeys.map((privateKey) =>
* Bls.getPublicKey({ privateKey }),
* )
* const signatures = privateKeys.map((privateKey) =>
* Bls.sign({ payload, privateKey }),
* )
*
* const publicKey = Bls.aggregate(publicKeys) // [!code focus]
* const signature = Bls.aggregate(signatures) // [!code focus]
*
* const valid = Bls.verify({ payload, publicKey, signature }) // [!code focus]
* ```
*
* @param options - Verification options.
* @returns Whether the payload was signed by the provided public key.
*/
export function verify(options: verify.Options): boolean {
const { payload, suite } = options
const publicKey = options.publicKey as unknown as BlsPoint.BlsPoint<any>
const signature = options.signature as unknown as BlsPoint.BlsPoint<any>
const isShortSig = typeof signature.x === 'bigint'
const group = isShortSig ? bls.G1 : bls.G2
const payloadPoint = group.hashToCurve(
Bytes.from(payload),
suite ? { DST: Bytes.fromString(suite) } : undefined,
) as ProjPointType<any>
const shortSigPairing = () =>
bls.pairingBatch([
{
g1: payloadPoint,
g2: new bls.G2.ProjectivePoint(publicKey.x, publicKey.y, publicKey.z),
},
{
g1: new bls.G1.ProjectivePoint(signature.x, signature.y, signature.z),
g2: bls.G2.ProjectivePoint.BASE.negate(),
},
])
const longSigPairing = () =>
bls.pairingBatch([
{
g1: new bls.G1.ProjectivePoint(
publicKey.x,
publicKey.y,
publicKey.z,
).negate(),
g2: payloadPoint,
},
{
g1: bls.G1.ProjectivePoint.BASE,
g2: new bls.G2.ProjectivePoint(signature.x, signature.y, signature.z),
},
])
return bls.fields.Fp12.eql(
isShortSig ? shortSigPairing() : longSigPairing(),
bls.fields.Fp12.ONE,
)
}
export declare namespace verify {
type Options = {
/**
* Payload that was signed.
*/
payload: Hex.Hex | Bytes.Bytes
/**
* Ciphersuite to use for verification. Defaults to "Basic".
*
* @see https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-05#section-4
* @default 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_'
*/
suite?: string | undefined
} & OneOf<
| {
publicKey: BlsPoint.G1
signature: BlsPoint.G2
}
| {
publicKey: BlsPoint.G2
signature: BlsPoint.G1
}
>
type ErrorType = Errors.GlobalErrorType
}