Skip to content

Commit

Permalink
Merge pull request #1815 from o1-labs/shigoto-ecdsa-ethers
Browse files Browse the repository at this point in the history
Enhance Ethereum Signature Verification 
Add Curve Parsing Methods
Split vKey regression CI tests into two calls
  • Loading branch information
Shigoto-dev19 authored Sep 12, 2024
2 parents 81dd73c + 10c2e56 commit 6ad5405
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 18 deletions.
3 changes: 2 additions & 1 deletion run-ci-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ case $TEST_TYPE in

"Verification Key Regression Check")
echo "Running Regression checks"
./run ./tests/vk-regression/vk-regression.ts --bundle
VK_TEST=1 ./run ./tests/vk-regression/vk-regression.ts --bundle
VK_TEST=2 ./run ./tests/vk-regression/vk-regression.ts --bundle
;;

"CommonJS test")
Expand Down
21 changes: 18 additions & 3 deletions src/examples/crypto/ecdsa/ecdsa.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import {
ZkProgram,
Crypto,
createEcdsa,
createEcdsaV2,
createForeignCurveV2,
Bool,
Bytes,
} from 'o1js';

export { keccakAndEcdsa, ecdsa, Secp256k1, Ecdsa, Bytes32 };
export { keccakAndEcdsa, ecdsa, Secp256k1, Ecdsa, Bytes32, ecdsaEthers };

class Secp256k1 extends createForeignCurveV2(Crypto.CurveParams.Secp256k1) {}
class Scalar extends Secp256k1.Scalar {}
class Ecdsa extends createEcdsa(Secp256k1) {}
class Ecdsa extends createEcdsaV2(Secp256k1) {}
class Bytes32 extends Bytes(32) {}

const keccakAndEcdsa = ZkProgram({
Expand Down Expand Up @@ -43,3 +43,18 @@ const ecdsa = ZkProgram({
},
},
});

const ecdsaEthers = ZkProgram({
name: 'ecdsa-ethers',
publicInput: Bytes32,
publicOutput: Bool,

methods: {
verifyEthers: {
privateInputs: [Ecdsa, Secp256k1],
async method(message: Bytes32, signature: Ecdsa, publicKey: Secp256k1) {
return signature.verifyEthers(message, publicKey);
},
},
},
});
48 changes: 47 additions & 1 deletion src/examples/crypto/ecdsa/run.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Secp256k1, Ecdsa, keccakAndEcdsa, ecdsa, Bytes32 } from './ecdsa.js';
import {
Secp256k1,
Ecdsa,
keccakAndEcdsa,
ecdsa,
ecdsaEthers,
Bytes32,
} from './ecdsa.js';
import assert from 'assert';

// create an example ecdsa signature
Expand Down Expand Up @@ -34,3 +41,42 @@ console.timeEnd('keccak + ecdsa verify (prove)');

proof.publicOutput.assertTrue('signature verifies');
assert(await keccakAndEcdsa.verify(proof), 'proof verifies');

// Hardcoded ethers.js signature and inputs for verification in o1js

// message signed using ethers.js
const msg = 'Secrets hidden, truth in ZKPs ;)';

// uncompressed public key generated by ethers.js
const uncompressedPublicKey =
'0x040957928494c38660d254dc03ba78f091a4aea0270afb447f193c4daf6648f02b720071af9b5bda4936998ec186e632f4be82886914851d7c753747b0a949d1a4';

// compressed public key generated by ethers.js
const compressedPublicKey =
'0x020957928494c38660d254dc03ba78f091a4aea0270afb447f193c4daf6648f02b';

// ECDSA signature generated by ethers.js
const rawSignature =
'0x6fada464c3bc2ae127f8c907c0c4bccbd05ba83a584156edb808b7400346b4c9558598d9c7869f5fd75d81128711f6621e4cb5ba2f52a2a51c46c859f49a833a1b';

const publicKeyE = Secp256k1.fromEthers(compressedPublicKey);
const signatureE = Ecdsa.fromHex(rawSignature);
const msgBytes = Bytes32.fromString(msg);

// investigate the constraint system generated by ECDSA verifyEthers
console.time('ethers verify only (build constraint system)');
let csEcdsaEthers = await ecdsaEthers.analyzeMethods();
console.timeEnd('ethers verify only (build constraint system)');
console.log(csEcdsaEthers.verifyEthers.summary());

// compile and prove
console.time('ecdsa / ethers verify (compile)');
await ecdsaEthers.compile();
console.timeEnd('ecdsa / ethers verify (compile)');

console.time('ecdsa / ethers verify (prove)');
let proofE = await ecdsaEthers.verifyEthers(msgBytes, signatureE, publicKeyE);
console.timeEnd('ecdsa / ethers verify (prove)');

proofE.publicOutput.assertTrue('signature verifies');
assert(await ecdsaEthers.verify(proofE), 'proof verifies');
121 changes: 120 additions & 1 deletion src/lib/provable/crypto/foreign-curve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { assert } from '../gadgets/common.js';
import { Provable } from '../provable.js';
import { provableFromClass } from '../types/provable-derivers.js';
import { l2Mask, multiRangeCheck } from '../gadgets/range-check.js';
import { Bytes } from '../bytes.js';

// external API
export {
Expand Down Expand Up @@ -43,7 +44,7 @@ class ForeignCurve {
* Create a new {@link ForeignCurve} from an object representing the (affine) x and y coordinates.
*
* Note: Inputs must be range checked if they originate from a different field with a different modulus or if they are not constants. Please refer to the {@link ForeignField} constructor comments for more details.
*
*
* @example
* ```ts
* let x = new ForeignCurve({ x: 1n, y: 1n });
Expand Down Expand Up @@ -74,6 +75,124 @@ class ForeignCurve {
return new this(g);
}

/**
* Parses a hexadecimal string representing an uncompressed elliptic curve point and coerces it into a {@link ForeignCurveV2} point.
*
* The method extracts the x and y coordinates from the provided hex string and verifies that the resulting point lies on the curve.
*
* **Note:** This method only supports uncompressed elliptic curve points, which are 65 bytes in total (1-byte prefix + 32 bytes for x + 32 bytes for y).
*
* @param hex - The hexadecimal string representing the uncompressed elliptic curve point.
* @returns - A point on the foreign curve, parsed from the given hexadecimal string.
*
* @throws - Throws an error if the input is not a valid public key.
*
* @example
* ```ts
* class Secp256k1 extends createForeignCurveV2(Crypto.CurveParams.Secp256k1) {}
*
* const publicKeyHex = '04f8b8db25c619d0c66b2dc9e97ecbafafae...'; // Example hex string for uncompressed point
* const point = Secp256k1.fromHex(publicKeyHex);
* ```
*
* **Important:** This method is only designed to handle uncompressed elliptic curve points in hex format.
*/
static fromHex(hex: string) {
// trim the '0x' prefix if present
if (hex.startsWith('0x')) {
hex = hex.slice(2);
}

const bytes = Bytes.fromHex(hex).toBytes();
const sizeInBytes = Math.ceil(this.Bigint.Field.sizeInBits / 8);

// extract x and y coordinates from the byte array
const tail = bytes.subarray(1); // skip the first byte (prefix)
const xBytes = tail.subarray(0, sizeInBytes); // first `sizeInBytes` bytes for x-coordinate
const yBytes = tail.subarray(sizeInBytes, 2 * sizeInBytes); // next `sizeInBytes` bytes for y-coordinate

// convert byte arrays to bigint
const x = BigInt('0x' + Bytes.from(xBytes).toHex());
const y = BigInt('0x' + Bytes.from(yBytes).toHex());

// construct the point on the curve using the x and y coordinates
let P = this.from({ x, y });

// ensure that the point is on the curve
P.assertOnCurve();

return P;
}

/**
* Create a new {@link ForeignCurveV2} instance from an Ethereum public key in hex format, which may be either compressed or uncompressed.
* This method is designed to handle the parsing of public keys as used by the ethers.js library.
*
* The input should represent the affine x and y coordinates of the point, in hexadecimal format.
* Compressed keys are 33 bytes long and begin with 0x02 or 0x03, while uncompressed keys are 65 bytes long and begin with 0x04.
*
* **Warning:** This method is specifically designed for use with the Secp256k1 curve. Using it with other curves may result in incorrect behavior or errors.
* Ensure that the curve setup matches Secp256k1, as shown in the example, to avoid unintended issues.
*
* @example
* ```ts
* import { Wallet, Signature, getBytes } from 'ethers';
*
* class Secp256k1 extends createForeignCurveV2(Crypto.CurveParams.Secp256k1) {}
*
* const wallet = Wallet.createRandom();
*
* const publicKey = Secp256k1.fromEthers(wallet.publicKey.slice(2));
* ```
*
* @param hex - The public key as a hexadecimal string (without the "0x" prefix).
* @returns A new instance of the curve representing the given public key.
*/
static fromEthers(hex: string) {
// trim the '0x' prefix if present
if (hex.startsWith('0x')) {
hex = hex.slice(2);
}

const bytes = Bytes.fromHex(hex).toBytes(); // convert hex string to Uint8Array
const len = bytes.length;
const head = bytes[0]; // first byte is the prefix (compression identifier)
const tail = bytes.slice(1); // remaining bytes contain the coordinates

const xBytes = tail.slice(0, 32); // extract the x-coordinate (first 32 bytes)
const x = BigInt('0x' + Bytes.from(xBytes).toHex()); // convert Uint8Array to bigint

let p: { x: bigint; y: bigint } | undefined = undefined;

// handle compressed points (33 bytes, prefix 0x02 or 0x03)
if (len === 33 && [0x02, 0x03].includes(head)) {
// ensure x is within the valid field range
assert(0n < x && x < this.Bigint.Field.modulus);

// compute the right-hand side of the curve equation: x³ + ax + b
const crvX = this.Bigint.Field.mod(
this.Bigint.Field.mod(x * x) * x + this.Bigint.b
);
// compute the square root (y-coordinate)
let y = this.Bigint.Field.sqrt(crvX)!;
const isYOdd = (y & 1n) === 1n; // determine whether y is odd
const headOdd = (head & 1) === 1; // determine whether the prefix indicates an odd y
if (headOdd !== isYOdd) y = this.Bigint.Field.mod(-y); // adjust y if necessary
p = { x, y };
}

// handle uncompressed points (65 bytes, prefix 0x04)
if (len === 65 && head === 0x04) {
const yBytes = tail.slice(32, 64); // extract the y-coordinate (next 32 bytes)
p = { x, y: BigInt('0x' + Bytes.from(yBytes).toHex()) };
}

const P = this.from(p!); // create the curve point from the parsed coordinates
P.assertOnCurve(); // verify the point lies on the curve

return P;
}

/**
* The constant generator point.
*/
Expand Down
54 changes: 53 additions & 1 deletion src/lib/provable/crypto/foreign-ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class EcdsaSignature {

/**
* Create a new {@link EcdsaSignature} from an object containing the scalars r and s.
*
*
* Note: Inputs must be range checked if they originate from a different field with a different modulus or if they are not constants. Please refer to the {@link ForeignField} constructor comments for more details.
*/
constructor(signature: {
Expand Down Expand Up @@ -122,6 +122,58 @@ class EcdsaSignature {
return this.verifySignedHashV2(msgHash, publicKey);
}

/**
* Verify an ECDSA signature generated by the ethers.js library, given the message (as a byte array) and a public key (a {@link Curve} point).
* The message digest used for signing follows the format defined in EIP-191, with the Ethereum-specific prefix.
*
* **Important:** This method returns a {@link Bool} which indicates whether the signature is valid.
* So, to actually prove validity of a signature, you need to assert that the result is true.
*
* **Note:** This method is specifically designed to verify signatures generated by ethers.js.
* Ensure that the curve being used is Secp256k1, as demonstrated in the example.
*
* @throws An error will be thrown if one of the signature scalars is zero or if the public key does not lie on the curve.
*
* @example
* ```ts
* import { Wallet } from 'ethers';
*
* // create the class for Secp256k1 curve
* class Secp256k1 extends createForeignCurveV2(Crypto.CurveParams.Secp256k1) {}
* class Ecdsa extends createEcdsaV2(Secp256k1) {}
*
* // outside provable code: create inputs
* let message = 'my message';
* let signatureRaw = await wallet.signMessage(message);
* let compressedPublicKey = wallet.signingKey.compressedPublicKey;
*
* // this also works for uncompressed public keys (wallet.signingKey.publicKey)
* let publicKey = Secp256k1.fromEthers(compressedPublicKey.slice(2));
* let signature = Ecdsa.fromHex(signatureRaw);
*
* // ...
* // in provable code: create input witnesses (or use method inputs, or constants)
* // and verify the signature
* let isValid = signature.verifyEthers(Bytes.fromString(message), publicKey);
* isValid.assertTrue('signature verifies');
* ```
*
* @param message - The original message as a byte array.
* @param publicKey - The public key as a point on the Secp256k1 elliptic curve.
* @returns - A {@link Bool} indicating the validity of the signature.
*/
verifyEthers(message: Bytes, publicKey: FlexiblePoint): Bool {
const MessagePrefix = '\x19Ethereum Signed Message:\n'; // Ethereum-specific prefix for signing
const msgHashBytes = Keccak.ethereum([
...Bytes.fromString(MessagePrefix).bytes, // prefix for Ethereum signed messages
...Bytes.fromString(String(message.length)).bytes, // message length as string
...message.bytes, // actual message bytes
]);

let msgHash = keccakOutputToScalar(msgHashBytes, this.Constructor.Curve);
return this.verifySignedHashV2(msgHash, publicKey);
}

/**
* @deprecated There is a security vulnerability in this method. Use {@link verifySignedHashV2} instead.
*/
Expand Down
Loading

0 comments on commit 6ad5405

Please sign in to comment.