Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert ECDSA hash/packing logic to previous iteration #1669

Merged
merged 8 commits into from
Jun 4, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- This can pose an attack surface, since it is easy to maliciously pick either the `+0` or the `-0` representation
- Use `Int64.isPositiveV2()` and `Int64.modV2()` instead
- Also deprecated `Int64.neg()` in favor of `Int64.negV2()`, for compatibility with v2 version of `Int64` that will use `Int64.checkV2()`
- `Ecdsa.verify()` and `Ecdsa.verifySignedHash()` deprecated in favor of `Ecdsa.verifyV2()` and `Ecdsa.verifySignedHashV2()` due to a security vulnerability found in the current implementation https://github.com/o1-labs/o1js/pull/1669

## [1.3.0](https://github.com/o1-labs/o1js/compare/6a1012162...54d6545bf)

### Added

- Added `base64Encode()` and `base64Decode(byteLength)` methods to the `Bytes` class. https://github.com/o1-labs/o1js/pull/1659
- Added `Ecdsa.verifyV2()` and `Ecdsa.verifySignedHashV2` methods to the `Ecdsa` class. https://github.com/o1-labs/o1js/pull/1669

### Fixes

Expand Down
4 changes: 2 additions & 2 deletions src/examples/crypto/ecdsa/ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const keccakAndEcdsa = ZkProgram({
verifyEcdsa: {
privateInputs: [Ecdsa.provable, Secp256k1.provable],
async method(message: Bytes32, signature: Ecdsa, publicKey: Secp256k1) {
return signature.verify(message, publicKey);
return signature.verifyV2(message, publicKey);
},
},
},
Expand All @@ -38,7 +38,7 @@ const ecdsa = ZkProgram({
verifySignedHash: {
privateInputs: [Ecdsa.provable, Secp256k1.provable],
async method(message: Scalar, signature: Ecdsa, publicKey: Secp256k1) {
return signature.verifySignedHash(message, publicKey);
return signature.verifySignedHashV2(message, publicKey);
},
},
},
Expand Down
63 changes: 63 additions & 0 deletions src/lib/provable/crypto/foreign-ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,61 @@ class EcdsaSignature {
* let isValid = sig.verify(msg, pk);
* isValid.assertTrue('signature verifies');
* ```
* @deprecated There is a security vulnerability in this method. Use {@link verifyV2} instead.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you completely remove any other doccomments here except the deprecation notice? we don't want to encourage people to use it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*/
verify(message: Bytes, publicKey: FlexiblePoint) {
let msgHashBytes = Keccak.ethereum(message);
let msgHash = keccakOutputToScalar(msgHashBytes, this.Constructor.Curve);
return this.verifySignedHash(msgHash, publicKey);
}

/**
* Verify the ECDSA signature given the message (an array of bytes) and public key (a {@link Curve} point).
*
* **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.
*
* @throws if one of the signature scalars is zero or if the public key is not on the curve.
*
* @example
* ```ts
* // create classes for your curve
* class Secp256k1 extends createForeignCurve(Crypto.CurveParams.Secp256k1) {}
* class Scalar extends Secp256k1.Scalar {}
* class Ecdsa extends createEcdsa(Secp256k1) {}
*
* let message = 'my message';
* let messageBytes = new TextEncoder().encode(message);
*
* // outside provable code: create inputs
* let privateKey = Scalar.random();
* let publicKey = Secp256k1.generator.scale(privateKey);
* let signature = Ecdsa.sign(messageBytes, privateKey.toBigInt());
*
* // ...
* // in provable code: create input witnesses (or use method inputs, or constants)
* let pk = Provable.witness(Secp256k1.provable, () => publicKey);
* let msg = Provable.witness(Provable.Array(Field, 9), () => messageBytes.map(Field));
* let sig = Provable.witness(Ecdsa.provable, () => signature);
*
* // verify signature
* let isValid = sig.verify(msg, pk);
* isValid.assertTrue('signature verifies');
* ```
*/
verifyV2(message: Bytes, publicKey: FlexiblePoint) {
let msgHashBytes = Keccak.ethereum(message);
let msgHash = keccakOutputToScalar(msgHashBytes, this.Constructor.Curve);
return this.verifySignedHashV2(msgHash, publicKey);
}

/**
* Verify the ECDSA signature given the message hash (a {@link Scalar}) and public key (a {@link Curve} point).
*
* This is a building block of {@link EcdsaSignature.verify}, where the input message is also hashed.
* In contrast, this method just takes the message hash (a curve scalar) as input, giving you flexibility in
* choosing the hashing algorithm.
* @deprecated There is a security vulnerability in this method. Use {@link verifySignedHashV2} instead.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you completely remove any other doccomments here except the deprecation notice? we don't want to encourage people to use it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*/
verifySignedHash(
msgHash: AlmostForeignField | bigint,
Expand All @@ -127,6 +169,27 @@ class EcdsaSignature {
);
}

/**
* Verify the ECDSA signature given the message hash (a {@link Scalar}) and public key (a {@link Curve} point).
*
* This is a building block of {@link EcdsaSignature.verify}, where the input message is also hashed.
* In contrast, this method just takes the message hash (a curve scalar) as input, giving you flexibility in
* choosing the hashing algorithm.
*/
verifySignedHashV2(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was it necessary to add a new verifySignedHashV2 function here? It seems so since the deprecated verify was being used here, but not sure if it's necessary to reflect everywhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah thanks for adding this as well!

msgHash: AlmostForeignField | bigint,
publicKey: FlexiblePoint
) {
let msgHash_ = this.Constructor.Curve.Scalar.from(msgHash);
let publicKey_ = this.Constructor.Curve.from(publicKey);
return Ecdsa.verifyV2(
this.Constructor.Curve.Bigint,
toObject(this),
msgHash_.value,
toPoint(publicKey_)
);
}

/**
* Create an {@link EcdsaSignature} by signing a message with a private key.
*
Expand Down
188 changes: 144 additions & 44 deletions src/lib/provable/gadgets/elliptic-curve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,33 +212,32 @@ function equals(p1: Point, p2: point, Curve: { modulus: bigint }) {
return xEquals.and(yEquals);
}

/**
* Verify an ECDSA signature.
*
* Details about the `config` parameter:
* - For both the generator point `G` and public key `P`, `config` allows you to specify:
* - the `windowSize` which is used in scalar multiplication for this point.
* this flexibility is good because the optimal window size is different for constant and non-constant points.
* empirically, `windowSize=4` for constants and 3 for variables leads to the fewest constraints.
* our defaults reflect that the generator is always constant and the public key is variable in typical applications.
* - a table of multiples of those points, of length `2^windowSize`, which is used in the scalar multiplication gadget to speed up the computation.
* if these are not provided, they are computed on the fly.
* for the constant G, computing multiples costs no constraints, so passing them in makes no real difference.
* for variable public key, there is a possible use case: if the public key is a public input, then its multiples could also be.
* in that case, passing them in would avoid computing them in-circuit and save a few constraints.
* - The initial aggregator `ia`, see {@link initialAggregator}. By default, `ia` is computed deterministically on the fly.
*/
function verifyEcdsa(
function verifyEcdsaGeneric(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored some of the duplicated logic between both verify functions and specified behaviour with a hashed parameter, which guards the code that is responsible for the vulnerability

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

Curve: CurveAffine,
signature: Ecdsa.Signature,
msgHash: Field3,
publicKey: Point,
multiScalarMul: (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds a parameter to specify the version of multiScalarMul we want (passing in hashed as true or false)

scalars: Field3[],
points: Point[],
Curve: CurveAffine,
tableConfigs?: (
| {
windowSize?: number;
multiples?: Point[];
}
| undefined
)[],
mode?: 'assert-nonzero' | 'assert-zero',
ia?: point,
hashed?: boolean
) => Point,
config: {
G?: { windowSize: number; multiples?: Point[] };
P?: { windowSize: number; multiples?: Point[] };
ia?: point;
} = { G: { windowSize: 4 }, P: { windowSize: 4 } }
) {
): Bool {
// constant case
if (
EcdsaSignature.isConstant(signature) &&
Expand Down Expand Up @@ -285,6 +284,83 @@ function verifyEcdsa(
return Provable.equal(Field3.provable, Rx, r);
}

/**
* Verify an ECDSA signature.
*
* Details about the `config` parameter:
* - For both the generator point `G` and public key `P`, `config` allows you to specify:
* - the `windowSize` which is used in scalar multiplication for this point.
* this flexibility is good because the optimal window size is different for constant and non-constant points.
* empirically, `windowSize=4` for constants and 3 for variables leads to the fewest constraints.
* our defaults reflect that the generator is always constant and the public key is variable in typical applications.
* - a table of multiples of those points, of length `2^windowSize`, which is used in the scalar multiplication gadget to speed up the computation.
* if these are not provided, they are computed on the fly.
* for the constant G, computing multiples costs no constraints, so passing them in makes no real difference.
* for variable public key, there is a possible use case: if the public key is a public input, then its multiples could also be.
* in that case, passing them in would avoid computing them in-circuit and save a few constraints.
* - The initial aggregator `ia`, see {@link initialAggregator}. By default, `ia` is computed deterministically on the fly.
* @deprecated There is a security vulnerability with this function, allowing a prover to modify witness values than what is expected. Use {@link verifyEcdsaV2} instead.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you completely remove any other doccomments here except the deprecation notice? we don't want to encourage people to use it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*/
function verifyEcdsa(
Curve: CurveAffine,
signature: Ecdsa.Signature,
msgHash: Field3,
publicKey: Point,
config: {
G?: { windowSize: number; multiples?: Point[] };
P?: { windowSize: number; multiples?: Point[] };
ia?: point;
} = { G: { windowSize: 4 }, P: { windowSize: 4 } }
) {
return verifyEcdsaGeneric(
Curve,
signature,
msgHash,
publicKey,
(scalars, points, Curve, configs, mode, ia) =>
multiScalarMul(scalars, points, Curve, configs, mode, ia, true),
config
);
}

/**
* Verify an ECDSA signature.
*
* Details about the `config` parameter:
* - For both the generator point `G` and public key `P`, `config` allows you to specify:
* - the `windowSize` which is used in scalar multiplication for this point.
* this flexibility is good because the optimal window size is different for constant and non-constant points.
* empirically, `windowSize=4` for constants and 3 for variables leads to the fewest constraints.
* our defaults reflect that the generator is always constant and the public key is variable in typical applications.
* - a table of multiples of those points, of length `2^windowSize`, which is used in the scalar multiplication gadget to speed up the computation.
* if these are not provided, they are computed on the fly.
* for the constant G, computing multiples costs no constraints, so passing them in makes no real difference.
* for variable public key, there is a possible use case: if the public key is a public input, then its multiples could also be.
* in that case, passing them in would avoid computing them in-circuit and save a few constraints.
* - The initial aggregator `ia`, see {@link initialAggregator}. By default, `ia` is computed deterministically on the fly.
*/
function verifyEcdsaV2(
Curve: CurveAffine,
signature: Ecdsa.Signature,
msgHash: Field3,
publicKey: Point,
config: {
G?: { windowSize: number; multiples?: Point[] };
P?: { windowSize: number; multiples?: Point[] };
ia?: point;
} = { G: { windowSize: 4 }, P: { windowSize: 4 } }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you try changing the default back to this?

Suggested change
} = { G: { windowSize: 4 }, P: { windowSize: 4 } }
} = { G: { windowSize: 4 }, P: { windowSize: 3 } }

it should use fewer constraints

Copy link
Contributor Author

@MartinMinkov MartinMinkov Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed here!

Here is the lower constraints

) {
return verifyEcdsaGeneric(
Curve,
signature,
msgHash,
publicKey,
(scalars, points, Curve, configs, mode, ia) =>
multiScalarMul(scalars, points, Curve, configs, mode, ia, false),
config
);
}

/**
* Bigint implementation of ECDSA verify
*/
Expand All @@ -311,6 +387,36 @@ function verifyEcdsaConstant(
return Curve.Scalar.equal(R.x, r);
}

function multiScalarMulConstant(
scalars: Field3[],
points: Point[],
Curve: CurveAffine,
mode: 'assert-nonzero' | 'assert-zero' = 'assert-nonzero'
): Point {
let n = points.length;
assert(scalars.length === n, 'Points and scalars lengths must match');
assertPositiveInteger(n, 'Expected at least 1 point and scalar');
let useGlv = Curve.hasEndomorphism;

// TODO dedicated MSM
let s = scalars.map(Field3.toBigint);
let P = points.map(Point.toBigint);
let sum = Curve.zero;
for (let i = 0; i < n; i++) {
if (useGlv) {
sum = Curve.add(sum, Curve.Endo.scale(P[i], s[i]));
} else {
sum = Curve.add(sum, Curve.scale(P[i], s[i]));
}
}
if (mode === 'assert-zero') {
assert(sum.infinity, 'scalar multiplication: expected zero result');
return Point.from(Curve.zero);
}
assert(!sum.infinity, 'scalar multiplication: expected non-zero result');
return Point.from(sum);
}

/**
* Multi-scalar multiplication:
*
Expand Down Expand Up @@ -339,7 +445,8 @@ function multiScalarMul(
| undefined
)[] = [],
mode: 'assert-nonzero' | 'assert-zero' = 'assert-nonzero',
ia?: point
ia?: point,
hashed: boolean = true
): Point {
let n = points.length;
assert(scalars.length === n, 'Points and scalars lengths must match');
Expand All @@ -348,23 +455,7 @@ function multiScalarMul(

// constant case
if (scalars.every(Field3.isConstant) && points.every(Point.isConstant)) {
// TODO dedicated MSM
let s = scalars.map(Field3.toBigint);
let P = points.map(Point.toBigint);
let sum = Curve.zero;
for (let i = 0; i < n; i++) {
if (useGlv) {
sum = Curve.add(sum, Curve.Endo.scale(P[i], s[i]));
} else {
sum = Curve.add(sum, Curve.scale(P[i], s[i]));
}
}
if (mode === 'assert-zero') {
assert(sum.infinity, 'scalar multiplication: expected zero result');
return Point.from(Curve.zero);
}
assert(!sum.infinity, 'scalar multiplication: expected non-zero result');
return Point.from(sum);
return multiScalarMulConstant(scalars, points, Curve, mode);
}

// parse or build point tables
Expand Down Expand Up @@ -444,14 +535,22 @@ function multiScalarMul(
if (i % windowSize === 0) {
// pick point to add based on the scalar chunk
let sj = scalarChunks[j][i / windowSize];
let sjP =
windowSize === 1
? points[j]
: arrayGetGeneric(
HashedPoint.provable,
hashedTables[j],
sj
).unhash();
let sjP;
if (hashed) {
sjP =
windowSize === 1
? points[j]
: arrayGetGeneric(
HashedPoint.provable,
hashedTables[j],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it costs constraints to create the hashedTables (on line 521), which we only need in the deprecated case.

could you also add a check there and make it an empty array when hashed = false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sj
).unhash();
} else {
sjP =
windowSize === 1
? points[j]
: arrayGetGeneric(Point.provable, tables[j], sj);
}

// ec addition
let added = add(sum, sjP, Curve);
Expand Down Expand Up @@ -725,6 +824,7 @@ const EcdsaSignature = {
const Ecdsa = {
sign: signEcdsa,
verify: verifyEcdsa,
verifyV2: verifyEcdsaV2,
Signature: EcdsaSignature,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ let program = ZkProgram({
let msgHash_ = Provable.witness(Field3.provable, () => msgHash);
let publicKey_ = Provable.witness(Point.provable, () => publicKey);

return Ecdsa.verify(Secp256k1, signature_, msgHash_, publicKey_);
return Ecdsa.verifyV2(Secp256k1, signature_, msgHash_, publicKey_);
},
},
},
Expand Down
Loading
Loading