Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changelog/fix-incompatible-mpp-voucher-key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
tempo-request: patch
---

Reject incompatible P-256/WebAuthn access keys before opening MPP payment sessions and explain how to create a secp256k1 key and clear stale sessions.
44 changes: 41 additions & 3 deletions src/commands/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ async function paySessionAndRetryRequest(
skipChannelId?: string | undefined,
) {
try {
const identity = await resolvePaymentIdentity(options);
const identity = await resolveSessionPaymentIdentity(options);
const details = sessionDetails(challenge, request.url, options);
const reusable = await reusableSessionRecord(details, identity, skipChannelId);
const challengeResponse = tempoPaymentChallengeResponse(paymentRequiredResponse);
Expand Down Expand Up @@ -645,6 +645,25 @@ async function paySessionAndRetryRequest(
}
}

export async function resolveSessionPaymentIdentity(options: RequestOptions) {
if (options.privateKey ?? process.env.TEMPO_PRIVATE_KEY) return resolvePaymentIdentity(options);

const walletState = await loadWalletState();
const secp256k1Identity = await storedAccessKeyIdentity(walletState, options, "secp256k1");
if (secp256k1Identity) return secp256k1Identity;

if (walletState.accounts[walletState.activeAccount ?? 0]) throw sessionAccessKeyError(options);

return resolvePaymentIdentity(options);
}

function sessionAccessKeyError(options: RequestOptions) {
const networkOption = options.network === "testnet" ? " --network testnet" : "";
return paymentError(
`MPP session vouchers require an active, unexpired secp256k1 access key; P-256/WebAuthn access keys are incompatible. Your passkey wallet is supported; only its access key needs to change. Run 'tempo wallet refresh${networkOption}' to create and select a secp256k1 access key. Close stale sessions with 'tempo wallet sessions close --all${networkOption}'. If a sponsored transaction was already prepared, discard it before retrying.`,
);
}

export async function resolvePaymentIdentity(options: RequestOptions) {
const privateKey = options.privateKey ?? process.env.TEMPO_PRIVATE_KEY;
if (privateKey) {
Expand Down Expand Up @@ -705,14 +724,21 @@ async function ensureProviderAccounts(provider: Parameters<typeof connect>[0]) {
await connect(provider);
}

export async function storedAccessKeyIdentity(walletState: WalletState, options: RequestOptions) {
export async function storedAccessKeyIdentity(
walletState: WalletState,
options: RequestOptions,
requiredKeyType?: "secp256k1" | "p256",
) {
const activeAccount = walletState.accounts[walletState.activeAccount ?? 0];
if (!activeAccount) return undefined;

const expectedChain = chainId(options.network);
for (const key of walletState.accessKeys) {
if (key.chainId !== expectedChain) continue;
if (key.access.toLowerCase() !== activeAccount.address.toLowerCase()) continue;
if (key.expiry !== undefined && key.expiry <= nowSeconds()) continue;
if (key.keyType && key.keyType !== "secp256k1" && key.keyType !== "p256") continue;
if (requiredKeyType && (key.keyType ?? "secp256k1") !== requiredKeyType) continue;

const keyAuthorizationManager = KeyAuthorizationManager.memory();
if (key.keyAuthorization) {
Expand Down Expand Up @@ -748,7 +774,19 @@ export async function storedAccessKeyIdentity(walletState: WalletState, options:
keyAuthorizationManager,
},
)
: undefined;
: key.keyType === "secp256k1" && key.handle && key.publicKey
? await Keystore.secp256k1().toAccount(
{
handle: key.handle as Keystore.Handle,
keyType: key.keyType,
publicKey: key.publicKey as `0x${string}`,
},
{
access: activeAccount.address as `0x${string}`,
keyAuthorizationManager,
},
)
: undefined;
if (!account) continue;
if (key.address.toLowerCase() !== account.accessKeyAddress.toLowerCase()) continue;

Expand Down
1 change: 1 addition & 0 deletions src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export async function connect(provider: CoreProvider.Provider) {
capabilities: {
authorizeAccessKey: {
expiry: Math.floor(Date.now() / 1000) + accessKeyAuthorizationSeconds,
keyType: "secp256k1",
},
},
},
Expand Down
1 change: 1 addition & 0 deletions test/identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ limit = "100000000"
capabilities: {
authorizeAccessKey: {
expiry: Math.floor(Date.now() / 1000) + accessKeyAuthorizationSeconds,
keyType: "secp256k1",
},
},
},
Expand Down
93 changes: 92 additions & 1 deletion test/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
isSessionInvalidationResponse,
parseRequestArgs,
resolvePaymentIdentity,
resolveSessionPaymentIdentity,
runRequest,
storedAccessKeyIdentity,
tempoPaymentChallengeResponse,
Expand Down Expand Up @@ -435,6 +436,7 @@ describe("request command", () => {
accessKeys: [
{
...walletState().accessKeys[0]!,
expiry: 2_000_000_000,
keyAuthorization,
},
],
Expand All @@ -456,6 +458,95 @@ describe("request command", () => {
expect(stored).toStrictEqual(keyAuthorization);
});

it("uses a stored secp256k1 access key for MPP session vouchers", async () => {
await useTempHome();
const p256Keystore = Keystore.webCryptoP256({ extractable: true });
const p256Key = await p256Keystore.createKey();
const p256Account = await p256Keystore.toAccount(
{ ...p256Key, keyType: "p256" },
{ access: testWallet, keyAuthorizationManager: KeyAuthorizationManager.memory() },
);
const secp256k1Keystore = Keystore.secp256k1();
const secp256k1Key = await secp256k1Keystore.createKey();
const secp256k1Account = await secp256k1Keystore.toAccount(
{ ...secp256k1Key, keyType: "secp256k1" },
{ access: testWallet, keyAuthorizationManager: KeyAuthorizationManager.memory() },
);
await writeWalletState(
walletState({
accessKeys: [
{
address: p256Account.accessKeyAddress,
access: testWallet,
chainId: 4217,
handle: p256Key.handle,
keyType: "p256",
limits: [],
publicKey: p256Key.publicKey,
},
{
address: secp256k1Account.accessKeyAddress,
access: testWallet,
chainId: 4217,
handle: secp256k1Key.handle,
keyType: "secp256k1",
limits: [],
publicKey: secp256k1Key.publicKey,
},
],
}),
);

const identity = await resolveSessionPaymentIdentity(
requestOptions("https://paid.example.com"),
);

expect(identity.signerAddress.toLowerCase()).toBe(
secp256k1Account.accessKeyAddress.toLowerCase(),
);
expect(identity.methodOptions).toMatchObject({
account: {
accessKeyAddress: secp256k1Account.accessKeyAddress,
keyType: "secp256k1",
},
mode: "pull",
});
});

it("rejects a P-256 access key before creating an MPP session voucher", async () => {
await useTempHome();
const keystore = Keystore.webCryptoP256({ extractable: true });
const { handle, publicKey } = await keystore.createKey();
const account = await keystore.toAccount(
{ handle, keyType: "p256", publicKey },
{ access: testWallet, keyAuthorizationManager: KeyAuthorizationManager.memory() },
);
await writeWalletState(
walletState({
accessKeys: [
{
address: account.accessKeyAddress,
access: testWallet,
chainId: 4217,
handle,
keyType: "p256",
limits: [],
publicKey,
},
],
}),
);

await expect(
resolveSessionPaymentIdentity(requestOptions("https://paid.example.com")),
).rejects.toMatchObject({
code: "E_PAYMENT",
message: expect.stringMatching(
/MPP session vouchers require an active, unexpired secp256k1 access key.*P-256\/WebAuthn.*passkey wallet is supported.*tempo wallet refresh.*tempo wallet sessions close --all.*sponsored transaction.*discard/is,
),
});
});

it("uses stored P-256 access keys for payment identity resolution", async () => {
await useTempHome();
const keystore = Keystore.webCryptoP256({ extractable: true });
Expand All @@ -471,7 +562,7 @@ describe("request command", () => {
address: account.accessKeyAddress,
access: testWallet,
chainId: 4217,
expiry: 1783809942,
expiry: 2_000_000_000,
handle,
keyType: "p256",
limits: [],
Expand Down