Skip to content

Commit 5d4ab05

Browse files
committed
feat: use rust unified combiner to support frost and multiple ecdsa signing schemes signature aggregation
1 parent 80b49b2 commit 5d4ab05

File tree

28 files changed

+1551
-444
lines changed

28 files changed

+1551
-444
lines changed

local-tests/build.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const createBuildConfig = (entry, outfile, globalName) => ({
3535
format: 'esm',
3636
inject: [getPath('./shim.mjs')],
3737
mainFields: ['module', 'main'],
38+
sourcemap: true,
3839
...(globalName ? { globalName } : {}),
3940
});
4041

local-tests/setup/tinny-person.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@ export class TinnyPerson {
2727
public authMethodOwnedPkp: PKPInfo;
2828

2929
// Pass this to data to sign
30-
public loveLetter: Uint8Array = ethers.utils.arrayify(
31-
ethers.utils.keccak256([1, 2, 3, 4, 5])
32-
);
30+
public loveLetter: Uint8Array = new Uint8Array([1, 2, 3, 4, 5]);
3331

3432
public provider: ethers.providers.StaticJsonRpcProvider;
3533

@@ -156,9 +154,9 @@ export class TinnyPerson {
156154
this.pkp = walletMintRes.pkp;
157155

158156
/**
159-
* ====================================
160-
* Mint a PKP wiuth eth wallet auth method
161-
* ====================================
157+
* ======================================
158+
* Mint a PKP with eth wallet auth method
159+
* ======================================
162160
*/
163161
console.log(
164162
'[𐬺🧪 Tinny Person𐬺] Minting a PKP with eth wallet auth method...'
Lines changed: 195 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,215 @@
1-
import { ethers } from 'ethers';
1+
import { hexToBytes } from '@noble/hashes/utils';
22

3+
import { UnknownSignatureError } from '@lit-protocol/constants';
4+
import {
5+
curveFunctions,
6+
hashLitMessage,
7+
verifyLitSignature,
8+
} from '@lit-protocol/crypto';
39
import { log } from '@lit-protocol/misc';
10+
import { SigningScheme } from '@lit-protocol/types';
11+
412
import { getEoaSessionSigs } from 'local-tests/setup/session-sigs/get-eoa-session-sigs';
513
import { TinnyEnvironment } from 'local-tests/setup/tinny-environment';
614

15+
interface SigningSchemeConfig {
16+
hasRecoveryId?: boolean;
17+
hashesMessage: boolean;
18+
recoversPublicKey?: boolean;
19+
signingScheme: SigningScheme;
20+
}
21+
722
/**
823
* Test Commands:
9-
* ✅ NETWORK=datil-dev yarn test:local --filter=testUseEoaSessionSigsToPkpSign
10-
* ✅ NETWORK=datil-test yarn test:local --filter=testUseEoaSessionSigsToPkpSign
24+
* ✅ NETWORK=naga-dev yarn test:local --filter=testUseEoaSessionSigsToPkpSign
25+
* ✅ NETWORK=naga-test yarn test:local --filter=testUseEoaSessionSigsToPkpSign
1126
* ✅ NETWORK=custom yarn test:local --filter=testUseEoaSessionSigsToPkpSign
1227
*/
1328
export const testUseEoaSessionSigsToPkpSign = async (
1429
devEnv: TinnyEnvironment
1530
) => {
1631
const alice = await devEnv.createRandomPerson();
32+
const signingSchemeConfigs: SigningSchemeConfig[] = [
33+
// BLS
34+
// {
35+
// signingScheme: 'Bls12381', // TODO NodeErrror: Unsupported key type when for Signable. No esta en tss_state.rs::TssState::get_signing_state, puede que no sea posible firmar con esto?
36+
// hashesMessage: false,
37+
// },
38+
// {
39+
// signingScheme: 'Bls12381G1ProofOfPossession', // TODO pkpSignature.signature: '{ProofOfPossession:984ffb9ef7a0e6225dd074bade4b9494fab3487ff543f25a90d86f794cbf190ed20179df6eb6dd3eb9a285838d3cf4980e5e7028688e0461bd1cb95c075046fcafa343d3702e7edff70fb8eb8ada130f58fa45140ab2d90f24b1309b026d98d6}'
40+
// hashesMessage: false,
41+
// },
42+
// ECDSA
43+
{
44+
hasRecoveryId: true,
45+
hashesMessage: true,
46+
recoversPublicKey: true,
47+
signingScheme: 'EcdsaK256Sha256',
48+
},
49+
{
50+
hasRecoveryId: true,
51+
hashesMessage: true,
52+
recoversPublicKey: true,
53+
signingScheme: 'EcdsaP256Sha256',
54+
},
55+
{
56+
hasRecoveryId: true,
57+
hashesMessage: true,
58+
recoversPublicKey: true,
59+
signingScheme: 'EcdsaP384Sha384',
60+
},
61+
// FROST
62+
{
63+
signingScheme: 'SchnorrEd25519Sha512',
64+
hashesMessage: false,
65+
},
66+
// {
67+
// signingScheme: 'SchnorrK256Sha256', // TODO signature of length 64 expected, got 65
68+
// hashesMessage: false,
69+
// },
70+
// {
71+
// signingScheme: 'SchnorrP256Sha256', // TODO Expected pkpSignature to consistently verify its components
72+
// hashesMessage: false,
73+
// },
74+
// {
75+
// signingScheme: 'SchnorrP384Sha384', // TODO Expected pkpSignature to consistently verify its components
76+
// hashesMessage: false,
77+
// },
78+
// {
79+
// signingScheme: 'SchnorrRistretto25519Sha512', // TODO curve.verify is not a function
80+
// hashesMessage: false,
81+
// },
82+
{
83+
signingScheme: 'SchnorrEd448Shake256',
84+
hashesMessage: false,
85+
},
86+
// {
87+
// signingScheme: 'SchnorrRedJubjubBlake2b512', // TODO Expected pkpSignature to consistently verify its components
88+
// hashesMessage: false,
89+
// },
90+
// {
91+
// signingScheme: 'SchnorrK256Taproot', // TODO Expected pkpSignature to consistently verify its components
92+
// hashesMessage: false,
93+
// },
94+
// {
95+
// signingScheme: 'SchnorrRedDecaf377Blake2b512', // TODO Expected pkpSignature to consistently verify its components
96+
// hashesMessage: false,
97+
// },
98+
// {
99+
// signingScheme: 'SchnorrkelSubstrate', // TODO Expected pkpSignature to consistently verify its components
100+
// hashesMessage: false,
101+
// },
102+
];
17103

18-
const eoaSessionSigs = await getEoaSessionSigs(devEnv, alice);
19-
const runWithSessionSigs = await devEnv.litNodeClient.pkpSign({
20-
toSign: alice.loveLetter,
21-
pubKey: alice.pkp.publicKey,
22-
sessionSigs: eoaSessionSigs,
23-
});
24-
25-
devEnv.releasePrivateKeyFromUser(alice);
26-
27-
// Expected output:
28-
// {
29-
// r: "25fc0d2fecde8ed801e9fee5ad26f2cf61d82e6f45c8ad1ad1e4798d3b747fd9",
30-
// s: "549fe745b4a09536e6e7108d814cf7e44b93f1d73c41931b8d57d1b101833214",
31-
// recid: 1,
32-
// signature: "0x25fc0d2fecde8ed801e9fee5ad26f2cf61d82e6f45c8ad1ad1e4798d3b747fd9549fe745b4a09536e6e7108d814cf7e44b93f1d73c41931b8d57d1b1018332141c",
33-
// publicKey: "04A3CD53CCF63597D3FFCD1DF1E8236F642C7DF8196F532C8104625635DC55A1EE59ABD2959077432FF635DF2CED36CC153050902B71291C4D4867E7DAAF964049",
34-
// dataSigned: "7D87C5EA75F7378BB701E404C50639161AF3EFF66293E9F375B5F17EB50476F4",
35-
// }
36-
37-
// -- assertions
38-
// r, s, dataSigned, and public key should be present
39-
if (!runWithSessionSigs.r) {
40-
throw new Error(`Expected "r" in runWithSessionSigs`);
41-
}
42-
if (!runWithSessionSigs.s) {
43-
throw new Error(`Expected "s" in runWithSessionSigs`);
44-
}
45-
if (!runWithSessionSigs.dataSigned) {
46-
throw new Error(`Expected "dataSigned" in runWithSessionSigs`);
47-
}
48-
if (!runWithSessionSigs.publicKey) {
49-
throw new Error(`Expected "publicKey" in runWithSessionSigs`);
50-
}
104+
for (const signingSchemeConfig of signingSchemeConfigs) {
105+
try {
51106

52-
// signature must start with 0x
53-
if (!runWithSessionSigs.signature.startsWith('0x')) {
54-
throw new Error(`Expected "signature" to start with 0x`);
55-
}
107+
const signingScheme = signingSchemeConfig.signingScheme;
108+
log(`Checking testUseEoaSessionSigsToPkpSign for ${signingSchemeConfig}`);
109+
const eoaSessionSigs = await getEoaSessionSigs(devEnv, alice);
56110

57-
// recid must be parseable as a number
58-
if (isNaN(runWithSessionSigs.recid)) {
59-
throw new Error(`Expected "recid" to be parseable as a number`);
60-
}
111+
const pkpSignature = await devEnv.litNodeClient.pkpSign({
112+
pubKey: alice.pkp.publicKey,
113+
sessionSigs: eoaSessionSigs,
114+
messageToSign: alice.loveLetter,
115+
signingScheme,
116+
});
61117

62-
const signature = ethers.utils.joinSignature({
63-
r: '0x' + runWithSessionSigs.r,
64-
s: '0x' + runWithSessionSigs.s,
65-
recoveryParam: runWithSessionSigs.recid,
66-
});
67-
const recoveredPubKey = ethers.utils.recoverPublicKey(
68-
alice.loveLetter,
69-
signature
70-
);
71-
72-
console.log('recoveredPubKey:', recoveredPubKey);
73-
74-
if (recoveredPubKey !== `0x${runWithSessionSigs.publicKey.toLowerCase()}`) {
75-
throw new Error(
76-
`Expected recovered public key to match runWithSessionSigs.publicKey`
77-
);
78-
}
79-
if (recoveredPubKey !== `0x${alice.pkp.publicKey.toLowerCase()}`) {
80-
throw new Error(
81-
`Expected recovered public key to match alice.pkp.publicKey`
82-
);
118+
devEnv.releasePrivateKeyFromUser(alice);
119+
120+
// -- Combined signature format assertions
121+
for (const hexString of [
122+
'signature',
123+
'verifyingKey',
124+
'signedData',
125+
'publicKey',
126+
]) {
127+
if (
128+
!pkpSignature[hexString] ||
129+
!pkpSignature[hexString].startsWith('0x')
130+
) {
131+
throw new Error(
132+
`Expected "${hexString}" hex string in pkpSignature. SigningScheme: ${signingScheme}`
133+
);
134+
}
135+
}
136+
// Verify correct recoveryId
137+
if (
138+
signingSchemeConfig.hasRecoveryId
139+
? ![0, 1].includes(pkpSignature.recoveryId)
140+
: pkpSignature.recoveryId !== null
141+
) {
142+
throw new Error(
143+
`Expected "recoveryId" to be 0/1 for ECDSA and "null" for the rest of curves. SigningScheme: ${signingScheme}`
144+
);
145+
}
146+
147+
// Signature, public key and signed data verification
148+
const signatureVerification = verifyLitSignature(
149+
signingSchemeConfig.signingScheme,
150+
pkpSignature.publicKey.replace('0x', ''),
151+
pkpSignature.signedData.replace('0x', ''),
152+
pkpSignature.signature.replace('0x', '')
153+
);
154+
if (!signatureVerification) {
155+
throw new Error(
156+
`Expected pkpSignature to consistently verify its components. SigningScheme: ${signingScheme}`
157+
);
158+
}
159+
160+
if (signingSchemeConfig.recoversPublicKey) {
161+
const curve = curveFunctions[signingScheme];
162+
const signatureBytes = hexToBytes(
163+
pkpSignature.signature.replace(/^0x/, '')
164+
);
165+
// @ts-expect-error In progress. ECDSA works, not yet Frost
166+
const signature = curve.Signature.fromCompact(
167+
signatureBytes
168+
).addRecoveryBit(pkpSignature.recoveryId);
169+
170+
const msgHash = hexToBytes(pkpSignature.signedData.replace(/^0x/, ''));
171+
const recoveredPubKeyBytes = signature.recoverPublicKey(msgHash);
172+
const recoveredPubKey = recoveredPubKeyBytes.toHex(false);
173+
174+
if (pkpSignature.publicKey.replace('0x', '') !== recoveredPubKey) {
175+
throw new Error(
176+
`Expected recovered public key to match nodesPublicKey`
177+
);
178+
}
179+
// PKP public key lives in k256, it cannot be directly compared in any other curve
180+
if (
181+
signingScheme === 'EcdsaK256Sha256' &&
182+
alice.pkp.publicKey !== recoveredPubKey
183+
) {
184+
throw new Error(
185+
`Expected recovered public key to match alice.pkp.publicKey. SigningScheme: ${signingSchemeConfig}`
186+
);
187+
}
188+
}
189+
190+
const messageHash = signingSchemeConfig.hashesMessage ? hashLitMessage(signingScheme, alice.loveLetter) : alice.loveLetter;
191+
const messageHashHex = Buffer.from(messageHash).toString('hex');
192+
if (pkpSignature.signedData.replace('0x', '') !== messageHashHex) {
193+
throw new Error(
194+
`Expected signed data to match hashLitMessage(signingScheme, alice.loveLetter). SigningScheme: ${signingScheme}`
195+
);
196+
}
197+
198+
log(`✅ testUseEoaSessionSigsToPkpSign - ${signingScheme}`);
199+
} catch (e) {
200+
throw new UnknownSignatureError(
201+
{
202+
info: {
203+
signingSchemeConfig,
204+
message: alice.loveLetter,
205+
pkp: alice.pkp,
206+
},
207+
cause: e,
208+
},
209+
`Signature failed with signing scheme ${signingSchemeConfig.signingScheme}`
210+
);
211+
}
83212
}
84213

85-
log('✅ testUseEoaSessionSigsToPkpSign');
214+
log('✅ testUseEoaSessionSigsToPkpSign all signing schemes');
86215
};

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@lit-protocol/contracts": "^0.0.86",
4545
"@metamask/eth-sig-util": "5.0.2",
4646
"@mysten/sui.js": "^0.37.1",
47+
"@noble/curves": "^1.8.1",
4748
"@openagenda/verror": "^3.1.4",
4849
"@simplewebauthn/browser": "^7.2.0",
4950
"@simplewebauthn/typescript-types": "^7.0.0",
@@ -60,6 +61,7 @@
6061
"cross-fetch": "3.1.8",
6162
"date-and-time": "^2.4.1",
6263
"depd": "^2.0.0",
64+
"elliptic": "^6.6.1",
6365
"ethers": "^5.7.1",
6466
"jose": "^4.14.4",
6567
"micromodal": "^0.4.10",
@@ -70,7 +72,8 @@
7072
"tslib": "^2.7.0",
7173
"tweetnacl": "^1.0.3",
7274
"tweetnacl-util": "^0.15.1",
73-
"uint8arrays": "^4.0.3"
75+
"uint8arrays": "^4.0.3",
76+
"zod": "^3.24.1"
7477
},
7578
"devDependencies": {
7679
"@nx/devkit": "17.3.0",
@@ -86,6 +89,7 @@
8689
"@nx/web": "17.3.0",
8790
"@solana/web3.js": "1.95.3",
8891
"@types/depd": "^1.1.36",
92+
"@types/elliptic": "^6.4.18",
8993
"@types/events": "^3.0.3",
9094
"@types/jest": "27.4.1",
9195
"@types/node": "18.19.18",

packages/constants/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ export * from './lib/version';
33

44
// ----------- Constants -----------
55
export * from './lib/constants/constants';
6-
export * from './lib/constants/mappers';
76
export * from './lib/constants/endpoints';
87
export * from './lib/constants/mappers';
98
export * from './lib/constants/curves';

0 commit comments

Comments
 (0)