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
1 change: 1 addition & 0 deletions conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Vectors live in `vectors/` and cover the core protocol surface:
| `receipt.json` | Parsing and formatting `Payment-Receipt: ...` headers. Base64url-encoded JSON with `status`, `method`, `timestamp`, `reference`. |
| `base64url.json` | RFC 4648 §5 encoding: no padding, URL-safe alphabet (`-`/`_` instead of `+`/`/`). |
| `challenge-id.json` | Deterministic challenge ID generation via HMAC-SHA256. Input is pipe-delimited (`realm\|method\|intent\|canonicalized_request\|expires\|digest\|opaque`), output is unpadded base64url. |
| `tempo-proof.json` | EIP-712 typed-data shape for zero-amount Tempo proof credentials. Binds `challengeId` and `realm` under domain `MPP` version `2`. |

Each vector file contains **scenarios** — individual test cases with a name, description, tags, and expected inputs/outputs:

Expand Down
3 changes: 2 additions & 1 deletion conformance/adapters/python/adapter.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"receipt.format",
"base64url.encode",
"base64url.decode",
"challenge.id"
"challenge.id",
"tempo.proof.typed_data"
]
}
30 changes: 30 additions & 0 deletions conformance/adapters/python/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,27 @@ def generate_conformance_challenge_id(*, secret_key: str, realm: str, method: st
return base64.urlsafe_b64encode(signature).decode("ascii").rstrip("=")


def tempo_proof_typed_data(*, chain_id: int, challenge_id: str, realm: str):
return {
"domain": {
"name": "MPP",
"version": "2",
"chainId": chain_id,
},
"types": {
"Proof": [
{"name": "challengeId", "type": "string"},
{"name": "realm", "type": "string"},
],
},
"primaryType": "Proof",
"message": {
"challengeId": challenge_id,
"realm": realm,
},
}


def challenge_to_dict(challenge: Challenge) -> dict:
"""Convert a Challenge to a JSON-serializable dict."""
result = {
Expand Down Expand Up @@ -148,6 +169,7 @@ def error(message: str, error_type: str = "unknown_error"):
"base64url.encode": "base64url-encode",
"base64url.decode": "base64url-decode",
"challenge.id": "generate-challenge-id",
"tempo.proof.typed_data": "tempo-proof-typed-data",
}


Expand Down Expand Up @@ -296,6 +318,14 @@ def main():
opaque=params.get("opaque"),
)
print(json.dumps(success(result)))
elif command == "tempo-proof-typed-data":
params = json.loads(input_data)
result = tempo_proof_typed_data(
chain_id=params["chainId"],
challenge_id=params["challengeId"],
realm=params["realm"],
)
print(json.dumps(success(result)))
else:
print(json.dumps(error(f"Unknown command: {command}")))
except Exception as e:
Expand Down
3 changes: 2 additions & 1 deletion conformance/adapters/rust/adapter.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"receipt.format",
"base64url.encode",
"base64url.decode",
"challenge.id"
"challenge.id",
"tempo.proof.typed_data"
]
}
31 changes: 31 additions & 0 deletions conformance/adapters/rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ fn main() {
"base64url-encode" => handle_base64url_encode(input),
"base64url-decode" => handle_base64url_decode(input),
"generate-challenge-id" => handle_generate_challenge_id(input),
"tempo-proof-typed-data" => handle_tempo_proof_typed_data(input),
_ => print_error(&format!("Unknown command: {}", command), "unknown_error"),
}
}
Expand Down Expand Up @@ -351,6 +352,35 @@ fn handle_generate_challenge_id(input: &str) {
}
}

fn handle_tempo_proof_typed_data(input: &str) {
match serde_json::from_str::<serde_json::Value>(input) {
Ok(params) => {
let chain_id = params.get("chainId").and_then(|v| v.as_u64()).unwrap_or(0);
let challenge_id = str_field(&params, "challengeId");
let realm = str_field(&params, "realm");
print_success(json!({
"domain": {
"name": "MPP",
"version": "2",
"chainId": chain_id,
},
"types": {
"Proof": [
{ "name": "challengeId", "type": "string" },
{ "name": "realm", "type": "string" },
],
},
"primaryType": "Proof",
"message": {
"challengeId": challenge_id,
"realm": realm,
},
}));
}
Err(e) => print_error(&e.to_string(), "generation_error"),
}
}

fn print_adapter_success<T: serde::Serialize>(value: T) {
println!(
"{}",
Expand Down Expand Up @@ -414,6 +444,7 @@ fn legacy_command_for_operation(op: &str) -> Option<&'static str> {
"base64url.encode" => Some("base64url-encode"),
"base64url.decode" => Some("base64url-decode"),
"challenge.id" => Some("generate-challenge-id"),
"tempo.proof.typed_data" => Some("tempo-proof-typed-data"),
_ => None,
}
}
Expand Down
3 changes: 2 additions & 1 deletion conformance/adapters/typescript/adapter.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"receipt.format",
"base64url.encode",
"base64url.decode",
"challenge.id"
"challenge.id",
"tempo.proof.typed_data"
]
}
31 changes: 31 additions & 0 deletions conformance/golden/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,31 @@ function generateConformanceChallengeId(params: {
return createHmac('sha256', params.secretKey).update(payload).digest('base64url')
}

function tempoProofTypedData(params: {
chainId: number
challengeId: string
realm: string
}) {
return {
domain: {
name: 'MPP',
version: '2',
chainId: params.chainId,
},
types: {
Proof: [
{ name: 'challengeId', type: 'string' },
{ name: 'realm', type: 'string' },
],
},
primaryType: 'Proof',
message: {
challengeId: params.challengeId,
realm: params.realm,
},
}
}

function hasDuplicateChallengeParameter(header: string): boolean {
const seen = new Set<string>()
for (const match of header.matchAll(/(?:^|,\s*)(\w+)=/g)) {
Expand Down Expand Up @@ -191,6 +216,11 @@ function runCommand(command: string, input: string): Result<unknown> {
return success(generateConformanceChallengeId(params))
}

case 'tempo-proof-typed-data': {
const params = JSON.parse(input)
return success(tempoProofTypedData(params))
}

default:
return error(`Unknown command: ${command}`, 'unknown_error')
}
Expand Down Expand Up @@ -221,6 +251,7 @@ const OP_TO_COMMAND: Record<string, string> = {
'base64url.encode': 'base64url-encode',
'base64url.decode': 'base64url-decode',
'challenge.id': 'generate-challenge-id',
'tempo.proof.typed_data': 'tempo-proof-typed-data',
}

function commandInputForRequest(op: string, input: unknown): string {
Expand Down
8 changes: 8 additions & 0 deletions conformance/operations.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@
"comparison": "exact",
"description": "Generate a deterministic challenge id from canonical challenge id parameters."
},
"tempo.proof.typed_data": {
"category": "vector",
"inputRef": "protocol.schema.json#/$defs/TempoProofInput",
"successRef": "protocol.schema.json#/$defs/TempoProofTypedData",
"errorTypes": ["generation_error"],
"comparison": "exact",
"description": "Generate the EIP-712 typed-data shape for a zero-amount Tempo proof credential."
},
"http.payment_request": {
"category": "flow",
"inputRef": "protocol.schema.json#/$defs/HttpPaymentRequest",
Expand Down
4 changes: 4 additions & 0 deletions conformance/schemas/adapter-request.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
"if": { "properties": { "op": { "const": "challenge.id" } }, "required": ["op"] },
"then": { "properties": { "input": { "$ref": "protocol.schema.json#/$defs/ChallengeIdInput" } } }
},
{
"if": { "properties": { "op": { "const": "tempo.proof.typed_data" } }, "required": ["op"] },
"then": { "properties": { "input": { "$ref": "protocol.schema.json#/$defs/TempoProofInput" } } }
},
{
"if": { "properties": { "op": { "const": "http.payment_request" } }, "required": ["op"] },
"then": { "properties": { "input": { "$ref": "protocol.schema.json#/$defs/HttpPaymentRequest" } } }
Expand Down
39 changes: 39 additions & 0 deletions conformance/schemas/protocol.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"base64url.encode",
"base64url.decode",
"challenge.id",
"tempo.proof.typed_data",
"http.payment_request"
]
},
Expand Down Expand Up @@ -127,6 +128,44 @@
"id": { "type": "string", "minLength": 1 }
}
},
"TempoProofInput": {
"type": "object",
"required": ["chainId", "challengeId", "realm"],
"additionalProperties": false,
"properties": {
"chainId": { "type": "integer", "minimum": 0 },
"challengeId": { "type": "string", "minLength": 1 },
"realm": { "type": "string" }
}
},
"TempoProofTypedData": {
"type": "object",
"required": ["domain", "types", "primaryType", "message"],
"additionalProperties": false,
"properties": {
"domain": {
"type": "object",
"required": ["name", "version", "chainId"],
"additionalProperties": false,
"properties": {
"name": { "type": "string" },
"version": { "type": "string" },
"chainId": { "type": "integer", "minimum": 0 }
}
},
"types": { "$ref": "#/$defs/JsonObject" },
"primaryType": { "type": "string" },
"message": {
"type": "object",
"required": ["challengeId", "realm"],
"additionalProperties": false,
"properties": {
"challengeId": { "type": "string", "minLength": 1 },
"realm": { "type": "string" }
}
}
}
},
"HttpPaymentMode": {
"type": "string",
"enum": [
Expand Down
3 changes: 2 additions & 1 deletion conformance/scripts/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"base64url-encode": "base64url.encode",
"base64url-decode": "base64url.decode",
"generate-challenge-id": "challenge.id",
"tempo-proof-typed-data": "tempo.proof.typed_data",
}


Expand Down Expand Up @@ -126,7 +127,7 @@ def request_input_for_command(command: str, input_data: str) -> tuple[str, Any]:
op = COMMAND_TO_OPERATION[command]
if op in {"challenge.parse", "credential.parse", "receipt.parse"}:
return op, {"header": input_data}
if op in {"challenge.format", "credential.format", "receipt.format", "challenge.id"}:
if op in {"challenge.format", "credential.format", "receipt.format", "challenge.id", "tempo.proof.typed_data"}:
return op, json.loads(input_data)
if op in {"base64url.encode", "base64url.decode"}:
return op, {"text": input_data}
Expand Down
3 changes: 2 additions & 1 deletion conformance/vectors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"./authorization.json": "./authorization.json",
"./receipt.json": "./receipt.json",
"./base64url.json": "./base64url.json",
"./challenge-id.json": "./challenge-id.json"
"./challenge-id.json": "./challenge-id.json",
"./tempo-proof.json": "./tempo-proof.json"
},
"files": [
"*.json"
Expand Down
50 changes: 50 additions & 0 deletions conformance/vectors/tempo-proof.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"version": "2.0.0",
"spec_ref": "Tempo zero-amount proof credential",
"description": "EIP-712 typed-data shape for zero-amount Tempo proof credentials. The proof binds challengeId + realm using domain MPP v2.",
"commands": {
"generate": "tempo-proof-typed-data"
},
"scenarios": [
{
"name": "zero_amount_proof_shape",
"description": "Proof typed data matches mppx: domain MPP v2 and message fields challengeId + realm.",
"tags": [
"happy-path",
"cross-sdk-validated",
"tempo",
"proof",
"zero-amount"
],
"input": {
"chainId": 42431,
"challengeId": "proof-challenge-id",
"realm": "api.example.com"
},
"expected": {
"domain": {
"name": "MPP",
"version": "2",
"chainId": 42431
},
"types": {
"Proof": [
{
"name": "challengeId",
"type": "string"
},
{
"name": "realm",
"type": "string"
}
]
},
"primaryType": "Proof",
"message": {
"challengeId": "proof-challenge-id",
"realm": "api.example.com"
}
}
}
]
}
Loading