Skip to content

feat: add HMAC-SHA256 challenge ID generation#10

Merged
brendanjryan merged 5 commits intomainfrom
hmac-challenge-id
Jan 29, 2026
Merged

feat: add HMAC-SHA256 challenge ID generation#10
brendanjryan merged 5 commits intomainfrom
hmac-challenge-id

Conversation

@brendanjryan
Copy link
Collaborator

Add support for HMAC-bound challenge IDs matching the TypeScript SDK.

Changes

  • Add generate_challenge_id() function for HMAC-SHA256 ID computation
  • Add Challenge.create() factory method with secret_key parameter
  • Add Challenge.verify() method for stateless challenge verification
  • Add optional secret_key parameter to verify_or_challenge() and @requires_payment decorator
  • Add cross-SDK compatibility tests using conformance test vectors from mpay-sdks

API

Challenge.create()

challenge = Challenge.create(
    secret_key="my-server-secret",
    realm="api.example.com",
    method="tempo",
    intent="charge",
    request={"amount": "1000000", "currency": "0x...", "recipient": "0x..."},
)

Challenge.verify()

is_valid = challenge.verify(secret_key="my-server-secret", realm="api.example.com")

verify_or_challenge() with secret_key

result = await verify_or_challenge(
    authorization=request.headers.get("Authorization"),
    intent=intent,
    request={...},
    realm="api.example.com",
    secret_key="my-server-secret",  # Enables HMAC-bound IDs
)

Algorithm

HMAC input format: realm|method|intent|request_b64|expires|digest (pipe-delimited)
Output: base64url(HMAC-SHA256(secret_key, input))

When secret_key is omitted, falls back to random IDs (secrets.token_urlsafe(16)).

Cross-SDK Compatibility

All 8 conformance test cases pass, matching TypeScript and Rust SDK outputs.

Related: tempoxyz/mpp-rs#23

Add support for HMAC-bound challenge IDs matching the TypeScript SDK:

- Add generate_challenge_id() function for HMAC-SHA256 ID computation
- Add Challenge.create() factory method with secret_key parameter
- Add Challenge.verify() method for stateless challenge verification
- Make secret_key REQUIRED in verify_or_challenge() and @requires_payment
- Add cross-SDK compatibility tests using conformance test vectors

HMAC input format: realm|method|intent|request_b64|expires|digest
Output: base64url(HMAC-SHA256(secret_key, input))

This matches TypeScript's API where secret_key is required for
server-side challenge creation (no random ID fallback).
Aligns Python SDK with TypeScript's Mpay.create() pattern:
- Add Mpay class that wraps Method + realm + secret_key
- Provides charge() and authorize() methods that use bound secret_key
- No need to pass secret_key on every verify_or_challenge() call

This enables the simpler API:
  payment = Mpay(method=TempoMethod(...), realm='...', secret_key='...')
  result = await payment.charge(authorization, request)
- Update api-server example to use Mpay instead of @requires_payment
- Shows the recommended pattern: create handler once, call intents on it
@brendanjryan brendanjryan merged commit 99a1c8e into main Jan 29, 2026
2 checks passed
@brendanjryan brendanjryan deleted the hmac-challenge-id branch January 29, 2026 04:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant