475 lines
16 KiB
Markdown
475 lines
16 KiB
Markdown
# 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<string>`
|
|
- 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`.
|