"""AP2 v0.2 reference SDK: an intermediate delegation hop ignores a supplied `expected_aud` / `expected_nonce` (and the `iat` presence check). Measured on 2026-09-13 against `google-agentic-commerce/AP2`, tag `v0.2.0` (commit b4587ac, 2026-04-28), CPython 3.12.13 with the dependency set pinned by the repository's own pyproject. The SDK is unchanged on `main` at that date. `ap2.sdk.sdjwt.kb_sd_jwt.verify()` accepts `expected_aud` and `expected_nonce` for any hop, but only calls `common.verify_expected_claims(...)` when the `typ` is terminal (`kb+sd-jwt`). On an intermediate hop (`kb+sd-jwt+kb`) a supplied expectation is silently ignored. The SDK's own suite says this is wrong: `kb_sd_jwt_intermediate_tests.py` asserts both rejections and both tests fail at that tag (186 passed, 2 failed). Already known upstream: PR #313 moves the check out of the terminal guard and would make this script print REJECTED; PR #325 adds the CI job that the repository lacks (no workflow runs pytest today); issue #319 / PR #342 cover the terminal-hop neighbour (presence of aud/nonce not required without an expectation). Scope, to be exact: the SDK's own chain verifier passes None on every non-terminal hop by design, and MandateClient.verify refuses key-bound presentations without both expectations, so nothing a merchant reaches through the public entry points is affected. What this POC takes from it is in `mandate-ap2-profile.md` section 6.1: the seller binds the terminal hop itself (presence and match), refuses chains with intermediate hops, and scopes the verifier's SDK opinion to the terminal hop. Run, from a venv with the SDK installed at that tag: python contracts/ap2-v0.2-intermediate-hop-repro.py Expected output at v0.2.0: ACCEPTED, with the token's real aud and nonce shown next to the ones the verifier asked for and did not get. """ import json from ap2.sdk.generated.open_payment_mandate import OpenPaymentMandate from ap2.sdk.sdjwt import kb_sd_jwt, parse_token, sd_jwt from jwcrypto.jwk import JWK def cnf(key: JWK) -> dict: return {'jwk': json.loads(key.export_public())} issuer = JWK.generate(kty='EC', crv='P-256') holder = JWK.generate(kty='EC', crv='P-256') delegate = JWK.generate(kty='EC', crv='P-256') # Root, signed by the issuer, bound to the holder's key. root = sd_jwt.create( payload=OpenPaymentMandate(constraints=[], cnf=cnf(holder)), issuer_key=issuer, ).sd_jwt_issuance # One INTERMEDIATE hop (typ kb+sd-jwt+kb): a delegation, carrying the next cnf. hop = kb_sd_jwt.create( prev_token=parse_token(root), holder_key=holder, payload=OpenPaymentMandate(constraints=[], cnf=cnf(delegate)), aud='https://merchant.example', nonce='quote-jti-1111', ).sd_jwt_issuance parsed_root = parse_token(root) verified_root = parsed_root.with_verified_payload( sd_jwt.verify(parsed_root.canonical, JWK.from_json(issuer.export_public())), [] ) print('hop typ:', parse_token(hop).typ) try: payload = kb_sd_jwt.verify( parse_token(hop), verified_root, expected_aud='https://attacker.example', # not the token's aud expected_nonce='some-other-quote', # not the token's nonce ) except ValueError as exc: print('REJECTED (what the SDK\'s own tests expect):', exc) else: print('ACCEPTED.') print(' aud asked for : https://attacker.example') print(' aud in token :', payload.get('aud')) print(' nonce asked : some-other-quote') print(' nonce in token:', payload.get('nonce')) print(' iat present :', 'iat' in payload) print() print('The verifier asked for both bindings and was told the token is valid') print('without either being checked.')