Skip to content

feat: unified combiner for multiple signing schemes aggregation #799

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions local-tests/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const createBuildConfig = (entry, outfile, globalName) => ({
format: 'esm',
inject: [getPath('./shim.mjs')],
mainFields: ['module', 'main'],
sourcemap: true,
...(globalName ? { globalName } : {}),
});

Expand Down
24 changes: 12 additions & 12 deletions local-tests/setup/tinny-environment.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { LitContracts } from '@lit-protocol/contracts-sdk';
import { LitNodeClient } from '@lit-protocol/lit-node-client';
import {
AuthSig,
LitContractContext,
LitContractResolverContext,
} from '@lit-protocol/types';
import { ProcessEnvs, TinnyEnvConfig } from './tinny-config';
import { TinnyPerson } from './tinny-person';
import { ethers, Signer } from 'ethers';

import { createSiweMessage, generateAuthSig } from '@lit-protocol/auth-helpers';
import {
CENTRALISATION_BY_NETWORK,
LIT_NETWORK,
LIT_NETWORK_VALUES,
PRODUCT_IDS,
RPC_URL_BY_NETWORK,
} from '@lit-protocol/constants';
import { ethers, Signer } from 'ethers';
import { LitContracts } from '@lit-protocol/contracts-sdk';
import { LitNodeClient } from '@lit-protocol/lit-node-client';
import {
AuthSig,
LitContractContext,
LitContractResolverContext,
} from '@lit-protocol/types';

import { ShivaClient, TestnetClient } from './shiva-client';
import { ProcessEnvs, TinnyEnvConfig } from './tinny-config';
import { TinnyPerson } from './tinny-person';
import { toErrorWithMessage } from './tinny-utils';

console.log('checking env', process.env['DEBUG']);
Expand Down Expand Up @@ -49,7 +49,7 @@ export class TinnyEnvironment {
DEBUG: process.env['DEBUG'] === 'true',
REQUEST_PER_KILOSECOND:
parseInt(process.env['REQUEST_PER_KILOSECOND']) ||
(process.env['NETWORK'] as LIT_NETWORK_VALUES) === 'datil-dev'
(process.env['NETWORK'] as LIT_NETWORK_VALUES) === LIT_NETWORK.NagaDev
? 1
: 200,
LIT_RPC_URL: process.env['LIT_RPC_URL'],
Expand Down
10 changes: 4 additions & 6 deletions local-tests/setup/tinny-person.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ export class TinnyPerson {
public authMethodOwnedPkp: PKPInfo;

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

public provider: ethers.providers.StaticJsonRpcProvider;

Expand Down Expand Up @@ -156,9 +154,9 @@ export class TinnyPerson {
this.pkp = walletMintRes.pkp;

/**
* ====================================
* Mint a PKP wiuth eth wallet auth method
* ====================================
* ======================================
* Mint a PKP with eth wallet auth method
* ======================================
*/
console.log(
'[𐬺🧪 Tinny Person𐬺] Minting a PKP with eth wallet auth method...'
Expand Down
246 changes: 185 additions & 61 deletions local-tests/tests/testUseEoaSessionSigsToPkpSign.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,210 @@
import { ethers } from 'ethers';
import { p256 } from '@noble/curves/p256';
import { p384 } from '@noble/curves/p384';
import { secp256k1 } from '@noble/curves/secp256k1';
import { hexToBytes } from '@noble/hashes/utils';

import {
UnknownSignatureError,
EcdsaSigType,
SigType,
} from '@lit-protocol/constants';
import { hashLitMessage } from '@lit-protocol/crypto';
import { log } from '@lit-protocol/misc';

import { getEoaAuthContext } from 'local-tests/setup/session-sigs/get-eoa-session-sigs';
import { TinnyEnvironment } from 'local-tests/setup/tinny-environment';

interface SigningSchemeConfig {
hasRecoveryId?: boolean;
hashesMessage: boolean;
recoversPublicKey?: boolean;
signingScheme: SigType;
}

// Map the right curve function per signing scheme
export const ecdsaCurveFunctions: Record<EcdsaSigType, any> = {
EcdsaK256Sha256: secp256k1,
EcdsaP256Sha256: p256,
EcdsaP384Sha384: p384,
} as const;

/**
* Test Commands:
* ✅ NETWORK=datil-dev yarn test:local --filter=testUseEoaSessionSigsToPkpSign
* ✅ NETWORK=datil-test yarn test:local --filter=testUseEoaSessionSigsToPkpSign
* ✅ NETWORK=naga-dev yarn test:local --filter=testUseEoaSessionSigsToPkpSign
* ✅ NETWORK=naga-test yarn test:local --filter=testUseEoaSessionSigsToPkpSign
* ✅ NETWORK=custom yarn test:local --filter=testUseEoaSessionSigsToPkpSign
*/
export const testUseEoaSessionSigsToPkpSign = async (
devEnv: TinnyEnvironment
) => {
const alice = await devEnv.createRandomPerson();
const signingSchemeConfigs: SigningSchemeConfig[] = [
// BLS
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we have this config in constants?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. This is just to apply more validations later in this test. Doesn't have much value in real scenarios as we are not doing signature parsing or validation anymore

// {
// signingScheme: 'Bls12381', // TODO nodes accept this signing scheme but they throw an unexpected error
// hashesMessage: false,
// },
// {
// signingScheme: 'Bls12381G1ProofOfPossession',
// hashesMessage: false,
// },
// ECDSA
{
hasRecoveryId: true,
hashesMessage: true,
recoversPublicKey: true,
signingScheme: 'EcdsaK256Sha256',
},
{
hasRecoveryId: true,
hashesMessage: true,
recoversPublicKey: true,
signingScheme: 'EcdsaP256Sha256',
},
{
hasRecoveryId: true,
hashesMessage: true,
recoversPublicKey: true,
signingScheme: 'EcdsaP384Sha384',
},
// FROST
{
signingScheme: 'SchnorrEd25519Sha512',
hashesMessage: false,
},
{
signingScheme: 'SchnorrK256Sha256',
hashesMessage: false,
},
{
signingScheme: 'SchnorrP256Sha256',
hashesMessage: false,
},
{
signingScheme: 'SchnorrP384Sha384',
hashesMessage: false,
},
{
signingScheme: 'SchnorrRistretto25519Sha512',
hashesMessage: false,
},
{
signingScheme: 'SchnorrEd448Shake256',
hashesMessage: false,
},
{
signingScheme: 'SchnorrRedJubjubBlake2b512',
hashesMessage: false,
},
{
signingScheme: 'SchnorrK256Taproot',
hashesMessage: false,
},
{
signingScheme: 'SchnorrRedDecaf377Blake2b512',
hashesMessage: false,
},
{
signingScheme: 'SchnorrkelSubstrate',
hashesMessage: false,
},
];

// const eoaSessionSigs = await getEoaSessionSigs(devEnv, alice);
const runWithSessionSigs = await devEnv.litNodeClient.pkpSign({
toSign: alice.loveLetter,
pubKey: alice.pkp.publicKey,
authContext: getEoaAuthContext(devEnv, alice),
});
for (const signingSchemeConfig of signingSchemeConfigs) {
try {
const signingScheme = signingSchemeConfig.signingScheme;
log(`Checking testUseEoaSessionSigsToPkpSign for ${signingSchemeConfig}`);

devEnv.releasePrivateKeyFromUser(alice);
const pkpSignature = await devEnv.litNodeClient.pkpSign({
pubKey: alice.pkp.publicKey,
authContext: getEoaAuthContext(devEnv, alice),
messageToSign: alice.loveLetter,
signingScheme,
});

// Expected output:
// {
// r: "25fc0d2fecde8ed801e9fee5ad26f2cf61d82e6f45c8ad1ad1e4798d3b747fd9",
// s: "549fe745b4a09536e6e7108d814cf7e44b93f1d73c41931b8d57d1b101833214",
// recid: 1,
// signature: "0x25fc0d2fecde8ed801e9fee5ad26f2cf61d82e6f45c8ad1ad1e4798d3b747fd9549fe745b4a09536e6e7108d814cf7e44b93f1d73c41931b8d57d1b1018332141c",
// publicKey: "04A3CD53CCF63597D3FFCD1DF1E8236F642C7DF8196F532C8104625635DC55A1EE59ABD2959077432FF635DF2CED36CC153050902B71291C4D4867E7DAAF964049",
// dataSigned: "7D87C5EA75F7378BB701E404C50639161AF3EFF66293E9F375B5F17EB50476F4",
// }
devEnv.releasePrivateKeyFromUser(alice);

// -- assertions
// r, s, dataSigned, and public key should be present
if (!runWithSessionSigs.r) {
throw new Error(`Expected "r" in runWithSessionSigs`);
}
if (!runWithSessionSigs.s) {
throw new Error(`Expected "s" in runWithSessionSigs`);
}
if (!runWithSessionSigs.dataSigned) {
throw new Error(`Expected "dataSigned" in runWithSessionSigs`);
}
if (!runWithSessionSigs.publicKey) {
throw new Error(`Expected "publicKey" in runWithSessionSigs`);
}
// -- Combined signature format assertions
for (const hexString of [
'signature',
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: can we use zod schema instead?

'verifyingKey',
'signedData',
'publicKey',
]) {
if (
!pkpSignature[hexString] ||
!pkpSignature[hexString].startsWith('0x')
) {
throw new Error(
`Expected "${hexString}" hex string in pkpSignature. Signing Scheme: ${signingScheme}`
);
}
}
// Verify correct recoveryId
if (
signingSchemeConfig.hasRecoveryId
? ![0, 1].includes(pkpSignature.recoveryId)
: pkpSignature.recoveryId !== null
) {
throw new Error(
`Expected "recoveryId" to be 0/1 for ECDSA and "null" for the rest of curves. Signing Scheme: ${signingScheme}`
);
}

// signature must start with 0x
if (!runWithSessionSigs.signature.startsWith('0x')) {
throw new Error(`Expected "signature" to start with 0x`);
}
if (signingSchemeConfig.recoversPublicKey) {
const curve = ecdsaCurveFunctions[signingScheme];
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: can we use zod schema here?

eg.

  const validConfig = SigningSchemeConfigSchema.parse(signingSchemeConfig);
  const validSignature = PKPSignatureSchema.parse(pkpSignature);

const signatureBytes = hexToBytes(
pkpSignature.signature.replace(/^0x/, '')
);
const signature = curve.Signature.fromCompact(
signatureBytes
).addRecoveryBit(pkpSignature.recoveryId);

// recid must be parseable as a number
if (isNaN(runWithSessionSigs.recid)) {
throw new Error(`Expected "recid" to be parseable as a number`);
}
const msgHash = hexToBytes(pkpSignature.signedData.replace(/^0x/, ''));
const recoveredPubKeyBytes = signature.recoverPublicKey(msgHash);
const recoveredPubKey = recoveredPubKeyBytes.toHex(false);

const signature = ethers.utils.joinSignature({
r: '0x' + runWithSessionSigs.r,
s: '0x' + runWithSessionSigs.s,
recoveryParam: runWithSessionSigs.recid,
});
const recoveredPubKey = ethers.utils.recoverPublicKey(
alice.loveLetter,
signature
);
if (pkpSignature.publicKey.replace('0x', '') !== recoveredPubKey) {
throw new Error(
`Expected recovered public key to match nodesPublicKey`
);
}
// PKP public key lives in k256, it cannot be directly compared in any other curve
if (
signingScheme === 'EcdsaK256Sha256' &&
alice.pkp.publicKey !== recoveredPubKey
) {
throw new Error(
`Expected recovered public key to match alice.pkp.publicKey. Signing Scheme: ${signingSchemeConfig}`
);
}
}

console.log('recoveredPubKey:', recoveredPubKey);
const messageHash = signingSchemeConfig.hashesMessage
? hashLitMessage(signingScheme as EcdsaSigType, alice.loveLetter)
: alice.loveLetter;
const messageHashHex = Buffer.from(messageHash).toString('hex');
if (pkpSignature.signedData.replace('0x', '') !== messageHashHex) {
throw new Error(
`Expected signed data to match hashLitMessage(signingScheme, alice.loveLetter). Signing Scheme: ${signingScheme}`
);
}

if (recoveredPubKey !== `0x${runWithSessionSigs.publicKey.toLowerCase()}`) {
throw new Error(
`Expected recovered public key to match runWithSessionSigs.publicKey`
);
}
if (recoveredPubKey !== `0x${alice.pkp.publicKey.toLowerCase()}`) {
throw new Error(
`Expected recovered public key to match alice.pkp.publicKey`
);
log(`✅ testUseEoaSessionSigsToPkpSign - ${signingScheme}`);
} catch (e) {
throw new UnknownSignatureError(
{
info: {
signingSchemeConfig,
message: alice.loveLetter,
pkp: alice.pkp,
},
cause: e,
},
`Signature failed with signing scheme ${signingSchemeConfig.signingScheme}`
);
}
}

log('✅ testUseEoaSessionSigsToPkpSign');
log('✅ testUseEoaSessionSigsToPkpSign all signing schemes');
};
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@lit-protocol/contracts": "^0.0.86",
"@metamask/eth-sig-util": "5.0.2",
"@mysten/sui.js": "^0.37.1",
"@noble/curves": "^1.8.1",
"@openagenda/verror": "^3.1.4",
"@simplewebauthn/browser": "^7.2.0",
"@simplewebauthn/typescript-types": "^7.0.0",
Expand All @@ -60,6 +61,7 @@
"cross-fetch": "3.1.8",
"date-and-time": "^2.4.1",
"depd": "^2.0.0",
"elliptic": "^6.6.1",
"ethers": "^5.7.1",
"jose": "^4.14.4",
"micromodal": "^0.4.10",
Expand All @@ -70,7 +72,8 @@
"tslib": "^2.7.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"uint8arrays": "^4.0.3"
"uint8arrays": "^4.0.3",
"zod": "^3.24.1"
},
"devDependencies": {
"@nx/devkit": "17.3.0",
Expand All @@ -86,6 +89,7 @@
"@nx/web": "17.3.0",
"@solana/web3.js": "1.95.3",
"@types/depd": "^1.1.36",
"@types/elliptic": "^6.4.18",
"@types/events": "^3.0.3",
"@types/jest": "27.4.1",
"@types/node": "18.19.18",
Expand Down
4 changes: 0 additions & 4 deletions packages/constants/src/lib/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import depd from 'depd';

import { LITChain, LITCosmosChain, LITEVMChain, LITSVMChain } from './types';

const deprecated = depd('lit-js-sdk:constants:constants');

/**
* Lit Protocol Network Public Key
*/
Expand Down
Loading
Loading