From 227f1b96657e5ebeaf7a93f213b551155cfdeea6 Mon Sep 17 00:00:00 2001 From: Alex Sedighi Date: Fri, 5 Jun 2026 16:42:41 +1200 Subject: [PATCH] Defer attested tier to beacon-relay; trust-relay stays wallet-only. Reverses the M3a attestation-result upgrade path. trust-relay no longer verifies beacon-relay attestation results or mints attested-tier tokens, and no longer emits tier/att (reserved for a future additive reintroduction; absence of tier means wallet tier). beacon-relay owns attested-session state and resolves tier from its own Redis via the wallet-tier token sub plus the device-key proof it already requires. Records the decision in ADR 0004 and marks the affected parts of ADR 0002 as superseded. Updates TOKEN-SPEC (reserved claims, RS checklist, attestation section), ARCHITECTURE (single-SIWE onboarding flow, no tier in token), and the docs index. The wallet-tier session prerequisite for attestation is retained as beacon-relay's admission control (binds attested session to token sub). --- docs/ARCHITECTURE.md | 52 ++++--- docs/README.md | 18 ++- docs/TOKEN-SPEC.md | 138 ++++++------------ ...002-attestation-upgrade-and-sole-issuer.md | 10 +- ...4-defer-attested-claims-to-beacon-relay.md | 112 ++++++++++++++ 5 files changed, 205 insertions(+), 125 deletions(-) create mode 100644 docs/adr/0004-defer-attested-claims-to-beacon-relay.md diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8f21255..1de5959 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -74,9 +74,10 @@ because it preserves a clean upgrade path to attestation without lock-in. 5. **Short trust windows.** 30–60 minute access tokens are the primary revocation mechanism; an explicit revocation list is the immediate escape hatch for abuse, consulted only on sensitive routes. -6. **Forward-compatible token shape.** The phase-0 token is a strict subset of - the attested-tier token beacon-relay will issue. Upgrading a caller to an - attested session changes claims, not the verification code. +6. **Forward-compatible token shape.** trust-relay issues a single wallet-tier + token. Attestation is owned by beacon-relay (see §13), and `tier`/`att` are + reserved for a future additive reintroduction — adding them later changes + claims, not the verification code. 7. **Elegant minimum.** Reuse the existing Nodle backend stack (Axum, Tokio, figment, tracing). Do not introduce a service mesh, a heavyweight IdP, or proof-of-possession schemes for phase 0. @@ -303,14 +304,14 @@ sequenceDiagram P->>R: validate nonce (exists, unused, unexpired, ip-bound?) P->>R: mark nonce used P->>P: optional wallet-heuristic gate for paid scopes - P->>P: mint access JWT (sub=wallet, exp, scopes, tier=wallet, jti) + P->>P: mint access JWT (sub=wallet, exp, scopes, jti) P->>R: store refresh token (optional) + session metadata P-->>C: { accessToken, tokenType:"Bearer", expiresIn, walletAddress, scopes, refreshToken? } Note over C,RS: Per-request (hot path) C->>RS: Authorization: Bearer RS->>RS: verify signature with cached JWKS public key - RS->>RS: check exp/nbf/iss/aud, required scope, tier + RS->>RS: check exp/nbf/iss/aud, required scope alt sensitive route (/ai/*, /mint/*) RS->>I: POST /quota/consume (user Bearer) I->>R: decrement quota / check revocation @@ -372,7 +373,7 @@ JWTs advertised via JWKS, so each ecosystem verifies with its own mature library | Service kind | Embeddable unit | | --- | --- | -| Rust services | A `trust-auth` crate: a Tower layer / Axum extractor yielding `AuthedWallet { address, scopes, tier }`, with JWKS fetch+cache and scope/quota guards. `beacon-relay` reuses it. | +| Rust services | A `trust-auth` crate: a Tower layer / Axum extractor yielding `AuthedWallet { address, scopes }`, with JWKS fetch+cache and scope/quota guards. `beacon-relay` reuses it (and adds its own attested-session lookup for tier). | | Node / Go / Python services | Their standard JWT libraries (`jose`, `golang-jwt`, `pyjwt`) configured against the JWKS URL. No bespoke code. | | Services that cannot be modified | An API-gateway JWT plugin (APISIX / Kong / Envoy `ext_authz`) validates the bearer at the edge. Optional; not a service mesh. | @@ -460,14 +461,16 @@ No token-contract or endpoint changes are required to add smart-account support. --- -## 13. Forward Compatibility with beacon-relay Attestation +## 13. Integration with beacon-relay Attestation -trust-relay is the **sole session issuer**. beacon-relay is the **attestation -authority** — it verifies platform attestation and zkSync state, then emits a -wallet-bound attestation result. trust-relay verifies that result and upgrades -the session to `tier: attested`. +trust-relay is the **sole session issuer** and issues **wallet-tier tokens +only**. beacon-relay is the **attestation authority** and owns attestation +end-to-end: it verifies platform attestation and zkSync state, signs the on-chain +registration, and keeps per-device attested-session state in its own Redis. The +session **tier is resolved by beacon-relay**, not carried in the token — see +[ADR 0004](adr/0004-defer-attested-claims-to-beacon-relay.md). -### Upgrade flow (concrete) +### Onboarding + ingest flow (concrete) ```mermaid sequenceDiagram @@ -476,25 +479,28 @@ sequenceDiagram participant B as beacon-relay D->>T: SIWE auth - T-->>D: wallet-tier JWT (tier=wallet, sub=wallet) + T-->>D: wallet-tier JWT (sub=wallet, no tier claim) - D->>B: POST /v2/attest/ios or android (wallet_address bound) + D->>B: POST /v2/attest/ios or android, Bearer wallet-tier JWT + B->>B: Verify wallet-tier JWT (trust-relay JWKS), enforce wallet_address == sub B->>B: App Attest / Play Integrity + zkSync attestDevice - B-->>D: attestation_result (signed by beacon-relay) + B->>B: Store attested session, sub to device-set index + B-->>D: attestation_result (status only, no new token) - D->>T: SIWE re-auth or session upgrade with resources: attestation:result - T->>T: Verify result via beacon-relay JWKS, wallet match - T-->>D: attested-tier JWT (tier=attested, att.* claims) - - D->>B: POST /v2/scan/ble Bearer attested JWT - B->>B: Verify via trust-relay JWKS, attested middleware path + D->>B: POST /v2/scan/ble, Bearer wallet-tier JWT + device-key proof + B->>B: Verify JWT, resolve tier from attested-session state, apply attested limits + Note over D,B: Stale attestation, 401 step-up (RFC 9470) prompts re-attest ``` +- **Single SIWE.** The wallet-tier token obtained before attestation is reused at + `/v2/attest/*` and on the data plane; there is no second SIWE and no attested + token mint. - **Phase 2 — hardware-backed wallet key.** Wallet key moves to secure enclave; SIWE signing unchanged from trust-relay's perspective; attestation enforces hardware-backed device key. -See ADR 0002 and `TOKEN-SPEC.md` §9 for attestation-result credential shape. +See [ADR 0004](adr/0004-defer-attested-claims-to-beacon-relay.md) and beacon-relay +`GATEWAY-SPEC.md`. --- @@ -560,7 +566,7 @@ possible. | M0 (this repo) | Architecture + ADR + token spec; Axum skeleton (`/healthz`). | | M1 | `GET /nonce` + Redis nonce store + per-IP rate limit. | | M2 | SIWE verification (EOA) + `POST /session` + JWT minting (ES256/EdDSA) + `/.well-known/jwks.json`. | -| M3 | Attestation upgrade: verify beacon-relay attestation results; mint attested tier; `trust-auth` crate; adopt in beacon-relay. | +| M3 | `trust-auth` crate + adoption in beacon-relay. Attestation is owned by beacon-relay (no attested-tier minting in trust-relay; see ADR 0004). | | M4 | Refresh + logout + `jti` revocation set + wallet blocklist. | | M5 | Per-wallet quota + wallet-heuristic gate for `/ai/*`, `/mint/*`. | | M6 | Dual-mode rollout across services, then enforce. | diff --git a/docs/README.md b/docs/README.md index 9d8e9c0..b99cc1f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ Reading order: 1. **[ARCHITECTURE.md](ARCHITECTURE.md)** — what the service is, the OAuth2 Authorization-Server / Resource-Server pattern, deployment topology, - attestation upgrade flow with beacon-relay, revocation authority, Redis + attestation integration with beacon-relay, revocation authority, Redis topology, and the implementation roadmap. 2. **[DEPLOYMENT.md](DEPLOYMENT.md)** — public vs internal route surfaces, @@ -20,8 +20,12 @@ Reading order: 5. **[adr/0003-public-internal-deployment.md](adr/0003-public-internal-deployment.md)** — split public/internal ingress on GCP; quota consume on VPC-only surface. -6. **[TOKEN-SPEC.md](TOKEN-SPEC.md)** — bearer JWT contract, scopes, attested - `att.*` claims, attestation-result credential, RS verification checklist. +6. **[adr/0004-defer-attested-claims-to-beacon-relay.md](adr/0004-defer-attested-claims-to-beacon-relay.md)** + — trust-relay issues wallet-tier tokens only; attestation and tier owned by + beacon-relay; `tier`/`att` reserved. Supersedes parts of ADR 0002. + +7. **[TOKEN-SPEC.md](TOKEN-SPEC.md)** — bearer JWT contract, scopes, reserved + `tier`/`att` claims, RS verification checklist. ## Two-service relationship @@ -30,11 +34,11 @@ Reading order: | **trust-relay** | Sole **session JWT** issuer; SIWE auth; revocation authority | | **beacon-relay** | **Attestation authority** (platform attestation + zkSync) + DePIN ingest RS | -| Phase | Credential | `tier` claim | +| Phase | Credential | Tier (resolved by beacon-relay; not a token claim) | | --- | --- | --- | -| 0 | SIWE wallet signature (EOA) | `wallet` | -| 1 | + beacon-relay platform attestation | `attested` | -| 2 | + hardware-backed keys (SDK) | `attested` (hardened) | +| 0 | SIWE wallet signature (EOA) | wallet | +| 1 | + beacon-relay platform attestation | attested | +| 2 | + hardware-backed keys (SDK) | attested (hardened) | All session tokens share one shape (`sub` = wallet, `aud` = `nodle-backend`). beacon-relay verifies trust-relay JWTs on ingest; it does not mint session tokens. diff --git a/docs/TOKEN-SPEC.md b/docs/TOKEN-SPEC.md index 463a612..f946bfb 100644 --- a/docs/TOKEN-SPEC.md +++ b/docs/TOKEN-SPEC.md @@ -32,22 +32,23 @@ RFC 2119. | Claim | Type | Req. | Meaning | | --- | --- | --- | --- | | `iss` | string (URL) | MUST | Session issuer. Nodle deployment: `https://trust-relay.nodle.com`. | -| `sub` | string | MUST | **Always the wallet address** (lowercase `0x`-hex). Same for wallet and attested tiers. Device identity lives under `att.*`. | +| `sub` | string | MUST | **Always the wallet address** (lowercase `0x`-hex). | | `aud` | string or array | MUST | Intended resource servers. Phase 0: `"nodle-backend"`. RSs MUST verify they are in `aud`. | | `iat` | NumericDate | MUST | Issued-at (seconds since epoch). | | `nbf` | NumericDate | SHOULD | Not-before. | | `exp` | NumericDate | MUST | Expiry. Access tokens: 30–60 min after `iat`. | | `jti` | string (UUID) | MUST | Unique token id. Basis for revocation. | | `scope` | string | MUST | Space-delimited scopes (OAuth2 convention), e.g. `"ai:invoke mint:request"`. | -| `tier` | string | MUST | Trust tier: `"wallet"` or `"attested"`. | -| `att` | object | MAY | **Required when `tier == "attested"`.** Attestation metadata from beacon-relay. See §2.1. | | `chain_id` | number | SHOULD | Chain the SIWE message was bound to (e.g. zkSync Era). | | `ver` | number | SHOULD | Token contract version. Starts at `1`. | +| `tier` | string | reserved | **Not emitted.** Reserved trust-tier claim. Absence MUST be interpreted as wallet tier. See §2.1. | +| `att` | object | reserved | **Not emitted.** Reserved attestation-metadata claim. See §2.1. | | `cnf` | object | MAY | Confirmation / proof-of-possession (reserved for future DPoP; unused in phase 0). | -Phase-0 (wallet tier) tokens set `tier: "wallet"` and omit `att`. Attested-tier -tokens set `tier: "attested"` and MUST include the `att` object populated from -a verified beacon-relay attestation result. +trust-relay issues **wallet-tier tokens only**: it omits `tier` and `att`. A +token with no `tier` claim MUST be treated as wallet tier by resource servers. +Attestation status is **not** carried in the token — it is owned by beacon-relay +(see §2.1 and [ADR 0004](adr/0004-defer-attested-claims-to-beacon-relay.md)). ### Example wallet-tier access token @@ -61,48 +62,28 @@ a verified beacon-relay attestation result. "exp": 1748491200, "jti": "a3f1c2e4-5b6d-4e8f-9012-3456789abcde", "scope": "ai:invoke mint:request scan:submit", - "tier": "wallet", "chain_id": 324, "ver": 1 } ``` -### 2.1 Attested-tier additive claims (`att` object) +### 2.1 Reserved claims (`tier`, `att`) — attestation owned by beacon-relay -When `tier == "attested"`, trust-relay MUST populate `att` after verifying a -beacon-relay attestation result (§10): +trust-relay does **not** emit `tier` or `att`. Attestation is owned end-to-end by +beacon-relay: it verifies platform attestation, signs the on-chain registration, +and keeps per-device attested-session state in its **own** Redis +(`session:{device_address}`). beacon-relay resolves a request's tier by matching +the token's `sub` (wallet) to that state plus the device-key proof it already +requires — not by reading a token claim. See +[ADR 0004](adr/0004-defer-attested-claims-to-beacon-relay.md). -| Field | Type | Meaning | -| ----- | ---- | ------- | -| `att.device_address` | string | On-chain device identity (may differ from wallet on iOS P-256) | -| `att.key_type` | string | `"p256"` or `"secp256k1"` | -| `att.pubkey_hash` | string | SHA-256 of device public key (liveness verification) | -| `att.app_id` | string | `keccak256(appIdentifier)` | -| `att.app_version` | string | Last accepted attested app version | -| `att.attester` | string | beacon-relay attester Ethereum address | -| `att.anchor_block` | number | L2 block anchor from attestation | +`tier` and `att` are **reserved** so they can be reintroduced additively if a +future, independent service needs cross-service tier visibility from the token. +Such a reintroduction is backward-compatible (verifiers ignore unknown claims; +absence of `tier` continues to mean wallet tier). It is **not** part of phase 0. -### Example attested-tier access token - -```json -{ - "iss": "https://trust-relay.nodle.com", - "sub": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18", - "aud": "nodle-backend", - "tier": "attested", - "scope": "scan:submit", - "jti": "...", - "att": { - "device_address": "0x...", - "key_type": "p256", - "pubkey_hash": "0x...", - "app_id": "0x...", - "app_version": "3.2.1", - "attester": "0x...", - "anchor_block": 22446688 - } -} -``` +Resource servers MUST treat a token with no `tier` claim as wallet tier and MUST +NOT require the `att` object to be present. --- @@ -233,8 +214,10 @@ is the hybrid revocation check, required only on sensitive routes. 6. Verify `aud` contains `"nodle-backend"`. 7. Verify time: `exp` in the future, `nbf`/`iat` not in the future (allow small clock skew, e.g. 60 s). -8. Verify the required `scope` is present; if the route requires it, verify - `tier == "attested"`. +8. Verify the required `scope` is present. The token carries no `tier` claim + (treat as wallet tier). Attested-only gating is enforced by beacon-relay from + its own attested-session state, not from a token claim — see + [ADR 0004](adr/0004-defer-attested-claims-to-beacon-relay.md). 9. **Sensitive routes only:** check the `jti` is not in the revocation set and `sub` is not in the wallet blocklist; check and decrement per-wallet quota (via `POST /v1/auth/quota/consume` on the **internal** trust-relay URL, or a @@ -305,58 +288,25 @@ Suggested `error` codes: `invalid_request`, `invalid_nonce`, `nonce_expired`, --- -## 9. Attestation-result credential (beacon-relay → trust-relay) - -beacon-relay signs **attestation results**, not session tokens. trust-relay -verifies them when upgrading a wallet-tier session to attested tier. - -### Consumption - -The client includes the attestation result in the SIWE message: - -```text -resources: -- attestation:eyJhbGciOiJFZERTQSIsInR5cCI6... -``` - -trust-relay: - -1. Parses the SIWE `resources` line. -2. Verifies the JWT signature using **beacon-relay's JWKS** (separate from - trust-relay session JWKS). -3. Confirms `wallet_address` in the result matches SIWE-recovered `sub`. -4. Confirms `exp` has not passed (short TTL, e.g. 5–15 min). -5. Mints an attested-tier session token with `att.*` claims copied from the result. - -### Attestation-result JWT payload (beacon-relay issuer) - -```json -{ - "iss": "https://beacon-relay.nodle.com", - "sub": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18", - "aud": "trust-relay", - "iat": 1748487600, - "exp": 1748488500, - "jti": "...", - "wallet_address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18", - "device_address": "0x...", - "app_id": "0x...", - "app_version": "3.2.1", - "key_type": "p256", - "pubkey_hash": "0x...", - "attester": "0x...", - "anchor_block": 22446688 -} -``` - -| Field | Meaning | -| ----- | ------- | -| `wallet_address` | MUST match SIWE `sub` and attestation ceremony binding | -| `device_address` | On-chain device identity | -| Other fields | Copied into session token `att.*` on upgrade | - -beacon-relay publishes verification keys at `GET /.well-known/jwks.json` on -the beacon-relay service (not trust-relay). +## 9. Attestation (deferred to beacon-relay) + +trust-relay does **not** consume attestation results and does **not** mint +attested-tier tokens. Attestation is owned end-to-end by beacon-relay: + +- The device authenticates to trust-relay via SIWE and receives a wallet-tier + token (this section's contract). It presents that wallet-tier bearer to + beacon-relay's `/v2/attest/*` endpoints; beacon-relay binds the resulting + attested session to the token's `sub` (`wallet_address == sub`). +- beacon-relay tracks attested-session state in its own Redis and resolves a + request's tier from `sub` + device-key proof. No `tier`/`att` claims are + minted or required. +- Because attestations are anchored to an L2 block, beacon-relay (the resource + server) signals stale attestation with an OAuth2 step-up challenge (RFC 9470): + `401` + `WWW-Authenticate: Bearer error="insufficient_user_authentication", + acr_values="attested", max_age=""`. + +See [ADR 0004](adr/0004-defer-attested-claims-to-beacon-relay.md) and beacon-relay +`GATEWAY-SPEC.md`. --- diff --git a/docs/adr/0002-attestation-upgrade-and-sole-issuer.md b/docs/adr/0002-attestation-upgrade-and-sole-issuer.md index ad3cbb5..ce4c5d0 100644 --- a/docs/adr/0002-attestation-upgrade-and-sole-issuer.md +++ b/docs/adr/0002-attestation-upgrade-and-sole-issuer.md @@ -1,11 +1,19 @@ # ADR 0002 — Attestation upgrade path, sole session issuer, and revocation authority -- **Status:** Accepted +- **Status:** Accepted; **superseded in part by [ADR 0004](0004-defer-attested-claims-to-beacon-relay.md)** - **Date:** 2026-06-02 - **Deciders:** Nodle backend / platform - **Context source:** beacon-relay integration plan; ADR 0001 - **Supersedes:** partial clarifications to ADR 0001 (issuer topology) +> **Superseded in part (2026-06-05):** ADR 0004 reverses the attestation-result +> upgrade path. trust-relay no longer verifies attestation results or mints +> attested-tier tokens, and `tier`/`att` are no longer emitted (reserved for +> future additive use). beacon-relay owns attested-session state and resolves +> tier from its own Redis. **§2 (sole session issuer)** and the **identity/session +> revocation** rows of §4 remain in force; **§1**, **§3**, and the +> attestation-validity coupling of §4 are superseded. See ADR 0004. + ## Context ADR 0001 established trust-relay as a standalone SIWE session authority with diff --git a/docs/adr/0004-defer-attested-claims-to-beacon-relay.md b/docs/adr/0004-defer-attested-claims-to-beacon-relay.md new file mode 100644 index 0000000..65db932 --- /dev/null +++ b/docs/adr/0004-defer-attested-claims-to-beacon-relay.md @@ -0,0 +1,112 @@ +# ADR 0004 — Defer attested-tier claims to beacon-relay-owned session state + +- **Status:** Accepted +- **Date:** 2026-06-05 +- **Deciders:** Nodle backend / platform +- **Context source:** ADR 0002; beacon-relay `GATEWAY-SPEC.md`, `DATA-INGEST-SPEC.md` +- **Supersedes (in part):** ADR 0002 §1 (trust-relay minting attested tokens), §3 + (`att.*` token claims), and the attested-tier coupling in §4. + +## Context + +ADR 0002 chose to have beacon-relay sign an attestation **result**, return it to +the client, and have trust-relay verify it and mint an **attested-tier** session +token carrying `att.*` claims. The first implementation of that path (M3a) added, +in trust-relay: a beacon-relay JWKS client (`reqwest`), an attestation-result +verifier, `tier`/`att` claims, attested-token minting, and refresh persistence of +tier/att. + +Before merging, we reviewed who actually consumes the session `tier`/`att`: + +- The **only** verifier of trust-relay session tokens that distinguishes + attested-vs-wallet today is **beacon-relay** itself, which is also the + attestation authority and already keeps full per-device state in its own Redis + (`session:{device_address}`, `GATEWAY-SPEC.md` §8.4). +- Downstream consumers (PubSub `dtn-ble-event` sinks, the rewards oracle) read + **beacon-relay-produced event metadata** (e.g. `metadata.device_status`), not + the session JWT. They never verify the token (`DATA-INGEST-SPEC.md`). +- trust-relay's own logic (quota, scope gating, heuristics) does **not** depend + on `tier`. + +So beacon-relay signs the attestation, trust-relay copies it into a token, and +beacon-relay reads it back — a round-trip whose only consumer is the producer. +The `att.*`-in-token property is forward-compat insurance for hypothetical +independent services, not an immediate need. JWT claims are additively +forward-compatible, so this can be reintroduced later without a breaking change. + +## Decision + +### 1. trust-relay does not verify attestation or mint attested tokens + +trust-relay issues **wallet-tier tokens only** (`sub` = wallet, `scope`, `jti`, +`exp`). It does not fetch beacon-relay's JWKS, does not parse `attestation:` +resources, and does not emit `tier` or `att`. Attestation is owned end-to-end by +beacon-relay. + +### 2. beacon-relay owns attested-session state and tier resolution + +beacon-relay creates the attested session at attestation time +(`session:{device_address}`) and cross-indexes `wallet:{sub} -> {device set}`. +Per request it identifies the wallet from the token's `sub`, identifies the +device from the device-key proof it already requires (assertion counter / +liveness / device-address header, `GATEWAY-SPEC.md` §6.5), and resolves tier via +a local Redis lookup. Tier is RS state, not a token claim. + +### 3. Attestation admission keeps the wallet-tier prerequisite (Q1) + +The device MUST hold a valid wallet-tier trust-relay session to attest +(`GATEWAY-SPEC.md` §5.1, retained). beacon-relay verifies that wallet-tier bearer +on `/v2/attest/*` (machinery it already has as an RS) and binds the attested +session to the token's `sub`, enforcing `wallet_address == sub`. This proves +wallet control at attestation time — replacing the SIWE/`wallet_address` match +that trust-relay used to perform — protects the paymaster from gas-DoS, and adds +no round-trip (the second SIWE is removed; onboarding is one SIWE). + +### 4. Staleness is signaled by the RS via OAuth2 step-up (Q2) + +Because attestations are anchored to an L2 block, beacon-relay (the resource +server) signals "attestation too old" with an OAuth2 step-up challenge +(RFC 9470): `401` + `WWW-Authenticate: Bearer error="insufficient_user_authentication", +acr_values="attested", max_age=""`, retaining the existing `reason` body and +the RFC 6750 baseline understood by all bearer clients. trust-relay is not in +this path since it no longer mints attested tokens. + +### 5. `tier`/`att` are reserved, not emitted + +`TOKEN-SPEC.md` reserves `tier` and `att` for a future additive reintroduction. +Absence of `tier` MUST be interpreted as wallet tier. When a genuinely +independent second consumer needs cross-service tier visibility, reintroducing +`tier`/`att` (and the trust-relay-side verification + minting, or a beacon-relay +query path) is a backward-compatible change. + +### 6. Unchanged from ADR 0002 + +trust-relay remains the **sole session issuer** (§2) and the **authoritative +identity/session revocation** source: `jti` denylist + wallet blocklist apply to +the wallet-tier token. beacon-relay revokes its local attested session and +propagates **identity-kill** to trust-relay on theft/liveness failure. + +## Consequences + +**Positive** + +- Removes the second SIWE, attested-token minting, and beacon-relay JWKS + verification at trust-relay. trust-relay carries no attestation dependency + (`reqwest` dropped). +- Smaller tokens; one token lifecycle instead of two; no duplication of + beacon-relay's device state in the token. +- Cleaner separation: attestation lives entirely in beacon-relay's domain. + +**Negative** + +- `tier`/`att` are not visible to other services from the token. Acceptable: no + such consumer exists today. +- A future independent tier consumer requires reintroducing `tier`/`att` + (additive) plus a way to assert tier (re-add trust-relay verification+minting, + or query beacon-relay). Recorded here so the path is explicit. + +**Follow-ups** + +- beacon-relay `GATEWAY-SPEC.md` / `DATA-INGEST-SPEC.md`: document tier-via-Redis + resolution (§2), wallet-tier admission binding (§3), and step-up staleness + signaling (§4). Tracked in a separate beacon-relay PR.