Skip to content

Commit 236ca19

Browse files
committed
fix: consider low-s rule application when defining combined ecdsa signature v value
1 parent a76688b commit 236ca19

File tree

4 files changed

+29
-17
lines changed

4 files changed

+29
-17
lines changed

packages/crypto/src/lib/crypto.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,20 @@ export const combineEcdsaShares = async (
202202
Buffer.from(share.signatureShare, 'hex')
203203
);
204204

205-
const [r, s, v] = await ecdsaCombine(variant!, presignature, signatureShares);
205+
const [r, s, recId] = await ecdsaCombine(
206+
variant!,
207+
presignature,
208+
signatureShares
209+
);
206210

207211
const publicKey = Buffer.from(anyValidShare.publicKey, 'hex');
208212
const messageHash = Buffer.from(anyValidShare.dataSigned!, 'hex');
209213

210-
await ecdsaVerify(variant!, messageHash, publicKey, [r, s, v]);
214+
await ecdsaVerify(variant!, messageHash, publicKey, [r, s, recId]);
211215

212-
const signature = splitSignature(Buffer.concat([r, s, Buffer.from([v])]));
216+
const signature = splitSignature(
217+
Buffer.concat([r, s, Buffer.from([recId + 27])])
218+
);
213219

214220
return {
215221
r: signature.r.slice('0x'.length),

packages/lit-node-client-nodejs/src/lib/helpers/get-signatures.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ export const getSignatures = async <T>(params: {
259259
const encodedSig = joinSignature({
260260
r: '0x' + signature.r,
261261
s: '0x' + signature.s,
262-
v: signature.recid,
262+
recoveryParam: signature.recid,
263263
});
264264

265265
signatures[key] = {

packages/types/src/lib/interfaces.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export interface AuthSig {
4242
/**
4343
* The signature produced by signing the `signMessage` property with the corresponding private key for the `address` property.
4444
*/
45-
sig: any;
45+
sig: string;
4646

4747
/**
4848
* The method used to derive the signature (e.g, `web3.eth.personal.sign`).
@@ -598,7 +598,7 @@ export interface SigResponse {
598598
r: string;
599599
s: string;
600600
recid: number;
601-
signature: string; // 0x...
601+
signature: `0x${string}`;
602602
publicKey: string; // pkp public key (no 0x prefix)
603603
dataSigned: string;
604604
}

packages/wasm/rust/src/ecdsa.rs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,22 @@ where
5858
presignature: Uint8Array,
5959
signature_shares: Vec<Uint8Array>,
6060
) -> JsResult<EcdsaSignature> {
61-
let (big_r, s) = Self::combine_inner(presignature, signature_shares)?;
62-
Self::signature_into_js(big_r.to_affine(), s)
61+
let (big_r, s, was_flipped) = Self::combine_inner(presignature, signature_shares)?;
62+
Self::signature_into_js(big_r.to_affine(), s, was_flipped)
6363
}
6464

6565
pub(crate) fn combine_inner(
6666
presignature: Uint8Array,
6767
signature_shares: Vec<Uint8Array>,
68-
) -> JsResult<(C::ProjectivePoint, C::Scalar)> {
68+
) -> JsResult<(C::ProjectivePoint, C::Scalar, bool)> {
6969
let signature_shares = signature_shares
7070
.into_iter()
7171
.map(Self::scalar_from_js)
7272
.collect::<JsResult<Vec<_>>>()?;
7373

7474
let big_r: C::AffinePoint = Self::point_from_js(presignature)?;
75-
let s = Self::sum_scalars(signature_shares)?;
76-
Ok((C::ProjectivePoint::from(big_r), s))
75+
let (s, was_flipped) = Self::sum_scalars(signature_shares)?;
76+
Ok((C::ProjectivePoint::from(big_r), s, was_flipped))
7777
}
7878

7979
pub fn verify(
@@ -108,13 +108,14 @@ where
108108
Ok(())
109109
}
110110

111-
fn sum_scalars(values: Vec<C::Scalar>) -> JsResult<C::Scalar> {
111+
fn sum_scalars(values: Vec<C::Scalar>) -> JsResult<(C::Scalar, bool)> {
112112
if values.is_empty() {
113113
return Err(JsError::new("no shares provided"));
114114
}
115115
let mut acc: C::Scalar = values.into_iter().sum();
116+
let acc_flipped = acc.is_high().into();
116117
acc.conditional_assign(&(-acc), acc.is_high());
117-
Ok(acc)
118+
Ok((acc, acc_flipped))
118119
}
119120

120121
pub fn derive_key(id: Uint8Array, public_keys: Vec<Uint8Array>) -> JsResult<Uint8Array> {
@@ -168,10 +169,15 @@ where
168169
Ok((r, s, v))
169170
}
170171

171-
fn signature_into_js(big_r: C::AffinePoint, s: C::Scalar) -> JsResult<EcdsaSignature> {
172+
fn signature_into_js(big_r: C::AffinePoint, s: C::Scalar, was_flipped: bool) -> JsResult<EcdsaSignature> {
172173
let r = Self::x_coordinate(&big_r).to_repr();
173174
let s = s.to_repr();
174-
let v = u8::conditional_select(&0, &1, big_r.y_is_odd());
175+
let mut v = u8::conditional_select(&0, &1, big_r.y_is_odd());
176+
177+
// Flip v if s was normalized (flipped, low-s rule)
178+
if was_flipped {
179+
v = 1 - v;
180+
}
175181

176182
Ok(EcdsaSignature {
177183
obj: into_js(&(Bytes::new(&r), Bytes::new(&s), v))?,
@@ -222,7 +228,7 @@ where
222228
public_key: C::ProjectivePoint,
223229
) -> JsResult<EcdsaSignature> {
224230
let z = Self::scalar_from_hash(message_hash)?;
225-
let (big_r, s) = Self::combine_inner(pre_signature, signature_shares)?;
231+
let (big_r, s, was_flipped) = Self::combine_inner(pre_signature, signature_shares)?;
226232
let r = Self::x_coordinate(&big_r.to_affine());
227233

228234
if z.is_zero().into() {
@@ -241,7 +247,7 @@ where
241247
.is_identity()
242248
.into()
243249
{
244-
Self::signature_into_js(big_r.to_affine(), s)
250+
Self::signature_into_js(big_r.to_affine(), s, was_flipped)
245251
} else {
246252
Err(JsError::new("invalid signature"))
247253
}

0 commit comments

Comments
 (0)