Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 29 additions & 23 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <jwt>
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
Expand Down Expand Up @@ -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. |

Expand Down Expand Up @@ -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
Expand All @@ -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`.

---

Expand Down Expand Up @@ -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. |
Expand Down
18 changes: 11 additions & 7 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand All @@ -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.
138 changes: 44 additions & 94 deletions docs/TOKEN-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

---

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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="<sec>"`.

See [ADR 0004](adr/0004-defer-attested-claims-to-beacon-relay.md) and beacon-relay
`GATEWAY-SPEC.md`.

---

Expand Down
10 changes: 9 additions & 1 deletion docs/adr/0002-attestation-upgrade-and-sole-issuer.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading
Loading