Skip to content

Commit 6074958

Browse files
committed
fix: bind zero-amount proof signatures to challenge realm
1 parent 0d1e548 commit 6074958

6 files changed

Lines changed: 80 additions & 28 deletions

File tree

.changeset/fair-eggs-love.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'mppx': patch
3+
---
4+
5+
Fixed zero-amount Tempo proof credentials to bind signatures to the challenge realm.

src/tempo/client/Charge.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function charge(parameters: charge.Parameters = {}) {
6565
domain: Proof.domain(chainId!),
6666
types: Proof.types,
6767
primaryType: 'Proof',
68-
message: Proof.message(challenge.id),
68+
message: Proof.message(challenge.id, challenge.realm),
6969
})
7070
return Credential.serialize({
7171
challenge,

src/tempo/internal/proof.test.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,30 @@ const parseProofSourceCases = [
4444
] as const
4545

4646
describe('Proof', () => {
47-
test('types has Proof with challengeId field', () => {
47+
test('types has Proof with challengeId and realm fields', () => {
4848
expect(Proof.types).toEqual({
49-
Proof: [{ name: 'challengeId', type: 'string' }],
49+
Proof: [
50+
{ name: 'challengeId', type: 'string' },
51+
{ name: 'realm', type: 'string' },
52+
],
5053
})
5154
})
5255

5356
test('domain returns EIP-712 domain with name, version, chainId', () => {
5457
const d = Proof.domain(42431)
55-
expect(d).toEqual({ name: 'MPP', version: '1', chainId: 42431 })
58+
expect(d).toEqual({ name: 'MPP', version: '2', chainId: 42431 })
5659
})
5760

5861
test('domain uses provided chainId', () => {
5962
expect(Proof.domain(1).chainId).toBe(1)
6063
expect(Proof.domain(99999).chainId).toBe(99999)
6164
})
6265

63-
test('message wraps challengeId', () => {
64-
expect(Proof.message('abc123')).toEqual({ challengeId: 'abc123' })
66+
test('message wraps challengeId and realm', () => {
67+
expect(Proof.message('abc123', 'api.example.com')).toEqual({
68+
challengeId: 'abc123',
69+
realm: 'api.example.com',
70+
})
6571
})
6672

6773
test('proofSource constructs did:pkh DID', () => {

src/tempo/internal/proof.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ import { isAddress, type Address } from 'viem'
22

33
/** EIP-712 typed data types for proof credentials. */
44
export const types = {
5-
Proof: [{ name: 'challengeId', type: 'string' }],
5+
Proof: [
6+
{ name: 'challengeId', type: 'string' },
7+
{ name: 'realm', type: 'string' },
8+
],
69
} as const
710

811
/** Constructs the EIP-712 domain for a proof credential. */
912
export function domain(chainId: number) {
10-
return { name: 'MPP', version: '1', chainId } as const
13+
return { name: 'MPP', version: '2', chainId } as const
1114
}
1215

1316
/** Constructs the EIP-712 message for a proof credential. */
14-
export function message(challengeId: string) {
15-
return { challengeId } as const
17+
export function message(challengeId: string, realm: string) {
18+
return { challengeId, realm } as const
1619
}
1720

1821
/** Constructs the expected `did:pkh` source DID for a proof credential. */

src/tempo/server/Charge.test.ts

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2292,7 +2292,7 @@ describe('tempo', () => {
22922292
domain: Proof.domain(chain.id),
22932293
types: Proof.types,
22942294
primaryType: 'Proof',
2295-
message: Proof.message(challenge.id),
2295+
message: Proof.message(challenge.id, challenge.realm),
22962296
})
22972297

22982298
const credential = Credential.from({
@@ -2330,7 +2330,7 @@ describe('tempo', () => {
23302330
domain: Proof.domain(chain.id),
23312331
types: Proof.types,
23322332
primaryType: 'Proof',
2333-
message: Proof.message(challenge.id),
2333+
message: Proof.message(challenge.id, challenge.realm),
23342334
})
23352335

23362336
const credential = Credential.from({
@@ -2445,7 +2445,7 @@ describe('tempo', () => {
24452445
domain: Proof.domain(chain.id),
24462446
types: Proof.types,
24472447
primaryType: 'Proof',
2448-
message: Proof.message(challenge.id),
2448+
message: Proof.message(challenge.id, challenge.realm),
24492449
})
24502450

24512451
const credential = Credential.from({
@@ -2507,7 +2507,7 @@ describe('tempo', () => {
25072507
domain: Proof.domain(chain.id),
25082508
types: Proof.types,
25092509
primaryType: 'Proof',
2510-
message: Proof.message(challenge.id),
2510+
message: Proof.message(challenge.id, challenge.realm),
25112511
})
25122512

25132513
const credential = Credential.from({
@@ -2568,7 +2568,7 @@ describe('tempo', () => {
25682568
domain: Proof.domain(chain.id),
25692569
types: Proof.types,
25702570
primaryType: 'Proof',
2571-
message: Proof.message(challenge.id),
2571+
message: Proof.message(challenge.id, challenge.realm),
25722572
})
25732573

25742574
const credential = Credential.serialize(
@@ -2642,7 +2642,7 @@ describe('tempo', () => {
26422642
domain: Proof.domain(chain.id),
26432643
types: Proof.types,
26442644
primaryType: 'Proof',
2645-
message: Proof.message(challenge.id),
2645+
message: Proof.message(challenge.id, challenge.realm),
26462646
})
26472647

26482648
const credential = Credential.from({
@@ -2703,7 +2703,7 @@ describe('tempo', () => {
27032703
domain: Proof.domain(chain.id),
27042704
types: Proof.types,
27052705
primaryType: 'Proof',
2706-
message: Proof.message(challenge1.id),
2706+
message: Proof.message(challenge1.id, challenge1.realm),
27072707
})
27082708

27092709
const credential1 = Credential.from({
@@ -2730,7 +2730,7 @@ describe('tempo', () => {
27302730
domain: Proof.domain(chain.id),
27312731
types: Proof.types,
27322732
primaryType: 'Proof',
2733-
message: Proof.message(challenge2.id),
2733+
message: Proof.message(challenge2.id, challenge2.realm),
27342734
})
27352735

27362736
const credential2 = Credential.from({
@@ -2767,7 +2767,7 @@ describe('tempo', () => {
27672767
domain: Proof.domain(chain.id),
27682768
types: Proof.types,
27692769
primaryType: 'Proof',
2770-
message: Proof.message(challenge.id),
2770+
message: Proof.message(challenge.id, challenge.realm),
27712771
})
27722772

27732773
const credential = Credential.from({
@@ -2803,7 +2803,7 @@ describe('tempo', () => {
28032803
domain: Proof.domain(chain.id),
28042804
types: Proof.types,
28052805
primaryType: 'Proof',
2806-
message: Proof.message(challenge.id),
2806+
message: Proof.message(challenge.id, challenge.realm),
28072807
})
28082808

28092809
const credential = Credential.from({
@@ -2902,7 +2902,7 @@ describe('tempo', () => {
29022902
domain: Proof.domain(chain.id),
29032903
types: Proof.types,
29042904
primaryType: 'Proof',
2905-
message: Proof.message(challenge.id),
2905+
message: Proof.message(challenge.id, challenge.realm),
29062906
})
29072907

29082908
const credential = Credential.from({
@@ -2940,7 +2940,7 @@ describe('tempo', () => {
29402940
domain: Proof.domain(chain.id),
29412941
types: Proof.types,
29422942
primaryType: 'Proof',
2943-
message: Proof.message(challenge.id),
2943+
message: Proof.message(challenge.id, challenge.realm),
29442944
})
29452945

29462946
const credential = Credential.from({
@@ -2979,7 +2979,43 @@ describe('tempo', () => {
29792979
domain: Proof.domain(99999),
29802980
types: Proof.types,
29812981
primaryType: 'Proof',
2982-
message: Proof.message(challenge.id),
2982+
message: Proof.message(challenge.id, challenge.realm),
2983+
})
2984+
2985+
const credential = Credential.from({
2986+
challenge,
2987+
payload: { signature, type: 'proof' as const },
2988+
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
2989+
})
2990+
2991+
const response2 = await fetch(httpServer.url, {
2992+
headers: { Authorization: Credential.serialize(credential) },
2993+
})
2994+
expect(response2.status).toBe(402)
2995+
2996+
httpServer.close()
2997+
})
2998+
2999+
test('behavior: rejects proof signed with wrong realm', async () => {
3000+
const httpServer = await Http.createServer(async (req, res) => {
3001+
const result = await Mppx_server.toNodeListener(
3002+
server.charge({ amount: '0', decimals: 6 }),
3003+
)(req, res)
3004+
if (result.status === 402) return
3005+
res.end('OK')
3006+
})
3007+
3008+
const response1 = await fetch(httpServer.url)
3009+
const challenge = Challenge.fromResponse(response1, {
3010+
methods: [tempo_client.charge()],
3011+
})
3012+
3013+
const signature = await signTypedData(client, {
3014+
account: accounts[1],
3015+
domain: Proof.domain(chain.id),
3016+
types: Proof.types,
3017+
primaryType: 'Proof',
3018+
message: Proof.message(challenge.id, 'evil.example.com'),
29833019
})
29843020

29853021
const credential = Credential.from({
@@ -3015,7 +3051,7 @@ describe('tempo', () => {
30153051
domain: Proof.domain(chain.id),
30163052
types: Proof.types,
30173053
primaryType: 'Proof',
3018-
message: Proof.message(challenge.id),
3054+
message: Proof.message(challenge.id, challenge.realm),
30193055
})
30203056

30213057
const credential = Credential.from({
@@ -3051,7 +3087,7 @@ describe('tempo', () => {
30513087
domain: Proof.domain(chain.id),
30523088
types: Proof.types,
30533089
primaryType: 'Proof',
3054-
message: Proof.message(challenge.id),
3090+
message: Proof.message(challenge.id, challenge.realm),
30553091
})
30563092

30573093
const credential = Credential.from({

src/tempo/server/Charge.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,13 +244,14 @@ export function charge<const parameters extends charge.Parameters>(
244244
domain: Proof.domain(resolvedChainId),
245245
types: Proof.types,
246246
primaryType: 'Proof',
247-
message: Proof.message(challenge.id),
247+
message: Proof.message(challenge.id, challenge.realm),
248248
signature: payload.signature as `0x${string}`,
249249
})
250250
if (!valid) {
251251
const proofSigner = recoverAuthorizedProofSigner({
252252
chainId: resolvedChainId,
253253
challengeId: challenge.id,
254+
realm: challenge.realm,
254255
signature: payload.signature as `0x${string}`,
255256
sourceAddress: source.address,
256257
})
@@ -712,18 +713,19 @@ async function markProofUsed(
712713
function recoverAuthorizedProofSigner(parameters: {
713714
chainId: number
714715
challengeId: string
716+
realm: string
715717
signature: `0x${string}`
716718
sourceAddress: `0x${string}`
717719
}): `0x${string}` | null {
718-
const { chainId, challengeId, signature, sourceAddress } = parameters
720+
const { chainId, challengeId, realm, signature, sourceAddress } = parameters
719721

720722
try {
721723
const envelope = SignatureEnvelope.from(signature)
722724
const proofHash = hashTypedData({
723725
domain: Proof.domain(chainId),
724726
types: Proof.types,
725727
primaryType: 'Proof',
726-
message: Proof.message(challengeId),
728+
message: Proof.message(challengeId, realm),
727729
})
728730

729731
if (envelope.type === 'keychain') {

0 commit comments

Comments
 (0)