Skip to content

Commit

Permalink
feat(eddsa-poseidon): add packSignature and unpackSignature to eddsa-…
Browse files Browse the repository at this point in the history
…poseidon

Signatures are packed into 64 bytes using the same format as circomlibjs
  • Loading branch information
artwyman committed Mar 14, 2024
1 parent d2cbbcc commit e5020e8
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 1 deletion.
60 changes: 59 additions & 1 deletion packages/eddsa-poseidon/src/eddsa-poseidon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
unpackPoint
} from "@zk-kit/baby-jubjub"
import type { BigNumberish } from "@zk-kit/utils"
import { crypto } from "@zk-kit/utils"
import { crypto, requireBuffer } from "@zk-kit/utils"
import { bigNumberishToBigInt, leBigIntToBuffer, leBufferToBigInt } from "@zk-kit/utils/conversions"
import { requireBigNumberish } from "@zk-kit/utils/error-handlers"
import F1Field from "@zk-kit/utils/f1-field"
Expand Down Expand Up @@ -174,6 +174,64 @@ export function unpackPublicKey(publicKey: BigNumberish): Point<string> {
return [unpackedPublicKey[0].toString(), unpackedPublicKey[1].toString()]
}

/**
* Packs an EdDSA signature into a buffer of 64 bytes for efficient storage.
* Use {@link unpackSignature} to reverse the process without needing to know
* the details of the format.
*
* The buffer contains the R8 point packed int 32 bytes (via
* {@link packSignature}) followed by the S scalar. All encodings are
* little-endian.
*
* @param signature the signature to pack
* @returns a 64 byte buffer containing the packed signature
*/
export function packSignature(signature: Signature): Buffer {
if (
!isSignature(signature) ||
!inCurve(signature.R8) ||
BigInt(signature.S) >= subOrder
) {
throw new Error("Invalid signature")
}

const numericSignature: Signature<bigint> = {
R8: signature.R8.map((c) => BigInt(c)) as Point<bigint>,
S: BigInt(signature.S)
}

const packedR8 = packPoint(numericSignature.R8)
const packedBytes = Buffer.alloc(64)
packedBytes.set(leBigIntToBuffer(packedR8), 0)
packedBytes.set(leBigIntToBuffer(numericSignature.S), 32)
return packedBytes
}

/**
* Unpacks a signature produced by {@link #packSignature}. See that function
* for the details of the format.
*
* @param packedSignature the 64 byte buffer to unpack
* @returns a Signature with numbers in string form
*/
export function unpackSignature(packedSignature: Buffer): Signature<string> {
requireBuffer(packedSignature, "packedSignature")
if (packedSignature.length !== 64) {
throw new Error("Packed signature must be 64 bytes")
}

const sliceR8 = packedSignature.subarray(0, 32)
const sliceS = packedSignature.subarray(32, 64)
const unpackedR8 = unpackPoint(leBufferToBigInt(sliceR8))
if (unpackedR8 === null) {
throw new Error(`Invalid packed signature point ${sliceS.toString("hex")}.`)
}
return {
R8: [unpackedR8[0].toString(), unpackedR8[1].toString()],
S: leBufferToBigInt(sliceS).toString()
}
}

/**
* Represents a cryptographic entity capable of signing messages and verifying signatures
* using the EdDSA scheme with Poseidon hash and the Baby Jubjub elliptic curve.
Expand Down
86 changes: 86 additions & 0 deletions packages/eddsa-poseidon/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import {
derivePublicKey,
deriveSecretScalar,
packPublicKey,
packSignature,
signMessage,
unpackPublicKey,
unpackSignature,
verifySignature
} from "../src"

Expand Down Expand Up @@ -251,6 +253,90 @@ describe("EdDSAPoseidon", () => {
expect(fun).toThrow("Invalid public key")
})

it("Should pack a signature (stringified)", async () => {
const signature = signMessage(privateKey, message)

const packedSignature = packSignature(signature)
expect(packedSignature).toHaveLength(64)

const circomlibSignature = eddsa.signPoseidon(privateKey, message)
const circomlibPackedSignature = eddsa.packSignature(circomlibSignature)

expect(packedSignature).toEqual(circomlibPackedSignature)
})

// TODO(artwyman): Add test case for numericSignature after #200 lands

it("Should still pack an incorrect signature", async () => {
const signature = signMessage(privateKey, message)

signature.S = "3"

const packedSignature = packSignature(signature)
expect(packedSignature).toHaveLength(64)
})

it("Should not pack a signature if the signature is not on the curve", async () => {
const signature = signMessage(privateKey, message)

signature.R8[1] = BigInt(3).toString()

const fun = () => packSignature(signature)

expect(fun).toThrow("Invalid signature")
})

it("Should not pack a signature S value exceeds the predefined sub order", async () => {
const signature = signMessage(privateKey, message)

signature.S = "3421888242871839275222246405745257275088614511777268538073601725287587578984328"

const fun = () => packSignature(signature)

expect(fun).toThrow("Invalid signature")
})

it("Should unpack a packed signature", async () => {
const signature = signMessage(privateKey, message)

const packedSignature = packSignature(signature)

const unpackedSignature = unpackSignature(packedSignature)
expect(unpackedSignature).toEqual(signature)

const circomlibSignature = eddsa.signPoseidon(privateKey, message)
const circomlibUnpackedSignature = eddsa.unpackSignature(packedSignature)
expect(circomlibSignature).toEqual(circomlibUnpackedSignature)
})

it("Should not unpack a packed signature of the wrong length", async () => {
const signature = signMessage(privateKey, message)

const packedSignature = packSignature(signature)

const fun = () => unpackSignature(packedSignature.subarray(0, 63))
expect(fun).toThrow("Packed signature must be 64 bytes")
})

it("Should not unpack a packed signature with point malformed", async () => {
const signature = signMessage(privateKey, message)

const packedSignature = packSignature(signature)
packedSignature.set([1], 3)

const fun = () => unpackSignature(packedSignature)
expect(fun).toThrow("Invalid packed signature point")
})

it("Should still unpack a packed signature with scalar malformed", async () => {
const signature = signMessage(privateKey, message)

const packedSignature = packSignature(signature)
packedSignature.set([1], 35)

unpackSignature(packedSignature)
})

it("Should create an EdDSAPoseidon instance", async () => {
const eddsa = new EdDSAPoseidon(privateKey)

Expand Down

0 comments on commit e5020e8

Please sign in to comment.