# Protocol Extensions Wishlist for Apophis **From:** Arbiter Team (Multi-tenant identity platform with OAuth 2.1, WIMSE S2S, Transaction Tokens, SPIFFE/SPIRE) **Date:** 2026-04-25 **Context:** We maintain 58 protocol conformance test files covering OAuth 2.1, WIMSE S2S, Transaction Tokens (RFC 8693), SPIFFE/SPIRE, and related security specs. We are migrating these to Apophis behavioral contracts and have identified gaps between what our protocols require and what APOSTL currently supports. --- ## 1. Executive Summary We have identified **three categories** of needs: 1. **Protocol-specific extensions** (JWT, X.509, SPIFFE) — these are domain-specific predicates that don't belong in core APOSTL but are essential for security protocol testing 2. **Core infrastructure enhancements** (time control, stateful predicates) — these would benefit all Apophis users, not just protocol testing 3. **Explicitly out of scope** — things we acknowledge are too heavy or complex for Apophis (certificate chain validation, replay across restarts) --- ## 2. Protocol Extensions ### 2.1 JWT Extension **Use cases:** OAuth 2.1, Transaction Tokens, WIMSE S2S, SPIFFE JWT-SVID **Proposed predicates:** ```apostl # Access JWT claims jwt_claims(this).sub # subject jwt_claims(this).aud # audience jwt_claims(this).iss # issuer jwt_claims(this).exp # expiration jwt_claims(this).iat # issued at jwt_claims(this).jti # JWT ID (for replay detection) jwt_claims(this).scope # scope jwt_claims(this).cnf.jwk # confirmation key (WIMSE) jwt_claims(this).txn # transaction token ID # Access JWT header jwt_header(this).alg # algorithm jwt_header(this).kid # key ID jwt_header(this).typ # type # Validation jwt_valid(this) # signature verifies against known key jwt_format(this) == "compact" # compact vs JSON serialization # Extensions would need: # - Extract JWT from: Authorization header, response body, custom headers # - Decode Base64URL without verification (for claim inspection) # - Verify signature against configured JWKS or key material ``` **Example contracts:** ```apostl # OAuth 2.1: Token response contains required claims if response_code(this) == 200 then jwt_claims(this).sub != null else T if response_code(this) == 200 then jwt_claims(this).exp > jwt_claims(this).iat else T # WIMSE: WPT expiration must be short-lived if response_code(this) == 200 then jwt_claims(this).exp <= jwt_claims(this).iat + 30 else T # Transaction Tokens: Token type must be transaction_token if response_code(this) == 200 then jwt_claims(this).txn != null else T ``` **Implementation notes:** - Needs `jwks` or `keys` option in extension config for signature verification - Should support extracting JWT from multiple sources (header, body, query param) - Extension state should track `seen_jtis` for replay detection within a test run --- ### 2.2 X.509 Extension **Use cases:** SPIFFE X509-SVID, mTLS certificate validation **Proposed predicates:** ```apostl # Certificate properties x509_uri_sans(this) # array of URI subject alternative names x509_uri_sans(this).length # count of URI SANs x509_ca(this) # is CA certificate? (boolean) x509_expired(this) # is expired? (boolean) x509_not_before(this) # notBefore timestamp x509_not_after(this) # notAfter timestamp # Chain validation (lightweight) x509_self_signed(this) # is self-signed? x509_issuer(this) # issuer DN x509_subject(this) # subject DN ``` **Example contracts:** ```apostl # SPIFFE: X509-SVID must have exactly 1 URI SAN if response_code(this) == 200 then x509_uri_sans(this).length == 1 else T # SPIFFE: X509-SVID leaf must not be CA if response_code(this) == 200 then x509_ca(this) == false else T # SPIFFE: Certificate must not be expired if response_code(this) == 200 then x509_expired(this) == false else T ``` **Explicitly NOT requested (too heavy for test extension):** - `x509_chain_valid(this)` — full RFC 5280 path validation requires trust store, revocation checking, policy validation. This belongs in the application under test, not the test framework. --- ### 2.3 SPIFFE Extension **Use cases:** SPIFFE ID validation, trust domain checks **Proposed predicates:** ```apostl # SPIFFE ID parsing spiffe_parse(this).trustDomain # trust domain string spiffe_parse(this).path # path segments (array) spiffe_parse(this).path.length # path depth spiffe_validate(this) # boolean: valid SPIFFE ID? # Properties spiffe_id(this) # full SPIFFE ID string spiffe_trust_domain(this) # alias for spiffe_parse(this).trustDomain ``` **Example contracts:** ```apostl # SPIFFE: Trust domain must be lowercase if response_code(this) == 200 then spiffe_parse(this).trustDomain matches "^[a-z0-9.-]+$" else T # SPIFFE: Path must not be empty if response_code(this) == 200 then spiffe_parse(this).path.length > 0 else T # SPIFFE: ID must be valid if response_code(this) == 200 then spiffe_validate(this) == true else T ``` --- ### 2.4 Token Hash Extension **Use cases:** WIMSE S2S `ath` (access token hash), `tth` (transaction token hash), `oth` (other token hash) **Proposed predicates:** ```apostl # Token hash validation ath_valid(this) # access token hash matches Authorization header tth_valid(this) # transaction token hash matches Txn-Token header oth_valid(this, "header-name") # custom token hash matches named header # Raw hash computation token_hash(this, "sha256") # SHA-256 hash of token from context ``` **Example contracts:** ```apostl # WIMSE: If ath claim present, must match access token if jwt_claims(this).ath != null then ath_valid(this) == true else T # WIMSE: If tth claim present, must match transaction token if jwt_claims(this).tth != null then tth_valid(this) == true else T ``` --- ### 2.5 HTTP Signature Extension **Use cases:** WIMSE S2S detached HTTP signatures **Proposed predicates:** ```apostl # Signature components signature_input(this) # Signature-Input header parsed signature(this) # Signature header value signature_valid(this) # signature verifies against key # Coverage signature_covers(this, "@method") # covers HTTP method signature_covers(this, "@request-target") # covers request target signature_covers(this, "authorization") # covers auth header signature_covers(this, "txn-token") # covers txn-token header ``` **Example contracts:** ```apostl # WIMSE: Signature must cover @method and @request-target if response_code(this) == 200 then signature_covers(this, "@method") == true else T if response_code(this) == 200 then signature_covers(this, "@request-target") == true else T ``` --- ## 3. Core Infrastructure Enhancements ### 3.1 Time Control **Problem:** Many protocol behaviors depend on time: - Token expiration (JWT `exp` claim) - Refresh token rotation windows - WIMSE WPT short TTL (≤30 seconds) - Challenge TTLs **Current limitation:** APOSTL has `response_time(this)` (wall clock duration) but no way to: - Compare JWT timestamps to "now" - Fast-forward time for expiration testing - Test DST transitions, leap seconds, clock skew **Proposed solutions:** **Option A: Server-level time mocking** ```typescript await fastify.register(apophis, { timeMock: true // enables apophis.time control }) // In tests or stateful sequences: await fastify.apophis.time.advance(30000) // +30 seconds await fastify.apophis.time.set('2026-04-25T12:00:00Z') ``` **Option B: Relative time predicates** ```apostl # Compare JWT exp to current time (server time) jwt_claims(this).exp > now() jwt_claims(this).exp <= now() + 30 # Time since previous request response_time(this) <= 5000 # already exists elapsed_since_previous(this) <= 30 # new: seconds since last request in stateful test ``` **Option C: Both** - `now()` for read-only time comparison (safe, no side effects) - `apophis.time.advance()` for stateful tests that need expiration (opt-in, explicit) **Use case — DST testing:** ```apostl # Test that tokens issued before DST transition work after if previous(jwt_claims(this).iat).hour == 1 then jwt_valid(this) == true else T ``` **Priority:** High. Without time control, we cannot test ~40% of our protocol behaviors. --- ### 3.2 Stateful Cross-Request Predicates **Problem:** Protocols have multi-step flows where step N depends on step N-1: 1. **OAuth 2.1 refresh token rotation:** First refresh succeeds and returns NEW token. Second refresh with OLD token fails. 2. **Transaction token single-use:** First consumption succeeds. Second consumption with same token fails. 3. **WIMSE WPT replay:** First verification succeeds. Second verification with same jti fails. **Current APOSTL limitation:** `previous()` only compares values, not state transitions. **Proposed enhancement:** ```apostl # Check if token was seen in previous requests already_seen(this, jwt_claims(this).jti) == false # Check if token was consumed is_consumed(this, jwt_claims(this).jti) == false # Reference specific previous request by category previous(constructor).jwt_claims(this).refresh_token # last constructor's refresh token ``` **Implementation approach:** - Extension state (already supported in v1.1) tracks `seenTokens: Set` - Provide built-in `already_seen()` and `is_consumed()` predicates - Support referencing by category: `previous(constructor)`, `previous(mutator)`, `previous(observer)` **Example contract:** ```apostl # OAuth 2.1 refresh: new token must differ from old if response_code(this) == 200 then response_body(this).refresh_token != previous(request_body(this)).refresh_token else T # Transaction token: single use if response_code(this) == 409 then response_body(this).error == "transaction_token_replay_detected" && already_seen(this, jwt_claims(this).jti) == true else T ``` **Priority:** High. Essential for refresh tokens, single-use tokens, and replay detection. --- ### 3.3 Request Context Predicates **Problem:** Protocol behaviors depend on request properties that aren't in standard APOSTL: ```apostl # URL components request_url(this) # full URL request_url(this).path # path only request_url(this).host # host header # TLS info (when available) request_tls(this).cipher # TLS cipher suite request_tls(this).version # TLS version request_tls(this).client_cert # client certificate (if mTLS) # Body hash (for content integrity) request_body_hash(this, "sha256") # SHA-256 of raw request body ``` **Use case — WIMSE audience validation:** ```apostl # WPT aud claim must match request URL if response_code(this) == 200 then jwt_claims(this).aud == request_url(this) else T ``` **Priority:** Medium. `request_url()` is straightforward. TLS info is complex (may not be available in all environments). --- ### 3.4 Parallel Execution for Race Detection **Problem:** Some protocol behaviors are inherently concurrent: - Compare-and-swap keyset rotation (S2S-030) - Token consumption races (two clients consume same single-use token simultaneously) - Rate limiting under concurrent load **Current limitation:** Apophis runs tests sequentially. **Proposed enhancement:** ```typescript const results = await fastify.apophis.contract({ depth: 'standard', concurrent: 4, // run 4 requests in parallel raceMode: true // detect race conditions }) ``` **Priority:** Low. We can test these with separate load testing tools. Not essential for contract testing. --- ## 4. Explicitly Out of Scope We acknowledge these are **too complex or inappropriate** for Apophis: | Feature | Why Out of Scope | |---------|-----------------| | **Replay detection across restarts** | Requires persistent state (database/files). Test frameworks should be stateless. Application should handle this. | | **Full X.509 chain validation** | Requires trust store, CRL/OCSP, policy validation. This is application logic, not test logic. | | **Cryptographic algorithm implementation** | Apophis should not implement crypto. It should verify signatures using existing libraries. | | **Protocol state machines** | OAuth flows (authorize → token → refresh) are too complex for declarative contracts. Use stateful testing or separate integration tests. | | **Network-level testing** | TCP behavior, packet inspection, MTU issues. Out of scope for HTTP contract testing. | --- ## 5. Implementation Suggestions ### 5.1 Extension Architecture Following the v1.1 extension architecture documented in `EXTENSION-ARCHITECTURE.md`: ```typescript // Extension registration await fastify.register(apophis, { extensions: [ jwtExtension({ jwks: 'https://auth.example.com/.well-known/jwks.json' }), x509Extension(), spiffeExtension(), tokenHashExtension() ] }) ``` ### 5.2 Configuration per Route Some routes need different validation keys: ```typescript fastify.get('/wimse/wit', { schema: { 'x-category': 'observer', 'x-extension-config': { jwt: { verify: false, extractFrom: 'body' } // don't verify, just parse }, 'x-ensures': [ 'jwt_claims(this).sub != null', 'jwt_claims(this).cnf.jwk != null' ] } }) fastify.post('/wimse/verify', { schema: { 'x-extension-config': { jwt: { verify: true, keySource: 'wit_cnfpubkey' }, tokenHash: { validate: ['ath', 'tth'] } } } }) ``` ### 5.3 Test Data Seeding For stateful tests that need pre-existing resources: ```typescript await fastify.apophis.seed([ { method: 'POST', url: '/oauth/clients', body: { client_id: 'test-client' } }, { method: 'POST', url: '/wimse/wit', body: { workload: 'frontend' } } ]) const results = await fastify.apophis.stateful({ depth: 'standard' }) ``` --- ## 6. Priority Matrix | Feature | Impact | Effort | Priority | |---------|--------|--------|----------| | JWT extension (claims + validation) | Very High | Medium | **P0** | | Time control (`now()`, `advance()`) | Very High | Medium | **P0** | | Stateful predicates (`previous()`, `already_seen()`) | High | Medium | **P1** | | X.509 extension (basic properties) | High | Low | **P1** | | SPIFFE extension | Medium | Low | **P2** | | Token hash extension | Medium | Low | **P2** | | HTTP signature extension | Medium | Medium | **P2** | | Request context (`request_url()`) | Medium | Low | **P2** | | Parallel execution | Low | High | **P3** | --- ## 7. Offer to Collaborate We are happy to: 1. **Contribute extension implementations** — We can build JWT, X.509, SPIFFE extensions and contribute them back 2. **Provide test cases** — We have 58 conformance tests that can serve as real-world validation for extensions 3. **Beta test** — We can test new features on our complex codebase before release 4. **Documentation** — We can write docs and examples for protocol testing patterns --- ## 8. Appendix: Protocol Test Inventory For reference, here's what we need to test: | Protocol | Test File | Behaviors | Needs Extensions | |----------|-----------|-----------|------------------| | OAuth 2.1 | `oauth21-profile-conformance.test.js` | 13 | JWT, time control | | WIMSE S2S | `draft-wimse-s2s-protocol-conformance.test.js` | 31 | JWT, token hash, HTTP sig, X.509 | | Transaction Tokens | `draft-oauth-transaction-tokens-conformance.test.js` | 25 | JWT, time control, stateful | | SPIFFE/SPIRE | `spiffe-spire-conformance.test.js` | 24 | SPIFFE, X.509, JWT | | Token Exchange | `rfc8693-token-exchange-conformance.test.js` | 15 | JWT | | Device Auth | `rfc8628-device-authorization-conformance.test.js` | 12 | JWT | | CIBA | `ciba-conformance.test.js` | 18 | JWT, time control | Total: **138 protocol behaviors** across 7 specifications. --- **Contact:** We'd love to discuss this via GitHub issues, PRs, or video call. Our codebase is open for inspection at `/home/johndvorak/Business/workspace/Arbiter`.