Files
apophis-fastify/docs/attic/root-history/FEEDBACK-protocol-extensions-wishlist.md

16 KiB

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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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

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

# 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:

# 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:

# 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<string>
  • Provide built-in already_seen() and is_consumed() predicates
  • Support referencing by category: previous(constructor), previous(mutator), previous(observer)

Example contract:

# 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:

# 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:

# 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:

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:

// 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:

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:

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.