Skip to content

Commit

Permalink
feat: add starknet signature verification (#442)
Browse files Browse the repository at this point in the history
* feat: add starknet signature verification

* starknet(signature): add comment for custom implementation of BigInt.modPow

* starknet(signature): remove comment about checking public key
  • Loading branch information
ptisserand authored Jan 27, 2025
1 parent 37d0192 commit 8a9b006
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 10 deletions.
26 changes: 17 additions & 9 deletions packages/starknet/lib/src/crypto/pedersen.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import 'package:pointycastle/ecc/api.dart';
import 'package:pointycastle/ecc/ecc_base.dart';
import 'package:pointycastle/ecc/ecc_fp.dart' as fp;
import 'package:starknet/starknet.dart';

import 'model/pedersen_params.dart';

final starknetCurve = fp.ECCurve(
pedersenParams.fieldPrime, pedersenParams.alpha, pedersenParams.beta);
Expand All @@ -13,14 +14,21 @@ final starknetSignatureECDomainParams = ECDomainParametersImpl(
'starknetSignature', starknetCurve, generatorPoint, pedersenParams.ecOrder);

ECPoint getPoint(List<BigInt> constantPoint) => fp.ECPoint(
starknetCurve,
fp.ECFieldElement(pedersenParams.fieldPrime, constantPoint[0]),
fp.ECFieldElement(pedersenParams.fieldPrime, constantPoint[1]));
starknetCurve,
fp.ECFieldElement(pedersenParams.fieldPrime, constantPoint[0]),
fp.ECFieldElement(pedersenParams.fieldPrime, constantPoint[1]),
);

final lowPartBits = 248;
final nElementBitsHash = 252;
const lowPartBits = 248;
const nElementBitsHash = 252;
final lowPartMask = BigInt.two.pow(lowPartBits) - BigInt.one;
final shiftPoint = getPoint(pedersenParams.constantPoints[0]);
final minusShiftPoint = fp.ECPoint(
starknetCurve,
fp.ECFieldElement(pedersenParams.fieldPrime, shiftPoint.x!.toBigInteger()),
fp.ECFieldElement(pedersenParams.fieldPrime, -shiftPoint.y!.toBigInteger()!),
);

final generatorPoint = getPoint(pedersenParams.constantPoints[1]);
final p0 = getPoint(pedersenParams.constantPoints[2]);
final p1 = getPoint(pedersenParams.constantPoints[2 + lowPartBits]);
Expand All @@ -29,7 +37,7 @@ final p3 =
getPoint(pedersenParams.constantPoints[2 + nElementBitsHash + lowPartBits]);

ECPoint processSingleElement(BigInt x, ECPoint p1, ECPoint p2) {
assert(x < pedersenParams.fieldPrime);
assert(x < pedersenParams.fieldPrime, 'Invalid value for x');
final highNibble = x >> lowPartBits;
final lowPart = x & lowPartMask;
final result = (p1 * lowPart)! + (p2 * highNibble)!;
Expand All @@ -40,8 +48,8 @@ ECPoint processSingleElement(BigInt x, ECPoint p1, ECPoint p2) {
}

BigInt pedersenHash(BigInt x, BigInt y) {
final hashPoint = ((shiftPoint + processSingleElement(x, p0, p1))! +
processSingleElement(y, p2, p3));
final hashPoint = (shiftPoint + processSingleElement(x, p0, p1))! +
processSingleElement(y, p2, p3);
if (hashPoint == null) {
throw TypeError();
}
Expand Down
158 changes: 157 additions & 1 deletion packages/starknet/lib/src/crypto/signature.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../starknet.dart';
import 'package:pointycastle/ecc/api.dart';

import '../convert.dart';
import 'model/pedersen_params.dart';
import 'pedersen.dart';

const nbFieldPrimeBits = 251;
final maxHash = BigInt.two.pow(nbFieldPrimeBits);
Expand Down Expand Up @@ -202,3 +206,155 @@ List<int> numberToString(BigInt v, BigInt order) {
int orderlen(BigInt order) {
return (order.bitLength + 7) ~/ 8;
}

/// Verifies a message hash signature with the given public key according to Starknet specs.
///
/// Spec: https://github.com/starkware-libs/cairo-lang/blob/13cef109cd811474de114925ee61fd5ac84a25eb/src/starkware/crypto/starkware/crypto/signature/signature.py#L191
bool starknetVerify({
required BigInt messageHash,
required Signature signature,
required BigInt publicKey,
}) {
try {
// convert public key to ECPoint
final y = _getYCoordinate(publicKey);
final pub1 = starknetCurve.createPoint(publicKey, y);
final pub2 = starknetCurve.createPoint(publicKey, -y);
return _starknetVerify(
messageHash: messageHash,
signature: signature,
publicKey: pub1,
) ||
_starknetVerify(
messageHash: messageHash,
signature: signature,
publicKey: pub2,
);
} catch (e) {
return false;
}
}

bool _starknetVerify({
required BigInt messageHash,
required Signature signature,
required ECPoint publicKey,
}) {
final r = signature.r;
final s = signature.s;
assert(s >= BigInt.one && s < pedersenParams.ecOrder, 'Invalid s value');
final w = s.modInverse(pedersenParams.ecOrder);

assert(r >= BigInt.one && r < maxHash, 'Invalid r value');
assert(w >= BigInt.one && w < maxHash, 'Invalid w value');
assert(
messageHash >= BigInt.zero && messageHash < maxHash,
'Invalid message hash value',
);
try {
var zG = _mimicECMultAIR(messageHash, generatorPoint, minusShiftPoint);
var rQ = _mimicECMultAIR(r, publicKey, shiftPoint);
var wB = _mimicECMultAIR(w, (zG + rQ)!, shiftPoint);
var x = (wB + minusShiftPoint)!.x;
return r == x!.toBigInteger();
} catch (e) {
return false;
}
}

ECPoint _mimicECMultAIR(BigInt m, ECPoint point, ECPoint shift) {
assert(m > BigInt.zero && m < maxHash, 'Invalid value for m');
var partialSum = shift;
var _point = point;
var _m = m;
for (int _ = 0; _ < nbFieldPrimeBits; _++) {
assert(partialSum.x != _point.x, 'Invalid point');
if (_m & BigInt.one != BigInt.zero) {
partialSum = (partialSum + _point)!;
}
_point = _point.twice()!;
_m >>= 1;
}
assert(_m == BigInt.zero, 'm should be zero at the end');
return partialSum;
}

BigInt _getYCoordinate(BigInt x) {
final ySquared =
(x * x * x + pedersenParams.alpha * x + pedersenParams.beta) %
pedersenParams.fieldPrime;
final y = _tonelliShanks(ySquared, pedersenParams.fieldPrime);
final negY = -y % pedersenParams.fieldPrime;
return negY < y ? negY : y;
}

/// Power modulo function using BigInt
///
/// We are using a custom implentation because with BigInt.modPow,
/// the Tonelli-Shanks algorithm enters an inifinite loop.
BigInt _modPow(BigInt x, BigInt n, BigInt p) {
if (n == BigInt.zero) return BigInt.one;
if (n.isOdd) {
return (_modPow(x, n - BigInt.one, p) * x) % p;
}
final temp = _modPow(x, n ~/ BigInt.two, p);
return (temp * temp) % p;
}

/// Tonelli-Shanks algorithm to find the square root of a modulo p
///
/// Takes as input an odd prime p and n < p and returns r
/// such that r * r = n [mod p].
/// https://en.wikipedia.org/wiki/Tonelli%E2%80%93Shanks_algorithm
BigInt _tonelliShanks(BigInt n, BigInt p) {
// Initialize variables as BigInt
var s = BigInt.zero;
var q = p - BigInt.one;

// Calculate s and q
while (q.isEven) {
q = q ~/ BigInt.two;
s += BigInt.one;
}

// Special case for s = 1
if (s == BigInt.one) {
var r = _modPow(n, (p + BigInt.one) ~/ BigInt.from(4), p);
if ((r * r) % p == n) return r;
return BigInt.zero;
}

// Find the first quadratic non-residue z
var z = BigInt.one;
while (true) {
z += BigInt.one;
if (_modPow(z, (p - BigInt.one) ~/ BigInt.two, p) == p - BigInt.one) break;
}

var c = _modPow(z, q, p);
var r = _modPow(n, (q + BigInt.one) ~/ BigInt.two, p);
var t = _modPow(n, q, p);
var m = s;

while (t != BigInt.one) {
var tt = t;
var i = BigInt.zero;

while (tt != BigInt.one) {
tt = (tt * tt) % p;
i += BigInt.one;
if (i == m) return BigInt.zero;
}

final b =
_modPow(c, _modPow(BigInt.two, m - i - BigInt.one, p - BigInt.one), p);
final b2 = (b * b) % p;
r = (r * b) % p;
t = (t * b2) % p;
c = b2;
m = i;
}

if ((r * r) % p == n) return r;
throw Exception('No square root found');
}
69 changes: 69 additions & 0 deletions packages/starknet/test/crypto/signature_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,73 @@ void main() {
},
tags: ['unit'],
);
group('starknet_verify', () {
test('verifies a valid signature', () {
// https://github.com/xJonathanLEI/starknet-rs/blob/5f6e173a5196e5aa6c33a35f01aa1ea1e783f4a7/starknet-crypto/src/ecdsa.rs#L314
final messageHash = hexStringToBigInt(
'0x0000000000000000000000000000000000000000000000000000000000000002',
);
final r = hexStringToBigInt(
'0x0411494b501a98abd8262b0da1351e17899a0c4ef23dd2f96fec5ba847310b20',
);
final s = hexStringToBigInt(
'0x0405c3191ab3883ef2b763af35bc5f5d15b3b4e99461d70e84c654a351a7c81b',
);
final signature = Signature(r, s);
final publicKey = hexStringToBigInt(
'0x1ef15c18599971b7beced415a40f0c7deacfd9b0d1819e03d723d8bc943cfca',
);
final verified = starknetVerify(
messageHash: messageHash,
signature: signature,
publicKey: publicKey,
);
expect(verified, isTrue);
});

test('verifies an invalid message', () {
// https://github.com/xJonathanLEI/starknet-rs/blob/5f6e173a5196e5aa6c33a35f01aa1ea1e783f4a7/starknet-crypto/src/ecdsa.rs#L333
final messageHash = hexStringToBigInt(
'0x0397e76d1667c4454bfb83514e120583af836f8e32a516765497823eabe16a3f',
);
final r = hexStringToBigInt(
'0x0173fd03d8b008ee7432977ac27d1e9d1a1f6c98b1a2f05fa84a21c84c44e882',
);
final s = hexStringToBigInt(
'0x01f2c44a7798f55192f153b4c48ea5c1241fbb69e6132cc8a0da9c5b62a4286e',
);
final signature = Signature(r, s);
final publicKey = hexStringToBigInt(
'0x077a4b314db07c45076d11f62b6f9e748a39790441823307743cf00d6597ea43',
);
final verified = starknetVerify(
messageHash: messageHash,
signature: signature,
publicKey: publicKey,
);
expect(verified, isFalse);
});
test('verifies an invalid public key', () {
// https://github.com/xJonathanLEI/starknet-rs/blob/5f6e173a5196e5aa6c33a35f01aa1ea1e783f4a7/starknet-crypto/src/ecdsa.rs#L333
final messageHash = hexStringToBigInt(
'0x0000000000000000000000000000000000000000000000000000000000000002',
);
final r = hexStringToBigInt(
'0x0411494b501a98abd8262b0da1351e17899a0c4ef23dd2f96fec5ba847310b20',
);
final s = hexStringToBigInt(
'0x0405c3191ab3883ef2b763af35bc5f5d15b3b4e99461d70e84c654a351a7c81b',
);
final signature = Signature(r, s);
final publicKey = hexStringToBigInt(
'0x03ee9bffffffffff26ffffffff60ffffffffffffffffffffffffffff004accff',
);
final verified = starknetVerify(
messageHash: messageHash,
signature: signature,
publicKey: publicKey,
);
expect(verified, isFalse);
});
});
}

0 comments on commit 8a9b006

Please sign in to comment.