From 0af898f816ed2e080acb81081059599a6389845e Mon Sep 17 00:00:00 2001 From: Ayoma Wijethunga Date: Sun, 3 May 2026 08:25:46 +0530 Subject: [PATCH 1/9] CREATE inline parameter explainer panels for all protocols Signed-off-by: Ayoma Wijethunga --- .../src/components/ParameterExplainer.tsx | 155 +++++ frontend/src/protocols/explainers/index.ts | 73 +++ frontend/src/protocols/explainers/oauth2.ts | 572 +++++++++++++++++ frontend/src/protocols/explainers/oid4vci.ts | 341 ++++++++++ frontend/src/protocols/explainers/oid4vp.ts | 378 +++++++++++ frontend/src/protocols/explainers/oidc.ts | 543 ++++++++++++++++ frontend/src/protocols/explainers/saml.ts | 600 ++++++++++++++++++ frontend/src/protocols/explainers/scim.ts | 505 +++++++++++++++ frontend/src/protocols/explainers/spiffe.ts | 596 +++++++++++++++++ frontend/src/protocols/explainers/ssf.ts | 520 +++++++++++++++ frontend/src/views/FlowDetail.tsx | 17 +- 11 files changed, 4294 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/ParameterExplainer.tsx create mode 100644 frontend/src/protocols/explainers/index.ts create mode 100644 frontend/src/protocols/explainers/oauth2.ts create mode 100644 frontend/src/protocols/explainers/oid4vci.ts create mode 100644 frontend/src/protocols/explainers/oid4vp.ts create mode 100644 frontend/src/protocols/explainers/oidc.ts create mode 100644 frontend/src/protocols/explainers/saml.ts create mode 100644 frontend/src/protocols/explainers/scim.ts create mode 100644 frontend/src/protocols/explainers/spiffe.ts create mode 100644 frontend/src/protocols/explainers/ssf.ts diff --git a/frontend/src/components/ParameterExplainer.tsx b/frontend/src/components/ParameterExplainer.tsx new file mode 100644 index 0000000..5d16ff9 --- /dev/null +++ b/frontend/src/components/ParameterExplainer.tsx @@ -0,0 +1,155 @@ +'use client' + +import { useState } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { + HelpCircle, + Info, + Unlock, + Bug, + Zap, + ExternalLink, +} from 'lucide-react' +import { + getParameterExplainer, + type ParameterExplainer as ParameterExplainerData, +} from '@/protocols/explainers' + +interface ParameterExplainerProps { + protocolId: string + name: string + value: string +} + +export function ParameterExplainer({ + protocolId, + name, + value, +}: ParameterExplainerProps) { + const [open, setOpen] = useState(false) + const explainer = getParameterExplainer(protocolId, name) + + return ( +
+
+
+ {name} + {explainer && ( + + )} +
+ {value} +
+ + + {open && explainer && ( + e.stopPropagation()} + > + + + )} + +
+ ) +} + +function ExplainerPanel({ explainer }: { explainer: ParameterExplainerData }) { + return ( +
+
+
+
+
+ + {explainer.references && explainer.references.length > 0 && ( + + )} +
+ ) +} + +function Section({ + icon: Icon, + label, + accent, + iconBg, + body, +}: { + icon: React.ElementType + label: string + accent: string + iconBg: string + body: string +}) { + return ( +
+
+ +
+
+
+ {label} +
+

{body}

+
+
+ ) +} diff --git a/frontend/src/protocols/explainers/index.ts b/frontend/src/protocols/explainers/index.ts new file mode 100644 index 0000000..dee156e --- /dev/null +++ b/frontend/src/protocols/explainers/index.ts @@ -0,0 +1,73 @@ +/** + * Parameter Explainer Registry — barrel + lookup + * + * Per-parameter educational metadata: what the parameter is for, what + * breaks without it, the concrete attack it mitigates, and the impact + * on the victim. Surfaced inline in FlowDetail via . + * + * The registry is split per-protocol (`oauth2.ts`, `oidc.ts`, + * `oid4vci.ts`, …) and assembled here. Lookup is by parameter name; the + * same explainer applies anywhere the name appears (e.g. `state` in + * OAuth2, OIDC, OID4VP) unless a per-protocol override (`oidc:nonce`) + * is registered. + * + * Adding a new protocol's entries: + * 1. Create `.ts` exporting `_EXPLAINERS: Record`. + * 2. Import it below and spread it into `EXPLAINERS`. + * 3. Per-protocol overrides use the key form `${protocolId}:${name}` and live + * in that protocol's file. + */ + +import { OAUTH2_EXPLAINERS } from './oauth2' +import { OIDC_EXPLAINERS } from './oidc' +import { OID4VCI_EXPLAINERS } from './oid4vci' +import { OID4VP_EXPLAINERS } from './oid4vp' +import { SAML_EXPLAINERS } from './saml' +import { SPIFFE_EXPLAINERS } from './spiffe' +import { SCIM_EXPLAINERS } from './scim' +import { SSF_EXPLAINERS } from './ssf' + +export interface ParameterReference { + label: string + href: string +} + +export interface ParameterExplainer { + /** What the parameter is and what it does (1–2 lines). */ + purpose: string + /** What still works without it — and where the gap actually is. */ + withoutIt: string + /** Named adversary, ordered steps of a concrete exploit. */ + attack: string + /** Worst-case outcome for the victim. */ + impact: string + /** Optional pointers to specs, threat models, CVEs, or write-ups. */ + references?: ParameterReference[] +} + +/** + * Merged registry. Spread order = priority on key collision: protocols + * spread later override earlier ones. Currently used only for explicit + * `protocol:name` override keys, not for replacing bare names. + */ +const EXPLAINERS: Record = { + ...OAUTH2_EXPLAINERS, + ...OIDC_EXPLAINERS, + ...OID4VCI_EXPLAINERS, + ...OID4VP_EXPLAINERS, + ...SAML_EXPLAINERS, + ...SPIFFE_EXPLAINERS, + ...SCIM_EXPLAINERS, + ...SSF_EXPLAINERS, +} + +/** + * Look up an explainer for a parameter. Tries protocol-scoped key first + * (e.g. `oidc:nonce`), then falls back to the bare name. + */ +export function getParameterExplainer( + protocolId: string, + name: string, +): ParameterExplainer | undefined { + return EXPLAINERS[`${protocolId}:${name}`] ?? EXPLAINERS[name] +} diff --git a/frontend/src/protocols/explainers/oauth2.ts b/frontend/src/protocols/explainers/oauth2.ts new file mode 100644 index 0000000..fd30198 --- /dev/null +++ b/frontend/src/protocols/explainers/oauth2.ts @@ -0,0 +1,572 @@ +/** + * OAuth 2.0 — Parameter Explainers + * + * Entries reused across OIDC, OID4VCI, OID4VP and other protocols that + * build on OAuth2. Lookup is by parameter name in the central registry + * (see `index.ts`). When a downstream protocol needs different + * semantics for the same parameter name, add a per-protocol override + * (e.g. `oidc:aud`) in that protocol's file. + */ + +import type { ParameterExplainer } from './index' + +export const OAUTH2_EXPLAINERS: Record = { + state: { + purpose: + 'An opaque, unguessable value the client generates per authorization request, ' + + 'persisted in the user session, and required to match on the redirect callback.', + withoutIt: + 'The /authorize endpoint and the redirect_uri callback both still work. ' + + 'Functionally everything succeeds — the gap is that the client has no way to ' + + 'know whether *it* started the flow that produced the code it just received.', + attack: + 'Account-linking CSRF. Mallory starts an authorization flow at the IdP under ' + + 'her own credentials and stops at the redirect, capturing a valid `code` tied ' + + 'to her IdP identity. She crafts a link or auto-submitting iframe pointing at ' + + 'the client\'s redirect_uri carrying that code, and tricks Alice (already ' + + 'signed in to the client) into loading it. The client exchanges Mallory\'s ' + + 'code, receives tokens for Mallory\'s IdP identity, and links that identity ' + + 'to Alice\'s session.', + impact: + 'Persistent account takeover: Mallory now logs into Alice\'s account at the ' + + 'client by signing in with her own IdP credentials. Particularly dangerous ' + + 'for "add a social login" flows on accounts that already have local ' + + 'credentials — the link is silent and survives password resets. ' + + '`state` remains the explicit CSRF check and is recommended as ' + + 'defence-in-depth even when other code-binding mitigations are in place.', + references: [ + { + label: 'RFC 6749 §10.12 (CSRF)', + href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-10.12', + }, + { + label: 'RFC 6819 §4.4.1.8 (Threat Model)', + href: 'https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.1.8', + }, + { + label: 'RFC 9700 §4.7 (CSRF Protection)', + href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-4.7', + }, + ], + }, + + redirect_uri: { + purpose: + 'The exact URL the Authorization Server will return the user to after consent. ' + + 'Must be pre-registered with the client and matched byte-for-byte at runtime.', + withoutIt: + 'If matching is loose (prefix match, wildcard subdomain, ignored query string, ' + + 'or "any path under the registered host"), the client still completes the flow ' + + 'normally for legitimate users — but the AS will also redirect to URLs the ' + + 'attacker controls under the same host.', + attack: + 'Authorization code interception. The client registers ' + + '`https://app.example.com/*` (or has an open-redirect endpoint at ' + + '`/redirect?to=…`). Mallory crafts an authorize URL using Alice\'s client_id ' + + 'but a redirect_uri pointing at her own callback ' + + '(`https://app.example.com/attacker-controlled/cb` or ' + + '`https://app.example.com/redirect?to=https://mallory.example`). Alice clicks, ' + + 'authenticates, consents — and the AS hands the `code` to Mallory.', + impact: + 'Mallory exchanges the stolen code for Alice\'s tokens (or, with PKCE, fails ' + + 'on the exchange but still gets a usable one-shot in non-PKCE deployments). ' + + 'Full impersonation of Alice at every Resource Server the tokens are valid for.', + references: [ + { + label: 'RFC 6749 §3.1.2 (Redirection Endpoint)', + href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2', + }, + { + label: 'RFC 9700 §4.1 (Insufficient redirect_uri Validation)', + href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-4.1', + }, + ], + }, + + code: { + purpose: + 'A short-lived, single-use credential issued at the redirect. The client ' + + 'exchanges it server-to-server for tokens. The code itself grants nothing ' + + 'without the matching client authentication (and PKCE verifier, if used).', + withoutIt: + 'If codes are long-lived or reusable, any place the code is logged or cached ' + + 'becomes a token-equivalent secret: browser history, referer headers, server ' + + 'access logs, proxy logs, error reporters that capture URLs.', + attack: + 'Code replay. A code leaks via referer header to a third-party script loaded ' + + 'on the redirect_uri page, or via a server access log shared with an analytics ' + + 'pipeline. Mallory finds it hours later. If the code is still valid and ' + + 'reusable, she exchanges it for tokens — the legitimate client already did the ' + + 'same exchange, but the AS happily issued a second token set.', + impact: + 'Silent token theft with no failed-login signal at the client. RFC 6749 ' + + 'mandates single-use ("MUST NOT be used more than once"); a properly ' + + 'implemented AS detects replay and revokes all tokens issued from that code.', + references: [ + { + label: 'RFC 6749 §4.1.2 (Authorization Response)', + href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2', + }, + { + label: 'RFC 6749 §10.5 (Authorization Codes)', + href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-10.5', + }, + ], + }, + + code_verifier: { + purpose: + 'A high-entropy random string the client generates and keeps locally. Sent ' + + 'only on the back-channel token exchange. The AS hashes it and compares to ' + + 'the `code_challenge` it received earlier — proving the same client instance ' + + 'started and finished the flow. Per RFC 9700 (BCP 240, Jan 2025), PKCE is ' + + 'mandatory for *all* clients — public and confidential — not only the ' + + 'public-client scenario it was originally designed for.', + withoutIt: + 'Two distinct attacks open up. First, on public clients (mobile, SPA) that ' + + 'cannot hold a client_secret: anyone who captures the code can redeem it. ' + + 'Second — and the reason RFC 9700 made PKCE universal — even confidential ' + + 'clients are vulnerable to *authorization code injection*: a code stolen ' + + 'from any victim can be injected into a legitimate session, with the ' + + 'attacker\'s identity getting linked to the victim\'s account.', + attack: + 'Authorization code interception on mobile. Alice\'s app registers a custom ' + + 'URL scheme (`myapp://callback`) for its redirect_uri. Mallory ships a ' + + 'malicious app on the same device that registers the same scheme. When the ' + + 'system resolves the redirect, Mallory\'s app receives the code first. ' + + 'Without PKCE, she calls /token with the stolen code and the (public) ' + + 'client_id, and gets Alice\'s tokens. With PKCE, she has the code but not ' + + 'the verifier — the exchange fails. Variant on confidential clients: ' + + 'authorization code injection (Mallory injects her own captured code into ' + + 'Alice\'s session), where PKCE binds the code to the verifier the legit ' + + 'client holds and breaks the injection.', + impact: + 'Without PKCE: full token theft on any public client where the redirect ' + + 'channel is not exclusive; account-takeover via code injection on ' + + 'confidential clients. With PKCE: both attacks reduced to a ' + + 'denial-of-service (the captured code is burned but no tokens issued).', + references: [ + { + label: 'RFC 7636 (PKCE)', + href: 'https://datatracker.ietf.org/doc/html/rfc7636', + }, + { + label: 'RFC 9700 §2.1.1 (PKCE mandatory)', + href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-2.1.1', + }, + { + label: 'RFC 9700 §4.5 (Code Injection)', + href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-4.5', + }, + { + label: 'OAuth 2.0 for Native Apps §8.1 (BCP 212)', + href: 'https://datatracker.ietf.org/doc/html/rfc8252#section-8.1', + }, + ], + }, + + code_challenge: { + purpose: + 'BASE64URL(SHA-256(code_verifier)) — sent on the front-channel /authorize ' + + 'request. The AS stores it bound to the issued code so the matching verifier ' + + 'can be checked at token exchange.', + withoutIt: + 'Without a challenge bound to the code, the AS has nothing to compare the ' + + 'verifier against — PKCE collapses to a no-op even if the client sends a ' + + 'verifier on /token.', + attack: + 'An attacker who intercepts the front-channel redirect can read the ' + + 'challenge but cannot reverse SHA-256 to recover the verifier — the ' + + 'point of the hash. The attack opens up only if the AS fails to bind ' + + 'the stored challenge to the issued code (so a stolen code can be ' + + 'redeemed at /token without supplying a matching verifier) or accepts ' + + 'a verifier whose hash does not match the stored challenge.', + impact: + 'A missing or unenforced challenge eliminates PKCE\'s protection ' + + 'against code interception — public clients become token-theft ' + + 'targets, confidential clients become code-injection targets.', + references: [ + { + label: 'RFC 7636 §4.2 (code_challenge)', + href: 'https://datatracker.ietf.org/doc/html/rfc7636#section-4.2', + }, + ], + }, + + code_challenge_method: { + purpose: + 'Tells the AS how the verifier maps to the challenge. `S256` means SHA-256; ' + + '`plain` means the challenge IS the verifier (no hashing).', + withoutIt: + 'If the method is `plain`, the front-channel redirect carries a value that ' + + 'is byte-identical to the secret needed at /token. Anyone who sees the ' + + 'redirect (browser history, referer, network tap on a non-TLS hop, malicious ' + + 'browser extension, OS-level URL handler) trivially has the verifier.', + attack: + 'Mallory observes Alice\'s front-channel redirect — say, via a referer ' + + 'header leaking to a third-party analytics script on the consent page. With ' + + '`S256`, she has SHA-256(verifier) and cannot invert it. With `plain`, the ' + + 'value she captured IS the verifier; she replays it on /token alongside the ' + + 'stolen code and gets tokens.', + impact: + 'Reduces PKCE to security theatre. RFC 7636 §4.2 specifies `S256` as ' + + 'mandatory for clients that can compute SHA-256 — `plain` exists only as a ' + + 'fallback for environments that genuinely cannot.', + references: [ + { + label: 'RFC 7636 §4.2 (Method Selection)', + href: 'https://datatracker.ietf.org/doc/html/rfc7636#section-4.2', + }, + ], + }, + + scope: { + purpose: + 'Space-delimited list of permissions the client is requesting. The user ' + + 'sees this on the consent screen; the AS enforces it on every token issued ' + + 'and every Resource Server validates it on every request.', + withoutIt: + 'Scope is the principle-of-least-privilege control. Without careful scope ' + + 'design, a single token compromise grants every permission the client was ' + + 'ever pre-authorised for.', + attack: + 'Over-broad scope amplification. A client requests `admin:*` because it ' + + 'occasionally needs admin operations, even though 95% of its calls only ' + + 'need `read:profile`. A token leak (XSS, log exposure, stolen device) hands ' + + 'the attacker the full admin surface, not just the read permissions actually ' + + 'in active use at the time of compromise.', + impact: + 'Blast radius of any token compromise is the union of all scopes ever ' + + 'granted, not the intersection of scopes currently in use. Rule of thumb: ' + + 'request the narrowest scope that satisfies the immediate user action; ' + + 'step up only when needed.', + references: [ + { + label: 'RFC 6749 §3.3 (Access Token Scope)', + href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-3.3', + }, + ], + }, + + response_type: { + purpose: + 'Selects which authorization grant flow to run. `code` runs the ' + + 'authorization code flow (token issued back-channel). `token` runs the ' + + 'legacy implicit flow (access token returned directly in the redirect URL ' + + 'fragment).', + withoutIt: + 'Choosing `token` (implicit) is the gap. The AS hands the access token to ' + + 'the browser as a URL fragment (`#access_token=…`). Fragments are not sent ' + + 'over the wire to servers, but they ARE visible to anything that can read ' + + 'the address bar, browser history, or DOM — including third-party scripts ' + + 'on the redirect page, browser extensions, and any code that reads ' + + '`window.location`.', + attack: + 'Implicit-flow token leak. The client SPA loads an analytics or ad-tech ' + + 'tag on its callback page. The token sits in `window.location.hash` while ' + + 'the SPA parses it. A third-party script reads `location.hash` (or hooks ' + + '`history.replaceState`) before the SPA can clear it, and exfiltrates ' + + 'the token. Browser history and HTTP referer headers leaked to embedded ' + + 'images/iframes can also expose it. There is no back-channel exchange where ' + + 'the AS could detect or revoke this.', + impact: + 'Direct token theft with no client authentication step to fail at. ' + + 'OAuth 2.0 Security BCP §2.1.2 says implicit MUST NOT be used; OAuth 2.1 ' + + 'removes it entirely. Always use `code` (with PKCE for public clients).', + references: [ + { + label: 'RFC 9700 §2.1.2 (Implicit Grant)', + href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-2.1.2', + }, + { + label: 'OAuth 2.1 §2.1.2 (Removed Grant Types)', + href: 'https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-2.1.2', + }, + ], + }, + + client_secret: { + purpose: + 'A long-term shared secret between a confidential client (server-side ' + + 'app) and the AS. Proves the request came from the registered client, not ' + + 'just someone who knows the public client_id.', + withoutIt: + 'For confidential clients: anyone with a stolen `code` (or refresh token) ' + + 'and the public `client_id` can redeem it at /token. The single thing ' + + 'binding the token issuance to the legitimate client is gone. For public ' + + 'clients (mobile, SPA): a client_secret cannot meaningfully exist — there ' + + 'is no place on the device to store it that the user (or an attacker on ' + + 'the same device) cannot read.', + attack: + 'The native-app trap. A team builds a mobile app, registers a confidential ' + + 'client because that\'s "more secure", and ships the client_secret in the ' + + 'binary. Mallory unzips the APK / IPA, runs `strings`, and pulls the ' + + 'secret in seconds. She can now mint tokens against any user\'s code or ' + + 'refresh token she observes. Same outcome when secrets land in JS bundles, ' + + 'public git repos, log files, or error reports.', + impact: + 'Persistent credential leak — rotating the secret breaks every legitimate ' + + 'install of the app. The fix is to register as a public client and use ' + + 'PKCE instead of a secret. Confidential client_secret only belongs in ' + + 'environments the end user cannot inspect: a server, a vault, a managed ' + + 'service identity.', + references: [ + { + label: 'RFC 6749 §2.3.1 (Client Authentication)', + href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1', + }, + { + label: 'OAuth 2.0 for Native Apps §8.5 (No client_secret in apps)', + href: 'https://datatracker.ietf.org/doc/html/rfc8252#section-8.5', + }, + ], + }, + + refresh_token: { + purpose: + 'Long-lived credential the client exchanges for fresh access tokens ' + + 'without re-prompting the user. Lives weeks to months; access tokens it ' + + 'mints live minutes to hours.', + withoutIt: + 'A leaked refresh token without rotation IS persistent access. It ' + + 'outlives session revocation, password changes, and most forms of ' + + '"log everyone out" because nothing in the access-token validation path ' + + 'sees it. The AS issues a new access token to whoever presents it, ' + + 'forever, until someone notices.', + attack: + 'Refresh token theft + replay. Mallory exfiltrates Alice\'s refresh ' + + 'token via an XSS bug, a stolen backup, a malicious browser extension, ' + + 'or a leaked log. Without rotation: Mallory mints fresh access tokens ' + + 'on demand, indefinitely; Alice sees nothing wrong because her own ' + + 'session also still works. With rotation + reuse detection: the moment ' + + 'either party uses an already-rotated refresh token, the AS revokes the ' + + 'whole token family — both Alice and Mallory get logged out, and the ' + + 'breach surfaces immediately.', + impact: + 'Without rotation: silent persistent account takeover bounded only by ' + + 'the refresh token lifetime. With rotation: theft becomes a noisy, ' + + 'self-detecting event. Storage rules apply at every layer — never in ' + + 'localStorage, never in front-end code, never in non-HttpOnly cookies.', + references: [ + { + label: 'RFC 6749 §10.4 (Refresh Token Security)', + href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-10.4', + }, + { + label: 'RFC 9700 §4.13 (Refresh Token Protection)', + href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-4.13', + }, + ], + }, + + access_token: { + purpose: + 'Bearer credential presented on every API call to a Resource Server. ' + + '"Bearer" means whoever holds it can use it — there is no second factor ' + + 'binding it to a specific client at the RS.', + withoutIt: + 'The risks are not about *omitting* the token but about how it travels ' + + 'and where it rests. Any place a Bearer token lands by accident is a ' + + 'token-equivalent secret: query strings (server logs, referer headers, ' + + 'browser history, analytics), localStorage (any XSS reads it), non-HTTPS ' + + 'hops (network taps), error reporters that capture URLs.', + attack: + 'Token leakage via referer + URL. A client passes the token in a query ' + + 'string (`?access_token=…`) to "make CORS easier". The Resource Server ' + + 'page returns HTML containing third-party images. Each image fetch sends ' + + 'a `Referer: https://api.example.com/?access_token=…` header to the ' + + 'third-party host. Their CDN logs include full URLs. A junior engineer ' + + 'on that team, six months later, opens the log archive for an unrelated ' + + 'investigation — and now has working credentials for thousands of users.', + impact: + 'Token theft with no audit signal at the issuer. Mitigations: send ' + + 'tokens ONLY in the `Authorization` header (RFC 6750 §2.1), keep ' + + 'lifetimes short (see `expires_in`), validate `aud` on the RS so a token ' + + 'for one service is rejected at another. Where the threat model warrants ' + + 'it, upgrade Bearer to a sender-constrained token: DPoP (RFC 9449) binds ' + + 'each token to a per-client key pair, so a stolen token is unusable ' + + 'without the matching private key — token theft becomes a non-event.', + references: [ + { + label: 'RFC 6750 §2 (Bearer Token Usage)', + href: 'https://datatracker.ietf.org/doc/html/rfc6750#section-2', + }, + { + label: 'RFC 6750 §5 (Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc6750#section-5', + }, + { + label: 'RFC 9700 §4.3 (Token Leakage)', + href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-4.3', + }, + { + label: 'RFC 9449 (DPoP)', + href: 'https://datatracker.ietf.org/doc/html/rfc9449', + }, + ], + }, + + aud: { + purpose: + 'The "audience" claim names the Resource Server(s) a token is valid for. ' + + 'Encoded into JWT access tokens at issuance and surfaced on introspection. ' + + 'Every Resource Server MUST verify that its own identifier appears in `aud` ' + + 'before honouring the token.', + withoutIt: + 'If the RS skips the `aud` check (or accepts any non-empty value), every ' + + 'token issued by that AS is interchangeable across every RS that trusts it. ' + + 'A token meant for the read-only "Photos API" works at the high-privilege ' + + '"Documents API" too.', + attack: + 'Confused deputy / token reuse. Mallory builds a low-privilege client (say, ' + + 'a "photo backup" tool) and gets users to authorise it. She receives ' + + 'access tokens from a shared AS that also fronts the "admin" API. Because ' + + 'no RS in the ecosystem checks `aud`, Mallory replays each user\'s token at ' + + 'the admin API — promoting a deliberately small permission set into full ' + + 'admin access. Audience injection (RFC 9700 §4.10): a malicious AS in a ' + + 'multi-AS deployment can also coerce clients into sending tokens to the ' + + 'wrong audience.', + impact: + 'Effective scope explosion: any client compromise on the AS becomes a ' + + 'compromise of every RS that trusts the AS. The fix is two-sided — the AS ' + + 'must populate `aud` based on the client\'s declared `resource` (RFC 8707) ' + + 'or registered audience, and every RS must reject tokens whose `aud` does ' + + 'not include its own identifier.', + references: [ + { + label: 'RFC 7519 §4.1.3 (aud claim)', + href: 'https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3', + }, + { + label: 'RFC 8707 (Resource Indicators)', + href: 'https://datatracker.ietf.org/doc/html/rfc8707', + }, + { + label: 'RFC 9700 §4.10 (Audience Injection)', + href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-4.10', + }, + ], + }, + + iss: { + purpose: + 'The "issuer" identifier — names which Authorization Server minted the ' + + 'token (introspection response, RFC 7662) or returned the response ' + + '(authorization response, RFC 9207). Lets the recipient cross-check that ' + + 'the response actually came from the AS it expected to be talking to.', + withoutIt: + 'A client that integrates with multiple ASes (typical for federated ' + + 'logins, B2B SaaS, multi-tenant identity) cannot tell from the redirect ' + + 'alone which AS produced a given `code`. The legacy authorization ' + + 'response carries no issuer identification at all.', + attack: + 'AS Mix-up Attack. The client trusts both `honest-as.example` and ' + + '`evil-as.example` (perhaps because Mallory registered her AS under a ' + + 'legitimate-looking federation). Mallory starts a flow where Alice picks ' + + '`honest-as` but Mallory swaps the discovery metadata so Alice\'s browser ' + + 'is redirected to `evil-as` instead. Alice authenticates at evil-as ' + + '(maybe with shared SSO) and the code comes back to the client. The ' + + 'client, with no issuer in the response, sends the code to honest-as\'s ' + + 'token endpoint together with honest-as\'s client_secret. RFC 9207\'s ' + + '`iss` parameter on the authorization response is the explicit ' + + 'countermeasure — the client checks that the issuer it expected matches ' + + 'the issuer that responded.', + impact: + 'In multi-AS deployments without `iss`: code/token confusion across ' + + 'authorization servers — the attacker captures codes or tokens minted by ' + + 'one AS by routing them through a confused client to another AS. Every ' + + 'RS that consumes introspection results MUST also validate `iss` so that ' + + 'tokens from a different (possibly attacker-controlled) AS in the same ' + + 'ecosystem are rejected.', + references: [ + { + label: 'RFC 9207 (Authorization Server Issuer Identification)', + href: 'https://datatracker.ietf.org/doc/html/rfc9207', + }, + { + label: 'RFC 9700 §4.4 (Mix-Up Attacks)', + href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-4.4', + }, + { + label: 'RFC 7662 §2.2 (Introspection Response)', + href: 'https://datatracker.ietf.org/doc/html/rfc7662#section-2.2', + }, + ], + }, + + expires_in: { + purpose: + 'Token lifetime in seconds, returned alongside the access token. Sets the ' + + 'window during which a stolen token is useful before the RS rejects it.', + withoutIt: + 'The risk is not omitting the field but choosing a long lifetime "for ' + + 'convenience". Hours-to-days lifetimes turn a single XSS, log leak, or ' + + 'stolen device into a long-running compromise — every minute of validity ' + + 'is a minute the attacker keeps working access while the legitimate user ' + + 'sees nothing wrong.', + attack: + 'Persistent XSS-driven theft. A momentary XSS exposes Alice\'s access ' + + 'token to Mallory. With a 5-minute lifetime, Mallory has roughly one API ' + + 'window to do damage and Alice\'s next refresh issues fresh tokens. With ' + + 'a 24-hour lifetime (still common in the wild), Mallory has a full day ' + + 'of access from a single capture, and revocation requires either ' + + 'introspection on every RS call or a token-blocklist propagation that ' + + 'most deployments don\'t have.', + impact: + 'Blast radius scales linearly with lifetime. RFC 9700 recommends short ' + + '(minutes) access tokens paired with rotated refresh tokens. The ' + + 'short-lived access token is the primary blast-radius control even when ' + + 'every other defence holds.', + references: [ + { + label: 'RFC 6749 §5.1 (Token Response)', + href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-5.1', + }, + { + label: 'RFC 9700 §2.2.1 (Access Token Privilege Restriction)', + href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-2.2.1', + }, + ], + }, + + token_type: { + purpose: + 'Names the token usage profile. `Bearer` (RFC 6750) means "whoever ' + + 'presents this is treated as the holder" — no further proof required at ' + + 'the RS. `DPoP` (RFC 9449) is the sender-constrained alternative: each ' + + 'request must carry a fresh proof-of-possession signed by the client\'s ' + + 'private key.', + withoutIt: + 'Bearer tokens are the default and the easiest to integrate, but they ' + + 'have no link to the legitimate holder. The first attacker to capture ' + + 'one (XSS, log, malicious extension, MITM on a downgraded link) is ' + + 'indistinguishable from the legitimate client at the RS.', + attack: + 'Token replay across context. Mallory captures Alice\'s Bearer token ' + + 'via any leakage path (see `access_token`). She replays it from an ' + + 'entirely different IP, browser, country — the RS has no way to detect ' + + 'the swap. With DPoP, every API call must be signed with the private ' + + 'key Alice\'s client holds; Mallory has the token but not the key, so ' + + 'replay fails on the first request.', + impact: + 'For high-value APIs (financial, healthcare, admin operations, FAPI 2.0 ' + + 'profile), Bearer is increasingly inadequate. Move to DPoP or mTLS-bound ' + + 'tokens (RFC 8705) where the cost of a single token theft is high. For ' + + 'lower-value APIs Bearer is acceptable provided the lifetime is short ' + + 'and `aud` is enforced.', + references: [ + { + label: 'RFC 6750 (Bearer)', + href: 'https://datatracker.ietf.org/doc/html/rfc6750', + }, + { + label: 'RFC 9449 (DPoP)', + href: 'https://datatracker.ietf.org/doc/html/rfc9449', + }, + { + label: 'RFC 8705 (mTLS-Bound Tokens)', + href: 'https://datatracker.ietf.org/doc/html/rfc8705', + }, + ], + }, +} diff --git a/frontend/src/protocols/explainers/oid4vci.ts b/frontend/src/protocols/explainers/oid4vci.ts new file mode 100644 index 0000000..ae23555 --- /dev/null +++ b/frontend/src/protocols/explainers/oid4vci.ts @@ -0,0 +1,341 @@ +/** + * OpenID for Verifiable Credential Issuance (OID4VCI) — Parameter Explainers + * + * Issuance-specific entries: credential offer phishing, pre-authorized + * code interception, c_nonce freshness, holder binding via proof JWT, + * deferred issuance, format confusion. + */ + +import type { ParameterExplainer } from './index' + +export const OID4VCI_EXPLAINERS: Record = { + credential_offer_uri: { + purpose: + 'A reference URL the Credential Issuer publishes containing the ' + + 'credential offer payload. Delivered to the wallet out-of-band — ' + + 'typically as a QR code, deep link, push notification, or email ' + + 'link. The wallet fetches the URL to learn what credential is being ' + + 'offered and which grant types are supported.', + withoutIt: + 'The offer URI travels over an unauthenticated out-of-band channel ' + + '(QR, deep link, email) with no protocol-level binding between ' + + '"the user who scanned this" and "the user this credential will be ' + + 'issued to". The OpenID Foundation\'s formal security analysis ' + + 'confirms cross-device flows in OID4VCI are inherently phishable ' + + 'and require user attention to be secure.', + attack: + 'Cross-device credential offer phishing. Mallory hosts a malicious ' + + 'website ("upgrade your driver\'s license now!") with a QR code or ' + + 'deep link embedding her own `credential_offer_uri`. Alice scans, the ' + + 'wallet fetches Mallory\'s offer, and Mallory\'s issuer serves a ' + + 'real-looking but attacker-controlled credential — or, in the more ' + + 'subtle variant, Alice approves an issuance flow that hands ' + + '*Mallory\'s* wallet a credential bound to *Alice\'s* identity, ' + + 'because the cross-device protocol has no link between the device ' + + 'that initiated the offer and the device that completes it. ' + + 'SquarePhish2 / Graphish OAuth phishing kits (active 2025) ' + + 'demonstrate the QR + cross-device attack pattern and translate ' + + 'directly to OID4VCI.', + impact: + 'Either malicious credentials installed in legitimate wallets, or ' + + 'legitimate credentials installed in attacker-controlled wallets. ' + + 'Defences are operational: wallet should pin a list of trusted ' + + 'issuers; same-device flows (where the offer link opens directly in ' + + 'the wallet on the same device the user initiated the request from) ' + + 'are formally proven secure and should be preferred.', + references: [ + { + label: 'OID4VCI 1.0 §4.1 (Credential Offer)', + href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer', + }, + { + label: 'OpenID Foundation — Formal Security Analysis of OpenID for VCs', + href: 'https://openid.net/formal-security-analysis-openid-verifiable-credentials/', + }, + { + label: 'ETH Zurich — Formal Analysis of OID4VCI (Zischg)', + href: 'https://ethz.ch/content/dam/ethz/special-interest/infk/inst-infsec/information-security-group-dam/research/software/zischg-oid4vci.pdf', + }, + ], + }, + + 'pre-authorized_code': { + purpose: + 'A short-lived bearer code embedded in a credential offer that ' + + 'authorizes the wallet to obtain an access token without an ' + + 'interactive authorization-code flow. Used when the issuer has ' + + 'already authenticated the user out-of-band (e.g. a kiosk after a ' + + 'driver\'s license renewal) and just needs to hand the credential ' + + 'to whichever wallet shows up with this code.', + withoutIt: + 'Without an additional binding factor, the `pre-authorized_code` ' + + 'is bearer-style — whoever receives the offer and presents the code ' + + 'at /token gets the credential. There is no authentication step; ' + + 'possession is sufficient.', + attack: + 'Pre-authorized code interception. Mallory eavesdrops on Alice\'s ' + + 'credential offer transmission (shoulder-surfs the QR code, ' + + 'intercepts the email, captures the deep-link via a malicious app ' + + 'with the same scheme registration). She redeems the code at the ' + + 'issuer\'s /token endpoint before Alice\'s wallet does, and the ' + + 'issuer happily issues the credential to Mallory\'s wallet. The race ' + + 'is winnable because pre-authorized codes are typically valid for ' + + 'minutes.', + impact: + 'Credential issued to attacker. Mitigation: pair the code with a ' + + '`tx_code` (PIN delivered via separate channel) so possession of ' + + 'the code alone is insufficient. OID4VCI 1.0 §3.5 recommends short ' + + 'expiry (a few minutes) and binding to a specific wallet identity ' + + 'where possible. There is no PKCE-style client-binding in this ' + + 'flow — `tx_code` is the primary defence.', + references: [ + { + label: 'OID4VCI 1.0 §3.5 (Pre-Authorized Code Flow)', + href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-pre-authorized-code-flow', + }, + ], + }, + + tx_code: { + purpose: + 'A short user-entered code (typically 4-8 digits) the issuer ' + + 'delivers to the user out-of-band via a separate channel from the ' + + 'credential offer (SMS to a known phone, email to a known address, ' + + 'displayed on a kiosk screen). The wallet prompts the user to type ' + + 'it and forwards it to /token alongside the pre-authorized code.', + withoutIt: + 'Pre-authorized code without `tx_code` collapses to bearer security ' + + '— see the `pre-authorized_code` attack. `tx_code` introduces a ' + + 'second factor (something the user knows / received separately) that ' + + 'an interceptor of the offer alone cannot supply.', + attack: + 'PIN-channel cross-protocol phishing (documented IN the OID4VCI ' + + 'specification §11.3). Mallory operates a malicious credential issuer ' + + 'and convinces Alice to scan its credential offer. In parallel, ' + + 'Mallory triggers Alice\'s real bank or payment service to send a ' + + 'transaction-confirmation PIN via SMS. The malicious wallet UX, ' + + 'showing Mallory\'s offer, prompts Alice for "the PIN you just ' + + 'received". Alice enters her bank\'s PIN, the wallet POSTs it as ' + + '`tx_code` to Mallory\'s /token endpoint — and Mallory now has ' + + 'a valid PIN for Alice\'s payment service. The PIN was for the ' + + 'wrong protocol entirely, but the user\'s mental model conflated ' + + 'them.', + impact: + 'Credential issuance becomes a phishing primitive for PINs from ' + + 'unrelated services. Mitigations are squarely on wallet UX: clearly ' + + 'attribute the PIN prompt to the specific issuer; refuse offers from ' + + 'untrusted issuers; never auto-fill PINs from notifications. The ' + + 'spec is explicit: this is a known design tension with no clean ' + + 'protocol-level fix.', + references: [ + { + label: 'OID4VCI 1.0 §3.5.1 (tx_code)', + href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-token-request', + }, + { + label: 'OID4VCI 1.0 §11.3 (PIN Phishing)', + href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-trust-between-wallet-and-is', + }, + ], + }, + + c_nonce: { + purpose: + 'Challenge nonce the Credential Issuer returns from /token (and ' + + 'subsequently from /credential). The wallet MUST include this exact ' + + 'value in the `nonce` claim of the proof JWT it sends with its ' + + 'credential request. Single-use; consumed on each call.', + withoutIt: + 'Without `c_nonce` freshness, a wallet\'s proof JWT becomes ' + + 'replayable. Anyone who captures a single proof can re-present it to ' + + 'the issuer indefinitely and receive new credentials bound to the ' + + 'wallet\'s key — turning a one-shot proof into a credential-issuing ' + + 'oracle.', + attack: + 'Proof-JWT replay against credential endpoint. Mallory captures one ' + + 'of Alice\'s proof JWTs (network tap on a non-TLS internal hop, ' + + 'malicious browser extension, leaked log). Without c_nonce ' + + 'enforcement, Mallory replays the proof against the issuer\'s ' + + '/credential endpoint and gets a fresh credential bound to Alice\'s ' + + 'wallet key. Even though Mallory cannot use that credential (she ' + + 'lacks Alice\'s private key for later presentation), she has now ' + + 'forced the issuer to mint duplicate credentials and may correlate ' + + 'identities or exhaust issuance budgets.', + impact: + 'Credential-replay leading to issuance amplification and ' + + 'correlation. The issuer MUST consume `c_nonce` on use and reject ' + + 'replays. Wallet MUST treat each c_nonce as single-use and refresh ' + + 'from the next response. ' + + 'OIDC `nonce` binds an ID Token to an authentication session; ' + + '`c_nonce` binds a proof JWT to a specific issuance request.', + references: [ + { + label: 'OID4VCI 1.0 §7.2.1 (c_nonce)', + href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request', + }, + ], + }, + + proof: { + purpose: + 'A wallet-signed JWT (or similar) that proves the wallet controls ' + + 'the private key the issued credential will be bound to. Sent on the ' + + 'credential request. JWT MUST contain header `typ=' + + 'openid4vci-proof+jwt`, payload claims `iss` (wallet/client ID), ' + + '`aud` (credential issuer identifier), `iat`, `exp`, `nonce` ' + + '(equal to current `c_nonce`), and `cnf.jwk` (the wallet\'s public ' + + 'key the credential will be bound to).', + withoutIt: + 'No holder binding. The issued credential becomes bearer-style — ' + + 'whoever holds the credential file can present it as their own. ' + + 'This breaks the entire trust model of verifiable credentials, ' + + 'where the verifier expects the holder to demonstrate possession ' + + 'of a key tied to the credential.', + attack: + 'Credential lift-and-replay. Without proof, Mallory who steals ' + + 'Alice\'s credential file (laptop backup, exfiltrated wallet ' + + 'database, malicious wallet extension) can present it directly to ' + + 'verifiers as her own credential. Verifiers trusting a non-key-bound ' + + 'credential have no way to detect the swap. With proof-of-possession ' + + 'binding, the credential is bound to Alice\'s wallet key (`cnf.jwk` ' + + 'in the credential), and presentation requires signing a Key Binding ' + + 'JWT with that key — Mallory has the credential but not the key.', + impact: + 'Without holder binding: trivial credential transfer attacks. The ' + + 'OID4VCI proof and the resulting `cnf.jwk` claim in SD-JWT-VC / ' + + 'mDoc credentials are the entire reason verifiable credentials are ' + + '"verifiable" rather than just "signed assertions". Issuer MUST ' + + 'reject credential requests without valid proofs (where ' + + 'proof_types_supported declares them required), MUST verify the ' + + 'proof JWT signature against the embedded `cnf.jwk`, and MUST ' + + 'verify `aud` matches its own credential_issuer identifier.', + references: [ + { + label: 'OID4VCI 1.0 §7.2.1.1 (Proof JWT)', + href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-proof-types', + }, + { + label: 'RFC 7800 (Proof-of-Possession Key Semantics — cnf claim)', + href: 'https://datatracker.ietf.org/doc/html/rfc7800', + }, + ], + }, + + credential: { + purpose: + 'The issuer-signed verifiable credential returned to the wallet. ' + + 'Format depends on `credential_configuration_id` — typically ' + + 'SD-JWT-VC (`dc+sd-jwt`), JWT-VC JSON, JWT-VC JSON-LD, or LDP-VC. ' + + 'Each format has its own signature and integrity model.', + withoutIt: + 'The risk is in *how the wallet validates and stores* the received ' + + 'credential. A wallet that doesn\'t verify the issuer signature, ' + + 'check the credential\'s `iss`/`vct`/`aud` against the issuer ' + + 'metadata, or store the credential in a tamper-evident container ' + + 'collapses the trust chain.', + attack: + '(1) Issuer signature skip — the wallet stores any credential the ' + + 'endpoint returns without verifying the signature against the ' + + 'issuer\'s advertised JWKS. A man-in-the-middle (compromised TLS, ' + + 'rogue CA in some jurisdictions) can substitute credentials. ' + + '(2) Format-specific attacks: SD-JWT disclosure tampering (omit ' + + 'or substitute disclosure values); JSON-LD context expansion ' + + 'attacks (different graphs canonicalize identically); signature ' + + 'algorithm confusion (alg=none, RS256→HS256 — applicable to any ' + + 'signed-JWT credential format).', + impact: + 'Forged or tampered credentials silently accepted into the ' + + 'wallet. Defences: verify issuer signature on receipt; pin ' + + 'expected issuer key and signing algorithms from issuer metadata; ' + + 'use battle-tested format libraries (do not hand-roll SD-JWT ' + + 'disclosure logic); store credentials in OS-level secure ' + + 'storage / TEE where available.', + references: [ + { + label: 'OID4VCI 1.0 §7.3 (Credential Response)', + href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-response', + }, + { + label: 'SD-JWT-VC Draft (IETF)', + href: 'https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/', + }, + { + label: 'W3C Verifiable Credentials Data Model 2.0', + href: 'https://www.w3.org/TR/vc-data-model-2.0/', + }, + ], + }, + + transaction_id: { + purpose: + 'Opaque identifier returned by the issuer when credential issuance ' + + 'cannot complete synchronously (manual review, batch processing, ' + + 'KYC backlog). The wallet polls the deferred-credential endpoint ' + + 'with this ID until the credential is ready.', + withoutIt: + 'Two distinct risks: (1) **Bearer secret** — anyone with the ' + + 'transaction_id can poll the deferred endpoint and retrieve the ' + + 'eventual credential. There is no client authentication on most ' + + 'deferred-poll implementations beyond the transaction_id itself. ' + + '(2) **Polling-timing leak** — the issuer\'s response timing ' + + '("not ready", "not ready", ..., "ready") leaks operational ' + + 'information about how long manual review takes.', + attack: + 'Deferred-credential interception. Mallory steals Alice\'s ' + + '`transaction_id` from any leakage path (logs, leaked backups, ' + + 'malicious wallet extension). She polls the deferred endpoint ' + + 'continuously and retrieves the credential the moment the issuer ' + + 'completes its review — possibly minutes before Alice\'s own wallet ' + + 'gets it. The credential is bound to Alice\'s key (per `cnf.jwk` ' + + 'from the original proof) so Mallory can\'t directly *use* it, but ' + + 'she has now learned: (a) Alice was issued this credential, (b) any ' + + 'metadata in the credential.', + impact: + 'Issuance-event correlation and metadata leak. Defences: short ' + + 'transaction_id lifetime; bind transaction_id to the original ' + + 'access_token (require both on poll); rate-limit polling; consider ' + + 'sender-constrained tokens (DPoP) on the deferred endpoint.', + references: [ + { + label: 'OID4VCI 1.0 §9 (Deferred Credential Endpoint)', + href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-deferred-credential-endpoin', + }, + ], + }, + + format: { + purpose: + 'Identifies the credential format being issued or presented. ' + + 'Common values: `dc+sd-jwt` (SD-JWT-VC, selective disclosure JWT), ' + + '`jwt_vc_json` (JWT-encoded W3C VC), `jwt_vc_json-ld` (JWT-encoded ' + + 'JSON-LD VC), `ldp_vc` (Linked Data Proofs VC), `mso_mdoc` ' + + '(ISO 18013-5 mobile drivers license).', + withoutIt: + 'The risk is *format-confusion attacks*: a verifier configured to ' + + 'accept format A receives a credential labelled format A but ' + + 'actually structured as format B. The verifier\'s parser, expecting ' + + 'A\'s integrity model, may skip checks that B\'s model relies on.', + attack: + 'Format-confusion bypass. Verifier accepts both `jwt_vc_json` and ' + + '`ldp_vc`. Mallory crafts a credential that parses successfully ' + + 'under both formats but encodes different claims under each parser ' + + '(JWT payload says one thing, JSON-LD canonicalisation says ' + + 'another). The verifier\'s integrity check operates on whichever ' + + 'subset of bytes the parser cared about, and Mallory\'s "bad" ' + + 'claims slip through the unverified portion. Variants exist ' + + 'specific to JSON-LD context-expansion ambiguity.', + impact: + 'Credential content manipulation that bypasses integrity checks. ' + + 'Verifiers should accept exactly one format per credential ' + + 'configuration and reject anything ambiguous. Where multiple ' + + 'formats are supported, parse and validate strictly per the ' + + 'declared `format` value, not per "first parser that doesn\'t ' + + 'crash".', + references: [ + { + label: 'OID4VCI 1.0 §11.4 (Credential Format Considerations)', + href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html', + }, + ], + }, +} diff --git a/frontend/src/protocols/explainers/oid4vp.ts b/frontend/src/protocols/explainers/oid4vp.ts new file mode 100644 index 0000000..4dbc14a --- /dev/null +++ b/frontend/src/protocols/explainers/oid4vp.ts @@ -0,0 +1,378 @@ +/** + * OpenID for Verifiable Presentations (OID4VP) — Parameter Explainers + * + * Presentation-side entries: VP token replay, DCQL query manipulation, + * verifier impersonation via client_id_scheme, response_mode confidentiality, + * KB-JWT binding. + * + * Per-protocol overrides: + * `oid4vp:nonce` — VP-binding semantics (different from OIDC ID Token nonce) + * `oid4vp:client_id` — verifier identity with cryptographic scheme prefix + */ + +import type { ParameterExplainer } from './index' + +export const OID4VP_EXPLAINERS: Record = { + vp_token: { + purpose: + 'The Verifiable Presentation token returned by the wallet. Contains ' + + 'one or more credentials (or selective disclosures of them) plus a ' + + 'Holder Binding signature proving the wallet controls the key the ' + + 'credential is bound to. The cryptographic deliverable of the entire ' + + 'OID4VP flow.', + withoutIt: + 'The risk is in *how the verifier validates the vp_token*, not in ' + + 'omitting it. Verifiers commonly skip one of: signature on the issuer ' + + 'credential, KB-JWT signature, nonce binding, audience binding, ' + + 'expiry. Each skip turns a useless captured token into a usable ' + + 'authentication.', + attack: + 'VP Token replay / forwarding. Mallory captures a vp_token from any ' + + 'flow she observes (compromised wallet plugin, captured response on ' + + 'a non-TLS internal hop, leaked log). She replays it to a *different* ' + + 'verifier — or back to the same verifier under a fresh state value. ' + + 'Without strict `nonce` and `aud` validation on the inner Key Binding ' + + 'JWT, the new verifier sees a cryptographically valid presentation ' + + 'and authenticates Mallory as Alice. The VP itself signs over the ' + + 'verifier\'s nonce and audience precisely to prevent this — the ' + + 'attack opens up wherever those checks are weak.', + impact: + 'Authentication bypass via captured-presentation replay. Verifier ' + + 'MUST validate, in order: (1) issuer signature on the credential ' + + 'against issuer\'s JWKS; (2) Holder Binding / KB-JWT signature ' + + 'against `cnf.jwk` in the credential; (3) `nonce` matches the value ' + + 'this verifier sent in the request; (4) `aud` matches this verifier\'s ' + + 'client_id; (5) creation time within an acceptable window; (6) ' + + '`typ=kb+jwt` for SD-JWT KB-JWTs; (7) reject `alg=none`. Skip any ' + + 'one of these and the protocol\'s authentication guarantee evaporates.', + references: [ + { + label: 'OID4VP 1.0 §6 (Authorization Response)', + href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-authorization-response', + }, + { + label: 'SD-JWT §5.4 (Key Binding JWT validation)', + href: 'https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/', + }, + { + label: 'OID4VP 1.0 §11.1 (Verifier Impersonation Threat)', + href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-verifier-impersonation', + }, + ], + }, + + dcql_query: { + purpose: + 'Digital Credentials Query Language object the verifier sends to ' + + 'specify which credentials and which claims it wants the wallet to ' + + 'present. Replaces the older Presentation Exchange `presentation_' + + 'definition` (which is removed from OID4VP draft 26+). Encodes ' + + 'credential type filters, claim path requirements, and disclosure ' + + 'preferences.', + withoutIt: + 'Without DCQL the verifier must rely on `scope` for selection, which ' + + 'is too coarse. With DCQL but without careful query design, two ' + + 'failure modes appear: (1) verifier requests *more* claims than its ' + + 'business purpose actually requires (privacy-leak amplification); ' + + '(2) verifier accepts presentations that match the query schema but ' + + 'satisfy it via attacker-controlled credentials.', + attack: + 'Over-broad-query privacy harvest. A verifier that needs to confirm ' + + 'the user is over 18 issues a DCQL query for the entire driver\'s ' + + 'license credential — full name, address, license number, ' + + 'photograph. The user, trusting the wallet UI, approves. The verifier ' + + 'now has data it had no legitimate business reason to collect, in a ' + + 'cryptographically authenticated package. SD-JWT selective disclosure ' + + 'mitigates this when the query is narrowly written (request only ' + + '`age_over_18`); blown when the query is loose.', + impact: + 'Wallet-displayed consent screens are the user\'s only protection — ' + + 'and most users approve quickly. Verifier-side: write the narrowest ' + + 'DCQL query that satisfies the legitimate purpose. Wallet-side: show ' + + 'the user *exactly* which claims will be disclosed before signing the ' + + 'presentation. Regulator-side: GDPR / data-minimisation principles ' + + 'apply.', + references: [ + { + label: 'OID4VP 1.0 §6.1 (DCQL)', + href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-digital-credentials-query-l', + }, + ], + }, + + client_id_scheme: { + purpose: + 'The cryptographic scheme by which the verifier (Client) authenticates ' + + 'itself to the wallet. Encoded as a prefix on `client_id` in current ' + + 'OID4VP drafts (e.g. `x509_san_dns:verifier.example.com`, ' + + '`did:web:verifier.example.com`, `verifier_attestation:...`). The ' + + 'scheme tells the wallet how to verify that the request actually came ' + + 'from the named verifier.', + withoutIt: + 'With `redirect_uri` scheme (the legacy default), the wallet has only ' + + 'the URL to identify the verifier — same trust model as OAuth2, which ' + + 'is the gap. Without a cryptographic scheme, the wallet cannot ' + + 'distinguish a legitimate verifier from an attacker who registered a ' + + 'similar-looking domain or hijacked DNS for the request URL.', + attack: + 'Verifier impersonation. Mallory hosts a fake verifier UI at ' + + '`accounts.googel.com` (typo) and sends Alice\'s wallet a ' + + 'presentation request claiming to be `accounts.google.com`. With ' + + '`client_id_scheme=redirect_uri`, the wallet has only the URL to go ' + + 'on — domain typo passes muster. With `x509_san_dns`, the request is ' + + 'signed by a certificate whose SAN MUST contain the claimed domain; ' + + 'the wallet verifies the chain to a trusted root before trusting the ' + + 'verifier identity. Without that, Alice presents her credential to ' + + 'Mallory thinking Mallory is Google.', + impact: + 'Verifier impersonation = credential phishing at scale. Use schemes ' + + 'that ground the verifier identity in something the wallet can ' + + 'cryptographically verify (`x509_san_dns`, `verifier_attestation`, ' + + '`did:web` with proper resolution). The OpenID4VC High Assurance ' + + 'Interoperability Profile (HAIP) 1.0 mandates signed Authorization ' + + 'Requests (JAR) with X.509 cert chains specifically to close this ' + + 'gap.', + references: [ + { + label: 'OID4VP 1.0 §5 (Client Identifier Scheme)', + href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-client-identifier-scheme', + }, + { + label: 'OID4VP 1.0 §11.1 (Verifier Impersonation)', + href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-verifier-impersonation', + }, + { + label: 'OpenID4VC HAIP 1.0 (mandatory signed requests)', + href: 'https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0.html', + }, + ], + }, + + response_mode: { + purpose: + 'Tells the wallet how to deliver the authorization response. ' + + '`direct_post` POSTs `vp_token` + `state` as form parameters to ' + + '`response_uri`. `direct_post.jwt` POSTs an encrypted JWE wrapping a ' + + 'signed inner JWT. Legacy `query` and `fragment` modes return the ' + + 'response in the redirect URL.', + withoutIt: + 'The choice is the entire confidentiality/integrity story for the ' + + 'response. `query`/`fragment` exposes the vp_token to browser ' + + 'history, referer headers, and any JS on the redirect page. ' + + '`direct_post` runs server-to-server but in plaintext over TLS. ' + + '`direct_post.jwt` adds end-to-end encryption so even a TLS-' + + 'terminating proxy at the verifier doesn\'t see the credential ' + + 'contents.', + attack: + 'Response leakage scaled by mode. With `fragment`, Mallory\'s ' + + 'analytics tag on the wallet-callback page reads `location.hash` and ' + + 'exfiltrates the vp_token. With `direct_post`, a TLS-terminating ' + + 'load balancer or compromised CDN at the verifier sees the full ' + + 'response — typically with full SD-JWT disclosures including PII ' + + 'fields the verifier had no business decrypting at the edge. With ' + + '`direct_post.jwt`, the JWE only opens at the verifier\'s actual ' + + 'private key.', + impact: + 'For high-assurance / privacy-sensitive presentations (mDL, ' + + 'health credentials, regulated identity) use `direct_post.jwt` with ' + + 'verifier-key-bound encryption. For lower-stakes attribute checks, ' + + '`direct_post` over TLS is acceptable. Never use `fragment`/`query` ' + + 'for VP responses outside test environments.', + references: [ + { + label: 'OID4VP 1.0 §7 (Response Mode)', + href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-response-modes', + }, + ], + }, + + response_uri: { + purpose: + 'The verifier endpoint to which the wallet POSTs the authorization ' + + 'response when `response_mode=direct_post` or `direct_post.jwt`. ' + + 'Replaces `redirect_uri` for direct_post modes — the wallet does not ' + + 'redirect a browser; it does a server-side POST.', + withoutIt: + 'Same exact-match story as OAuth2 `redirect_uri`. The wallet must ' + + 'verify that `response_uri` is consistent with the verifier identity ' + + '(`client_id_scheme` cryptographic check) — otherwise an attacker ' + + 'who controls the request can redirect responses to their own ' + + 'collection endpoint.', + attack: + 'Response-collection hijack. Mallory crafts a presentation request ' + + 'with `client_id` claiming to be a legit verifier but `response_uri` ' + + 'pointing at her own server. If the wallet does not bind ' + + '`response_uri` to the verifier\'s authenticated identity (via ' + + '`client_id_scheme`), the wallet POSTs the credential to Mallory.', + impact: + 'Credential exfiltration directly to attacker. Defence: wallet MUST ' + + 'verify `response_uri` matches an allowlist tied to the authenticated ' + + 'verifier (e.g. for `x509_san_dns` scheme, verify ' + + '`response_uri` host appears in the certificate\'s SAN). ' + + '`direct_post.jwt` adds defence-in-depth via verifier-key-bound JWE ' + + 'so even hijacked POSTs cannot be decrypted by the attacker.', + references: [ + { + label: 'OID4VP 1.0 §7.1 (response_uri)', + href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-response-modes', + }, + ], + }, + + request_uri: { + purpose: + 'A URL the wallet fetches to obtain the full authorization request ' + + 'as a signed JWT (a JAR — JWT-Secured Authorization Request). ' + + 'Lets verifiers send long, signed, integrity-protected requests via ' + + 'a short URL that fits in a QR code.', + withoutIt: + 'Without JAR, the entire authorization request travels in the QR\'s ' + + 'URL parameters — query-string-tampering attacks become possible if ' + + 'the request reaches the wallet via any intermediate channel that ' + + 'can modify URLs. With JAR, parameters are wrapped in a verifier-' + + 'signed JWT; tampering becomes detectable.', + attack: + 'Request-object tampering. Without JAR, an intermediate (malicious ' + + 'app handling the URL scheme, compromised browser extension, ' + + 'rogue clipboard manager) modifies the request URL to swap ' + + '`response_uri` to an attacker endpoint or relax the DCQL query to ' + + 'disclose more claims. The wallet has no signature to verify, so it ' + + 'trusts the modified request. With JAR fetched via `request_uri`, ' + + 'the wallet validates the verifier\'s signature on the request JWT ' + + 'before processing — modifications fail.', + impact: + 'Use `request_uri` (JAR) for any production OID4VP deployment. ' + + 'Combine with cryptographic `client_id_scheme` so the wallet can ' + + 'verify the signature against a key it actually trusts for the ' + + 'claimed verifier identity. HAIP 1.0 mandates this combination.', + references: [ + { + label: 'OID4VP 1.0 §5 (Authorization Request)', + href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-authorization-request', + }, + { + label: 'RFC 9101 (JAR)', + href: 'https://datatracker.ietf.org/doc/html/rfc9101', + }, + ], + }, + + response: { + purpose: + 'In `response_mode=direct_post.jwt`, the encrypted JWE the wallet ' + + 'POSTs to `response_uri`. Wraps a signed inner JWT (typ=' + + '`oauth-authz-resp+jwt`) which carries `vp_token` and `state`. The ' + + 'JWE is encrypted to a key the verifier published in its client ' + + 'metadata.', + withoutIt: + 'Without the JWE wrapper (i.e. plain `direct_post`), the response ' + + 'is exposed to anything between the wallet and the verifier that ' + + 'terminates TLS — load balancers, WAFs, edge proxies, observability ' + + 'pipelines. Sensitive credential disclosures (PII, biometrics) sit ' + + 'in cleartext in any of those layers.', + attack: + 'Edge-tier credential capture. Mallory works at a CDN provider that ' + + 'does TLS termination for the verifier. Without `direct_post.jwt`, ' + + 'every vp_token she sees in proxy logs includes the user\'s full ' + + 'disclosure set. With `direct_post.jwt`, the JWE is opaque to ' + + 'everything except the verifier\'s actual private key — TLS ' + + 'termination at the edge sees only ciphertext.', + impact: + 'For privacy-sensitive credentials (especially under GDPR / eIDAS 2 ' + + 'high-assurance regimes), plain `direct_post` may not satisfy ' + + '"data minimisation" or "encryption in transit" requirements. ' + + '`direct_post.jwt` is the conformant choice for regulated VP flows. ' + + 'Validate the inner JWT thoroughly: `typ=oauth-authz-resp+jwt`, ' + + '`aud=response_uri`, state consistency, signature.', + references: [ + { + label: 'OID4VP 1.0 §7.2 (direct_post.jwt)', + href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-response-modes', + }, + { + label: 'RFC 7516 (JWE)', + href: 'https://datatracker.ietf.org/doc/html/rfc7516', + }, + ], + }, + + // Per-protocol override: in OID4VP, `nonce` binds the *presentation* to + // the verifier's challenge for this transaction (different scope from + // OIDC's nonce, which binds an ID Token to an authentication session). + 'oid4vp:nonce': { + purpose: + 'A high-entropy random value the verifier generates per presentation ' + + 'request, included in the request, and required to appear in the ' + + 'KB-JWT (Key Binding JWT) inside the vp_token. Ties the *cryptographic ' + + 'proof* of holder possession to *this specific verifier transaction*.', + withoutIt: + 'Without nonce binding, a vp_token is reusable. Captured once, it ' + + 'authenticates the wallet to any verifier that accepts the same ' + + 'credential — the entire selling point of "fresh" cryptographic ' + + 'proof of possession evaporates.', + attack: + 'Cross-verifier vp_token replay. Verifier A is honest. Verifier B is ' + + 'Mallory. Alice presents to A; Mallory is on the network path or ' + + 'operates a proxy and captures the vp_token. Without nonce checking, ' + + 'Mallory replays the captured token to her own infrastructure ' + + '(claiming to be a third verifier C) or to verifier A under a fresh ' + + 'session — and the credential authenticates her. The verifier-issued ' + + 'nonce inside the KB-JWT is the only thing that ties the proof to ' + + '*this* request.', + impact: + 'Authentication bypass via captured-presentation replay across ' + + 'verifiers. Wallet MUST sign over verifier nonce in the KB-JWT. ' + + 'Verifier MUST: (1) generate fresh nonce per request, (2) verify ' + + 'nonce in KB-JWT matches request nonce, (3) reject KB-JWTs older ' + + 'than acceptable window, (4) consume nonce on use. ' + + 'OIDC `nonce` binds an ID Token to an authentication session; ' + + 'OID4VP `nonce` binds a presentation to a verification transaction.', + references: [ + { + label: 'OID4VP 1.0 §11.2 (Nonce Binding)', + href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-replay-of-vp-tokens', + }, + { + label: 'SD-JWT §4.3 (Key Binding JWT)', + href: 'https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/', + }, + ], + }, + + // Per-protocol override: OID4VP client_id is the *verifier identity*, + // typically prefixed with a client_id_scheme, and is cryptographically + // bound by the request signature — a substantively different concept + // from OAuth2's "public application identifier" client_id. + 'oid4vp:client_id': { + purpose: + 'The verifier\'s identifier, typically prefixed by a `client_id_' + + 'scheme` (e.g. `x509_san_dns:verifier.example.com`). Unlike ' + + 'OAuth2 where `client_id` is a public lookup key, in OID4VP the ' + + 'client_id IS the cryptographic identity that the request ' + + 'signature binds to — wallets verify the request signature ' + + 'against keys derived from this identifier.', + withoutIt: + 'A client_id without a verifiable scheme (the legacy `redirect_uri` ' + + 'scheme just uses the URL) gives the wallet no cryptographic basis ' + + 'to authenticate the verifier. Same trust model as web SSO — and ' + + 'the same phishing surface.', + attack: + 'See `client_id_scheme` for the full attack walk-through. Short ' + + 'form: a verifier-impersonation attack hinges on the wallet\'s ' + + 'inability to distinguish "the verifier this request claims to be" ' + + 'from "the verifier whose key signed this request". Cryptographic ' + + 'schemes (`x509_san_dns`, `verifier_attestation`, `did:web`) close ' + + 'the gap; the legacy `redirect_uri` scheme leaves it open.', + impact: + 'Use only schemes that ground the verifier identity in a key the ' + + 'wallet can independently verify. Pair with JAR (`request_uri`) so ' + + 'the request itself is signed. HAIP 1.0 (the eIDAS 2 compliance ' + + 'profile) mandates this combination.', + references: [ + { + label: 'OID4VP 1.0 §5 (client_id and client_id_scheme)', + href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-client-identifier-scheme', + }, + ], + }, +} diff --git a/frontend/src/protocols/explainers/oidc.ts b/frontend/src/protocols/explainers/oidc.ts new file mode 100644 index 0000000..add9584 --- /dev/null +++ b/frontend/src/protocols/explainers/oidc.ts @@ -0,0 +1,543 @@ +/** + * OpenID Connect — Parameter Explainers + * + * OIDC-specific entries plus per-protocol overrides for OAuth2 entries + * whose semantics differ in OIDC (e.g. `oidc:aud` for ID Token audience). + */ + +import type { ParameterExplainer } from './index' + +export const OIDC_EXPLAINERS: Record = { + nonce: { + purpose: + 'A high-entropy random value the client generates per authentication ' + + 'request, persisted in the user\'s session, and required to appear ' + + 'unchanged in the `nonce` claim of the ID Token. Binds the issued ID ' + + 'Token to this specific authentication request — OIDC\'s `nonce` ' + + 'is to the *token* what OAuth\'s `state` is to the *redirect ' + + 'callback*; both are needed.', + withoutIt: + 'Without `nonce`, the client cannot tell whether the ID Token it just ' + + 'received was minted for *this* authentication or replayed from an ' + + 'earlier session. The signature still verifies, the `iss` and `aud` ' + + 'still match — but the token may have been captured weeks earlier and ' + + 'replayed now.', + attack: + 'ID Token Replay. Mallory captures a legitimate ID Token bearing Alice\'s ' + + 'identity from any leakage path — browser history of an implicit-flow ' + + 'redirect, a referer header, a server log, an old session backup. Some ' + + 'time later (still within the token\'s exp window, or against a client ' + + 'with lax exp checks) Mallory injects the captured ID Token into a fresh ' + + 'authentication response landing at the client. Without nonce binding, ' + + 'the client accepts the token as the result of "the authentication that ' + + 'just happened" and signs Mallory in as Alice.', + impact: + 'Account takeover via stale-token replay. Real-world implementation ' + + 'flaw — academic study of OIDC deployments found nonce checking is one ' + + 'of the most commonly broken validation steps because it requires ' + + 'session-state plumbing the developer has to wire up explicitly. RFC ' + + 'requires the client to compare the ID Token\'s nonce claim against ' + + 'the session-stored value before trusting any other claim.', + references: [ + { + label: 'OpenID Connect Core 1.0 §15.5.2 (Nonce Implementation Notes)', + href: 'https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes', + }, + { + label: 'OpenID Connect Core 1.0 §3.1.3.7 (ID Token Validation)', + href: 'https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation', + }, + ], + }, + + id_token: { + purpose: + 'A signed JWT carrying authentication assertions about the end user — ' + + 'the *identity* output of OIDC. Claims include `iss`, `sub`, `aud`, ' + + '`exp`, `iat`, `nonce`, optionally `auth_time`, `acr`, `amr`, `azp`, ' + + '`at_hash`, `c_hash`. Its signature is verified against the OP\'s JWKS.', + withoutIt: + 'The risks are not in receiving an ID Token but in *how it is validated*. ' + + 'A non-trivial percentage of OIDC libraries and integrations ship with ' + + 'one or more validation flaws that a forged token slips through.', + attack: + 'Multiple distinct, repeatedly-CVE\'d patterns: (1) **alg=none** — ' + + 'attacker sets the JWT header to `{"alg":"none"}`, strips the signature, ' + + 'and the library accepts the unsigned token (CVE-2026-31946 OpenOlat ' + + 'OIDC, multiple historical CVEs). (2) **Algorithm confusion (RS256 → ' + + 'HS256)** — server is configured to verify with an RSA public key but ' + + 'the library, on receiving a token with `alg:HS256`, treats the public ' + + 'key as an HMAC secret; attacker who has the public key (it\'s public!) ' + + 'forges arbitrary tokens. (3) **Default-fallback verification** — ' + + 'CVE-2026-28802 in Authlib defaulted to HMAC verification when `alg` ' + + 'was missing or unknown, fail-open. (4) **Skipped claim validation** — ' + + 'signature passes but `iss`, `aud`, `exp`, `nonce` are not checked, ' + + 'turning a valid signature on the wrong token into a successful login.', + impact: + 'Authentication bypass for anyone who can craft a JWT — i.e. anyone, ' + + 'since JWT structure is public. Mitigations: pin allowed `alg` values ' + + 'on the verifying side (do not honour the JWT header\'s alg field for ' + + 'algorithm selection); fail closed on unknown alg; validate the full ' + + 'claim set (iss, aud, exp, nbf, iat-recency, nonce) on every token; ' + + 'use a single battle-tested library, not hand-rolled verification.', + references: [ + { + label: 'OpenID Connect Core 1.0 §3.1.3.7 (ID Token Validation)', + href: 'https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation', + }, + { + label: 'CVE-2026-31946 (OpenOlat OIDC signature skip)', + href: 'https://www.thehackerwire.com/critical-jwt-signature-bypass-in-openolat-openid-connect/', + }, + { + label: 'CVE-2026-28802 (Authlib alg default fail-open)', + href: 'https://www.armosec.io/blog/authlib-cve-2026-28802-jwt-signature-verification-bypass/', + }, + { + label: 'JWT alg confusion (RS256 → HS256)', + href: 'https://medium.com/@instatunnel/jwt-algorithm-confusion-turning-rs256-tokens-into-hs256-disasters-db1923774873', + }, + ], + }, + + at_hash: { + purpose: + 'Access Token Hash claim in the ID Token. Equals BASE64URL(left-half(' + + 'hash(access_token))) using the hash matching the ID Token\'s `alg`. ' + + 'Cryptographically binds the ID Token to the access token delivered ' + + 'alongside it in the same response.', + withoutIt: + 'In flows where both an ID Token and an access token come back through ' + + 'the front-channel (implicit, hybrid), there is nothing else linking the ' + + 'two — an attacker who can substitute the access token in transit can ' + + 'pair an honest ID Token with an attacker-controlled access token, or ' + + 'vice versa.', + attack: + 'Access token substitution in front-channel responses. Mallory runs a ' + + 'malicious browser extension or a script on a redirect-page subresource. ' + + 'When Alice\'s implicit/hybrid response lands in `window.location.hash`, ' + + 'Mallory swaps `access_token=…` for one tied to her own IdP identity ' + + 'while leaving the ID Token (signed) intact. The client validates the ' + + 'ID Token (signature OK) and uses the swapped access token for ' + + 'subsequent UserInfo / API calls — which then return data for *Mallory*, ' + + 'not Alice. Without `at_hash` validation, the swap is undetectable.', + impact: + 'Effective: client logs Alice in (per ID Token) but its API calls run ' + + 'as Mallory — privilege confusion. **Fail-open vulnerabilities** are ' + + 'real: Authlib CVE-2026-28498 silently returned `True` when the ID ' + + 'Token\'s alg was unknown, defeating at_hash validation. Verify ' + + 'libraries fail *closed* on unknown alg.', + references: [ + { + label: 'OpenID Connect Core 1.0 §3.2.2.10 (at_hash)', + href: 'https://openid.net/specs/openid-connect-core-1_0.html#ImplicitTokenValidation', + }, + { + label: 'CVE-2026-28498 (Authlib at_hash/c_hash fail-open)', + href: 'https://advisories.gitlab.com/pkg/pypi/authlib/CVE-2026-28498/', + }, + ], + }, + + c_hash: { + purpose: + 'Code Hash claim in the ID Token, used in the Hybrid Flow. Equals ' + + 'BASE64URL(left-half(hash(code))). Binds the ID Token (delivered ' + + 'immediately on the front-channel) to the authorization code that ' + + 'will later be exchanged on the back-channel.', + withoutIt: + 'Hybrid flow returns a `code` and an `id_token` together in the same ' + + 'redirect. Without `c_hash`, an attacker who can swap one for another ' + + 'in the front-channel can pair an honest ID Token with a substituted ' + + 'code — the OIDC manifestation of the OAuth Authorization Code ' + + 'Injection attack class.', + attack: + 'Code substitution in hybrid flow. Mallory captures her own valid ' + + 'authorization code from the OP, then runs a malicious extension or ' + + 'subresource on the client\'s redirect page. When Alice\'s response ' + + 'arrives, Mallory rewrites `code=…` to her own captured code while ' + + 'leaving the (signed) ID Token unchanged. The client validates the ID ' + + 'Token, reads the rewritten code, exchanges it for tokens at /token — ' + + 'and gets tokens for Mallory\'s identity, which it then links into ' + + 'Alice\'s session.', + impact: + 'Account-linking takeover via swapped code. PKCE additionally ' + + 'prevents the token exchange from succeeding for a code the attacker ' + + 'captured (her verifier does not match). Where PKCE is unavailable, ' + + 'c_hash is the primary defence. Authlib CVE-2026-28498 affects ' + + 'c_hash validation specifically.', + references: [ + { + label: 'OpenID Connect Core 1.0 §3.3.2.11 (c_hash)', + href: 'https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken', + }, + { + label: 'CVE-2026-28498 (Authlib at_hash/c_hash fail-open)', + href: 'https://advisories.gitlab.com/pkg/pypi/authlib/CVE-2026-28498/', + }, + ], + }, + + azp: { + purpose: + 'Authorized Party claim in the ID Token. When the token\'s `aud` ' + + 'contains multiple audiences (or one audience that is not the ' + + 'requesting client), `azp` MUST equal the client_id of the party the ' + + 'token was actually issued to. Lets a recipient distinguish "I am the ' + + 'audience" from "I am the audience the token was minted for".', + withoutIt: + 'In multi-audience deployments (one ID Token shared across a primary ' + + 'app and several backend microservices) the recipient cannot tell ' + + 'which client started the flow. A malicious client in the same audience ' + + 'set can forward an ID Token it received and have a sibling service ' + + 'accept it as if that service had been the original RP.', + attack: + 'Cross-client ID Token reuse. Service A and Service B both list ' + + 'themselves in the AS audience configuration. Mallory operates ' + + 'Service A and persuades Alice to sign in. Mallory takes the resulting ' + + 'ID Token and replays it to Service B, which sees `aud` containing its ' + + 'own client_id and accepts the login. Without `azp` validation Service ' + + 'B has no way to know the token was originally minted for Service A.', + impact: + 'Cross-service identity bleed in shared-audience configurations. ' + + 'Recipients in multi-audience tokens MUST validate `azp == this ' + + 'client_id` in addition to `client_id ∈ aud`. Most production setups ' + + 'avoid the issue by issuing single-audience tokens; the failure mode ' + + 'kicks in when teams "save tokens" by reusing one across services.', + references: [ + { + label: 'OpenID Connect Core 1.0 §2 (azp claim)', + href: 'https://openid.net/specs/openid-connect-core-1_0.html#IDToken', + }, + ], + }, + + prompt: { + purpose: + 'Controls how the OP interacts with the user during authentication. ' + + 'Values: `none` (return immediately without UI — fail if interaction ' + + 'needed), `login` (force re-auth even if a session exists), `consent` ' + + '(force consent re-prompt), `select_account` (account picker).', + withoutIt: + 'The risks are around `prompt=none` specifically. Clients use it for ' + + 'silent re-auth in iframes — refresh an expired session without ' + + 'interrupting the user. The iframe is invisible by design; that same ' + + 'invisibility is a phishing/clickjacking primitive when the OP doesn\'t ' + + 'block framing.', + attack: + 'Silent re-auth clickjacking. Mallory hosts a page containing a hidden ' + + 'iframe pointing at the OP\'s `/authorize?prompt=none&...` for her own ' + + 'malicious client_id. Alice visits Mallory\'s page; if Alice has an ' + + 'active session at the OP, the frame returns tokens to Mallory\'s ' + + 'callback without any visible UI — Alice has no way to notice she just ' + + 'authorized a third-party app. Defence is OP-side: send `X-Frame-' + + 'Options: DENY` and `Content-Security-Policy: frame-ancestors \'none\'` ' + + 'on authorization endpoints, with explicit allowlist for legitimate ' + + 'silent-auth origins.', + impact: + 'Silent token issuance to attacker-controlled clients. Compounds with ' + + 'consent phishing and over-broad scope: a malicious client with ' + + '`offline_access` plus `prompt=none` clickjack gets a refresh token ' + + 'with no user interaction.', + references: [ + { + label: 'OpenID Connect Core 1.0 §3.1.2.1 (Authentication Request)', + href: 'https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest', + }, + { + label: 'OAuth Clickjacking write-up', + href: 'https://melmanm.github.io/misc/2023/10/01/article10-oauth-clickjacking.html', + }, + ], + }, + + email_verified: { + purpose: + 'Boolean claim asserting the OP has verified the user\'s email. ' + + 'Intended for relying parties to use as a signal when linking accounts ' + + 'across providers ("if Google says the email is verified, treat it as ' + + 'authoritative").', + withoutIt: + 'The trap is *trusting* `email_verified` blindly. OPs vary wildly: some ' + + 'always set true; some never set the field; some let users in their ' + + 'tenant set arbitrary unverified email values. The OIDC spec itself ' + + 'says: "ultimately it is unsafe to rely on the Issuer to verify the ' + + 'email of a user."', + attack: + 'nOAuth (Descope, June 2023; ~9% of Entra multi-tenant SaaS apps still ' + + 'vulnerable per 2025 study). Mallory creates her own Entra tenant where ' + + 'she has admin rights. She assigns Alice\'s corporate email address to ' + + 'an account she controls in her tenant. The token Entra mints carries ' + + '`email: alice@corp.com` and (in some configurations) `email_verified: ' + + 'true`. Mallory signs into a target SaaS app via "Sign in with ' + + 'Microsoft" using her tenant. The SaaS app — which matches accounts by ' + + '`email` because it\'s convenient — links Mallory\'s sign-in to ' + + 'Alice\'s existing account. Full account takeover, MFA does not help, ' + + 'EDR does not help, the attack uses Entra exactly as designed.', + impact: + 'Cross-tenant account takeover. Use the immutable `sub` claim (paired ' + + 'with `iss` for tenant scope) for account matching, never `email` or ' + + '`email_verified`. If account linking by email is unavoidable, perform ' + + 'an out-of-band email verification step on the RP side — do not trust ' + + 'the IdP\'s assertion. Microsoft has added domain-verified-email ' + + 'claims in 2024-25 to mitigate; many integrations still use the old ' + + 'pattern.', + references: [ + { + label: 'Descope nOAuth disclosure', + href: 'https://www.descope.com/blog/post/noauth', + }, + { + label: 'Semperis nOAuth Cross-Tenant Takeover', + href: 'https://www.semperis.com/blog/noauth-abuse-alert-full-account-takeover/', + }, + { + label: 'OpenID Connect Core 1.0 §5.7 (Claim Stability)', + href: 'https://openid.net/specs/openid-connect-core-1_0.html#ClaimStability', + }, + ], + }, + + id_token_signing_alg_values_supported: { + purpose: + 'Discovery-document field listing which JWS algorithms the OP will use ' + + 'to sign ID Tokens (e.g. `["RS256", "ES256"]`). Clients use this to ' + + 'configure their verifying side.', + withoutIt: + 'The risk is in *how the client uses this list*. If the client trusts ' + + 'the JWT header\'s own `alg` field to choose the verification ' + + 'algorithm, every JWT-validation attack opens up — alg=none, RS256→' + + 'HS256 confusion, fail-open on unknown alg.', + attack: + 'The discovery field is the *correct* source of truth for which alg ' + + 'the client will accept; the JWT header is the *attacker-controlled* ' + + 'source. A client that selects the verification algorithm from the ' + + 'token header instead of the metadata opens up the full JWT-attack ' + + 'family (alg=none, RS256→HS256 confusion, fail-open on unknown ' + + 'alg). Trust order matters.', + impact: + 'Pin verification to the discovery doc\'s advertised alg(s); reject ' + + 'tokens whose header `alg` is not on that list before doing any ' + + 'cryptographic work. Symmetric algorithms (`HS256`) on this list for ' + + 'a public-key OP are a misconfiguration on their own — the "shared ' + + 'secret" is the OP\'s public key, which is public.', + references: [ + { + label: 'OpenID Connect Discovery 1.0 §3 (Provider Metadata)', + href: 'https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata', + }, + ], + }, + + auth_time: { + purpose: + 'Time of the user\'s last actual authentication at the OP, as a Unix ' + + 'timestamp — when the user last typed a password / completed MFA, ' + + 'not when this token was minted. Used by RPs to enforce step-up: ' + + '"for this sensitive operation, require authentication within the ' + + 'last N seconds".', + withoutIt: + 'A long-lived OP session (the user logged in once two days ago and ' + + 'has been getting silent re-auth ever since) silently authenticates ' + + 'high-assurance operations the user has not consciously approved in ' + + 'days.', + attack: + 'Stale-session privilege escalation. Mallory finds Alice\'s laptop ' + + 'unlocked. Alice signed into the OP days ago and has SSO sessions ' + + 'across the org. Without `auth_time` enforcement on sensitive RPs, ' + + 'Mallory can perform admin/financial actions immediately because the ' + + 'silent-auth flow returns a fresh ID Token with `iat=now` — looks ' + + 'recent — even though the actual user authentication is two days old.', + impact: + 'Sensitive RPs MUST also send `max_age` on the request and verify ' + + '`auth_time + max_age >= now` on the response. Pair with `prompt=login` ' + + 'when re-auth must be visible. Without this discipline the OP\'s ' + + 'long-lived session becomes the weakest link.', + references: [ + { + label: 'OpenID Connect Core 1.0 §2 (auth_time)', + href: 'https://openid.net/specs/openid-connect-core-1_0.html#IDToken', + }, + ], + }, + + sub: { + purpose: + 'Subject identifier — the immutable, unique-per-user (per-OP) string ' + + 'that names the end user. Stable across logins. The ONE identifier the ' + + 'OIDC spec calls out as the right thing to key user accounts on.', + withoutIt: + 'Using `email`, `preferred_username`, or any other user-mutable ' + + 'attribute as the account-matching key is the gap. `sub` is the only ' + + 'claim guaranteed stable, unique, and not user-controllable.', + attack: + 'Cross-tenant identity confusion when RPs key user accounts on a ' + + 'mutable claim (`email`, `preferred_username`) rather than `sub`. An ' + + 'attacker who controls a tenant in a multi-tenant IdP assigns a ' + + 'target user\'s email to her own account; the IdP issues a token ' + + 'where `email` matches the target but `sub` does not. RPs that key ' + + 'on `(iss, sub)` are immune; RPs that key on `email` link the ' + + 'attacker\'s sign-in to the target\'s account.', + impact: + 'Use `(iss, sub)` as the composite primary key for federated accounts. ' + + 'PPID (Pairwise Pseudonymous Identifier) variants exist where ' + + 'different RPs each receive a different `sub` for the same user — ' + + 'good for privacy, fine for account matching as long as the RP ' + + 'remembers its own pair.', + references: [ + { + label: 'OpenID Connect Core 1.0 §5.7 (Claim Stability)', + href: 'https://openid.net/specs/openid-connect-core-1_0.html#ClaimStability', + }, + { + label: 'OpenID Connect Core 1.0 §8 (Subject Identifier Types)', + href: 'https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes', + }, + ], + }, + + issuer: { + purpose: + 'The OP\'s canonical identifier URL, returned in the discovery document ' + + 'and required to match the `iss` claim on every ID Token verbatim. ' + + 'Anchors the chain of trust: discovery → JWKS → token signature → iss ' + + 'comparison.', + withoutIt: + 'If the client trusts whatever issuer the token claims and looks up ' + + 'the JWKS based on the token\'s issuer, an attacker who can host their ' + + 'own discovery document can mint tokens that pass every check.', + attack: + 'AS impersonation via metadata. Mallory registers ' + + '`accounts.googel.com` (typo) and hosts a full discovery document ' + + 'pointing at her own JWKS. She tricks the client (via configuration ' + + 'mistake, DNS hijack, or attacker-controlled federation) into using ' + + 'that issuer. Tokens she mints pass signature, iss, aud, exp checks ' + + '— all derived from her own metadata.', + impact: + 'Pin the expected issuer string at deployment time; never derive it ' + + 'from the token. Always fetch discovery metadata over HTTPS with ' + + 'cert validation. In multi-AS federation, pair issuer pinning with ' + + 'the RFC 9207 `iss` parameter on the authorization response so the ' + + 'mix-up class of attacks is also closed.', + references: [ + { + label: 'OpenID Connect Discovery 1.0 §3 (Issuer Identifier)', + href: 'https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata', + }, + { + label: 'RFC 9207 (Authorization Server Issuer Identification)', + href: 'https://datatracker.ietf.org/doc/html/rfc9207', + }, + ], + }, + + jwks_uri: { + purpose: + 'URL where the OP publishes its current set of public verification ' + + 'keys (JWK Set). Clients fetch this to verify ID Token and (often) ' + + 'access token signatures.', + withoutIt: + 'Two failure modes: (1) Caching stale JWKS — the OP rotated keys ' + + 'hours ago and the cached set no longer contains the active signing ' + + 'key; tokens signed with the new key fail to verify, breaking logins. ' + + '(2) Refreshing too aggressively — every unrecognized `kid` triggers ' + + 'a JWKS refetch, opening a denial-of-wallet / amplification path ' + + 'against the OP and a potential SSRF if `jwks_uri` is not pinned.', + attack: + 'JWKS-driven SSRF. If the client honours `jwks_uri` from the discovery ' + + 'doc without strict allowlisting, an attacker who can poison the ' + + 'discovery cache (or who controls a federation entry) points it at ' + + '`http://169.254.169.254/...` (cloud metadata service) or an internal ' + + 'service. The client fetches "JWKS" from that URL and hands the ' + + 'response back to the attacker via error messages, log inclusion, or ' + + 'side-channel timing.', + impact: + 'Cache JWKS with sane TTL aligned to the OP\'s rotation cadence; on ' + + 'unknown `kid`, refetch *once* with a cooldown — never per-request. ' + + 'Pin `jwks_uri` to the OP\'s domain via configuration; do not trust ' + + 'discovery for endpoint URLs in security-critical paths without ' + + 'allowlisting.', + references: [ + { + label: 'RFC 7517 (JSON Web Key)', + href: 'https://datatracker.ietf.org/doc/html/rfc7517', + }, + { + label: 'OpenID Connect Discovery 1.0 §3 (jwks_uri)', + href: 'https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata', + }, + ], + }, + + kid: { + purpose: + 'Key Identifier in the JWT header. Tells the verifying side which key ' + + 'in the JWKS produced this signature. Enables key rotation: the OP can ' + + 'publish multiple active keys and rotate by changing which `kid` ' + + 'signs new tokens.', + withoutIt: + 'Two attack classes: (1) **Trivial mismatch** — verifier accepts the ' + + 'first key in JWKS regardless of `kid`, defeating rotation\'s ability ' + + 'to revoke a compromised key. (2) **kid-injection** — verifier uses ' + + 'the JWT-supplied `kid` as a lookup key into a filesystem path or SQL ' + + 'query without validation; attacker sets `kid` to `../../etc/passwd` ' + + 'or `\' OR 1=1 --` and gets path traversal / SQLi or, more usefully, ' + + 'points the verifier at a key the attacker controls.', + attack: + 'kid-injection token forgery. Mallory crafts an ID Token with ' + + '`{"alg":"HS256","kid":"../../public/static/file_under_attacker_' + + 'control.txt"}`. The verifier reads the file at that path, treats its ' + + 'contents as the HMAC key, and validates the token. Mallory now has ' + + 'a forged token signed with a "key" of her choosing.', + impact: + 'Treat `kid` as untrusted input — look it up in an in-memory JWKS ' + + 'cache, never use it directly as a path or query value. Reject tokens ' + + 'whose `kid` is not in the current JWKS. Maintain a brief grace window ' + + 'for recently-rotated keys to avoid breaking clients during rotation.', + references: [ + { + label: 'RFC 7515 §4.1.4 (kid Header Parameter)', + href: 'https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4', + }, + { + label: 'PortSwigger — JWT attacks (kid injection)', + href: 'https://portswigger.net/web-security/jwt', + }, + ], + }, + + // Per-protocol override: in OIDC, `aud` carries identity-token semantics + // (must contain client_id) on top of the OAuth2 audience-confusion story. + 'oidc:aud': { + purpose: + 'On an OIDC ID Token, `aud` MUST contain the requesting client\'s ' + + 'client_id. May be a string (single audience) or an array (multiple). ' + + 'When multiple, `azp` indicates the actual authorized party.', + withoutIt: + 'A client that accepts an ID Token without verifying its own ' + + 'client_id appears in `aud` will accept tokens minted for any other ' + + 'client at the same OP. In a multi-tenant SaaS, that means tokens ' + + 'from any tenant\'s sign-in flow can be replayed at any other tenant\'s ' + + 'login endpoint.', + attack: + 'Cross-client token forwarding. A malicious sibling app at the same ' + + 'OP (different client_id, same signing key) forwards an ID Token it ' + + 'received to a target client that does not verify `aud`. The target ' + + 'accepts the login because the signature is valid and iss/exp are ' + + 'fine — unaware that the token was issued for a different RP ' + + 'entirely. When `aud` is a multi-element array, the `azp` claim ' + + 'names the actual authorized party — recipients in multi-audience ' + + 'tokens MUST validate `azp` matches their client_id.', + impact: + 'Always verify `client_id` appears in `aud`. When `aud` is a ' + + 'multi-element array, additionally verify `azp` equals this ' + + 'client_id. RP libraries that accept any signed token from the ' + + 'configured OP are common and consistently exploitable.', + references: [ + { + label: 'OpenID Connect Core 1.0 §3.1.3.7 (ID Token Validation, item 3)', + href: 'https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation', + }, + ], + }, +} diff --git a/frontend/src/protocols/explainers/saml.ts b/frontend/src/protocols/explainers/saml.ts new file mode 100644 index 0000000..6d31237 --- /dev/null +++ b/frontend/src/protocols/explainers/saml.ts @@ -0,0 +1,600 @@ +/** + * SAML 2.0 — Parameter Explainers + * + * XML-based SSO. Parameter names use PascalCase per the SAML wire + * format (`SAMLResponse`, `RelayState`, `NameID`, etc.). Attack surface + * is fundamentally different from JSON-based protocols: XML Signature + * Wrapping (XSW), Golden SAML, XXE, comment injection, signature + * stripping all live here. + */ + +import type { ParameterExplainer } from './index' + +export const SAML_EXPLAINERS: Record = { + SAMLResponse: { + purpose: + 'Base64-encoded XML SAML Response containing one or more Assertions ' + + 'about the authenticated user. Posted by the IdP to the SP\'s ' + + 'Assertion Consumer Service URL after successful authentication. ' + + 'The cryptographic deliverable of the entire SP-Initiated SSO flow.', + withoutIt: + 'The risk is in *how the SP parses and validates* the Response, not ' + + 'in receiving it. SAML\'s XML model is large enough that the ' + + 'parser and the signature verifier can disagree about which bytes ' + + 'matter — and every disagreement is a potential auth bypass.', + attack: + 'XML Signature Wrapping (XSW). The IdP signs a Response containing ' + + 'a legitimate Assertion. Mallory intercepts it, embeds a *second* ' + + 'malicious Assertion (with her chosen NameID, attributes, audience) ' + + 'into the same XML document at a different location, and forwards ' + + 'it to the SP. The signature verifier follows ID/URI references and ' + + 'validates the legitimate Assertion — signature checks pass. The SP\'s ' + + 'business logic, however, walks the DOM and consumes the *first* ' + + 'Assertion it finds (Mallory\'s). Auth bypass with no broken ' + + 'cryptography. Multiple 2024 high-severity CVEs: Ruby-SAML ' + + 'CVE-2024-45409 (CVSS 9.8, full impersonation, GitLab impacted); ' + + 'GitHub Enterprise Server CVE-2024-6800; HaloITSM CVE-2024-6202. ' + + 'Companion attacks: signature stripping (remove the Signature ' + + 'element entirely and the verifier returns "no signature to ' + + 'check" instead of failing), XXE (DTDs enabled by default in many ' + + 'XML parsers — `` reads ' + + 'arbitrary files; CVE-2024-52806 in simplesamlphp), and comment ' + + 'injection (`admin@victim.com@evil.com` — ' + + 'signature canonicalisation and application parser disagree on ' + + 'where the value ends).', + impact: + 'Authentication bypass with full identity impersonation. Defences: ' + + '(1) verify signature *first*, then operate only on the verified ' + + 'subtree — do not re-traverse the DOM; (2) reject Responses ' + + 'containing more than one Assertion or unexpected sibling nodes; ' + + '(3) disable DTD processing in your XML parser; (4) use a battle-' + + 'tested library and keep it patched (XSW research is still ' + + 'discovering new bypasses against widely-deployed libraries — ' + + 'PortSwigger\'s 2026 "Fragile Lock" research broke several).', + references: [ + { + label: 'PortSwigger — The Fragile Lock: Novel XSW Bypasses (2026)', + href: 'https://portswigger.net/research/the-fragile-lock', + }, + { + label: 'CVE-2024-45409 (Ruby-SAML, CVSS 9.8)', + href: 'https://nvd.nist.gov/vuln/detail/CVE-2024-45409', + }, + { + label: 'USENIX 2012 — On Breaking SAML: Be Whoever You Want to Be', + href: 'https://www.usenix.org/system/files/conference/usenixsecurity12/sec12-final91.pdf', + }, + { + label: 'CVE-2024-52806 (simplesamlphp/saml2 XXE)', + href: 'https://security.snyk.io/vuln/SNYK-PHP-SIMPLESAMLPHPSAML2-8449140', + }, + ], + }, + + SAMLRequest: { + purpose: + 'Base64-encoded (and DEFLATE-compressed for HTTP-Redirect binding) ' + + 'XML AuthnRequest or LogoutRequest the SP sends to the IdP. ' + + 'Specifies the SP\'s identity (Issuer), where to deliver the ' + + 'response (AssertionConsumerServiceURL), session controls ' + + '(ForceAuthn, IsPassive), and NameID format preferences.', + withoutIt: + 'A SAML deployment that does not require signed AuthnRequests ' + + '(`AuthnRequestsSigned=false` in IdP-side metadata about the SP) ' + + 'accepts any request claiming to be from the SP. An attacker can ' + + 'forge requests — different impact than forging Responses, but ' + + 'still useful for nuisance, session fixation, or as part of a chain.', + attack: + 'AuthnRequest forgery for session fixation. Mallory crafts an ' + + 'AuthnRequest with `AssertionConsumerServiceURL` pointing at her ' + + 'own collection endpoint (or a legitimate SP endpoint plus a ' + + '`RelayState` she controls). She sends Alice the link. Alice ' + + 'authenticates at her IdP — which produces a real, signed Response ' + + 'and POSTs it to the URL Mallory specified. Without strict ACS-URL ' + + 'allowlisting at the IdP (matched against the SP\'s registered ' + + 'metadata), this completes successfully and Mallory captures the ' + + 'Response. Compounds with weak signature requirements on the ' + + 'request side.', + impact: + 'Response delivery to attacker, plus secondary effects (session ' + + 'fixation, RelayState manipulation). Defence: IdP MUST validate ' + + 'AssertionConsumerServiceURL against the SP\'s pre-registered ' + + 'metadata; SHOULD require signed AuthnRequests for security-' + + 'sensitive deployments (set `WantAuthnRequestsSigned=true`).', + references: [ + { + label: 'SAML 2.0 Core §3.4.1 (AuthnRequest)', + href: 'http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf', + }, + ], + }, + + RelayState: { + purpose: + 'Opaque value (max 80 bytes) carried alongside SAMLRequest / ' + + 'SAMLResponse to preserve SP application state across the SSO round ' + + 'trip. Common uses: original target URL the user wanted before being ' + + 'redirected to login, session correlation tokens, simple deep-link ' + + 'parameters.', + withoutIt: + 'The risk is the convenience pattern of "after login, redirect the ' + + 'user to whatever URL is in RelayState". Without strict allowlisting ' + + 'this turns the SP\'s ACS endpoint into an open redirect on a ' + + 'trusted domain.', + attack: + 'Open redirect via RelayState. Mallory crafts a SAML SSO link where ' + + '`RelayState=https://mallory.example/credentials-page`. Alice clicks, ' + + 'authenticates via the IdP, comes back to the legitimate SP\'s ACS ' + + 'endpoint, and the SP\'s post-auth handler redirects her to ' + + 'Mallory\'s site. Mallory\'s site renders a perfect copy of the ' + + 'original SP\'s login page (the IdP authentication just happened, ' + + 'so the Referer chain looks normal). Real CVEs: Directus ' + + 'CVE-2026-22032, OpenCTI advisory, GitLab CVE-2023-1965 (and its ' + + 'bypass). Bonus: an attacker who can write to RelayState can also ' + + 'plant tracking pixels or cross-origin scripts on the post-auth ' + + 'page.', + impact: + 'Phishing on a trusted-domain Referer chain that bypasses many ' + + 'anti-phishing heuristics. Defence: maintain an allowlist of ' + + 'permitted post-auth redirect targets and validate RelayState ' + + 'against it before issuing any 302. If RelayState carries an ' + + 'arbitrary URL, it must originate from the SP itself (e.g. a ' + + 'signed value) — never from query parameters on the inbound flow.', + references: [ + { + label: 'CVE-2026-22032 (Directus RelayState open redirect)', + href: 'https://www.miggo.io/vulnerability-database/cve/CVE-2026-22032', + }, + { + label: 'Snyk — Common SAML vulnerabilities', + href: 'https://snyk.io/blog/common-saml-vulnerabilities-remediate/', + }, + ], + }, + + Signature: { + purpose: + 'XML Digital Signature element wrapping cryptographic protection ' + + 'over the SAMLRequest, SAMLResponse, or Assertion. Built on ' + + 'XMLDSig — the most footgun-prone signature standard in widespread ' + + 'production use.', + withoutIt: + 'Without signature verification: every Assertion is forgeable. With ' + + 'signature verification *but done wrong*: the protocol *appears* to ' + + 'work but the signature\'s cryptographic guarantee does not transfer ' + + 'to the bytes the application logic actually consumes. XMLDSig\'s ' + + 'flexibility (signed subtree referenced by ID, multiple ID ' + + 'attributes, transforms, canonicalisations) is the source of nearly ' + + 'every SAML auth-bypass class.', + attack: + 'XML Signature Wrapping (XSW) is the marquee attack — see ' + + '`SAMLResponse` for the full walk-through. Other XMLDSig-specific ' + + 'attacks: (1) **Comment injection** (Duo Security 2018) — ' + + '`admin@victim.evil.com` ' + + 'canonicalises differently than a naive parser reads it, so the ' + + 'signature covers one identity while the application sees another. ' + + '(2) **Algorithm downgrade** — signature uses SHA-1 (long deprecated) ' + + 'or HMAC-SHA1 with a guessable key; attacker recomputes a valid ' + + 'signature. (3) **KeyInfo trust** — verifier extracts the signing ' + + 'cert from the Response\'s own `` element instead of ' + + 'comparing against the trusted IdP cert from metadata; attacker ' + + 'embeds her own cert and the verifier validates against it ' + + 'successfully. (4) **Reference manipulation** — multiple ' + + '`` elements where one signs the legitimate Assertion ' + + 'and another a malicious one; verifier checks the first, ' + + 'application reads the second.', + impact: + 'Signature verification done wrong = no signature verification. ' + + 'Defences: (1) extract the trusted IdP signing certificate from ' + + 'pre-registered metadata, *never* from the Response\'s own KeyInfo; ' + + '(2) reject SHA-1 and other weak algorithms; (3) require exactly ' + + 'one Signature with one Reference covering the expected element; ' + + '(4) operate only on the post-verification subtree — do not re-' + + 'traverse the DOM after verification; (5) fix `xml:id` ambiguities ' + + 'by using whitelisted ID attribute names only.', + references: [ + { + label: 'PortSwigger — The Fragile Lock (2026 XSW research)', + href: 'https://portswigger.net/research/the-fragile-lock', + }, + { + label: 'Duo Security — SAML Comment Injection (2018)', + href: 'https://duo.com/blog/duo-finds-saml-vulnerabilities-affecting-multiple-implementations', + }, + { + label: 'IBM — XML Signature Wrapping Explained', + href: 'https://www.ibm.com/think/topics/xml-signature-wrapping', + }, + ], + }, + + Issuer: { + purpose: + 'EntityID of the party that issued the SAML message. On a Response, ' + + 'this is the IdP\'s Entity ID; on a Request, it is the SP\'s. ' + + 'Anchors the trust chain: SP looks up the Issuer in pre-registered ' + + 'metadata to find the public key for signature verification.', + withoutIt: + 'If the SP looks up trust by something *other* than Issuer (or ' + + 'derives the trust key from the Response itself rather than from ' + + 'metadata), the entire chain collapses. The Issuer claim is the ' + + 'protocol-level statement of "this came from this IdP" — and it is ' + + 'unsigned bytes the verifier reads to *find the key that signs ' + + 'them*, a chicken-and-egg problem solved only by pre-registered ' + + 'trust.', + attack: + 'Golden SAML. Mallory gains administrative access to the IdP server ' + + '(typically AD FS — but Entra ID and Okta have been hit in 2024-25 ' + + 'with variants known as "Silver SAML"). She extracts the IdP\'s ' + + 'private signing certificate. She now mints arbitrary Responses ' + + 'with any `Issuer`, `NameID`, `Conditions`, attribute set she ' + + 'wants — all with valid signatures from the legitimate IdP key. ' + + 'Used in the SUNBURST / SolarWinds intrusion chain to escalate ' + + 'from on-premises AD compromise to persistent cloud access. MFA ' + + 'is bypassed because the forged Response asserts the user was ' + + 'authenticated via "PasswordProtectedTransport" or any ' + + 'AuthnContextClassRef the attacker likes.', + impact: + 'Persistent, MFA-bypassing access to every cloud service the IdP ' + + 'federates to (Microsoft 365, AWS, Salesforce, Workday, …). ' + + 'Detection requires correlating SP-side SAML logins against IdP-side ' + + 'authentication events: a SAML login at an SP without a corresponding ' + + 'authentication event at the IdP is a Golden SAML signal. ' + + 'Remediation: rotate the IdP signing certificate (which invalidates ' + + 'every active session and every forged token), then audit. ' + + 'Prevention: hardware-protect the IdP signing key (HSM), restrict ' + + 'IdP server access aggressively, monitor cert export operations.', + references: [ + { + label: 'CyberArk — Golden SAML Attack Technique (original disclosure)', + href: 'https://www.cyberark.com/resources/threat-research-blog/golden-saml-newly-discovered-attack-technique-forges-authentication-to-cloud-apps', + }, + { + label: 'Microsoft Entra — Understanding and Mitigating Golden SAML', + href: 'https://techcommunity.microsoft.com/blog/microsoft-entra-blog/understanding-and-mitigating-golden-saml-attacks/4418864', + }, + { + label: 'Semperis — Silver SAML (Cloud variant of Golden SAML)', + href: 'https://www.semperis.com/blog/meet-silver-saml/', + }, + ], + }, + + NameID: { + purpose: + 'The user identifier carried in the Subject of an Assertion. Format ' + + 'depends on `NameIDFormat` — `persistent` (stable opaque ID per SP), ' + + '`transient` (single-session pseudonym), `emailAddress`, ' + + '`unspecified`. The SP\'s primary key for matching the SAML user to ' + + 'a local account.', + withoutIt: + 'Two failure modes: (1) **Mutable identifier** — using ' + + '`emailAddress` NameIDFormat for account matching imports the ' + + 'OIDC nOAuth attack class into SAML (cross-tenant impersonation ' + + 'via attacker-controlled email values); (2) **Comment injection** ' + + '— XML parser quirks where signature canonicalisation and ' + + 'application read disagree on the identifier value.', + attack: + 'Comment injection on NameID (Duo Security, 2018). Mallory has a ' + + 'legitimate account `mallory@evil.com` at the same IdP that also ' + + 'authenticates `admin@victim.com`. She authenticates as herself, ' + + 'gets a real signed Response, then alters the Assertion in transit ' + + 'to set `admin@victim.com.evil.com`. The ' + + 'signature verifier canonicalises the XML (removing comments per ' + + 'C14N) and produces a hash that matches the original signature — ' + + 'OR fails to, depending on library and canonicalisation method. ' + + 'In multiple widely-deployed SP implementations (OneLogin, Shibboleth, ' + + 'OmniAuth-SAML, etc.) the application read returns ' + + '`admin@victim.com` while the signature still validates. Result: ' + + 'authenticated as the wrong user.', + impact: + 'Cross-account impersonation against vulnerable SP libraries. ' + + 'Defences: (1) prefer `persistent` NameIDFormat over `emailAddress` ' + + 'for account-matching; (2) match users by `(Issuer, NameID)` — ' + + 'never by attribute claims; (3) use SAML libraries patched against ' + + 'the 2018 comment-injection class; (4) treat NameID as untrusted ' + + 'input until both signature verification and post-canonicalisation ' + + 'identifier extraction agree.', + references: [ + { + label: 'Duo Security — Hacking SAML (Comment Injection)', + href: 'https://duo.com/blog/duo-finds-saml-vulnerabilities-affecting-multiple-implementations', + }, + { + label: 'SAML 2.0 Core §2.2 (NameIDType)', + href: 'http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf', + }, + ], + }, + + Conditions: { + purpose: + 'XML element wrapping the assertion\'s validity constraints: ' + + '`NotBefore`, `NotOnOrAfter` (temporal validity), ' + + '`AudienceRestriction` (which SPs may consume the assertion), ' + + '`OneTimeUse` (single-use marker). The verifier MUST evaluate every ' + + 'condition before trusting the assertion.', + withoutIt: + 'Skipping any condition opens a corresponding attack: skip ' + + '`NotOnOrAfter` and stale assertions become reusable indefinitely; ' + + 'skip `AudienceRestriction` and assertions for one SP work at ' + + 'another; skip `OneTimeUse` and replay attacks succeed silently.', + attack: + 'Assertion replay across the validity window. Mallory captures a ' + + 'legitimate Response — perhaps from a non-TLS internal hop, a ' + + 'malicious browser extension, an HTTP-Redirect binding URL leaked ' + + 'to a server log. Without strict `NotOnOrAfter` enforcement (or with ' + + 'overly generous clock skew), she replays the same Response to the ' + + 'SP minutes or hours later. Without an assertion-ID replay cache, ' + + 'the SP accepts the same Assertion as a fresh login. Compounds with ' + + 'the IdP-Initiated SSO flow where there is no `InResponseTo` to ' + + 'correlate against — replay defence is *only* the temporal window ' + + 'plus replay cache.', + impact: + 'Authentication via captured-Response replay. Defences: (1) ' + + 'reject Responses with `NotOnOrAfter` in the past (small clock-skew ' + + 'tolerance, ~5 minutes max); (2) cache Assertion `ID` values for the ' + + 'duration of the validity window and reject duplicates; (3) require ' + + 'short validity windows (~5 minutes) — long windows are unsafe ' + + 'regardless of caching; (4) for IdP-Initiated SSO, treat replay-' + + 'cache as mandatory because there is no `InResponseTo` cross-check.', + references: [ + { + label: 'SAML 2.0 Core §2.5 (Conditions)', + href: 'http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf', + }, + ], + }, + + AudienceRestriction: { + purpose: + 'A child of `Conditions` listing one or more `` URIs. ' + + 'The Assertion is valid *only* when consumed by an SP whose ' + + 'EntityID appears in the list. SAML\'s equivalent of the JWT ' + + '`aud` claim — same concept, same mitigation responsibilities ' + + 'on the receiving side.', + withoutIt: + 'If the SP skips the Audience check, every Assertion the IdP ever ' + + 'issued for any SP works at this SP. In a federation with shared ' + + 'IdP across many tenants or apps, that means a malicious app ' + + 'operator (or a compromised co-tenant) can replay assertions ' + + 'collected from their own users at the target SP.', + attack: + 'Cross-SP assertion forwarding. Mallory operates SP A (a low-' + + 'privilege "free utility" service) in the same federation as the ' + + 'high-value SP B. Alice signs into A. Mallory captures the ' + + 'Assertion the IdP issued for A (it arrived at her own server in ' + + 'plaintext form post-decryption). Mallory replays the Assertion to ' + + 'SP B. SP B verifies the IdP signature (valid), the temporal ' + + 'window (valid), but skips Audience check — and authenticates ' + + 'Alice. Mallory now has a session at SP B as Alice without ' + + 'compromising the IdP, MFA, or Alice\'s credentials.', + impact: + 'Cross-SP identity bleed in shared-IdP federations. Defence: every ' + + 'SP MUST verify its own EntityID appears in ``. ' + + 'IdP MUST set `` to the specific requesting SP, ' + + 'never a wildcard or a shared placeholder. Same risk profile and ' + + 'mitigation as JWT `aud` validation.', + references: [ + { + label: 'SAML 2.0 Core §2.5.1.4 (AudienceRestriction)', + href: 'http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf', + }, + ], + }, + + SubjectConfirmation: { + purpose: + 'Element on the Assertion\'s Subject specifying *how* the SP can ' + + 'confirm the Assertion is being presented by the right party. ' + + 'For SP-Initiated SSO via HTTP-POST (the common case), method is ' + + '`bearer` and the `` carries `Recipient` ' + + '(target ACS URL), `NotOnOrAfter` (delivery deadline), and ' + + '`InResponseTo` (the original AuthnRequest ID).', + withoutIt: + 'Two specific checks lift here: `InResponseTo` ties the Response ' + + 'to a request *this* SP started (defence against unsolicited ' + + 'response injection in SP-Initiated SSO); `Recipient` ties the ' + + 'Response to *this* SP\'s ACS URL (defence against cross-SP ' + + 'forwarding).', + attack: + 'Unsolicited Response injection into SP-Initiated SSO. The SP\'s ' + + 'ACS endpoint accepts any well-formed POST. Mallory captures a ' + + 'legitimate Response (or generates one with a different ' + + 'InResponseTo) and injects it into Alice\'s browser session at ' + + 'the target SP. Without `InResponseTo` validation, the SP accepts ' + + 'the Response as if it had requested it. Variant: replay across ' + + 'SPs by skipping `Recipient` validation. ' + + 'IdP-Initiated SSO is structurally exposed to this class — it has ' + + 'no `InResponseTo` to validate against (the SP never sent a ' + + 'request), so the only replay defence is the assertion-ID cache.', + impact: + 'Authentication bypass via cross-flow Response injection. ' + + 'Defences: SP-Initiated SSO MUST validate `InResponseTo` against ' + + 'the AuthnRequest ID stored in the user\'s session at request time ' + + '(reject if mismatch or missing). MUST validate `Recipient` ' + + 'matches this SP\'s ACS URL exactly. For IdP-Initiated SSO, ' + + 'consider whether the security trade-off justifies the convenience — ' + + 'many SPs disable IdP-Initiated entirely.', + references: [ + { + label: 'SAML 2.0 Core §2.4 (Subject Confirmation)', + href: 'http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf', + }, + { + label: 'SAML 2.0 Profiles §4.1.4.5 (POST Binding Validation)', + href: 'http://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf', + }, + ], + }, + + AssertionConsumerServiceURL: { + purpose: + 'The URL at which the SP receives SAML Responses. Sent on the ' + + 'AuthnRequest by the SP and pre-registered in the SP\'s metadata. ' + + 'The IdP MUST validate it matches a registered ACS URL before ' + + 'sending the Response. SAML\'s analogue of OAuth\'s `redirect_uri` ' + + '— the same exact-match story, same attack class when matching ' + + 'is loose.', + withoutIt: + 'If the IdP doesn\'t strictly validate AssertionConsumerServiceURL ' + + 'against pre-registered SP metadata, the SP\'s authenticated ' + + 'Responses can be redirected to attacker endpoints — exactly the ' + + 'OAuth redirect_uri loose-matching attack class.', + attack: + 'Response redirection attack. Mallory crafts an AuthnRequest using ' + + 'the legitimate SP\'s Issuer but `AssertionConsumerServiceURL` ' + + 'pointing at her own server (`https://victim-sp.example.com.evil.' + + 'com/acs`, or exploiting wildcard/prefix matching in the IdP\'s ' + + 'allowlist). Alice clicks the link, authenticates at the IdP, the ' + + 'IdP signs a Response and POSTs it to Mallory\'s endpoint. Mallory ' + + 'now has a fully-signed valid Response for Alice, which she can ' + + 'replay (within the validity window) to the legitimate SP.', + impact: + 'Response delivery to attacker, leading to assertion replay against ' + + 'the legitimate SP. Defence: IdP MUST require exact-match against ' + + 'pre-registered ACS URLs in SP metadata — no prefix matching, no ' + + 'wildcards, no derived URLs. SP metadata SHOULD register ACS URLs ' + + 'narrowly (one per binding, not a wildcard pattern).', + references: [ + { + label: 'SAML 2.0 Profiles §4.1.4.1 (AssertionConsumerServiceURL)', + href: 'http://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf', + }, + ], + }, + + entityID: { + purpose: + 'A globally unique identifier (typically a URL) for an SP or IdP ' + + 'in a SAML federation. Anchors all metadata — the IdP looks up the ' + + 'SP\'s `entityID` in its trust store to find ACS URLs, supported ' + + 'NameIDFormats, signing certs, and so on.', + withoutIt: + 'Two failure modes around the metadata document itself, not the ' + + 'entityID per se: (1) **Metadata MITM** — fetching SP/IdP metadata ' + + 'over HTTP (or HTTPS without certificate pinning) lets an attacker ' + + 'substitute the trust anchors during initial federation setup; ' + + '(2) **Metadata XXE** — SP-provided metadata XML processed by the ' + + 'IdP with DTDs enabled is a parser-side vulnerability surface ' + + 'before any signature is verified.', + attack: + 'Federation-onboarding MITM. The SP and IdP team are setting up a ' + + 'new federation. They exchange metadata URLs over email. Mallory, ' + + 'who has compromised the network path or the email channel, ' + + 'substitutes her own metadata at the URL the IdP fetches — ' + + 'including her own signing certificate. The IdP, trusting the ' + + 'metadata fetched at federation-setup time, now treats Mallory\'s ' + + 'cert as the SP\'s. Mallory can then forge SP-side requests at ' + + 'will (less impactful than Golden SAML, but a foothold). Combined ' + + 'with XXE in metadata XML processing (CVE-2024-52806, ' + + 'CVE-2017-1000452, others), a malicious metadata document at ' + + 'parse time can read files / SSRF before signature checks even run.', + impact: + 'Federation trust subversion at onboarding. Defences: (1) fetch ' + + 'metadata over HTTPS with strict certificate validation; (2) ' + + 'verify metadata XML signatures (yes, metadata can itself be ' + + 'signed and SHOULD be in production federations); (3) disable ' + + 'DTD/external-entity processing in the XML parser; (4) for ' + + 'high-assurance federations (eduGAIN, government), use a federation ' + + 'metadata aggregator that signs the entire metadata bundle.', + references: [ + { + label: 'SAML 2.0 Metadata §2.3 (entityID)', + href: 'http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf', + }, + { + label: 'CVE-2024-52806 (simplesamlphp/saml2 XXE)', + href: 'https://security.snyk.io/vuln/SNYK-PHP-SIMPLESAMLPHPSAML2-8449140', + }, + ], + }, + + WantAssertionsSigned: { + purpose: + 'Boolean attribute on the SP\'s `` in metadata ' + + 'declaring whether the SP requires the IdP to sign Assertions ' + + '(in addition to or instead of signing the enclosing Response).', + withoutIt: + '`WantAssertionsSigned=false` (often the default) means the SP ' + + 'accepts an unsigned Assertion as long as the enclosing Response ' + + 'is signed. That sounds equivalent — and it is the *single biggest ' + + 'configuration trap in SAML deployments*. With only the Response ' + + 'signed, XSW attacks become trivial: the attacker swaps the ' + + 'Assertion inside an unchanged Response wrapper and the signature ' + + 'still verifies (because the Response\'s signature didn\'t cover the ' + + 'Assertion bytes).', + attack: + 'XSW exploiting Response-only signing. The IdP signs only the ' + + '`` element, not the inner ``. Mallory ' + + 'intercepts a legitimate Response and replaces the Assertion with ' + + 'one she crafted (her chosen NameID, her chosen attributes). The ' + + 'Response\'s signature still references the Response element — ' + + 'which the verifier validates successfully — but the Assertion ' + + 'inside is now Mallory\'s. Application logic reads the swapped ' + + 'Assertion and authenticates her as anyone she wants. This is the ' + + 'low-effort XSW variant — no clever DOM manipulation needed, just ' + + '"signing the wrong element".', + impact: + 'Configuration-driven authentication bypass. Defences: every SP ' + + 'metadata MUST declare `WantAssertionsSigned=true`. Every IdP ' + + 'producing Responses for production SPs MUST sign the Assertion ' + + '(in addition to or instead of the Response). Auditing tip: parse ' + + 'every SP\'s `SPSSODescriptor` in your federation and flag any ' + + 'with `WantAssertionsSigned=false` or missing.', + references: [ + { + label: 'SAML 2.0 Metadata §2.4.2 (WantAssertionsSigned)', + href: 'http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf', + }, + { + label: 'WorkOS — Fun with SAML SSO Footguns', + href: 'https://workos.com/blog/fun-with-saml-sso-vulnerabilities-and-footguns', + }, + ], + }, + + NameIDPolicy: { + purpose: + 'On the AuthnRequest, the SP\'s preference for which NameIDFormat ' + + 'the IdP should use in the resulting Assertion. Common values: ' + + '`urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` (stable ' + + 'pseudonym per SP), `transient` (single-session pseudonym), ' + + '`emailAddress`, `unspecified`.', + withoutIt: + 'Choosing `emailAddress` or `unspecified` as the account-matching ' + + 'identifier opens cross-tenant impersonation: in multi-tenant ' + + 'IdPs (Entra ID, multi-domain ADFS) where users in different ' + + 'tenants can share email values, an attacker-controlled tenant ' + + 'can mint Assertions claiming any email address — and SPs ' + + 'matching by email link the attacker into the legitimate user\'s ' + + 'account. The same pattern in OIDC is known as the nOAuth attack ' + + '(Descope, 2023); the SAML version has the same shape.', + attack: + 'Cross-tenant identifier confusion. Mallory operates her own ' + + 'tenant in a multi-tenant IdP federated with the target SP. She ' + + 'creates a user in her tenant with email ' + + '`alice@victim-corp.com`. She authenticates and the IdP issues a ' + + 'signed Assertion with that email as NameID. The SP, matching ' + + 'users by email, links Mallory\'s sign-in to Alice\'s legitimate ' + + 'account. Cryptography is fine; the failure is in trusting a ' + + 'mutable identifier as a primary key.', + impact: + 'Cross-tenant account takeover. Defences: SP MUST request and ' + + 'match by `persistent` NameIDFormat — the IdP issues a stable ' + + 'opaque pseudonym unique to (this user, this SP), with no ' + + 'cross-tenant collision. Use `(Issuer, NameID)` as the composite ' + + 'primary key. `transient` is for stateless single-session use; ' + + '`emailAddress` is for non-security-critical applications.', + references: [ + { + label: 'SAML 2.0 Core §3.4.1.1 (NameIDPolicy)', + href: 'http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf', + }, + { + label: 'Descope nOAuth disclosure (same pattern, OIDC context)', + href: 'https://www.descope.com/blog/post/noauth', + }, + ], + }, +} diff --git a/frontend/src/protocols/explainers/scim.ts b/frontend/src/protocols/explainers/scim.ts new file mode 100644 index 0000000..5fe219f --- /dev/null +++ b/frontend/src/protocols/explainers/scim.ts @@ -0,0 +1,505 @@ +/** + * SCIM 2.0 — Parameter Explainers + * + * REST/JSON identity-provisioning protocol. Different category from + * authentication protocols: SCIM is a CRUD API for User and Group + * resources, with PATCH semantics, filter queries, and bulk operations. + * Attack surface revolves around identifier confusion (CVE-2025-41115 + * Grafana externalId, CVSS 10.0), PATCH path traversal, filter + * injection, and over-privileged long-lived bearer tokens. + */ + +import type { ParameterExplainer } from './index' + +export const SCIM_EXPLAINERS: Record = { + userName: { + purpose: + 'The SCIM standard human-readable identifier for a User resource. ' + + 'Required, unique within the tenant, often used for sign-in. ' + + 'Comparable to a username — *mutable*, set by the SCIM client at ' + + 'create time, may be updated.', + withoutIt: + 'The trap is using `userName` as the SP-side primary key for the ' + + 'user record. `userName` is mutable: an IdP-driven rename through ' + + 'PATCH changes it. If the SP keys account records on userName, a ' + + 'rename either silently creates a new account (orphaning the old ' + + 'one with its data) or collides with another user\'s old userName ' + + 'still cached somewhere.', + attack: + 'Account-takeover via username recycling. Alice\'s account is ' + + 'deleted (perhaps after she leaves the company). Months later, ' + + 'the SCIM client provisions a new user `bob` and later renames ' + + 'them to `alice` (now-free username). The SP, keying by ' + + 'userName, links Bob to Alice\'s historical account state — ' + + 'which may include retained data, stale group memberships, or ' + + 'session artifacts. Variant: case/Unicode normalization mismatch ' + + 'between SCIM client and SP (`Alice` vs `alice` vs `Аlice` with ' + + 'Cyrillic А) lets an attacker provision a visually-identical ' + + 'second account.', + impact: + 'Use the server-assigned `id` (immutable) as the account primary ' + + 'key, never `userName`. SCIM RFC 7643 §4.1.1 explicitly says ' + + 'userName MAY be changed; account-record stability requires ' + + 'binding to `id`. Apply Unicode normalization (NFKC) and ' + + 'case-folding consistently when comparing.', + references: [ + { + label: 'RFC 7643 §4.1.1 (userName)', + href: 'https://datatracker.ietf.org/doc/html/rfc7643#section-4.1.1', + }, + ], + }, + + externalId: { + purpose: + 'A *client-assigned* identifier the SCIM client (typically the ' + + 'IdP) uses to correlate its own user record with the SCIM ' + + 'server\'s. The SCIM server stores it but treats it as opaque. ' + + 'Caller-controlled, free-form, optional.', + withoutIt: + 'The trap is *trusting* `externalId` as anything more than an ' + + 'opaque correlation token. SCIM servers that map externalId ' + + 'into authorization-relevant identifiers (internal user IDs, ' + + 'role names, group memberships) hand the SCIM client a privilege-' + + 'escalation primitive.', + attack: + 'CVE-2025-41115 (Grafana Enterprise, CVSS 10.0, Nov 2025). ' + + 'Grafana\'s SCIM provisioning code mapped the caller-supplied ' + + '`externalId` directly to the internal `user.uid`. An attacker ' + + 'with access to a SCIM client crafts a provisioning request with ' + + '`externalId: "1"` — which is the UID of Grafana\'s built-in ' + + 'admin account. The provisioned user is silently linked to admin; ' + + 'the attacker now logs in as administrator without ever ' + + 'authenticating through the standard login flow. CVSS 10.0 — full ' + + 'remote impersonation, no authentication required beyond SCIM ' + + 'client access. Affected versions 12.0.0–12.2.1; patched ' + + '2025-11. Pattern is broader than Grafana: any SCIM server that ' + + 'uses externalId for internal mapping has the same shape.', + impact: + 'Maximum-severity privilege escalation when externalId is ' + + 'naively trusted. Defences: (1) treat externalId as opaque — ' + + 'never use it as an authoritative identifier internally; (2) ' + + 'reject externalId values that are syntactically internal IDs ' + + '(numeric where IDs are numeric, UUID-formatted where IDs are ' + + 'UUIDs); (3) audit how externalId flows through your provisioning ' + + 'code path — anywhere it ends up in a database column other than ' + + 'a dedicated `external_id` field is a finding.', + references: [ + { + label: 'CVE-2025-41115 (Grafana Enterprise SCIM, CVSS 10.0)', + href: 'https://thehackernews.com/2025/11/grafana-patches-cvss-100-scim-flaw.html', + }, + { + label: 'SOC Prime — CVE-2025-41115 deep dive', + href: 'https://socprime.com/blog/cve-2025-41115-vulnerability/', + }, + { + label: 'RFC 7643 §3.1 (externalId)', + href: 'https://datatracker.ietf.org/doc/html/rfc7643#section-3.1', + }, + ], + }, + + id: { + purpose: + 'Server-assigned, immutable, globally-unique identifier for a ' + + 'SCIM resource. Returned in the `Location` header on create. The ' + + 'authoritative key for a resource — every subsequent operation ' + + 'targets the resource via `/Users/{id}` URL path.', + withoutIt: + 'IDOR (Insecure Direct Object Reference) is the canonical risk: ' + + 'allowing the caller to influence which `id` is operated on by ' + + 'reading the `id` from request *body* rather than the URL path.', + attack: + 'Body-vs-URL ID override (Keycloak SCIM PUT IDOR, GitHub issue ' + + '#46658). The handler reads `/Users/{id-A}` from the URL to check ' + + 'authorization ("can the caller modify resource A?") but then ' + + 'reads the `id` field from the request body and updates resource ' + + 'B. Mallory has permission for her own user A but crafts a PUT ' + + 'with `id: ""` in the body — server authorizes against ' + + 'A, modifies admin. SCIM\'s structure (id appears in both URL and ' + + 'body) makes this mistake easy to miss in code review.', + impact: + 'Cross-resource modification with bypassed authorization. ' + + 'Defences: (1) authorize against the URL-path id; (2) ignore body ' + + '`id` fields entirely on PUT/PATCH (RFC 7643 §3.1: id is ' + + 'read-only); (3) reject requests where body `id` differs from ' + + 'URL-path id with 400 Bad Request.', + references: [ + { + label: 'Keycloak Issue #46658 (SCIM PUT IDOR)', + href: 'https://github.com/keycloak/keycloak/issues/46658', + }, + { + label: 'RFC 7643 §3.1 (id is server-assigned and immutable)', + href: 'https://datatracker.ietf.org/doc/html/rfc7643#section-3.1', + }, + ], + }, + + op: { + purpose: + 'PATCH operation type: `add` (insert/append), `replace` (overwrite), ' + + '`remove` (delete). One of three operations applied at a JSON ' + + 'Pointer `path` on a target resource. SCIM PATCH wraps these in a ' + + 'PatchOp document with multiple operations applied transactionally.', + withoutIt: + 'Each `op` value has different authorization requirements. SCIM ' + + 'servers that authorize at "can the caller PATCH this resource at ' + + 'all?" granularity but not at "can they replace `groups`?" level ' + + 'allow privilege escalation through legitimate-looking PATCHes.', + attack: + 'PATCH op-specific privilege escalation. Mallory has SCIM client ' + + 'access scoped to "manage user profile fields" (intended: name, ' + + 'email). She crafts a PATCH with ' + + '`op=add, path=groups, value=[{value: "admins"}]` — adding herself ' + + 'to a privileged group. The SCIM server authorizes the PATCH as a ' + + 'profile update because the *resource* is a user profile, missing ' + + 'that the *attribute path* `groups` is privilege-relevant. ' + + 'Variant: `op=replace, path=active, value=true` to reactivate a ' + + 'disabled account.', + impact: + 'Privilege escalation via attribute-path-aware authorization gap. ' + + 'Defences: (1) authorization MUST be per-attribute, not per-' + + 'resource; (2) maintain an allowlist of paths each caller may ' + + 'modify; (3) treat `groups`, `active`, `roles`, ' + + '`entitlements`, anything role-relevant as security-critical paths ' + + 'requiring elevated permission.', + references: [ + { + label: 'RFC 7644 §3.5.2 (PatchOp)', + href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-3.5.2', + }, + ], + }, + + path: { + purpose: + 'JSON Pointer-like path identifying which attribute the PATCH ' + + 'operation targets. May include filters: ' + + '`emails[type eq "work"].value`, `members[value eq "abc"]`. Drives ' + + 'most of SCIM PATCH\'s expressive power — and most of its attack ' + + 'surface.', + withoutIt: + 'Without strict path validation, attackers craft paths that reach ' + + 'attributes the caller has no business modifying. SCIM\'s nested-' + + 'attribute syntax (filters within paths) lets a single PATCH ' + + 'operation surgically modify deeply-nested resource state.', + attack: + 'PATCH path traversal to admin-relevant attributes. Mallory\'s ' + + 'authorization is "edit own profile". She crafts PATCH paths ' + + 'targeting attributes outside her permitted set: ' + + '`groups[display eq "Administrators"]`, `meta.resourceType`, ' + + 'extension-schema attributes that map to backend role assignments. ' + + 'Sloppy SCIM servers parse the path and execute the operation ' + + 'without checking whether the *attribute* is in the caller\'s ' + + 'permitted set. Variant: empty / null path (`{op: "replace", ' + + 'value: }`) replaces the entire resource — used ' + + 'in some SCIM implementations as a backdoor for full-resource ' + + 'updates that bypass per-attribute checks.', + impact: + 'Attribute-level privilege escalation. Defences: (1) parse and ' + + 'normalize paths into structured form before authorization; ' + + '(2) authorize against the resolved attribute, not the raw path ' + + 'string; (3) reject empty/null paths in PATCH operations unless ' + + 'the caller has full-resource write permission; (4) validate ' + + 'against the resource schema — paths to undefined attributes ' + + 'should 400.', + references: [ + { + label: 'RFC 7644 §3.5.2 (path attribute)', + href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-3.5.2', + }, + ], + }, + + value: { + purpose: + 'The data carried by the PATCH operation: scalar for simple ' + + 'attributes, object/array for complex/multi-valued attributes, ' + + 'omitted for `remove`. Type-checked against the schema definition ' + + 'of the attribute named by `path`.', + withoutIt: + 'Without strict schema-bound type validation, callers can submit ' + + '`value` content that exploits the SP\'s downstream processing — ' + + 'classic injection territory whenever SCIM data flows into ' + + 'queries, log entries, email templates, or rendered HTML.', + attack: + 'Stored-XSS or template injection via SCIM `value`. The SCIM ' + + 'client provisions a user with `displayName: ""`. The SP stores it and ' + + 'later renders the displayName unsanitised in an admin UI — ' + + 'every admin viewing the user list executes the attacker\'s ' + + 'script.', + impact: + 'XSS scaled across every admin who views the user list.', + }, + { + id: 'log-injection-via-scim-value', + name: 'Log-injection via newline-bearing values', + scenario: + 'A SCIM `value` containing newline characters and forged log ' + + 'fields gets written to logs that downstream tools parse — ' + + 'spoofing log entries that appear to come from the system.', + impact: + 'Audit-trail forgery; misdirection during incident response.', + }, + { + id: 'sql-nosql-injection-via-scim-value', + name: 'SQL/NoSQL injection via SCIM value', + scenario: + 'If `value` is concatenated into a query for downstream storage ' + + '(rather than parameterized), a crafted value injects SQL or ' + + 'NoSQL operators.', + impact: + 'Database compromise via injection during provisioning.', + }, + ], + mitigations: [ + { + action: + 'Enforce SCIM schema constraints on every attribute (string ' + + 'length, pattern, enum) at write time.', + mitigates: [ + 'stored-xss-via-scim-value', + 'log-injection-via-scim-value', + 'sql-nosql-injection-via-scim-value', + ], + }, + { + action: + 'Sanitise/escape on render — never trust SCIM-stored data as ' + + 'safe HTML.', + mitigates: ['stored-xss-via-scim-value'], + }, + { + action: + 'Never use SCIM data directly in shell, SQL, LDAP, or template ' + + 'expressions without context-appropriate escaping or ' + + 'parameterization.', + mitigates: [ + 'log-injection-via-scim-value', + 'sql-nosql-injection-via-scim-value', + ], + }, + ], references: [ { label: 'RFC 7644 §3.5.2 (value attribute)', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-3.5.2', }, + { + label: 'RFC 7644 §7 (Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-7', + }, ], }, filter: { purpose: 'SCIM\'s SQL-like query language for resource search: ' + - '`userName eq "alice"`, `emails.type eq "work" and active eq true`, ' + + '`userName eq "alice"`, ' + + '`emails.type eq "work" and active eq true`, ' + '`name.familyName sw "Smi"`. Operators include `eq`, `ne`, `co`, ' + '`sw`, `ew`, `pr`, `gt`, `ge`, `lt`, `le`, plus logical `and`, ' + - '`or`, `not`. Carried in URL query string on GET, embedded in ' + - 'PATCH paths, and used in bulk operations.', - withoutIt: - 'SCIM filters are user input that translates to backend storage ' + - 'queries. Without proper translation, the filter language becomes ' + - 'an injection vehicle into the underlying database — SQL injection ' + - 'if the SCIM layer concatenates filter text into SQL, NoSQL ' + - 'injection if into MongoDB query objects, LDAP injection if into ' + - 'LDAP search filters.', - attack: - 'SCIM filter → backend query injection. Filter text ' + - '`userName eq "alice"` should compile to a parameterized backend ' + - 'query. Hand-rolled SCIM implementations sometimes string-' + - 'concatenate the filter into SQL: `SELECT * FROM users WHERE ' + - 'userName = "alice"`. Mallory submits ' + - '`?filter=userName eq "alice" or 1=1 --"` and the resulting SQL ' + - 'becomes `WHERE userName = "alice" or 1=1 --"`, returning every ' + - 'user. Variant for NoSQL: `?filter=userName eq {"$ne":""}` if ' + - 'the SCIM layer JSON-decodes filter fragments. Variant for LDAP: ' + - 'filter values containing `*)(uid=*` confuse LDAP query parsers.', - impact: - 'Mass user enumeration → potential bulk modification if the ' + - 'injection works on PATCH-via-filter operations. Defences: (1) ' + - 'parse the SCIM filter into a typed AST; (2) translate the AST to ' + - 'parameterized backend queries — never string-concatenate filter ' + - 'text into SQL/LDAP/Mongo; (3) impose hard rate limits on filtered ' + - 'GETs; (4) when filters embed in PATCH paths, apply the same ' + - 'parsing and validation.', + '`or`, `not`. Carried in URL query string on GET, embedded in PATCH ' + + 'paths, and used in bulk operations.', + attacks: [ + { + id: 'scim-filter-sql-injection', + name: 'SQL injection via filter concatenation', + scenario: + 'Hand-rolled SCIM implementations sometimes string-concatenate ' + + 'the filter into SQL: `SELECT * FROM users WHERE userName = ' + + '"alice"`. Mallory submits ' + + '`?filter=userName eq "alice" or 1=1 --"` and the resulting SQL ' + + 'becomes `WHERE userName = "alice" or 1=1 --"`, returning every ' + + 'user.', + impact: + 'Mass user enumeration → potential bulk modification if the ' + + 'injection works on PATCH-via-filter operations.', + }, + { + id: 'scim-filter-nosql-injection', + name: 'NoSQL injection via filter JSON decoding', + scenario: + '`?filter=userName eq {"$ne":""}` if the SCIM layer JSON-decodes ' + + 'filter fragments into MongoDB operator objects.', + impact: + 'Authentication / authorization bypass in NoSQL-backed SCIM ' + + 'servers.', + }, + { + id: 'scim-filter-ldap-injection', + name: 'LDAP injection via filter values', + scenario: + 'Filter values containing `*)(uid=*` confuse LDAP query parsers ' + + 'that translate SCIM filters directly into LDAP search filters.', + impact: + 'LDAP query manipulation enabling enumeration or auth bypass.', + }, + ], + mitigations: [ + { + action: + 'Parse the SCIM filter into a typed AST; translate the AST to ' + + 'parameterized backend queries — never string-concatenate filter ' + + 'text into SQL/LDAP/Mongo.', + mitigates: [ + 'scim-filter-sql-injection', + 'scim-filter-nosql-injection', + 'scim-filter-ldap-injection', + ], + }, + { + action: + 'Impose hard rate limits on filtered GETs to bound enumeration ' + + 'damage even if injection succeeds.', + mitigates: [ + 'scim-filter-sql-injection', + 'scim-filter-nosql-injection', + 'scim-filter-ldap-injection', + ], + }, + { + action: + 'When filters embed in PATCH paths, apply the same parsing and ' + + 'validation — the injection surface is the same.', + mitigates: [ + 'scim-filter-sql-injection', + 'scim-filter-nosql-injection', + 'scim-filter-ldap-injection', + ], + }, + ], references: [ { label: 'RFC 7644 §3.4.2.2 (Filtering)', @@ -291,82 +494,148 @@ export const SCIM_EXPLAINERS: Record = { label: 'OWASP — LDAP Injection Prevention', href: 'https://cheatsheetseries.owasp.org/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.html', }, + { + label: 'RFC 7644 §7 (Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-7', + }, ], }, active: { purpose: - 'Boolean attribute on a User resource indicating whether the ' + - 'account is enabled. The IdP\'s SCIM client typically toggles ' + - '`active=false` to deactivate users on offboarding, and the SP ' + - 'is expected to enforce that flag at sign-in / API access time.', - withoutIt: - 'Two failure modes: (1) **Deactivation race** — the SCIM PATCH ' + - 'completes (returns 200) but propagation to the auth-enforcement ' + - 'layer (session store, cached permissions) is delayed; the user ' + - 'has a window to authenticate even after de-provisioning; (2) ' + - '**Re-activation via PATCH** — same auth-relevant attribute the ' + - '`op` entry warned about, used to bypass admin de-provisioning.', - attack: - 'IdP-de-provisioning bypass via SP session lag. Alice is fired. ' + - 'IdP issues SCIM PATCH `active=false` to the SP at 09:00. SP ' + - 'updates its user record at 09:00:01 but does NOT invalidate ' + - 'Alice\'s active sessions, refresh tokens, or API keys. Alice (or ' + - 'someone with her credentials) continues using the SP for hours ' + - 'or days until the existing tokens naturally expire. Variant: SP ' + - 'caches the `active` flag in a denormalised join table updated ' + - 'asynchronously; the cached value lags behind the SCIM update.', - impact: - 'Persistent access despite de-provisioning. Defences: SCIM ' + - '`active=false` MUST trigger session/token revocation — not just ' + - 'a database flag flip. Refresh tokens, API keys, downstream ' + - 'service sessions all need invalidation. Alert on PATCH ops ' + - 'modifying `active` for unexpected escalation patterns ' + - '(reactivation of recently-deactivated users).', + 'Boolean attribute on a User resource indicating whether the account ' + + 'is enabled. The IdP\'s SCIM client typically toggles `active=false` ' + + 'to deactivate users on offboarding, and the SP is expected to ' + + 'enforce that flag at sign-in / API access time.', + attacks: [ + { + id: 'deactivation-session-lag', + name: 'IdP-de-provisioning bypass via SP session lag', + scenario: + 'Alice is fired. IdP issues SCIM PATCH `active=false` to the SP ' + + 'at 09:00. SP updates its user record at 09:00:01 but does NOT ' + + 'invalidate Alice\'s active sessions, refresh tokens, or API ' + + 'keys. Alice (or someone with her credentials) continues using ' + + 'the SP for hours or days until the existing tokens naturally ' + + 'expire. Variant: SP caches the `active` flag in a denormalised ' + + 'join table updated asynchronously; the cached value lags behind ' + + 'the SCIM update.', + impact: + 'Persistent access despite de-provisioning.', + }, + { + id: 'unauthorized-reactivation', + name: 'Unauthorized reactivation via PATCH', + scenario: + 'A caller with profile-edit permission crafts ' + + '`op=replace, path=active, value=true` to reactivate a disabled ' + + 'account. Same auth-relevant-attribute gap as `op` warned about.', + impact: + 'Disabled accounts re-enabled outside admin oversight.', + }, + ], + mitigations: [ + { + action: + 'SCIM `active=false` MUST trigger session/token revocation — ' + + 'not just a database flag flip. Refresh tokens, API keys, ' + + 'downstream service sessions all need invalidation.', + mitigates: ['deactivation-session-lag'], + }, + { + action: + 'Restrict PATCH access to `active` to admin-class SCIM clients ' + + '(treat as security-critical attribute per `op` mitigations).', + mitigates: ['unauthorized-reactivation'], + }, + { + action: + 'Alert on PATCH ops modifying `active` for unexpected escalation ' + + 'patterns (reactivation of recently-deactivated users).', + mitigates: ['unauthorized-reactivation'], + }, + ], references: [ { label: 'RFC 7643 §4.1.1 (active attribute)', href: 'https://datatracker.ietf.org/doc/html/rfc7643#section-4.1.1', }, + { + label: 'RFC 7644 §7 (Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-7', + }, ], }, bulkId: { purpose: - 'Caller-supplied placeholder identifier within a Bulk request. ' + - 'Lets a single bulk operation reference resources created by ' + - '*earlier* operations in the same bulk: operation 1 creates a ' + - 'group with `bulkId="g1"`, operation 2 adds a member referring to ' + - '`bulkId:g1` — the server resolves it to the actual id once ' + - 'operation 1 completes.', - withoutIt: - 'Two specific failure modes around bulkId resolution: (1) ' + - '**Circular references** — operation A references bulkId B, ' + - 'operation B references bulkId A; servers without cycle detection ' + - 'enter infinite-resolve loops. (2) **bulkId forgery** — caller ' + - 'submits a bulkId-style reference that wasn\'t actually defined ' + - 'in the bulk; server tries to resolve and either fails or, ' + - 'depending on implementation, leaks information about which IDs ' + - 'exist.', - attack: - 'Bulk-ID-driven information disclosure / resource creation race. ' + - 'Mallory submits a bulk request that creates a user with ' + - '`bulkId="probe"` then attempts to read user ' + - '`bulkId:`. Servers that don\'t ' + - 'strictly validate bulkId references (per the SCIM-SDK pattern of ' + - 'returning 409 on circular and 400 on self-reference) may leak ' + - 'whether internal IDs follow predictable patterns, enabling ' + - 'enumeration. Self-reference attack: operation references its own ' + - 'bulkId — without explicit rejection (per SCIM-SDK\'s "invalidValue" ' + - 'response), the server may end up creating a resource that ' + - 'references itself with consequences the schema didn\'t anticipate.', - impact: - 'Mostly information disclosure and DoS via cycle detection ' + - 'failures. Defences: (1) strictly validate every bulkId reference ' + - 'is defined within the same bulk request; (2) reject circular ' + - 'references with 409; (3) reject self-references with 400 ' + - 'invalidValue; (4) cap bulk request size (`failOnErrors` and ' + - 'overall operation count limits).', + 'Caller-supplied placeholder identifier within a Bulk request. Lets ' + + 'a single bulk operation reference resources created by *earlier* ' + + 'operations in the same bulk: operation 1 creates a group with ' + + '`bulkId="g1"`, operation 2 adds a member referring to `bulkId:g1` ' + + '— the server resolves it to the actual id once operation 1 ' + + 'completes.', + attacks: [ + { + id: 'bulkid-circular-reference', + name: 'Circular bulkId references', + scenario: + 'Operation A references bulkId B, operation B references bulkId ' + + 'A. Servers without cycle detection enter infinite-resolve loops.', + impact: + 'Denial of service via cycle exhaustion.', + }, + { + id: 'bulkid-forgery', + name: 'bulkId forgery / undefined reference probing', + scenario: + 'Caller submits a bulkId-style reference that wasn\'t actually ' + + 'defined in the bulk. Server tries to resolve and either fails ' + + 'or, depending on implementation, leaks information about which ' + + 'IDs exist via differential responses. Mallory submits a bulk ' + + 'request that creates a user with `bulkId="probe"` then attempts ' + + 'to read user `bulkId:`.', + impact: + 'Information disclosure (predictable internal ID patterns) → ' + + 'enumeration.', + }, + { + id: 'bulkid-self-reference', + name: 'Self-reference attack', + scenario: + 'Operation references its own bulkId — without explicit rejection ' + + '(per SCIM-SDK\'s "invalidValue" response), the server may end ' + + 'up creating a resource that references itself with consequences ' + + 'the schema didn\'t anticipate.', + impact: + 'State corruption — depends on schema, ranges from minor ' + + 'inconsistency to authorization bypass.', + }, + ], + mitigations: [ + { + action: + 'Strictly validate every bulkId reference is defined within the ' + + 'same bulk request before resolution.', + mitigates: ['bulkid-forgery'], + }, + { + action: 'Reject circular references with HTTP 409 Conflict.', + mitigates: ['bulkid-circular-reference'], + }, + { + action: + 'Reject self-references with HTTP 400 invalidValue (per SCIM-SDK ' + + 'pattern).', + mitigates: ['bulkid-self-reference'], + }, + { + action: + 'Cap bulk request size (`failOnErrors` and overall operation ' + + 'count limits) to bound resource consumption.', + mitigates: ['bulkid-circular-reference', 'bulkid-forgery'], + }, + ], references: [ { label: 'RFC 7644 §3.7 (Bulk Operations)', @@ -376,6 +645,10 @@ export const SCIM_EXPLAINERS: Record = { label: 'SCIM-SDK BulkId Reference Resolving (cycle detection)', href: 'https://github.com/Captain-P-Goldfish/SCIM-SDK/wiki/BulkId-reference-resolving', }, + { + label: 'RFC 7644 §7 (Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-7', + }, ], }, @@ -384,32 +657,48 @@ export const SCIM_EXPLAINERS: Record = { 'Multi-valued attribute on a Group resource listing User IDs (or ' + 'nested Group IDs) belonging to the group. Typically updated via ' + 'PATCH operations on the path `members` (add/remove). Group ' + - 'membership is the standard SCIM mechanism for role assignment.', - withoutIt: - '`members` is the privilege-bearing attribute on Groups. Every ' + - 'PATCH that touches `members` is potentially a role assignment ' + - 'change — and SCIM authorization that doesn\'t treat `members` ' + - 'specially lets group manipulation slip through generic ' + - '"can-modify-Groups" checks.', - attack: - 'Group-membership privilege escalation (Keycloak SCIM-class ' + - 'vulnerability per 2025 SOC Prime advisory). Mallory has SCIM ' + - 'client permission to create or modify Group resources for some ' + - 'legitimate purpose (managing project teams). She PATCHes the ' + - '`Administrators` group with `op=add, path=members, ' + - 'value=[{value: ""}]` — adding herself. The SCIM ' + - 'server authorizes the PATCH because the *resource* (Group ' + - 'Administrators) is in her writable set; it doesn\'t check that ' + - 'modifying *that specific group\'s members* requires elevated ' + - 'authority. She now has admin role through group membership.', - impact: - 'Privilege escalation through group manipulation. Defences: (1) ' + - 'treat privileged groups (admin, root, owner, etc.) as restricted ' + - 'resources requiring elevated SCIM client authority to modify; ' + - '(2) authorize on the *combination* of resource, attribute, AND ' + - 'specific value — adding to Administrators is not the same ' + - 'authorization as adding to ProjectTeamA; (3) audit every ' + - '`members` PATCH that touches privileged groups.', + 'membership is the standard SCIM mechanism for role assignment — so ' + + '`members` is the privilege-bearing attribute on Groups.', + attacks: [ + { + id: 'group-membership-privilege-escalation', + name: 'Group-membership privilege escalation', + scenario: + 'Keycloak SCIM-class vulnerability per 2025 SOC Prime advisory. ' + + 'Mallory has SCIM client permission to create or modify Group ' + + 'resources for some legitimate purpose (managing project teams). ' + + 'She PATCHes the `Administrators` group with ' + + '`op=add, path=members, value=[{value: ""}]` — ' + + 'adding herself. The SCIM server authorizes the PATCH because ' + + 'the *resource* (Group Administrators) is in her writable set; ' + + 'it doesn\'t check that modifying *that specific group\'s ' + + 'members* requires elevated authority.', + impact: + 'Admin role acquisition through group membership manipulation.', + }, + ], + mitigations: [ + { + action: + 'Treat privileged groups (admin, root, owner, etc.) as ' + + 'restricted resources requiring elevated SCIM client authority ' + + 'to modify.', + mitigates: ['group-membership-privilege-escalation'], + }, + { + action: + 'Authorize on the *combination* of resource, attribute, AND ' + + 'specific value — adding to Administrators is not the same ' + + 'authorization as adding to ProjectTeamA.', + mitigates: ['group-membership-privilege-escalation'], + }, + { + action: + 'Audit every `members` PATCH that touches privileged groups; ' + + 'alert on unexpected membership changes.', + mitigates: ['group-membership-privilege-escalation'], + }, + ], references: [ { label: 'RFC 7643 §4.2 (Group Schema)', @@ -419,6 +708,10 @@ export const SCIM_EXPLAINERS: Record = { label: 'SOC Prime — SCIM PATCH escalation (related class)', href: 'https://socprime.com/blog/cve-2025-41115-vulnerability/', }, + { + label: 'RFC 7644 §7 (Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-7', + }, ], }, @@ -426,80 +719,140 @@ export const SCIM_EXPLAINERS: Record = { purpose: 'Query parameter on GET requests selecting which attributes to ' + 'return in the response (projection). Sibling `excludedAttributes` ' + - 'inverts the selection. Lets clients reduce payload size and ' + - 'limit data exposure.', - withoutIt: - 'Two opposing failure modes: (1) **Projection ignored** — server ' + - 'returns full resources regardless of `attributes`, leaking ' + - 'attributes the caller didn\'t need (and may not have been ' + - 'authorized for); (2) **Projection trusted for authorization** — ' + - 'server uses `attributes` to decide what to *check authorization ' + - 'on* rather than what to return, so excluding sensitive attributes ' + - 'from the projection bypasses the check entirely.', - attack: - 'Authorization bypass via attribute exclusion. Server logic: ' + - '"return user record, but only check `groups` ACL if `groups` is ' + - 'in the requested `attributes`". Mallory requests ' + - '`GET /Users/{id}?attributes=name,email` — no `groups` requested, ' + - 'so no `groups` ACL check, and the response also returns the ' + - 'lighter projection. But the ACL was the gating control; without ' + - 'it, even non-privileged callers receive responses on resources ' + - 'they shouldn\'t see at all. Variant: `excludedAttributes=groups` ' + - 'with the same effect.', - impact: - 'Authorization-control bypass via projection abuse. Defences: ' + - '(1) enforce resource-level authorization independent of ' + - 'projection (does the caller have GET permission on this user ' + - 'at all?); (2) treat `attributes` strictly as a response-shaping ' + - 'hint, never as input to authorization decisions; (3) attribute-' + - 'level authorization filters the response *after* the resource-' + - 'level check passes.', + 'inverts the selection. Lets clients reduce payload size and limit ' + + 'data exposure.', + attacks: [ + { + id: 'attributes-projection-auth-bypass', + name: 'Authorization bypass via attribute exclusion', + scenario: + 'Server logic: "return user record, but only check `groups` ACL ' + + 'if `groups` is in the requested `attributes`". Mallory requests ' + + '`GET /Users/{id}?attributes=name,email` — no `groups` ' + + 'requested, so no `groups` ACL check, and the response also ' + + 'returns the lighter projection. But the ACL was the gating ' + + 'control; without it, even non-privileged callers receive ' + + 'responses on resources they shouldn\'t see at all. Variant: ' + + '`excludedAttributes=groups` with the same effect.', + impact: + 'Authorization-control bypass via projection abuse.', + }, + { + id: 'attributes-projection-ignored', + name: 'Projection ignored — over-disclosure', + scenario: + 'Server returns full resources regardless of `attributes`, ' + + 'leaking attributes the caller didn\'t need (and may not have ' + + 'been authorized for).', + impact: + 'Information disclosure beyond caller\'s intent or authorization.', + }, + ], + mitigations: [ + { + action: + 'Enforce resource-level authorization independent of projection ' + + '— "does the caller have GET permission on this user at all?" ' + + 'is checked before projection is considered.', + mitigates: ['attributes-projection-auth-bypass'], + }, + { + action: + 'Treat `attributes` strictly as a response-shaping hint, never ' + + 'as input to authorization decisions.', + mitigates: ['attributes-projection-auth-bypass'], + }, + { + action: + 'Attribute-level authorization filters the response *after* the ' + + 'resource-level check passes — and respects the projection ' + + 'parameter for shaping, not for gating.', + mitigates: [ + 'attributes-projection-auth-bypass', + 'attributes-projection-ignored', + ], + }, + ], references: [ { label: 'RFC 7644 §3.9 (attributes / excludedAttributes)', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-3.9', }, + { + label: 'RFC 7644 §7 (Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-7', + }, ], }, ETag: { purpose: 'Resource version identifier returned in the ETag response header ' + - 'and stored in `meta.version`. Clients send it back via ' + - '`If-Match` / `If-None-Match` request headers to detect concurrent ' + - 'modifications. SCIM\'s optimistic-concurrency-control mechanism.', - withoutIt: - 'Without ETag enforcement, two concurrent PATCH operations on ' + - 'the same resource race: the second write silently overwrites ' + - 'the first. For security-relevant attributes (`active`, ' + - '`groups`), this is a real attack window when the operations ' + - 'have opposing intent.', - attack: - 'Concurrent-PATCH race on `active` toggle. Admin Bob initiates ' + - 'PATCH `active=false` to lock Mallory\'s account at 12:00:00.000. ' + - 'Mallory, knowing she\'s about to be locked out, simultaneously ' + - 'initiates PATCH `active=true` (perhaps via stolen SCIM client ' + - 'credentials with self-management scope). Without ETag/If-Match, ' + - 'the second operation to land wins regardless of which started ' + - 'first. Variant: an inconsistent set of concurrent writes leaves ' + - 'the resource in a partial state (Bob\'s deactivation completes ' + - 'but Mallory\'s group-membership-add to `Administrators` lands ' + - 'after — inactive admin account that gets reactivated on ' + - 'next legitimate PATCH).', - impact: - 'Lost-update class of race conditions on security-relevant ' + - 'state. Defences: (1) require `If-Match` on PATCH/PUT for ' + - 'security-critical attributes (`active`, `groups`, ' + - '`entitlements`); (2) reject 412 Precondition Failed on ETag ' + - 'mismatch; (3) when the SCIM client retries on 412, the ' + - 'application logic must re-evaluate the desired state against ' + - 'the new server state — naive retry-with-same-payload re-' + - 'introduces the race.', + 'and stored in `meta.version`. Clients send it back via `If-Match` / ' + + '`If-None-Match` request headers to detect concurrent modifications. ' + + 'SCIM\'s optimistic-concurrency-control mechanism.', + attacks: [ + { + id: 'concurrent-patch-race-active', + name: 'Concurrent-PATCH race on `active` toggle', + scenario: + 'Admin Bob initiates PATCH `active=false` to lock Mallory\'s ' + + 'account at 12:00:00.000. Mallory, knowing she\'s about to be ' + + 'locked out, simultaneously initiates PATCH `active=true` ' + + '(perhaps via stolen SCIM client credentials with self-management ' + + 'scope). Without ETag/If-Match, the second operation to land ' + + 'wins regardless of which started first.', + impact: + 'Lost-update on security-relevant attribute — Bob\'s lockout ' + + 'silently loses to Mallory\'s reactivation.', + }, + { + id: 'partial-state-from-concurrent-writes', + name: 'Partial state from concurrent writes', + scenario: + 'An inconsistent set of concurrent writes leaves the resource ' + + 'in a partial state: Bob\'s deactivation completes but Mallory\'s ' + + 'group-membership-add to `Administrators` lands after — ' + + 'inactive admin account that gets reactivated on next legitimate ' + + 'PATCH.', + impact: + 'Inconsistent persisted state that bypasses intended security ' + + 'invariants.', + }, + ], + mitigations: [ + { + action: + 'Require `If-Match` on PATCH/PUT for security-critical ' + + 'attributes (`active`, `groups`, `entitlements`).', + mitigates: [ + 'concurrent-patch-race-active', + 'partial-state-from-concurrent-writes', + ], + }, + { + action: + 'Reject 412 Precondition Failed on ETag mismatch — caller must ' + + 'refresh and retry.', + mitigates: ['concurrent-patch-race-active'], + }, + { + action: + 'When the SCIM client retries on 412, the application logic ' + + 'must re-evaluate the desired state against the new server ' + + 'state — naive retry-with-same-payload re-introduces the race.', + mitigates: ['concurrent-patch-race-active'], + }, + ], references: [ { label: 'RFC 7644 §3.14 (ETag / If-Match)', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-3.14', }, + { + label: 'RFC 7644 §7 (Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-7', + }, ], }, } diff --git a/frontend/src/protocols/explainers/spiffe.ts b/frontend/src/protocols/explainers/spiffe.ts index b99f87d..3a3e8e1 100644 --- a/frontend/src/protocols/explainers/spiffe.ts +++ b/frontend/src/protocols/explainers/spiffe.ts @@ -13,35 +13,59 @@ import type { ParameterExplainer } from './index' export const SPIFFE_EXPLAINERS: Record = { spiffe_id: { purpose: - 'A SPIFFE Verifiable Identity Document (SVID) names a workload via ' + - 'a URI: `spiffe://trust-domain/path`. The trust domain (authority) ' + + 'A SPIFFE Verifiable Identity Document (SVID) names a workload via a ' + + 'URI: `spiffe://trust-domain/path`. The trust domain (authority) ' + 'identifies the issuing SPIRE deployment; the path identifies the ' + - 'specific workload within it. Carried in the URI SAN of an X.509-' + - 'SVID or in the `sub` claim of a JWT-SVID.', - withoutIt: - 'Two failure classes around SPIFFE ID parsing and authorization: ' + - '(1) **URI SAN ambiguity** — a certificate may technically have ' + - 'multiple URI SANs; SPIFFE spec says exactly one MUST be a SPIFFE ' + - 'ID, but lax parsers accept the first match anywhere. (2) ' + - '**Authorization-by-prefix** — checking only `spiffe://prod-domain/` ' + - 'as a prefix lets `spiffe://prod-domain/anyone` pass when policy ' + - 'meant `spiffe://prod-domain/specific-service`.', - attack: - 'Multi-URI-SAN smuggling. Mallory obtains a legitimate certificate ' + - 'for some innocuous workload, but at issuance she gets the CA to ' + - 'include both her real SPIFFE ID and a higher-privilege one she ' + - 'wants to impersonate (CA hardening prevents this in SPIRE itself, ' + - 'but bridge implementations and home-grown CAs are looser). The ' + - 'verifier reads "the first URI SAN" and gets her chosen target. ' + - 'Defence: SPIFFE spec MANDATES exactly one URI SAN must be a SPIFFE ' + - 'ID and that\'s the one to use; reject certs with multiple URI ' + - 'SANs that match `spiffe://*`.', - impact: - 'Identity confusion → unauthorized service-to-service access. ' + - 'Authorization MUST match by full SPIFFE ID, not by trust-domain ' + - 'prefix unless trust-domain-wide access is genuinely intended. ' + - 'Use `(trust_domain, path)` as the composite key — never the path ' + - 'alone.', + 'specific workload within it. Carried in the URI SAN of an X.509-SVID ' + + 'or in the `sub` claim of a JWT-SVID.', + attacks: [ + { + id: 'multi-uri-san-smuggling', + name: 'Multi-URI-SAN smuggling', + scenario: + 'A certificate may technically have multiple URI SANs; SPIFFE ' + + 'spec says exactly one MUST be a SPIFFE ID, but lax parsers ' + + 'accept the first match anywhere. Mallory obtains a legitimate ' + + 'certificate for some innocuous workload, but at issuance she ' + + 'gets the CA to include both her real SPIFFE ID and a higher-' + + 'privilege one she wants to impersonate (CA hardening prevents ' + + 'this in SPIRE itself, but bridge implementations and home-grown ' + + 'CAs are looser). The verifier reads "the first URI SAN" and ' + + 'gets her chosen target.', + impact: + 'Identity confusion → unauthorized service-to-service access.', + }, + { + id: 'authorization-by-prefix', + name: 'Authorization-by-prefix', + scenario: + 'Checking only `spiffe://prod-domain/` as a prefix lets ' + + '`spiffe://prod-domain/anyone` pass when policy meant ' + + '`spiffe://prod-domain/specific-service`.', + impact: + 'Privilege escalation across workloads in the same trust domain.', + }, + ], + mitigations: [ + { + action: + 'Reject certificates with more than one SPIFFE-shaped URI SAN. ' + + 'SPIFFE spec MANDATES exactly one URI SAN must be a SPIFFE ID.', + mitigates: ['multi-uri-san-smuggling'], + }, + { + action: + 'Authorization MUST match by full SPIFFE ID, not by trust-domain ' + + 'prefix unless trust-domain-wide access is genuinely intended.', + mitigates: ['authorization-by-prefix'], + }, + { + action: + 'Use `(trust_domain, path)` as the composite key — never the ' + + 'path alone.', + mitigates: ['authorization-by-prefix'], + }, + ], references: [ { label: 'SPIFFE ID specification', @@ -51,84 +75,149 @@ export const SPIFFE_EXPLAINERS: Record = { label: 'SPIFFE X.509-SVID §2 (URI SAN)', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md', }, + { + label: 'SPIFFE X.509-SVID §4 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#4-security-considerations', + }, ], }, trust_domain: { purpose: - 'The authority component of a SPIFFE ID — names the SPIRE ' + - 'deployment (one per organisational unit, environment, or security ' + - 'boundary). All SVIDs in a trust domain are signed by CAs rooted in ' + - 'that domain\'s trust bundle. The trust domain IS the cryptographic ' + + 'The authority component of a SPIFFE ID — names the SPIRE deployment ' + + '(one per organisational unit, environment, or security boundary). ' + + 'All SVIDs in a trust domain are signed by CAs rooted in that ' + + 'domain\'s trust bundle. The trust domain IS the cryptographic ' + 'security boundary in SPIFFE.', - withoutIt: - 'If a verifier accepts SVIDs from any trust domain it has bundles ' + - 'for (without checking *which* trust domain the SVID is from), an ' + - 'SVID minted in one trust domain can authorize actions intended ' + - 'only for another. This is the workload-identity counterpart of ' + - 'cross-tenant attacks in human-identity protocols: the ' + - 'cryptography is fine; the failure is loose authorization scope.', - attack: - 'Trust domain spoofing in federations. The verifier has both ' + - '`spiffe://prod.example` and `spiffe://test.example` trust bundles ' + - 'configured for federation. A workload in `test.example` (where ' + - 'admin access is broad and registration loose) gets an SVID for ' + - '`spiffe://test.example/superuser`. Without strict trust-domain ' + - 'binding in the verifier\'s policy, the test-domain SVID is ' + - 'accepted as if it had come from prod. Authorization rules MUST ' + - 'name the expected trust domain, not just the path.', - impact: - 'Cross-trust-domain authorization bleed. Defence: every authz ' + - 'check matches the full SPIFFE ID (including trust domain); ' + - 'federation should be limited to the minimum cross-domain trust ' + - 'actually required. Treat each trust domain as a separate security ' + - 'principal even when federating — they are not equivalent.', + attacks: [ + { + id: 'trust-domain-spoofing', + name: 'Trust domain spoofing in federations', + scenario: + 'The verifier has both `spiffe://prod.example` and ' + + '`spiffe://test.example` trust bundles configured for federation. ' + + 'A workload in `test.example` (where admin access is broad and ' + + 'registration loose) gets an SVID for ' + + '`spiffe://test.example/superuser`. Without strict trust-domain ' + + 'binding in the verifier\'s policy, the test-domain SVID is ' + + 'accepted as if it had come from prod. The cryptography is fine; ' + + 'the failure is loose authorization scope — the workload-' + + 'identity counterpart of cross-tenant attacks in human-identity ' + + 'protocols.', + impact: + 'Cross-trust-domain authorization bleed.', + }, + ], + mitigations: [ + { + action: + 'Every authz check matches the full SPIFFE ID (including trust ' + + 'domain) — not the path alone. Authorization rules MUST name the ' + + 'expected trust domain.', + mitigates: ['trust-domain-spoofing'], + }, + { + action: + 'Federation should be limited to the minimum cross-domain trust ' + + 'actually required. Treat each trust domain as a separate ' + + 'security principal even when federating — they are not ' + + 'equivalent.', + mitigates: ['trust-domain-spoofing'], + }, + ], references: [ { label: 'SPIFFE Trust Domain and Bundle spec', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md', }, + { + label: 'SPIFFE Trust Domain and Bundle §5 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#5-security-considerations', + }, ], }, selectors: { purpose: 'Workload attributes the SPIRE Agent collects via OS introspection ' + - 'to identify a calling process: `unix:uid:1000`, `unix:path:/usr/' + - 'bin/myapp`, `docker:label:app:myapp`, `k8s:ns:default`, ' + - '`k8s:sa:default`, `k8s:pod-name:myapp-7d8c`. Registration entries ' + - 'are matched by selector subset — entry selectors must all be ' + - 'present in the workload\'s collected selectors.', - withoutIt: - 'Selectors are *only as trustworthy as the OS/container runtime ' + - 'reporting them*. Two failure classes: (1) **Spoofable selectors** ' + - '— Kubernetes node-name is easy to spoof if the agent SVID is ' + - 'stolen; pod label selectors trust whoever can write to the pod ' + - 'spec; (2) **Over-broad selectors** — `unix:uid:0` matches every ' + - 'root process on the node, including any that the attacker can ' + - 'spawn.', - attack: - 'Selector spoofing via container compromise. Mallory compromises ' + - 'one container on a Kubernetes node — say, via a vulnerable ' + - 'sidecar. She launches a process inside that container matching ' + - 'the high-privilege workload\'s selectors (same `unix:path`, ' + - 'forged `docker:label`s by manipulating the container labels). ' + - 'The SPIRE Agent\'s introspection reports the spoofed values to ' + - 'the workload-attestation step; the registration entry matches; ' + - 'and Mallory\'s process is issued an SVID for the legitimate ' + - 'workload. Variant: rogue agent registration via compromised ' + - 'Kubernetes API server → fake service-account tokens → bogus ' + - 'agent → arbitrary workload SVIDs on that "node".', - impact: - 'Workload identity spoofing within a node. Defences: (1) use ' + - 'attestor-specific selectors that the runtime cannot forge from ' + - 'within a container (cgroup paths verified by the kernel; SHA-256 ' + - 'binary checksums; TPM-backed measurements where available); (2) ' + - 'enforce selector specificity — `unix:uid:1000` alone is too ' + - 'broad; combine with `unix:path:/usr/bin/myapp` AND ' + - '`unix:sha256:abc…`; (3) protect the SPIRE Agent\'s identity ' + - 'aggressively — node compromise = all-workloads-on-that-node ' + - 'compromise.', + 'to identify a calling process: `unix:uid:1000`, ' + + '`unix:path:/usr/bin/myapp`, `docker:label:app:myapp`, ' + + '`k8s:ns:default`, `k8s:sa:default`, `k8s:pod-name:myapp-7d8c`. ' + + 'Registration entries are matched by selector subset — entry ' + + 'selectors must all be present in the workload\'s collected ' + + 'selectors. Selectors are *only as trustworthy as the OS/container ' + + 'runtime reporting them*.', + attacks: [ + { + id: 'selector-spoofing-container-compromise', + name: 'Selector spoofing via container compromise', + scenario: + 'Mallory compromises one container on a Kubernetes node — say, ' + + 'via a vulnerable sidecar. She launches a process inside that ' + + 'container matching the high-privilege workload\'s selectors ' + + '(same `unix:path`, forged `docker:label`s by manipulating the ' + + 'container labels). The SPIRE Agent\'s introspection reports the ' + + 'spoofed values to the workload-attestation step; the ' + + 'registration entry matches; and Mallory\'s process is issued ' + + 'an SVID for the legitimate workload.', + impact: + 'Workload identity spoofing within a node.', + }, + { + id: 'over-broad-selector', + name: 'Over-broad selector matches everyone', + scenario: + '`unix:uid:0` matches every root process on the node, including ' + + 'any that the attacker can spawn. K8s node-name selectors are ' + + 'easy to spoof if the agent SVID is stolen; pod label selectors ' + + 'trust whoever can write to the pod spec.', + impact: + 'Identity match too permissive — wrong workload gets the SVID.', + }, + { + id: 'rogue-agent-via-k8s-api', + name: 'Rogue agent registration via compromised Kubernetes API', + scenario: + 'The Kubernetes API server is compromised. Attacker mints fake ' + + 'service-account tokens for any namespace, registers a bogus ' + + 'agent claiming to attest some "node", and now the bogus agent ' + + 'can issue arbitrary workload SVIDs claiming to be "on that node".', + impact: + 'Trust-domain compromise via control-plane compromise.', + }, + ], + mitigations: [ + { + action: + 'Use attestor-specific selectors that the runtime cannot forge ' + + 'from within a container: cgroup paths verified by the kernel, ' + + 'SHA-256 binary checksums, TPM-backed measurements where ' + + 'available.', + mitigates: ['selector-spoofing-container-compromise'], + }, + { + action: + 'Enforce selector specificity — `unix:uid:1000` alone is too ' + + 'broad. Combine with `unix:path:/usr/bin/myapp` AND ' + + '`unix:sha256:abc…`.', + mitigates: [ + 'over-broad-selector', + 'selector-spoofing-container-compromise', + ], + }, + { + action: + 'Protect the SPIRE Agent\'s identity aggressively — node ' + + 'compromise = all-workloads-on-that-node compromise.', + mitigates: ['selector-spoofing-container-compromise'], + }, + { + action: + 'Restrict who can mint Kubernetes service-account tokens; ' + + 'monitor for unexpected agent registrations.', + mitigates: ['rogue-agent-via-k8s-api'], + }, + ], references: [ { label: 'SPIRE Concepts — Workload Attestation', @@ -138,6 +227,10 @@ export const SPIFFE_EXPLAINERS: Record = { label: 'SPIRE Agent attestor plugins', href: 'https://github.com/spiffe/spire/tree/main/pkg/agent/plugin/workloadattestor', }, + { + label: 'SPIFFE Workload API §6 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md#6-security-considerations', + }, ], }, @@ -147,66 +240,101 @@ export const SPIFFE_EXPLAINERS: Record = { 'workload, in nested deployments) authorized to issue this SVID. ' + 'Restricts the entry: only the named parent\'s agent can produce ' + 'matching SVIDs. Pivotal for the agent-as-trusted-attestor model.', - withoutIt: - 'Without `parent_id` constraints, every agent in the trust domain ' + - 'can issue every SVID — collapsing the per-node isolation that ' + - 'limits blast radius after agent compromise.', - attack: - 'Lateral movement after agent compromise. Mallory roots one node ' + - 'in the cluster and obtains its SPIRE Agent\'s SVID. With ' + - '`parent_id` properly scoped, that agent can only issue SVIDs for ' + - 'workloads registered with `parent_id` matching that specific ' + - 'agent — typically just the workloads scheduled to that one node. ' + - 'Without `parent_id` scoping (e.g. registrations using a wildcard ' + - 'parent), Mallory\'s compromised agent can issue SVIDs for *any* ' + - 'workload anywhere in the trust domain.', - impact: - 'Agent compromise = full trust-domain compromise without parent_id ' + - 'scoping; agent compromise = single-node compromise with proper ' + - 'scoping. Always set `parent_id` to a specific agent or specific ' + - 'pre-registered ancestor; do not use wildcard or trust-domain-wide ' + - 'parent IDs in production.', + attacks: [ + { + id: 'lateral-movement-after-agent-compromise', + name: 'Lateral movement after agent compromise', + scenario: + 'Mallory roots one node in the cluster and obtains its SPIRE ' + + 'Agent\'s SVID. With `parent_id` properly scoped, that agent ' + + 'can only issue SVIDs for workloads registered with `parent_id` ' + + 'matching that specific agent — typically just the workloads ' + + 'scheduled to that one node. Without `parent_id` scoping (e.g. ' + + 'registrations using a wildcard parent), Mallory\'s compromised ' + + 'agent can issue SVIDs for *any* workload anywhere in the trust ' + + 'domain.', + impact: + 'Without `parent_id` scoping: agent compromise = full ' + + 'trust-domain compromise. With proper scoping: agent compromise ' + + '= single-node compromise.', + }, + ], + mitigations: [ + { + action: + 'Always set `parent_id` to a specific agent or specific ' + + 'pre-registered ancestor.', + mitigates: ['lateral-movement-after-agent-compromise'], + }, + { + action: + 'Do not use wildcard or trust-domain-wide parent IDs in ' + + 'production registration entries.', + mitigates: ['lateral-movement-after-agent-compromise'], + }, + ], references: [ { label: 'SPIRE Registering Workloads — parent_id', href: 'https://spiffe.io/docs/latest/deploying/registering/', }, + { + label: 'SPIFFE Workload API §6 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md#6-security-considerations', + }, ], }, csr: { purpose: - 'Certificate Signing Request submitted by the agent (on behalf of ' + - 'a workload) to the SPIRE Server. Contains the workload\'s public ' + - 'key. The CSR\'s Subject and SAN fields are *advisory* — the ' + - 'server sets the actual SPIFFE ID from the registration entry, ' + - 'NOT from the CSR.', - withoutIt: - 'The defining property of SPIFFE\'s CSR handling is that the *CSR ' + - 'cannot influence the issued identity*. The server discards Subject/' + - 'SAN fields the workload tries to put in the CSR and uses the ' + - 'registration entry\'s SPIFFE ID instead. Implementations that ' + - 'honour CSR-supplied SANs become identity-forgery primitives.', - attack: - 'CSR-driven identity forgery (against weak implementations only). ' + - 'A non-conformant SPIRE-clone or bridge CA reads the URI SAN from ' + - 'the workload\'s CSR and includes it in the issued cert. Mallory\'s ' + - 'compromised workload submits a CSR claiming `spiffe://domain/' + - 'admin` and gets a cert with that identity, bypassing the ' + - 'registration entry mechanism entirely. SPIRE itself does not have ' + - 'this bug; downstream CAs and integrations sometimes do.', - impact: - 'Identity forgery via CSR field trust. Defences: SPIRE Server ' + - '(and any CA acting in this role) MUST ignore Subject/SAN fields ' + - 'in the CSR and synthesize them from the registration entry. ' + - 'Audit: run a workload that submits a CSR with bogus URI SAN and ' + - 'verify the issued cert has the *registered* identity, not the ' + - 'requested one.', + 'Certificate Signing Request submitted by the agent (on behalf of a ' + + 'workload) to the SPIRE Server. Contains the workload\'s public key. ' + + 'The CSR\'s Subject and SAN fields are *advisory* — the server sets ' + + 'the actual SPIFFE ID from the registration entry, NOT from the CSR. ' + + 'The defining property of SPIFFE\'s CSR handling: the CSR cannot ' + + 'influence the issued identity.', + attacks: [ + { + id: 'csr-driven-identity-forgery', + name: 'CSR-driven identity forgery (against weak implementations)', + scenario: + 'A non-conformant SPIRE-clone or bridge CA reads the URI SAN ' + + 'from the workload\'s CSR and includes it in the issued cert. ' + + 'Mallory\'s compromised workload submits a CSR claiming ' + + '`spiffe://domain/admin` and gets a cert with that identity, ' + + 'bypassing the registration entry mechanism entirely. SPIRE ' + + 'itself does not have this bug; downstream CAs and integrations ' + + 'sometimes do.', + impact: + 'Identity forgery — workload claims arbitrary SPIFFE ID by ' + + 'submitting a crafted CSR.', + }, + ], + mitigations: [ + { + action: + 'SPIRE Server (and any CA acting in this role) MUST ignore ' + + 'Subject/SAN fields in the CSR and synthesize them from the ' + + 'registration entry.', + mitigates: ['csr-driven-identity-forgery'], + }, + { + action: + 'Audit: run a workload that submits a CSR with bogus URI SAN ' + + 'and verify the issued cert has the *registered* identity, not ' + + 'the requested one.', + mitigates: ['csr-driven-identity-forgery'], + }, + ], references: [ { label: 'SPIFFE X.509-SVID §3 (Issuance)', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md', }, + { + label: 'SPIFFE X.509-SVID §4 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#4-security-considerations', + }, ], }, @@ -216,65 +344,115 @@ export const SPIFFE_EXPLAINERS: Record = { 'ID is encoded. SPIFFE spec: exactly ONE URI SAN MUST be a valid ' + 'SPIFFE ID; certs MAY have other (non-SPIFFE) URI SANs but the ' + 'verifier MUST identify the SPIFFE one as the authoritative identity.', - withoutIt: - 'Multi-URI-SAN parsing is the gap. A naive verifier that takes ' + - '"the first URI SAN" or "any URI SAN starting with spiffe://" can ' + - 'be fooled by certs with multiple SPIFFE-shaped URI SANs.', - attack: - 'Multi-URI-SAN attack. A misbehaving CA (or a SPIRE clone bridging ' + - 'to legacy PKI) issues a cert with two URI SANs: ' + - '`spiffe://domain/innocent-service` (the legitimate one Mallory ' + - 'is registered for) and `spiffe://domain/admin-service` (her ' + - 'target). A naive verifier that iterates URI SANs and stops at the ' + - 'first SPIFFE-shaped match returns whichever comes first — and ' + - 'Mallory crafts the cert order to put the privileged ID first.', - impact: - 'Identity confusion. Defences: count SPIFFE-shaped URI SANs; ' + - 'reject certs with more than one. Use SPIFFE\'s reference parsing ' + - 'libraries (go-spiffe, spiffe-rs) which enforce this rule. Do NOT ' + - 'hand-roll certificate-to-SPIFFE-ID parsing.', + attacks: [ + { + id: 'multi-uri-san-attack', + name: 'Multi-URI-SAN attack', + scenario: + 'A misbehaving CA (or a SPIRE clone bridging to legacy PKI) ' + + 'issues a cert with two URI SANs: ' + + '`spiffe://domain/innocent-service` (the legitimate one Mallory ' + + 'is registered for) and `spiffe://domain/admin-service` (her ' + + 'target). A naive verifier that iterates URI SANs and stops at ' + + 'the first SPIFFE-shaped match returns whichever comes first — ' + + 'and Mallory crafts the cert order to put the privileged ID ' + + 'first.', + impact: + 'Identity confusion in the verifier.', + }, + ], + mitigations: [ + { + action: + 'Count SPIFFE-shaped URI SANs; reject certs with more than one.', + mitigates: ['multi-uri-san-attack'], + }, + { + action: + 'Use SPIFFE\'s reference parsing libraries (go-spiffe, ' + + 'spiffe-rs) which enforce the single-SPIFFE-SAN rule. Do NOT ' + + 'hand-roll certificate-to-SPIFFE-ID parsing.', + mitigates: ['multi-uri-san-attack'], + }, + ], references: [ { label: 'SPIFFE X.509-SVID §2 (URI SAN constraints)', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md', }, + { + label: 'SPIFFE X.509-SVID §4 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#4-security-considerations', + }, ], }, trust_bundle: { purpose: - 'The set of root CA certificates for a trust domain. Verifiers ' + - 'use it to validate the certificate chain on incoming SVIDs. ' + - 'Distributed by the SPIRE Server to all agents (which deliver to ' + - 'workloads via the Workload API) — and to any external verifier ' + - 'that needs to authenticate workloads in this trust domain.', - withoutIt: - 'Two failure modes: (1) **Stale bundle** — CA rotation happened ' + - 'but the verifier\'s cache has not refreshed; new SVIDs fail to ' + - 'verify, breaking service-to-service auth (availability impact). ' + - '(2) **Tampered bundle** — verifier loaded a bundle that includes ' + - 'attacker-controlled CA roots; SVIDs the attacker minted now ' + - 'verify as legitimate.', - attack: - 'Bundle tampering at distribution time. Mallory has compromised ' + - 'the storage backing a verifier\'s trust bundle cache (a Kubernetes ' + - 'ConfigMap, a file on disk, an environment variable supplying a ' + - 'PEM bundle). She adds her own self-signed CA to the bundle. The ' + - 'verifier now trusts SVIDs Mallory mints with that CA — full ' + - 'identity forgery within the trust domain (from the verifier\'s ' + - 'perspective).', - impact: - 'Trust subversion at the trust-bundle layer. Defences: deliver ' + - 'bundles via the Workload API (not file-system distribution); ' + - 'protect the storage path / ConfigMap with appropriate RBAC; ' + - 'verify bundle freshness against the SPIRE Server periodically; ' + - 'consider using bundle endpoints with mTLS authentication for ' + - 'external verifiers.', + 'The set of root CA certificates for a trust domain. Verifiers use it ' + + 'to validate the certificate chain on incoming SVIDs. Distributed by ' + + 'the SPIRE Server to all agents (which deliver to workloads via the ' + + 'Workload API) — and to any external verifier that needs to ' + + 'authenticate workloads in this trust domain.', + attacks: [ + { + id: 'stale-trust-bundle', + name: 'Stale bundle after CA rotation', + scenario: + 'CA rotation happened but the verifier\'s cache has not refreshed; ' + + 'new SVIDs fail to verify, breaking service-to-service auth.', + impact: + 'Availability impact — failed auth, support tickets — until ' + + 'cache refreshes.', + }, + { + id: 'tampered-trust-bundle', + name: 'Tampered bundle (attacker CA injected)', + scenario: + 'Mallory has compromised the storage backing a verifier\'s trust ' + + 'bundle cache (a Kubernetes ConfigMap, a file on disk, an ' + + 'environment variable supplying a PEM bundle). She adds her own ' + + 'self-signed CA to the bundle. The verifier now trusts SVIDs ' + + 'Mallory mints with that CA — full identity forgery within the ' + + 'trust domain (from the verifier\'s perspective).', + impact: + 'Trust subversion at the trust-bundle layer.', + }, + ], + mitigations: [ + { + action: + 'Deliver bundles via the Workload API rather than file-system ' + + 'distribution where possible.', + mitigates: ['tampered-trust-bundle'], + }, + { + action: + 'Protect the storage path / ConfigMap with appropriate RBAC so ' + + 'only the SPIRE Server can write.', + mitigates: ['tampered-trust-bundle'], + }, + { + action: + 'Verify bundle freshness against the SPIRE Server periodically.', + mitigates: ['stale-trust-bundle', 'tampered-trust-bundle'], + }, + { + action: + 'For external verifiers, use bundle endpoints with mTLS ' + + 'authentication.', + mitigates: ['tampered-trust-bundle'], + }, + ], references: [ { label: 'SPIFFE Trust Domain and Bundle §4', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md', }, + { + label: 'SPIFFE Trust Domain and Bundle §5 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#5-security-considerations', + }, ], }, @@ -283,118 +461,187 @@ export const SPIFFE_EXPLAINERS: Record = { 'Trust bundles for *foreign* trust domains, fetched via federation ' + 'and used to validate SVIDs from those domains. Each entry maps ' + '`trust_domain → root_cas`. Distributed alongside the local trust ' + - 'bundle to workloads that participate in cross-domain ' + - 'communication.', - withoutIt: + 'bundle to workloads that participate in cross-domain communication. ' + 'Federation extends trust beyond the local domain — every additional ' + 'federated bundle is an additional set of CAs that can mint SVIDs ' + - 'the local verifier will accept. A single compromised foreign ' + - 'bundle endpoint compromises every workload that consumes its ' + - 'output.', - attack: - 'Federation chain compromise. Trust domain A federates with B; B ' + - 'federates with C. Mallory compromises C\'s bundle endpoint and ' + - 'starts publishing her own CA in C\'s bundle. B fetches it (B ' + - 'trusts C\'s endpoint), then B redistributes the federated bundle. ' + - 'A fetches B\'s redistributed view (A trusts B), and now A\'s ' + - 'workloads trust SVIDs that Mallory minted from C\'s "compromised" ' + - 'CA — even though A and C have no direct relationship. Per the ' + - 'SPIFFE Federation spec: "compromise of a trust domain or bundle ' + - 'endpoint server in the chain would result in the compromise of ' + - 'the next trust domain."', - impact: - 'Cascading trust compromise across federation chains. Defences: ' + - 'minimise federation depth (avoid B-trusts-C-trusts-D chains; ' + - 'establish direct relationships); use `https_spiffe` profile for ' + - 'bundle endpoints (mTLS, not Web PKI); monitor federated bundle ' + - 'changes and alert on unexpected CA additions; treat federated ' + - 'trust domains with the same scrutiny as direct ones.', + 'the local verifier will accept.', + attacks: [ + { + id: 'federation-chain-compromise', + name: 'Cascading federation chain compromise', + scenario: + 'Trust domain A federates with B; B federates with C. Mallory ' + + 'compromises C\'s bundle endpoint and starts publishing her own ' + + 'CA in C\'s bundle. B fetches it (B trusts C\'s endpoint), then ' + + 'B redistributes the federated bundle. A fetches B\'s ' + + 'redistributed view (A trusts B), and now A\'s workloads trust ' + + 'SVIDs that Mallory minted from C\'s "compromised" CA — even ' + + 'though A and C have no direct relationship. Per the SPIFFE ' + + 'Federation spec: "compromise of a trust domain or bundle ' + + 'endpoint server in the chain would result in the compromise of ' + + 'the next trust domain."', + impact: + 'Cascading trust compromise across federation chains.', + }, + ], + mitigations: [ + { + action: + 'Minimise federation depth — avoid B-trusts-C-trusts-D chains; ' + + 'establish direct relationships where possible.', + mitigates: ['federation-chain-compromise'], + }, + { + action: + 'Use `https_spiffe` profile for bundle endpoints (mTLS, not ' + + 'Web PKI).', + mitigates: ['federation-chain-compromise'], + }, + { + action: + 'Monitor federated bundle changes and alert on unexpected CA ' + + 'additions.', + mitigates: ['federation-chain-compromise'], + }, + { + action: + 'Treat federated trust domains with the same scrutiny as direct ' + + 'ones — each is an additional source of SVIDs your verifier will ' + + 'accept.', + mitigates: ['federation-chain-compromise'], + }, + ], references: [ { label: 'SPIFFE Federation spec', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md', }, + { + label: 'SPIFFE Federation §6 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md#6-security-considerations', + }, ], }, bundle_endpoint_url: { purpose: 'URL the local SPIRE Server fetches to obtain a foreign trust ' + - 'domain\'s bundle. Standard path is `/.well-known/spiffe-bundle` ' + - 'on the foreign SPIRE Server but custom URLs are supported. The ' + - 'choice of `endpoint_profile` (`https_spiffe` vs `https_web`) ' + - 'determines how this URL\'s authenticity is verified.', - withoutIt: - 'The bundle endpoint URL itself is sensitive configuration. Per ' + - 'SPIFFE Federation spec: "Compromise of the configuration of a ' + - 'federation relationship can weaken or completely break security ' + - 'guarantees." URL tampering with `https_web` profile lets an ' + - 'attacker issue fraudulent keys and impersonate any identity in ' + - 'the corresponding trust domain.', - attack: - 'Endpoint URL substitution. Mallory has write access to the SPIRE ' + - 'Server\'s federation configuration (via compromised Kubernetes ' + - 'ConfigMap, leaked admin credentials, etc.). She changes ' + - '`bundle_endpoint_url` for `partner.example.com` from ' + - '`https://spire.partner.example.com:8443/bundle` to ' + - '`https://attacker.example.com/fake-bundle`. With `https_web` ' + - 'profile, the SPIRE Server fetches the fake bundle (TLS valid for ' + - 'attacker domain), trusts the attacker\'s CA, and from this point ' + - 'forward every SVID minted by the attacker is accepted as if it ' + - 'were a legitimate `partner.example.com` workload.', - impact: - 'Total federation compromise via configuration tampering. ' + - 'Defences: (1) use `https_spiffe` endpoint_profile — the bundle ' + - 'endpoint server must present a SPIFFE SVID matching ' + - '`endpoint_spiffe_id`, which the attacker cannot forge without ' + - 'compromising the foreign trust domain; (2) protect federation ' + - 'configuration storage with strict RBAC; (3) audit federation ' + - 'config changes; (4) pin endpoint URLs against tampering at the ' + - 'configuration layer.', + 'domain\'s bundle. Standard path is `/.well-known/spiffe-bundle` on ' + + 'the foreign SPIRE Server but custom URLs are supported. The choice ' + + 'of `endpoint_profile` (`https_spiffe` vs `https_web`) determines ' + + 'how this URL\'s authenticity is verified.', + attacks: [ + { + id: 'endpoint-url-substitution', + name: 'Endpoint URL substitution', + scenario: + 'Mallory has write access to the SPIRE Server\'s federation ' + + 'configuration (via compromised Kubernetes ConfigMap, leaked ' + + 'admin credentials, etc.). She changes `bundle_endpoint_url` ' + + 'for `partner.example.com` from ' + + '`https://spire.partner.example.com:8443/bundle` to ' + + '`https://attacker.example.com/fake-bundle`. With `https_web` ' + + 'profile, the SPIRE Server fetches the fake bundle (TLS valid ' + + 'for attacker domain), trusts the attacker\'s CA, and from this ' + + 'point forward every SVID minted by the attacker is accepted as ' + + 'if it were a legitimate `partner.example.com` workload. Per ' + + 'SPIFFE Federation spec: "Compromise of the configuration of a ' + + 'federation relationship can weaken or completely break security ' + + 'guarantees."', + impact: + 'Total federation compromise via configuration tampering.', + }, + ], + mitigations: [ + { + action: + 'Use `https_spiffe` endpoint_profile — the bundle endpoint ' + + 'server must present a SPIFFE SVID matching ' + + '`endpoint_spiffe_id`, which the attacker cannot forge without ' + + 'compromising the foreign trust domain.', + mitigates: ['endpoint-url-substitution'], + }, + { + action: + 'Protect federation configuration storage with strict RBAC ' + + '(only platform admins can edit; audit every change).', + mitigates: ['endpoint-url-substitution'], + }, + { + action: + 'Audit federation config changes; alert on bundle_endpoint_url ' + + 'modifications.', + mitigates: ['endpoint-url-substitution'], + }, + ], references: [ { label: 'SPIFFE Federation §5 (Bundle Endpoint Distribution)', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md', }, + { + label: 'SPIFFE Federation §6 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md#6-security-considerations', + }, ], }, endpoint_profile: { purpose: - 'Authentication profile for the bundle endpoint: `https_spiffe` ' + - '(the foreign SPIRE Server presents a SPIFFE SVID; mTLS) or ' + - '`https_web` (Web PKI TLS — the foreign endpoint presents a cert ' + - 'from a public CA chain). `https_spiffe` is the secure choice; ' + - '`https_web` is provided for bootstrapping / cross-organization ' + - 'cases where SPIFFE identities are not yet exchanged.', - withoutIt: - '`https_web` is the gap. It anchors federation trust in Web PKI — ' + - 'meaning anyone able to obtain a TLS cert for the bundle endpoint ' + - 'domain (DNS hijack, BGP attack, lax CA, abused ACME challenge) ' + - 'can serve a fake bundle and the local SPIRE Server will accept ' + - 'it.', - attack: - 'Bundle endpoint TLS hijack. Mallory performs a BGP hijack against ' + - '`partner.example.com`\'s IP space, obtains a Let\'s Encrypt cert ' + - 'for it via HTTP-01 challenge during the hijack, and serves a ' + - 'fake bundle from her infrastructure. The local SPIRE Server, ' + - 'configured with `endpoint_profile=https_web`, sees a TLS ' + - 'connection that validates against Web PKI roots — accepts it. ' + - 'With `https_spiffe`, the connection would have failed because ' + - 'Mallory cannot mint a partner-domain SPIFFE SVID without ' + - 'compromising partner\'s SPIRE Server.', - impact: - 'Federation trust subversion via Web PKI weaknesses. Defences: ' + - 'use `https_spiffe` for any production federation; reserve ' + - '`https_web` for the *initial* bundle exchange only, then switch ' + - 'to `https_spiffe` once both sides have each other\'s bundles. ' + - 'Treat any federation still on `https_web` as a configuration ' + - 'finding to remediate.', + 'Authentication profile for the bundle endpoint: `https_spiffe` (the ' + + 'foreign SPIRE Server presents a SPIFFE SVID; mTLS) or `https_web` ' + + '(Web PKI TLS — the foreign endpoint presents a cert from a public ' + + 'CA chain). `https_spiffe` is the secure choice; `https_web` is ' + + 'provided for bootstrapping / cross-organization cases where SPIFFE ' + + 'identities are not yet exchanged.', + attacks: [ + { + id: 'bundle-endpoint-tls-hijack', + name: 'Bundle endpoint TLS hijack (https_web profile)', + scenario: + 'Mallory performs a BGP hijack against `partner.example.com`\'s ' + + 'IP space, obtains a Let\'s Encrypt cert for it via HTTP-01 ' + + 'challenge during the hijack, and serves a fake bundle from her ' + + 'infrastructure. The local SPIRE Server, configured with ' + + '`endpoint_profile=https_web`, sees a TLS connection that ' + + 'validates against Web PKI roots — accepts it. With ' + + '`https_spiffe`, the connection would have failed because ' + + 'Mallory cannot mint a partner-domain SPIFFE SVID without ' + + 'compromising partner\'s SPIRE Server.', + impact: + 'Federation trust subversion via Web PKI weaknesses (DNS hijack, ' + + 'BGP attack, lax CA, abused ACME challenge).', + }, + ], + mitigations: [ + { + action: + 'Use `https_spiffe` for any production federation.', + mitigates: ['bundle-endpoint-tls-hijack'], + }, + { + action: + 'Reserve `https_web` for the *initial* bundle exchange only, ' + + 'then switch to `https_spiffe` once both sides have each other\'s ' + + 'bundles.', + mitigates: ['bundle-endpoint-tls-hijack'], + }, + { + action: + 'Treat any federation still on `https_web` as a configuration ' + + 'finding to remediate.', + mitigates: ['bundle-endpoint-tls-hijack'], + }, + ], references: [ { label: 'SPIFFE Federation §5.2 (Endpoint Profiles)', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md', }, + { + label: 'SPIFFE Federation §6 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md#6-security-considerations', + }, ], }, @@ -402,43 +649,84 @@ export const SPIFFE_EXPLAINERS: Record = { purpose: 'Plugin identifier for the node attestor: `join_token` (one-time ' + 'bootstrap secret), `aws_iid` (AWS Instance Identity Document), ' + - '`gcp_iit` (GCP Instance Identity Token), `azure_msi`, `k8s_psat` ' + - '/ `k8s_sat` (Kubernetes service account token), `x509pop` (X.509 ' + + '`gcp_iit` (GCP Instance Identity Token), `azure_msi`, `k8s_psat` / ' + + '`k8s_sat` (Kubernetes service account token), `x509pop` (X.509 ' + 'proof of possession), `tpm_devid` (TPM-backed). Determines what ' + - 'evidence the agent presents to prove its node identity to the ' + - 'SPIRE Server.', - withoutIt: - 'Each attestor has a different threat model. `join_token` is the ' + - 'simplest but the weakest — it\'s a bearer secret. `aws_iid` and ' + - 'similar cloud attestors trust the cloud platform\'s identity ' + - 'document signing. `tpm_devid` is the strongest — hardware-rooted ' + - 'attestation. Picking the wrong attestor for the threat model ' + - 'produces a deployment that *looks* secure but isn\'t.', - attack: - 'Attestor mismatch attacks: (1) **join_token in production** — ' + - 'tokens leak via terraform state files, CI logs, configuration ' + - 'management. Mallory finds an unused token and registers a rogue ' + - 'agent in the trust domain. (2) **k8s_psat on a compromised cluster** ' + - '— if the Kubernetes API server is compromised, attacker mints ' + - 'arbitrary projected service account tokens for any namespace, ' + - 'registering rogue agents. (3) **aws_iid metadata-service hijack** ' + - '— SSRF in a workload that lets the attacker query the AWS metadata ' + - 'service of a different EC2 instance, then submitting that IID as ' + - 'their own.', - impact: - 'Rogue agent registration → ability to mint workload SVIDs in the ' + - 'trust domain. Defences: (1) use the strongest attestor the ' + - 'platform supports — TPM-backed where available, cloud-platform ' + - 'attestors otherwise, `join_token` only for ephemeral / bootstrap ' + - 'cases; (2) constrain attestor results — `aws_iid` agents should ' + - 'be scoped to specific account IDs and instance roles, not ' + - 'open-ended; (3) for `join_token`, use very short TTLs and ' + - 'consume-on-first-use semantics (SPIRE default).', + 'evidence the agent presents to prove its node identity to the SPIRE ' + + 'Server. Each attestor has a different threat model.', + attacks: [ + { + id: 'join-token-in-production', + name: 'join_token in production', + scenario: + 'Tokens leak via terraform state files, CI logs, configuration ' + + 'management. Mallory finds an unused token and registers a ' + + 'rogue agent in the trust domain.', + impact: + 'Rogue agent registration → ability to mint workload SVIDs in ' + + 'the trust domain.', + }, + { + id: 'k8s-psat-compromised-cluster', + name: 'k8s_psat on a compromised cluster', + scenario: + 'If the Kubernetes API server is compromised, attacker mints ' + + 'arbitrary projected service account tokens for any namespace, ' + + 'registering rogue agents.', + impact: + 'Trust-domain compromise via control-plane compromise.', + }, + { + id: 'aws-iid-metadata-hijack', + name: 'aws_iid metadata-service hijack', + scenario: + 'SSRF in a workload lets the attacker query the AWS metadata ' + + 'service of a different EC2 instance, then submitting that IID ' + + 'as their own to register a rogue agent.', + impact: + 'Attacker registers an agent claiming to be a specific EC2 ' + + 'instance they don\'t actually control.', + }, + ], + mitigations: [ + { + action: + 'Use the strongest attestor the platform supports — TPM-backed ' + + 'where available, cloud-platform attestors otherwise, ' + + '`join_token` only for ephemeral / bootstrap cases.', + mitigates: [ + 'join-token-in-production', + 'k8s-psat-compromised-cluster', + ], + }, + { + action: + 'Constrain attestor results — `aws_iid` agents should be scoped ' + + 'to specific account IDs and instance roles, not open-ended.', + mitigates: ['aws-iid-metadata-hijack'], + }, + { + action: + 'For `join_token`, use very short TTLs and consume-on-first-use ' + + 'semantics (SPIRE default).', + mitigates: ['join-token-in-production'], + }, + { + action: + 'Audit Kubernetes RBAC restricting who can mint service-account ' + + 'tokens; defence-in-depth against control-plane compromise.', + mitigates: ['k8s-psat-compromised-cluster'], + }, + ], references: [ { label: 'SPIRE Node Attestation', href: 'https://spiffe.io/docs/latest/deploying/configuring/', }, + { + label: 'SPIFFE Workload API §6 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md#6-security-considerations', + }, ], }, @@ -447,73 +735,124 @@ export const SPIFFE_EXPLAINERS: Record = { 'Single-use bearer token the SPIRE Server administrator generates ' + 'and provisions out-of-band onto a node. The agent presents it on ' + 'first run; the server consumes the token and issues an agent SVID. ' + - 'Simplest node-attestation method, useful for development and ' + - 'small static deployments.', - withoutIt: - 'A bearer secret with the same threat model as any other shared ' + - 'token: visible to anyone who can read the provisioning channel ' + - 'before consumption. Production deployments should prefer ' + - 'platform attestors (cloud or TPM) precisely because join_tokens ' + - 'are operationally fragile.', - attack: - 'Join token theft from provisioning channels. Mallory has read ' + - 'access to a Terraform state file, a CI build log, a Slack ' + - 'channel, an email thread — anywhere the join token might appear ' + - 'before reaching the target node. She submits the token to the ' + - 'SPIRE Server before the legitimate node does. Server consumes ' + - 'the token, issues an agent SVID to Mallory\'s rogue agent. The ' + - 'legitimate node then fails to attest (token already used) — the ' + - 'failure is the only signal.', - impact: - 'Rogue agent in the trust domain → arbitrary workload SVID issuance ' + - 'on Mallory\'s infrastructure. Defences: (1) use platform attestors ' + - 'in production, not join_token; (2) for join_token, keep TTLs ' + - 'short (minutes, not days); (3) treat token leakage as detected ' + - 'breach — rotate trust domain credentials, audit which workloads ' + - 'were issued SVIDs by the rogue agent.', + 'Simplest node-attestation method, useful for development and small ' + + 'static deployments. A bearer secret with the same threat model as ' + + 'any other shared token.', + attacks: [ + { + id: 'join-token-channel-theft', + name: 'Token theft from provisioning channels', + scenario: + 'Mallory has read access to a Terraform state file, a CI build ' + + 'log, a Slack channel, an email thread — anywhere the join ' + + 'token might appear before reaching the target node. She submits ' + + 'the token to the SPIRE Server before the legitimate node does. ' + + 'Server consumes the token, issues an agent SVID to Mallory\'s ' + + 'rogue agent. The legitimate node then fails to attest (token ' + + 'already used) — the failure is the only signal.', + impact: + 'Rogue agent in the trust domain → arbitrary workload SVID ' + + 'issuance on Mallory\'s infrastructure.', + }, + ], + mitigations: [ + { + action: + 'Use platform attestors (cloud or TPM) in production rather than ' + + 'join_token.', + mitigates: ['join-token-channel-theft'], + }, + { + action: + 'For join_token, keep TTLs short (minutes, not days).', + mitigates: ['join-token-channel-theft'], + }, + { + action: + 'Treat token leakage as detected breach — rotate trust domain ' + + 'credentials, audit which workloads were issued SVIDs by the ' + + 'rogue agent.', + mitigates: ['join-token-channel-theft'], + }, + ], references: [ { label: 'SPIRE Server — Join Token attestor', href: 'https://github.com/spiffe/spire/blob/main/doc/plugin_server_nodeattestor_jointoken.md', }, + { + label: 'SPIFFE Workload API §6 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md#6-security-considerations', + }, ], }, audience: { purpose: - 'On a JWT-SVID issuance request, names the intended recipient of ' + - 'the token (a SPIFFE ID, URI, or arbitrary string the receiving ' + - 'service identifies as). The SPIRE Server binds this value into ' + - 'the JWT\'s `aud` claim. Verifiers MUST check that the value they ' + - 'identify as appears in `aud` before trusting the token.', - withoutIt: - 'Two attack classes. (1) **No audience binding** — token issued ' + - 'without specific audience can be replayed at any service that ' + - 'consumes JWT-SVIDs from this trust domain. (2) **Multi-audience ' + - 'tokens** — JWT-SVIDs with multiple `aud` values can be replayed ' + - 'across audiences: a token sent to one of the listed audiences is ' + - 'reusable by that audience to impersonate the original sender at ' + - 'the other listed audiences.', - attack: - 'Cross-audience replay (per the JWT-SVID spec\'s explicit warning). ' + - 'Alice mints a JWT-SVID with `aud=[Bob, Chuck]` and sends it to ' + - 'Chuck. Chuck now has a token that *Bob* will accept as proof of ' + - 'Alice\'s identity. Chuck replays the token to Bob; Bob sees a ' + - 'valid JWT-SVID with `aud` containing his identifier, signed by ' + - 'the trust domain CA — accepts it. Chuck has now successfully ' + - 'impersonated Alice at Bob.', - impact: - 'Identity confusion across services. Defences: (1) request ' + - 'JWT-SVIDs with EXACTLY ONE audience, identifying the specific ' + - 'recipient; (2) verifiers MUST reject tokens missing `aud` or ' + - 'with `aud` not matching their identifier; (3) avoid the ' + - 'multi-audience pattern entirely — issue separate tokens for ' + - 'separate recipients.', + 'On a JWT-SVID issuance request, names the intended recipient of the ' + + 'token (a SPIFFE ID, URI, or arbitrary string the receiving service ' + + 'identifies as). The SPIRE Server binds this value into the JWT\'s ' + + '`aud` claim. Verifiers MUST check that the value they identify as ' + + 'appears in `aud` before trusting the token.', + attacks: [ + { + id: 'jwt-svid-cross-audience-replay', + name: 'Cross-audience replay (multi-audience tokens)', + scenario: + 'Per the JWT-SVID spec\'s explicit warning: Alice mints a ' + + 'JWT-SVID with `aud=[Bob, Chuck]` and sends it to Chuck. Chuck ' + + 'now has a token that *Bob* will accept as proof of Alice\'s ' + + 'identity. Chuck replays the token to Bob; Bob sees a valid ' + + 'JWT-SVID with `aud` containing his identifier, signed by the ' + + 'trust domain CA — accepts it. Chuck has now successfully ' + + 'impersonated Alice at Bob.', + impact: + 'Identity confusion across services in multi-audience ' + + 'configurations.', + }, + { + id: 'jwt-svid-no-audience-binding', + name: 'No audience binding', + scenario: + 'Token issued without specific audience can be replayed at any ' + + 'service that consumes JWT-SVIDs from this trust domain.', + impact: + 'Single-token-stolen → any-service-compromised within the trust ' + + 'domain.', + }, + ], + mitigations: [ + { + action: + 'Request JWT-SVIDs with EXACTLY ONE audience, identifying the ' + + 'specific recipient — avoid the multi-audience pattern entirely.', + mitigates: [ + 'jwt-svid-cross-audience-replay', + 'jwt-svid-no-audience-binding', + ], + }, + { + action: + 'Verifiers MUST reject tokens missing `aud` or with `aud` not ' + + 'matching their identifier.', + mitigates: ['jwt-svid-no-audience-binding'], + }, + { + action: + 'Issue separate tokens for separate recipients rather than ' + + 'reusing one across services.', + mitigates: ['jwt-svid-cross-audience-replay'], + }, + ], references: [ { label: 'SPIFFE JWT-SVID §3 (audience claim)', href: 'https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md', }, + { + label: 'SPIFFE JWT-SVID §6 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#6-security-considerations', + }, ], }, @@ -522,75 +861,117 @@ export const SPIFFE_EXPLAINERS: Record = { 'The X.509-SVID issued to the SPIRE Agent itself after successful ' + 'node attestation. The agent uses it for mTLS to the SPIRE Server ' + 'when fetching workload SVIDs and trust bundle updates. Forms the ' + - 'agent\'s identity as a "trusted attestor" for workloads on its ' + - 'node.', - withoutIt: - 'An attacker who steals the agent SVID *and* the corresponding ' + - 'private key gets the agent\'s authority — can ask the SPIRE ' + - 'Server for any workload SVID that lists this agent as ' + - '`parent_id`. Agent compromise = full workload-SVID issuance ' + - 'authority for that agent\'s scope.', - attack: - 'Agent identity theft → workload SVID issuance. Mallory roots a ' + - 'node and reads the SPIRE Agent\'s on-disk SVID and private key ' + - '(typically stored in a Kubernetes Secret or local file). She ' + - 'connects to the SPIRE Server from her own infrastructure ' + - 'presenting the stolen agent SVID, requests workload SVIDs for ' + - 'the registration entries scoped to that agent — and gets them. ' + - 'She can now run "those workloads" anywhere with full trust-' + - 'domain identity.', - impact: - 'Lateral movement bounded by `parent_id` scoping (which is why ' + - '`parent_id` matters — see that entry). Defences: (1) store agent ' + - 'private keys in HSM / TPM where available, not on disk; (2) ' + - 'enable agent SVID rotation with short TTLs so stolen credentials ' + - 'expire quickly; (3) monitor for agent SVID use from unexpected ' + - 'IPs (the SPIRE Server can log the source of agent connections); ' + - '(4) minimise `parent_id` scope — never use trust-domain-wide ' + - 'parents.', + 'agent\'s identity as a "trusted attestor" for workloads on its node.', + attacks: [ + { + id: 'agent-identity-theft', + name: 'Agent identity theft → workload SVID issuance', + scenario: + 'Mallory roots a node and reads the SPIRE Agent\'s on-disk SVID ' + + 'and private key (typically stored in a Kubernetes Secret or ' + + 'local file). She connects to the SPIRE Server from her own ' + + 'infrastructure presenting the stolen agent SVID, requests ' + + 'workload SVIDs for the registration entries scoped to that ' + + 'agent — and gets them. She can now run "those workloads" ' + + 'anywhere with full trust-domain identity.', + impact: + 'Lateral movement bounded by `parent_id` scoping (which is why ' + + '`parent_id` matters).', + }, + ], + mitigations: [ + { + action: + 'Store agent private keys in HSM / TPM where available, not on ' + + 'disk.', + mitigates: ['agent-identity-theft'], + }, + { + action: + 'Enable agent SVID rotation with short TTLs so stolen ' + + 'credentials expire quickly.', + mitigates: ['agent-identity-theft'], + }, + { + action: + 'Monitor for agent SVID use from unexpected IPs (the SPIRE ' + + 'Server can log the source of agent connections).', + mitigates: ['agent-identity-theft'], + }, + { + action: + 'Minimise `parent_id` scope — never use trust-domain-wide ' + + 'parents.', + mitigates: ['agent-identity-theft'], + }, + ], references: [ { label: 'SPIRE Concepts — Agent SVID lifecycle', href: 'https://spiffe.io/docs/latest/spire-about/spire-concepts/', }, + { + label: 'SPIFFE Workload API §6 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md#6-security-considerations', + }, ], }, peer_creds: { purpose: - 'OS-kernel-verified PID, UID, GID of the process connecting to ' + - 'the SPIRE Agent\'s Workload API socket. Obtained via SO_PEERCRED ' + - '(Linux) or equivalent. Distinct from anything the workload could ' + - 'tell the agent — these come from the kernel.', - withoutIt: - 'Kernel-verified PID/UID/GID is the ground truth that workload ' + - 'attestation is anchored to. *But* — PID is a small integer that ' + - 'the kernel reuses. If a process exits and its PID is recycled, ' + - 'a later process with the same PID is a different program; an ' + - 'agent that caches PID-based attestation results without ' + - 'detecting this race issues SVIDs to the wrong process.', - attack: - 'PID-reuse race against attestation cache. Process A (legitimate, ' + - 'attested) exits. Process B (Mallory\'s, malicious) starts on the ' + - 'same node and the kernel happens to assign it Process A\'s old ' + - 'PID. A naive agent that looks up "what SPIFFE ID did PID 12345 ' + - 'attest to last time" returns Process A\'s identity to Process B. ' + - 'SPIRE\'s real defence: when the agent re-reads `/proc/{pid}/exe` ' + - 'and other process attributes, the kernel returns the *new* ' + - 'process\'s attributes (or fails if the FD became stale), so ' + - 'attestation re-runs and matches the new process\'s actual ' + - 'selectors — not the old ones.', - impact: - 'Wrong-workload-identity issuance under PID reuse. Defences: ' + - 'agent MUST re-attest on every Workload API call (do not cache ' + - 'the attestation across calls); use process-start-time alongside ' + - 'PID for cache keying; verify the executable hasn\'t changed ' + - 'between attestation and SVID handover.', + 'OS-kernel-verified PID, UID, GID of the process connecting to the ' + + 'SPIRE Agent\'s Workload API socket. Obtained via SO_PEERCRED (Linux) ' + + 'or equivalent — distinct from anything the workload could tell the ' + + 'agent. Kernel-verified PID/UID/GID is the ground truth that ' + + 'workload attestation is anchored to. *But* — PID is a small integer ' + + 'that the kernel reuses.', + attacks: [ + { + id: 'pid-reuse-race', + name: 'PID-reuse race against attestation cache', + scenario: + 'Process A (legitimate, attested) exits. Process B (Mallory\'s, ' + + 'malicious) starts on the same node and the kernel happens to ' + + 'assign it Process A\'s old PID. A naive agent that looks up ' + + '"what SPIFFE ID did PID 12345 attest to last time" returns ' + + 'Process A\'s identity to Process B. SPIRE\'s real defence: when ' + + 'the agent re-reads `/proc/{pid}/exe` and other process ' + + 'attributes, the kernel returns the *new* process\'s attributes ' + + '(or fails if the FD became stale), so attestation re-runs and ' + + 'matches the new process\'s actual selectors — not the old ones.', + impact: + 'Wrong-workload-identity issuance under PID reuse.', + }, + ], + mitigations: [ + { + action: + 'Agent MUST re-attest on every Workload API call — do not cache ' + + 'the attestation across calls.', + mitigates: ['pid-reuse-race'], + }, + { + action: + 'Use process-start-time alongside PID for cache keying so a new ' + + 'process with a recycled PID gets a fresh attestation.', + mitigates: ['pid-reuse-race'], + }, + { + action: + 'Verify the executable hasn\'t changed between attestation and ' + + 'SVID handover.', + mitigates: ['pid-reuse-race'], + }, + ], references: [ { label: 'SPIFFE Workload API §3 (Process Identity Verification)', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md', }, + { + label: 'SPIFFE Workload API §6 (Security Considerations)', + href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md#6-security-considerations', + }, ], }, } diff --git a/frontend/src/protocols/explainers/ssf.ts b/frontend/src/protocols/explainers/ssf.ts index 224b70f..c59b64b 100644 --- a/frontend/src/protocols/explainers/ssf.ts +++ b/frontend/src/protocols/explainers/ssf.ts @@ -24,33 +24,64 @@ export const SSF_EXPLAINERS: Record = { 'standard JWT claims (`iss`, `aud`, `iat`, `jti`) plus the ' + 'SET-specific `events` claim, which is what makes a JWT a SET ' + 'rather than an ID Token, access token, or any other JWT type.', - withoutIt: - 'The same JWT-validation pitfalls as `id_token`: alg=none, ' + - 'algorithm confusion (RS256→HS256), fail-open on unknown alg, ' + - 'skipped claim validation. SETs additionally require the ' + - '`events` claim presence as a token-type discriminator (RFC ' + - '8417 §2.2) — and missing that check enables token-type ' + - 'confusion attacks unique to SSF.', - attack: - 'Token-type confusion. Mallory captures Alice\'s legitimate ID ' + - 'Token (via any JWT-leakage path — referer, log, browser ' + - 'extension). Without `events`-claim validation on the SSF ' + - 'Receiver, Mallory POSTs the captured ID Token to the receiver\'s ' + - 'push endpoint. The token is signed by the same OP/Transmitter, ' + - '`iss`/`aud`/`iat` all valid — receiver accepts it as a SET. ' + - 'Depending on what code path runs without an `events` claim, this ' + - 'either crashes (best case) or silently triggers default ' + - 'behaviour (worst case — forced logout for the subject named in ' + - '`sub`). Same hazard for SET-vs-ID-Token in the other direction: ' + - 'a SET replayed at an OIDC RP\'s ID Token consumer.', - impact: - 'Cross-token-type confusion enabling either auth bypass (SET ' + - 'accepted as ID Token) or DoS-via-forced-logout (ID Token ' + - 'accepted as SET). Defences: (1) RFC 8417 §2.2 — Receiver MUST ' + - 'reject any token without an `events` claim; (2) verify the SET\'s ' + - '`typ` header is `secevent+jwt`; (3) standard JWT validation ' + - '(alg pinning, iss/aud check, signature) applies as for any ' + - 'JWT — and is just as easy to get wrong here as in OIDC.', + attacks: [ + { + id: 'set-token-type-confusion', + name: 'Token-type confusion (SET vs ID Token)', + scenario: + 'Mallory captures Alice\'s legitimate ID Token (via any ' + + 'JWT-leakage path — referer, log, browser extension). Without ' + + '`events`-claim validation on the SSF Receiver, Mallory POSTs ' + + 'the captured ID Token to the receiver\'s push endpoint. The ' + + 'token is signed by the same OP/Transmitter, `iss`/`aud`/`iat` ' + + 'all valid — receiver accepts it as a SET. Depending on what ' + + 'code path runs without an `events` claim, this either crashes ' + + '(best case) or silently triggers default behaviour (worst case ' + + '— forced logout for the subject named in `sub`). Same hazard ' + + 'in the other direction: a SET replayed at an OIDC RP\'s ID ' + + 'Token consumer.', + impact: + 'Cross-token-type confusion enabling either auth bypass (SET ' + + 'accepted as ID Token) or DoS-via-forced-logout (ID Token ' + + 'accepted as SET).', + }, + { + id: 'set-jwt-validation-pitfalls', + name: 'Standard JWT validation pitfalls (alg=none, alg confusion, etc.)', + scenario: + 'The same JWT-validation pitfalls as `id_token`: alg=none, ' + + 'algorithm confusion (RS256→HS256), fail-open on unknown alg, ' + + 'skipped claim validation. SETs are JWTs and inherit every JWT ' + + 'validation footgun.', + impact: + 'Authentication / authorization bypass at the SET-receiver layer ' + + '— forged events accepted, real events spoofed.', + }, + ], + mitigations: [ + { + action: + 'Receiver MUST reject any token without an `events` claim (RFC ' + + '8417 §2.2 — token-type discriminator).', + mitigates: ['set-token-type-confusion'], + }, + { + action: + 'Verify the SET\'s `typ` header is `secevent+jwt` before any ' + + 'further processing.', + mitigates: ['set-token-type-confusion'], + }, + { + action: + 'Apply standard JWT validation: pin allowed `alg` from ' + + 'Transmitter metadata; verify signature; validate `iss`, `aud`, ' + + 'and `iat`/exp.', + mitigates: [ + 'set-jwt-validation-pitfalls', + 'set-token-type-confusion', + ], + }, + ], references: [ { label: 'RFC 8417 (Security Event Token)', @@ -60,6 +91,10 @@ export const SSF_EXPLAINERS: Record = { label: 'RFC 8417 §2.2 (events claim — token-type discriminator)', href: 'https://datatracker.ietf.org/doc/html/rfc8417#section-2.2', }, + { + label: 'RFC 8417 §5 (Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc8417#section-5', + }, ], }, @@ -70,39 +105,80 @@ export const SSF_EXPLAINERS: Record = { 'session-revoked`) and whose values are event-specific payloads. ' + 'Doubles as the *token-type discriminator* — the absence of ' + '`events` means "this JWT is not a SET, do not process it as one".', - withoutIt: - 'Without strict `events` validation, two failure classes: (1) ' + - 'a non-SET JWT (ID Token, access token) gets processed as a SET ' + - 'because nothing rejected the missing claim; (2) processing event ' + - 'types the receiver doesn\'t actually support — receivers that ' + - 'naively iterate `events` keys and call generic handlers may ' + - 'execute logic the implementer never intended for unrecognized ' + - 'event types.', - attack: - 'Unknown-event-type abuse. Mallory submits a SET with ' + - '`events: { "https://attacker.example/custom-event": {...} }`. A ' + - 'receiver implementing "process all events in the events claim" ' + - 'with a generic dispatcher may invoke per-event hooks, log the ' + - 'unknown event with sensitive context, or, in the worst case, ' + - 'allow the payload to flow into downstream processing where its ' + - 'unexpected shape causes harm (parser confusion, type-coercion ' + - 'bugs). Variant: receivers that accept events from event-type ' + - 'URIs they technically know about but for which they have no ' + - 'meaningful local action — silently no-oping is fine, ' + - 'silently logging the (potentially attacker-crafted) payload is ' + - 'an information-leak surface.', - impact: - 'Event-type allowlist bypass + payload-driven side effects. ' + - 'Defences: (1) explicit allowlist of event-type URIs the receiver ' + - 'understands; (2) reject SETs containing any unrecognised event ' + - 'type (don\'t silently ignore — the spec allows ignoring, but ' + - 'rejecting catches misconfigurations); (3) per-event-type schema ' + - 'validation on the payload before processing.', + attacks: [ + { + id: 'unknown-event-type-abuse', + name: 'Unknown-event-type abuse', + scenario: + 'Mallory submits a SET with `events: ' + + '{ "https://attacker.example/custom-event": {...} }`. A receiver ' + + 'implementing "process all events in the events claim" with a ' + + 'generic dispatcher may invoke per-event hooks, log the unknown ' + + 'event with sensitive context, or, in the worst case, allow the ' + + 'payload to flow into downstream processing where its unexpected ' + + 'shape causes harm (parser confusion, type-coercion bugs).', + impact: + 'Event-type allowlist bypass + payload-driven side effects.', + }, + { + id: 'silent-log-info-leak', + name: 'Silent logging of attacker-crafted payload', + scenario: + 'Receivers that accept events from event-type URIs they ' + + 'technically know about but for which they have no meaningful ' + + 'local action — silently no-oping is fine; silently logging the ' + + '(potentially attacker-crafted) payload is an information-leak ' + + 'surface.', + impact: + 'Log pollution / information disclosure to log consumers.', + }, + { + id: 'set-vs-other-jwt-confusion-via-events', + name: 'Non-SET JWT processed as SET', + scenario: + 'A non-SET JWT (ID Token, access token) gets processed as a SET ' + + 'because nothing rejected the missing `events` claim — the ' + + 'token-type discriminator was not enforced.', + impact: + 'See `SET` entry — same cross-token-type confusion class.', + }, + ], + mitigations: [ + { + action: + 'Maintain an explicit allowlist of event-type URIs the receiver ' + + 'understands.', + mitigates: ['unknown-event-type-abuse'], + }, + { + action: + 'Reject SETs containing any unrecognised event type. The spec ' + + 'allows ignoring, but rejecting catches misconfigurations.', + mitigates: ['unknown-event-type-abuse'], + }, + { + action: + 'Per-event-type schema validation on the payload before ' + + 'processing — reject malformed payloads, do not log raw ' + + 'attacker content.', + mitigates: ['unknown-event-type-abuse', 'silent-log-info-leak'], + }, + { + action: + 'Reject any token without an `events` claim before further ' + + 'processing — enforces the token-type discriminator.', + mitigates: ['set-vs-other-jwt-confusion-via-events'], + }, + ], references: [ { label: 'RFC 8417 §2.2 (events claim)', href: 'https://datatracker.ietf.org/doc/html/rfc8417#section-2.2', }, + { + label: 'RFC 8417 §5 (Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc8417#section-5', + }, ], }, @@ -112,34 +188,50 @@ export const SSF_EXPLAINERS: Record = { '`.../caep/event-type/session-revoked`, ' + '`.../caep/event-type/credential-change`, ' + '`.../risc/event-type/account-disabled`, ' + - '`.../risc/event-type/credential-compromise`. Used as a key in ' + - 'the `events` claim. Drives receiver-side dispatch — different ' + - 'event types trigger different actions.', - withoutIt: - 'The receiver\'s response to an event is *exactly* as scoped as ' + - 'its event-type allowlist. A receiver that processes ' + - '`account-disabled` will block all access for the subject; one ' + - 'that processes `credential-compromise` will additionally force ' + - 'password reset. Mishandled or unrecognised event types either ' + - 'fail open (no response) or fail confused (wrong response).', - attack: - 'Event-type spoofing. Mallory injects a SET (via any path that ' + - 'lets her reach the receiver — see `SET` and `events` entries) ' + - 'with `event_type=account-disabled` for an executive\'s subject ' + - 'identifier. The receiver, treating the event as authoritative, ' + - 'terminates the executive\'s sessions and revokes their tokens — ' + - 'denial of service via false signal. RISC events (`account-' + - 'disabled`, `credential-compromise`) are particularly dangerous ' + - 'because their *intended* response is destructive; misuse turns ' + - 'them into weaponised disruption.', - impact: - 'False-signal DoS. Defences: (1) authenticate every SET against ' + - 'a known Transmitter via signature on the trust-domain JWKS; (2) ' + - 'restrict which Transmitters can publish each event type — not ' + - 'every Transmitter should be authoritative for ' + - '`account-disabled`; (3) human-in-the-loop / staged enforcement ' + - 'for high-impact RISC events when possible (alert-then-enforce ' + - 'with a small delay window for the security team to override).', + '`.../risc/event-type/credential-compromise`. Used as a key in the ' + + '`events` claim. Drives receiver-side dispatch — different event ' + + 'types trigger different actions.', + attacks: [ + { + id: 'event-type-spoofing-false-signal', + name: 'Event-type spoofing → false-signal DoS', + scenario: + 'Mallory injects a SET (via any path that lets her reach the ' + + 'receiver) with `event_type=account-disabled` for an ' + + 'executive\'s subject identifier. The receiver, treating the ' + + 'event as authoritative, terminates the executive\'s sessions ' + + 'and revokes their tokens — denial of service via false signal. ' + + 'RISC events (`account-disabled`, `credential-compromise`) are ' + + 'particularly dangerous because their *intended* response is ' + + 'destructive; misuse turns them into weaponised disruption.', + impact: + 'Targeted DoS by triggering destructive responses for chosen ' + + 'subjects.', + }, + ], + mitigations: [ + { + action: + 'Authenticate every SET against a known Transmitter via ' + + 'signature on the trust-domain JWKS — only signed events from ' + + 'trusted Transmitters get processed.', + mitigates: ['event-type-spoofing-false-signal'], + }, + { + action: + 'Restrict which Transmitters can publish each event type — not ' + + 'every Transmitter should be authoritative for `account-' + + 'disabled`.', + mitigates: ['event-type-spoofing-false-signal'], + }, + { + action: + 'For high-impact RISC events, consider human-in-the-loop / ' + + 'staged enforcement (alert-then-enforce with a small delay ' + + 'window for the security team to override).', + mitigates: ['event-type-spoofing-false-signal'], + }, + ], references: [ { label: 'OpenID CAEP Spec', @@ -149,39 +241,61 @@ export const SSF_EXPLAINERS: Record = { label: 'OpenID RISC Event Types', href: 'https://openid.net/specs/openid-risc-profile-1_0-ID1.html', }, + { + label: 'RFC 8417 §5 (Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc8417#section-5', + }, + { + label: 'OpenID CAEP §6 (Security Considerations)', + href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + }, + { + label: 'OpenID RISC §6 (Security Considerations)', + href: 'https://openid.net/specs/openid-risc-profile-1_0-ID1.html', + }, ], }, jti: { purpose: - 'JWT ID claim — a Transmitter-assigned unique identifier for ' + - 'this specific SET. Receivers cache `jti` values to detect ' + - 'replay; pollers also use `jti` as the acknowledgment key.', - withoutIt: - 'Without `jti` replay tracking, the same SET can be processed ' + - 'multiple times. For idempotent event types this is harmless; ' + - 'for events that trigger one-shot side effects it is a ' + - 'denial-of-service primitive (force a user through repeated ' + - 'forced-logouts) or a state-corruption primitive (downstream ' + - 'systems that count events).', - attack: - 'SET replay against a receiver without `jti` caching. Mallory ' + - 'captures one of Alice\'s legitimate `session-revoked` SETs ' + - 'from a transmitter\'s push delivery (network tap on a non-TLS ' + - 'internal hop, leaked log, malicious receiver-side load balancer). ' + - 'She replays it to the same receiver some time later. Without jti ' + - 'caching, the receiver processes the event again — and because ' + - 'session-revoked is destructive, this terminates Alice\'s ' + - '*current* session even though the original event was about a ' + - 'session she\'d already logged out of. Mallory can replay this ' + - 'on any cadence she likes to prevent Alice from ever staying ' + - 'logged in.', - impact: - 'DoS via replay-driven destructive action. Defences: (1) cache ' + - '`jti` for the duration of the validity window; (2) reject ' + - 'duplicate `jti` from the same Transmitter; (3) align cache TTL ' + - 'with the SET\'s implied validity (typically 5-15 minutes for ' + - 'real-time events).', + 'JWT ID claim — a Transmitter-assigned unique identifier for this ' + + 'specific SET. Receivers cache `jti` values to detect replay; ' + + 'pollers also use `jti` as the acknowledgment key.', + attacks: [ + { + id: 'set-replay-no-jti-cache', + name: 'SET replay against receiver without jti caching', + scenario: + 'Mallory captures one of Alice\'s legitimate `session-revoked` ' + + 'SETs from a transmitter\'s push delivery (network tap on a ' + + 'non-TLS internal hop, leaked log, malicious receiver-side load ' + + 'balancer). She replays it to the same receiver some time later. ' + + 'Without jti caching, the receiver processes the event again — ' + + 'and because session-revoked is destructive, this terminates ' + + 'Alice\'s *current* session even though the original event was ' + + 'about a session she\'d already logged out of. Mallory can ' + + 'replay this on any cadence she likes to prevent Alice from ' + + 'ever staying logged in.', + impact: + 'DoS via replay-driven destructive action; for state-tracking ' + + 'systems, downstream count corruption.', + }, + ], + mitigations: [ + { + action: + 'Cache `jti` for the duration of the validity window; reject ' + + 'duplicate `jti` from the same Transmitter.', + mitigates: ['set-replay-no-jti-cache'], + }, + { + action: + 'Align cache TTL with the SET\'s implied validity (typically ' + + '5-15 minutes for real-time events) — long enough to catch ' + + 'replay, short enough to bound memory.', + mitigates: ['set-replay-no-jti-cache'], + }, + ], references: [ { label: 'RFC 8417 §2.2 (jti)', @@ -191,47 +305,79 @@ export const SSF_EXPLAINERS: Record = { label: 'RFC 7519 §4.1.7 (jti claim)', href: 'https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7', }, + { + label: 'RFC 8417 §5 (Security Considerations — replay)', + href: 'https://datatracker.ietf.org/doc/html/rfc8417#section-5', + }, ], }, delivery: { purpose: - 'How SETs flow from Transmitter to Receiver. Two profiles: ' + - '**Push** (RFC 8935) — Transmitter POSTs each SET to the ' + - 'Receiver\'s endpoint as events occur; **Poll** (RFC 8936) — ' + - 'Receiver POSTs to fetch any pending SETs, with optional ' + - 'long-polling. Choice affects which side bears the resource cost ' + - 'and which side is on the attack surface.', - withoutIt: - 'Each delivery method has its own DoS profile: Push exposes the ' + - 'Receiver\'s endpoint to whoever can reach it (flood the receiver ' + - 'with SETs to force resource exhaustion); Poll exposes the ' + - 'Transmitter\'s endpoint to long-poll connection holding (open ' + - 'thousands of long-polls to consume Transmitter sockets and ' + - 'memory).', - attack: - 'Push-endpoint flooding. Mallory either compromises one ' + - 'Transmitter in the trust mesh or finds a Receiver whose push ' + - 'endpoint has lax authentication. She POSTs a high volume of ' + - 'SETs (each individually valid-looking) to consume the receiver\'s ' + - 'parsing and verification capacity — every SET requires a JWS ' + - 'verification, a jti cache lookup, and event-handler dispatch. ' + - 'The receiver either crashes or starts rejecting traffic, ' + - 'including legitimate events that the receiver actually needed ' + - '(e.g. real `account-disabled` events for compromised accounts ' + - 'now stuck in the queue). Variant for poll: hold thousands of ' + - 'long-poll connections to exhaust the Transmitter\'s socket / ' + - 'goroutine / thread pool.', - impact: - 'DoS plus availability impact on legitimate SET delivery — and ' + - 'the events being missed are precisely the ones the receiver ' + - 'most needs to act on. Defences: (1) authenticate every push ' + - 'request — receiver MUST validate the Transmitter\'s identity ' + - '(typically Bearer token bound to the stream); (2) rate-limit ' + - 'per-Transmitter; (3) for poll, cap concurrent long-poll ' + - 'connections per Receiver and reject excess; (4) timeouts and ' + - 'circuit breakers on push verification — don\'t let one bad ' + - 'transmitter exhaust the pool.', + 'How SETs flow from Transmitter to Receiver. Two profiles: **Push** ' + + '(RFC 8935) — Transmitter POSTs each SET to the Receiver\'s endpoint ' + + 'as events occur; **Poll** (RFC 8936) — Receiver POSTs to fetch any ' + + 'pending SETs, with optional long-polling. Choice affects which ' + + 'side bears the resource cost and which side is on the attack ' + + 'surface.', + attacks: [ + { + id: 'push-endpoint-flooding', + name: 'Push-endpoint flooding (DoS receiver)', + scenario: + 'Mallory either compromises one Transmitter in the trust mesh or ' + + 'finds a Receiver whose push endpoint has lax authentication. ' + + 'She POSTs a high volume of SETs (each individually valid-' + + 'looking) to consume the receiver\'s parsing and verification ' + + 'capacity — every SET requires a JWS verification, a jti cache ' + + 'lookup, and event-handler dispatch. The receiver either crashes ' + + 'or starts rejecting traffic, including legitimate events that ' + + 'the receiver actually needed (e.g. real `account-disabled` ' + + 'events for compromised accounts now stuck in the queue).', + impact: + 'DoS plus availability impact on legitimate SET delivery — and ' + + 'the events being missed are precisely the ones the receiver ' + + 'most needs to act on.', + }, + { + id: 'long-poll-connection-exhaustion', + name: 'Long-poll connection exhaustion (DoS transmitter)', + scenario: + 'Open thousands of long-poll connections against the ' + + 'Transmitter to consume Transmitter sockets, goroutines, ' + + 'thread pool — exhausting the resources legitimate Receivers ' + + 'need.', + impact: + 'Transmitter unable to serve legitimate Receivers; events ' + + 'queue up undelivered.', + }, + ], + mitigations: [ + { + action: + 'Receiver MUST authenticate every push request — validate the ' + + 'Transmitter\'s identity (typically Bearer token bound to the ' + + 'stream).', + mitigates: ['push-endpoint-flooding'], + }, + { + action: + 'Rate-limit per-Transmitter on the Receiver side; one bad ' + + 'transmitter must not exhaust the pool.', + mitigates: ['push-endpoint-flooding'], + }, + { + action: + 'For poll, cap concurrent long-poll connections per Receiver ' + + 'and reject excess.', + mitigates: ['long-poll-connection-exhaustion'], + }, + { + action: + 'Timeouts and circuit breakers on push verification.', + mitigates: ['push-endpoint-flooding'], + }, + ], references: [ { label: 'RFC 8935 (SET Push Delivery)', @@ -241,6 +387,14 @@ export const SSF_EXPLAINERS: Record = { label: 'RFC 8936 (SET Poll Delivery)', href: 'https://datatracker.ietf.org/doc/html/rfc8936', }, + { + label: 'RFC 8935 §5 (Push — Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc8935#section-5', + }, + { + label: 'RFC 8936 §5 (Poll — Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc8936#section-5', + }, ], }, @@ -249,81 +403,145 @@ export const SSF_EXPLAINERS: Record = { 'Identifier for an SSF stream — the relationship between one ' + 'Transmitter and one Receiver, including its delivery method, ' + 'subscribed event types, and authentication credentials. Created ' + - 'via POST to the configuration_endpoint; managed via stream-' + - 'control endpoints.', - withoutIt: - 'Streams are the access-control unit for SSF — who can subscribe ' + - 'to events for which subjects, with which event types. Stream ' + - 'creation that lacks proper authorization lets attackers create ' + - 'their own streams subscribing to events for arbitrary subjects.', - attack: - 'Unauthorized stream creation. The Transmitter\'s configuration ' + - 'endpoint accepts stream-creation requests with weak ' + - 'authentication (no token, shared bearer token leaked widely). ' + - 'Mallory POSTs a stream-creation request asking to receive ' + - '`account-disabled` events for Alice\'s subject identifier. The ' + - 'Transmitter creates the stream; from this point on, every ' + - '`account-disabled` event for Alice is also delivered to ' + - 'Mallory\'s receiver. Mallory now has real-time intelligence on ' + - 'when Alice\'s account is disabled — useful for timing attacks, ' + - 'social engineering ("we noticed your account was just disabled, ' + - 'click here to reactivate"), or simply confirming when ' + - 'compromise has been detected.', - impact: - 'Information disclosure of security-event signals to attackers. ' + - 'Defences: (1) protect the stream-management endpoint with ' + - 'strong authentication (OAuth2 client credentials, mTLS); (2) ' + - 'authorize stream-creation by the Receiver\'s legitimate ' + - 'identity, not just possession of a token; (3) limit which ' + - 'subjects each Receiver may subscribe to (a Receiver representing ' + - 'AppA shouldn\'t subscribe to events for users it doesn\'t ' + - 'manage); (4) audit stream creations.', + 'via POST to the configuration_endpoint; managed via stream-control ' + + 'endpoints.', + attacks: [ + { + id: 'unauthorized-stream-creation', + name: 'Unauthorized stream creation', + scenario: + 'The Transmitter\'s configuration endpoint accepts ' + + 'stream-creation requests with weak authentication (no token, ' + + 'shared bearer token leaked widely). Mallory POSTs a ' + + 'stream-creation request asking to receive `account-disabled` ' + + 'events for Alice\'s subject identifier. The Transmitter creates ' + + 'the stream; from this point on, every `account-disabled` event ' + + 'for Alice is also delivered to Mallory\'s receiver. Mallory ' + + 'now has real-time intelligence on when Alice\'s account is ' + + 'disabled — useful for timing attacks, social engineering ' + + '("we noticed your account was just disabled, click here to ' + + 'reactivate"), or simply confirming when compromise has been ' + + 'detected.', + impact: + 'Information disclosure of security-event signals to attackers.', + }, + ], + mitigations: [ + { + action: + 'Protect the stream-management endpoint with strong ' + + 'authentication (OAuth2 client credentials, mTLS).', + mitigates: ['unauthorized-stream-creation'], + }, + { + action: + 'Authorize stream-creation by the Receiver\'s legitimate ' + + 'identity, not just possession of a token.', + mitigates: ['unauthorized-stream-creation'], + }, + { + action: + 'Limit which subjects each Receiver may subscribe to — a ' + + 'Receiver representing AppA shouldn\'t subscribe to events for ' + + 'users it doesn\'t manage.', + mitigates: ['unauthorized-stream-creation'], + }, + { + action: + 'Audit stream creations; alert on unexpected new streams or ' + + 'subject additions.', + mitigates: ['unauthorized-stream-creation'], + }, + ], references: [ { label: 'OpenID SSF §7 (Stream Management)', href: 'https://openid.net/specs/openid-sharedsignals-framework-1_0-final.html', }, + { + label: 'OpenID SSF §11 (Security Considerations)', + href: 'https://openid.net/specs/openid-sharedsignals-framework-1_0-final.html', + }, ], }, subject: { purpose: - 'Identifies the entity (user, device, session, application) the ' + - 'SET is about. Multiple Subject Identifier formats per RFC 9493: ' + - '`email`, `iss_sub`, `opaque`, `phone_number`, `account`, ' + - '`did`, `uri`, `aliases`. The Receiver maps the subject to its ' + - 'local user record before taking action.', - withoutIt: - 'Subject identifier mapping is the choke point where the ' + - 'Transmitter\'s view of "who" meets the Receiver\'s view of ' + - '"who". Mismatched mappings either fail safe (event ignored) or ' + - 'fail dangerously (event applied to the wrong user).', - attack: - 'Subject confusion → wrong-user enforcement. The Transmitter ' + - 'identifies users by `email`. The Receiver matches incoming ' + - 'events by email too. Mallory has a colliding email — same ' + - 'address at a different IdP, or a legitimately-owned email that ' + - 'happens to match Alice\'s historical address (recycled ' + - 'corporate domain). A `credential-compromise` event for Mallory ' + - 'arrives at the Receiver, which keys on email and applies the ' + - 'compromise response to *Alice* (forced logout, password reset, ' + - 'token revocation). False-positive enforcement against the wrong ' + - 'subject. Variant: `iss_sub` (issuer + subject) format provides ' + - 'tenant scoping but only if the Receiver implements the ' + - 'composite-key match correctly.', - impact: - 'Cross-user effect of legitimate events — the SET-delivery ' + - 'analogue of the OIDC nOAuth attack (matching users by email ' + - 'instead of stable identifier). Defences: (1) prefer `iss_sub` ' + - '(issuer + subject pair) Subject Identifier format over mutable ' + - 'claims like email; (2) explicit mapping tables, not best-effort ' + - 'heuristics; (3) reject events whose subject doesn\'t map to a ' + - 'known local user (don\'t silently no-op — log and alert).', + 'Identifies the entity (user, device, session, application) the SET ' + + 'is about. Multiple Subject Identifier formats per RFC 9493: ' + + '`email`, `iss_sub`, `opaque`, `phone_number`, `account`, `did`, ' + + '`uri`, `aliases`. The Receiver maps the subject to its local user ' + + 'record before taking action.', + attacks: [ + { + id: 'subject-confusion-wrong-user', + name: 'Subject confusion → wrong-user enforcement', + scenario: + 'The Transmitter identifies users by `email`. The Receiver ' + + 'matches incoming events by email too. Mallory has a colliding ' + + 'email — same address at a different IdP, or a legitimately-' + + 'owned email that happens to match Alice\'s historical address ' + + '(recycled corporate domain). A `credential-compromise` event ' + + 'for Mallory arrives at the Receiver, which keys on email and ' + + 'applies the compromise response to *Alice* (forced logout, ' + + 'password reset, token revocation). False-positive enforcement ' + + 'against the wrong subject.', + impact: + 'Legitimate events trigger destructive responses against the ' + + 'wrong user — the SET-delivery analogue of the OIDC nOAuth ' + + 'attack (matching users by email instead of stable identifier).', + }, + { + id: 'iss-sub-composite-key-bug', + name: 'iss_sub composite-key match implementation bug', + scenario: + '`iss_sub` (issuer + subject) format provides tenant scoping ' + + 'but only if the Receiver implements the composite-key match ' + + 'correctly — a Receiver that compares only the `sub` portion ' + + 'reintroduces the cross-tenant collision class.', + impact: + 'Same wrong-user-enforcement outcome as plain email-based ' + + 'matching, despite using the safer identifier format.', + }, + ], + mitigations: [ + { + action: + 'Prefer `iss_sub` (issuer + subject pair) Subject Identifier ' + + 'format over mutable claims like email.', + mitigates: [ + 'subject-confusion-wrong-user', + 'iss-sub-composite-key-bug', + ], + }, + { + action: + 'Use explicit mapping tables, not best-effort heuristics, to ' + + 'translate Subject Identifiers to local user records.', + mitigates: ['subject-confusion-wrong-user'], + }, + { + action: + 'Reject events whose subject doesn\'t map to a known local user ' + + '— don\'t silently no-op; log and alert.', + mitigates: ['subject-confusion-wrong-user'], + }, + { + action: + 'When using `iss_sub`, match on the full composite key — ' + + 'never reduce to `sub` alone.', + mitigates: ['iss-sub-composite-key-bug'], + }, + ], references: [ { label: 'RFC 9493 (Subject Identifiers for SETs)', href: 'https://datatracker.ietf.org/doc/html/rfc9493', }, + { + label: 'RFC 9493 §6 (Security Considerations)', + href: 'https://datatracker.ietf.org/doc/html/rfc9493#section-6', + }, ], }, @@ -331,190 +549,303 @@ export const SSF_EXPLAINERS: Record = { purpose: 'CAEP claim identifying who/what triggered the event: `admin`, ' + '`user`, `policy`, `system`. Drives receiver-side response ' + - 'differentiation — admin-initiated revocation may warrant ' + - 'different handling than policy-driven revocation.', - withoutIt: - 'Without `initiating_entity`, the receiver treats every event ' + - 'identically — losing the ability to differentiate "user clicked ' + - 'sign-out" (benign) from "security system detected anomaly" ' + - '(potentially attacker-driven false signal).', - attack: - 'Audit-trail confusion. An attacker who can submit SETs may set ' + - '`initiating_entity=user` to make malicious revocations appear ' + - 'as if the user requested them — masking attacker activity in ' + - 'the audit logs. Conversely, missing `initiating_entity` gives ' + - 'the receiver no way to weight responses (an admin-initiated ' + - 'session-revoked might warrant a security alert; a user-' + - 'initiated one is routine).', - impact: - 'Audit and response-policy differentiation lost. Defences: (1) ' + - 'include `initiating_entity` on every CAEP event the Transmitter ' + - 'emits; (2) Receiver logs `initiating_entity` as part of every ' + - 'event-driven action; (3) consider differentiated response ' + - 'policies (rate-limit `policy`-initiated revocations more ' + - 'aggressively than `user`-initiated, since attacker-injected ' + - 'false signals will most often claim `policy`).', + 'differentiation — admin-initiated revocation may warrant different ' + + 'handling than policy-driven revocation.', + attacks: [ + { + id: 'initiating-entity-audit-trail-confusion', + name: 'Audit-trail confusion', + scenario: + 'An attacker who can submit SETs may set ' + + '`initiating_entity=user` to make malicious revocations appear ' + + 'as if the user requested them — masking attacker activity in ' + + 'the audit logs. Conversely, missing `initiating_entity` gives ' + + 'the receiver no way to weight responses (an admin-initiated ' + + 'session-revoked might warrant a security alert; a user-' + + 'initiated one is routine).', + impact: + 'Audit and response-policy differentiation lost; attacker ' + + 'activity disguised as user-initiated routine events.', + }, + ], + mitigations: [ + { + action: + 'Transmitter MUST include `initiating_entity` on every CAEP ' + + 'event.', + mitigates: ['initiating-entity-audit-trail-confusion'], + }, + { + action: + 'Receiver logs `initiating_entity` as part of every event-driven ' + + 'action — preserves the audit trail for post-incident analysis.', + mitigates: ['initiating-entity-audit-trail-confusion'], + }, + { + action: + 'Consider differentiated response policies — rate-limit ' + + '`policy`-initiated revocations more aggressively than ' + + '`user`-initiated, since attacker-injected false signals will ' + + 'most often claim `policy`.', + mitigates: ['initiating-entity-audit-trail-confusion'], + }, + ], references: [ { label: 'OpenID CAEP §2 (Common Event Properties)', href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', }, + { + label: 'OpenID CAEP §6 (Security Considerations)', + href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + }, ], }, reason: { purpose: - 'RISC `account-disabled` event property naming the high-level ' + - 'reason: `hijacking` (account-takeover detected) or ' + - '`bulk-account` (mass-compromise scenario). Lets Receivers ' + - 'differentiate response intensity.', - withoutIt: - 'Without `reason`, every account-disabled event triggers the ' + - 'same response. With `reason`, the Receiver can distinguish ' + - '"individual account compromised, contain it" from "bulk event ' + - 'affecting many accounts, also alert SOC and check correlated ' + - 'subjects".', - attack: - 'Reason-driven response amplification or suppression. An ' + - 'attacker submitting a forged SET can choose `reason` to either ' + - 'amplify response (claim `bulk-account` for a single account to ' + - 'trigger broader investigation overhead and alert fatigue) or ' + - 'suppress it (claim no specific reason for what is actually a ' + - 'mass event, hoping the receiver\'s default response is ' + - 'lighter).', - impact: - 'Response-policy manipulation when `reason` is taken at face ' + - 'value. Defences: (1) authenticate the Transmitter (signature on ' + - 'JWKS) before trusting `reason` to drive policy; (2) cross-check ' + - 'reason against observed event volume — a `bulk-account` claim ' + - 'with no other corroborating events is suspicious.', + 'RISC `account-disabled` event property naming the high-level reason: ' + + '`hijacking` (account-takeover detected) or `bulk-account` ' + + '(mass-compromise scenario). Lets Receivers differentiate response ' + + 'intensity.', + attacks: [ + { + id: 'reason-driven-amplification', + name: 'Response amplification via inflated reason', + scenario: + 'An attacker submitting a forged SET claims `bulk-account` for ' + + 'a single account to trigger broader investigation overhead and ' + + 'alert fatigue across the security team.', + impact: + 'Operational disruption — investigators spend time on a ' + + 'fabricated mass event.', + }, + { + id: 'reason-driven-suppression', + name: 'Response suppression via omitted/under-stated reason', + scenario: + 'An attacker claims no specific reason for what is actually a ' + + 'mass event, hoping the receiver\'s default response is ' + + 'lighter than what `bulk-account` would have triggered.', + impact: + 'Real bulk-compromise events handled with under-scoped response.', + }, + ], + mitigations: [ + { + action: + 'Authenticate the Transmitter (signature on JWKS) before ' + + 'trusting `reason` to drive policy.', + mitigates: [ + 'reason-driven-amplification', + 'reason-driven-suppression', + ], + }, + { + action: + 'Cross-check `reason` against observed event volume — a ' + + '`bulk-account` claim with no other corroborating events is ' + + 'suspicious.', + mitigates: ['reason-driven-amplification'], + }, + ], references: [ { label: 'OpenID RISC Profile §2.2 (account-disabled)', href: 'https://openid.net/specs/openid-risc-profile-1_0-ID1.html', }, + { + label: 'OpenID RISC §6 (Security Considerations)', + href: 'https://openid.net/specs/openid-risc-profile-1_0-ID1.html', + }, ], }, reason_admin: { purpose: 'Administrative log message attached to a CAEP/RISC event. ' + - 'Free-form text intended for the Receiver\'s logs and ' + - 'investigation pipelines. Sibling `reason_user` is intended for ' + - 'end-user-facing display.', - withoutIt: - 'The "without it" risk is *including too much* in `reason_admin` ' + - 'rather than omitting it. Transmitters that embed sensitive ' + - 'detection details ("user detected on Tor exit at $IP", ' + - '"matched breach database hit for password $hash") leak that ' + - 'information to Receivers — and through them to logs, SIEMs, ' + - 'and analytics pipelines that may not have appropriate ' + - 'sensitivity controls.', - attack: - 'Cross-organisation information leakage via SET payloads. The ' + - 'Transmitter is an enterprise IdP. The Receiver is a SaaS the ' + - 'enterprise federates to — operationally trusted but not under ' + - 'the enterprise\'s direct administrative control. The ' + - 'Transmitter emits a `credential-compromise` event with ' + - '`reason_admin` containing the user\'s name, the detection ' + - 'method, the suspected attacker\'s IP, and internal ticket ID. ' + - 'All of that lands in the SaaS\'s logs, accessible to the SaaS\'s ' + - 'support staff and any subprocessor of the SaaS\'s logging ' + - 'pipeline. RISC §2.1 explicitly warns: "Do NOT include actual ' + - 'compromised credential values in the SET" — but the broader ' + - 'principle (don\'t leak detection details) is often missed.', - impact: - 'Privacy and operational-information leakage outside the ' + - 'organisation. Defences: (1) treat `reason_admin` as crossing a ' + - 'data-sharing boundary; (2) include only what the Receiver ' + - 'needs to differentiate response policies (a category code, not ' + - 'free-form details); (3) NEVER include actual credential ' + - 'values, raw IP addresses, or PII the Receiver doesn\'t already ' + - 'have a legitimate processing basis for.', + 'Free-form text intended for the Receiver\'s logs and investigation ' + + 'pipelines. Sibling `reason_user` is intended for end-user-facing ' + + 'display.', + attacks: [ + { + id: 'cross-org-info-leak-reason-admin', + name: 'Cross-organisation information leakage via SET payloads', + scenario: + 'The Transmitter is an enterprise IdP. The Receiver is a SaaS ' + + 'the enterprise federates to — operationally trusted but not ' + + 'under the enterprise\'s direct administrative control. The ' + + 'Transmitter emits a `credential-compromise` event with ' + + '`reason_admin` containing the user\'s name, the detection ' + + 'method, the suspected attacker\'s IP, and internal ticket ID. ' + + 'All of that lands in the SaaS\'s logs, accessible to the SaaS\'s ' + + 'support staff and any subprocessor of the SaaS\'s logging ' + + 'pipeline. RISC §2.1 explicitly warns: "Do NOT include actual ' + + 'compromised credential values in the SET" — but the broader ' + + 'principle (don\'t leak detection details) is often missed.', + impact: + 'Privacy and operational-information leakage outside the ' + + 'organisation.', + }, + ], + mitigations: [ + { + action: + 'Treat `reason_admin` as crossing a data-sharing boundary — ' + + 'evaluate every field against your data-sharing policy before ' + + 'emitting.', + mitigates: ['cross-org-info-leak-reason-admin'], + }, + { + action: + 'Include only what the Receiver needs to differentiate response ' + + 'policies — a category code, not free-form details.', + mitigates: ['cross-org-info-leak-reason-admin'], + }, + { + action: + 'NEVER include actual credential values, raw IP addresses, or ' + + 'PII the Receiver doesn\'t already have a legitimate processing ' + + 'basis for.', + mitigates: ['cross-org-info-leak-reason-admin'], + }, + ], references: [ { label: 'OpenID CAEP §2 (reason_admin / reason_user)', href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', }, + { + label: 'OpenID CAEP §6 (Security Considerations) / RISC §2.1 Privacy Warning', + href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + }, ], }, credential_type: { purpose: 'CAEP `credential-change` event property identifying which ' + - 'credential changed: `password`, `pin`, `x509`, ' + - '`fido2-platform`, `fido2-roaming`, `fido-u2f`, ' + - '`verifiable-credential`, `phone-voice`, `phone-sms`, `app`. ' + - 'Lets Receivers scope their response to invalidating tokens ' + - 'derived from the changed credential.', - withoutIt: - 'Without `credential_type`, the Receiver either treats every ' + - 'credential change as full revocation (over-broad — annoying ' + - 'users when only their fingerprint enrollment changed) or as a ' + - 'soft signal (under-broad — keeps password-derived tokens valid ' + - 'after a password change).', - attack: - 'Tokens-from-stale-credential persistence. The user changes ' + - 'their password. The Transmitter emits a `credential-change` ' + - 'event with no `credential_type`. The Receiver, lacking enough ' + - 'detail, defaults to "log the event but don\'t revoke anything". ' + - 'Tokens issued during the original-password session remain ' + - 'valid until natural expiry — defeating the user\'s intent in ' + - 'changing the password. The user *thinks* they\'re secured; they ' + - 'are not.', - impact: - 'Persistence of tokens past credential rotation. Defences: ' + - '(1) Transmitter MUST include `credential_type` on every ' + - '`credential-change` event; (2) Receiver MUST scope its ' + - 'response to tokens / sessions actually derived from the ' + - 'specific credential type — and trigger full revocation when ' + - 'the credential type can\'t be determined; (3) for password / ' + - 'high-assurance changes, force re-authentication with the new ' + - 'credential before reissuing any tokens.', + 'credential changed: `password`, `pin`, `x509`, `fido2-platform`, ' + + '`fido2-roaming`, `fido-u2f`, `verifiable-credential`, ' + + '`phone-voice`, `phone-sms`, `app`. Lets Receivers scope their ' + + 'response to invalidating tokens derived from the changed credential.', + attacks: [ + { + id: 'tokens-from-stale-credential-persistence', + name: 'Tokens-from-stale-credential persistence', + scenario: + 'The user changes their password. The Transmitter emits a ' + + '`credential-change` event with no `credential_type`. The ' + + 'Receiver, lacking enough detail, defaults to "log the event ' + + 'but don\'t revoke anything". Tokens issued during the original-' + + 'password session remain valid until natural expiry — defeating ' + + 'the user\'s intent in changing the password. The user *thinks* ' + + 'they\'re secured; they are not.', + impact: + 'Persistence of tokens past credential rotation.', + }, + ], + mitigations: [ + { + action: + 'Transmitter MUST include `credential_type` on every ' + + '`credential-change` event.', + mitigates: ['tokens-from-stale-credential-persistence'], + }, + { + action: + 'Receiver MUST scope its response to tokens / sessions actually ' + + 'derived from the specific credential type — and trigger full ' + + 'revocation when the credential type can\'t be determined.', + mitigates: ['tokens-from-stale-credential-persistence'], + }, + { + action: + 'For password / high-assurance changes, force re-authentication ' + + 'with the new credential before reissuing any tokens.', + mitigates: ['tokens-from-stale-credential-persistence'], + }, + ], references: [ { label: 'OpenID CAEP §3.2 (credential-change)', href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', }, + { + label: 'OpenID CAEP §6 (Security Considerations)', + href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + }, ], }, change_type: { purpose: - 'CAEP `credential-change` event property: `create` (new ' + - 'credential added), `revoke` (credential removed), or `update` ' + - '(credential modified, e.g. password reset). Drives the ' + - 'Receiver\'s response — `revoke` triggers token invalidation; ' + - '`create` may be informational; `update` requires nuanced ' + - 'response.', - withoutIt: - 'Without `change_type`, the Receiver cannot tell "user added a ' + - 'new MFA factor" (benign) from "user removed all credentials" ' + - '(emergency). Default-to-strict means every credential add ' + - 'forces re-auth (annoying); default-to-permissive means ' + - 'credential revocation events fail to trigger token revocation ' + - '(dangerous).', - attack: - 'Same persistence-of-tokens class as `credential_type` — but at ' + - 'the create/revoke/update granularity. An attacker who can ' + - 'manipulate `change_type` (forged SET, compromised Transmitter) ' + - 'sends `create` instead of `revoke` for a removed credential — ' + - 'Receiver logs the event as benign rather than triggering ' + - 'revocation. Or sends `update` instead of `revoke` to muddy the ' + - 'audit trail when removing the user\'s only remaining MFA factor.', - impact: - 'Audit-trail manipulation and missed revocation. Defences: ' + - '(1) authenticate every SET signature before trusting `change_' + - 'type`; (2) cross-check change_type values against IdP logs ' + - 'periodically (a stream of credential-change events with no ' + - 'corresponding IdP-side credential modifications is a forged-' + - 'SET signal); (3) if `change_type=revoke`, treat as the highest-' + - 'urgency CAEP event after RISC events.', + 'CAEP `credential-change` event property: `create` (new credential ' + + 'added), `revoke` (credential removed), or `update` (credential ' + + 'modified, e.g. password reset). Drives the Receiver\'s response — ' + + '`revoke` triggers token invalidation; `create` may be ' + + 'informational; `update` requires nuanced response.', + attacks: [ + { + id: 'change-type-revoke-as-create', + name: 'Revoke disguised as create — missed revocation', + scenario: + 'An attacker who can manipulate `change_type` (forged SET, ' + + 'compromised Transmitter) sends `create` instead of `revoke` ' + + 'for a removed credential. The Receiver logs the event as ' + + 'benign rather than triggering token revocation.', + impact: + 'Tokens that should have been revoked remain valid — silent ' + + 'persistence past credential removal.', + }, + { + id: 'change-type-update-disguise', + name: 'Update disguising revocation in audit trail', + scenario: + 'Sends `update` instead of `revoke` to muddy the audit trail ' + + 'when removing the user\'s only remaining MFA factor — ' + + 'investigators looking for revocation events miss it.', + impact: + 'Audit-trail manipulation that delays incident response.', + }, + ], + mitigations: [ + { + action: + 'Authenticate every SET signature before trusting `change_type` ' + + 'to drive enforcement decisions.', + mitigates: [ + 'change-type-revoke-as-create', + 'change-type-update-disguise', + ], + }, + { + action: + 'Cross-check change_type values against IdP logs periodically — ' + + 'a stream of credential-change events with no corresponding ' + + 'IdP-side credential modifications is a forged-SET signal.', + mitigates: [ + 'change-type-revoke-as-create', + 'change-type-update-disguise', + ], + }, + { + action: + 'Treat `change_type=revoke` as the highest-urgency CAEP event ' + + 'after RISC events — bias toward over-revocation rather than ' + + 'under-revocation.', + mitigates: ['change-type-revoke-as-create'], + }, + ], references: [ { label: 'OpenID CAEP §3.2 (credential-change change_type)', href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', }, + { + label: 'OpenID CAEP §6 (Security Considerations)', + href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + }, ], }, } diff --git a/frontend/src/views/FlowDetail.tsx b/frontend/src/views/FlowDetail.tsx index 659d3e5..dd81f9e 100644 --- a/frontend/src/views/FlowDetail.tsx +++ b/frontend/src/views/FlowDetail.tsx @@ -332,6 +332,7 @@ export function FlowDetail({ step={step} index={index} protocolId={protocolId} + flowId={flow.id} isActive={activeStep === step.order} isLast={index === flow.steps.length - 1} onClick={() => setActiveStep(activeStep === step.order ? -1 : step.order)} @@ -387,10 +388,11 @@ export function FlowDetail({ export default FlowDetail // Step Row Component -function StepRow({ step, index, protocolId, isActive, isLast, onClick }: { +function StepRow({ step, index, protocolId, flowId, isActive, isLast, onClick }: { step: FlowStep & { security?: string[] } index: number protocolId: string + flowId: string isActive: boolean isLast: boolean onClick: () => void @@ -462,6 +464,9 @@ function StepRow({ step, index, protocolId, isActive, isLast, onClick }: { From ad32aa796799a6131b9877f6f8faa264faf9ea54 Mon Sep 17 00:00:00 2001 From: Ayoma Wijethunga Date: Sun, 3 May 2026 14:41:15 +0530 Subject: [PATCH 3/9] CREATE specs & references panel on protocol and flow pages Signed-off-by: Ayoma Wijethunga --- .../src/components/ProtocolReferences.tsx | 123 +++++ frontend/src/protocols/explainers/ssf.ts | 50 +- .../presentation/protocol-catalog-data.ts | 500 ++++++++++++++++-- .../presentation/protocol-catalog.ts | 4 +- frontend/src/views/FlowDetail.tsx | 17 +- frontend/src/views/ProtocolDemo.tsx | 14 +- 6 files changed, 644 insertions(+), 64 deletions(-) create mode 100644 frontend/src/components/ProtocolReferences.tsx diff --git a/frontend/src/components/ProtocolReferences.tsx b/frontend/src/components/ProtocolReferences.tsx new file mode 100644 index 0000000..d742c83 --- /dev/null +++ b/frontend/src/components/ProtocolReferences.tsx @@ -0,0 +1,123 @@ +import { BookMarked, ExternalLink, ShieldCheck, FileText, Layers, Award } from 'lucide-react' +import type { + ProtocolReference, + ProtocolReferenceCategory, +} from '@/protocols/presentation/protocol-catalog-data' + +interface ProtocolReferencesProps { + title: string + description: string + references: ProtocolReference[] +} + +const CATEGORY_ORDER: ProtocolReferenceCategory[] = ['core', 'security', 'companion', 'profile'] + +const CATEGORY_META: Record< + ProtocolReferenceCategory, + { title: string; description: string; icon: typeof FileText; accent: string } +> = { + core: { + title: 'Core specs', + description: 'The specifications that define this protocol.', + icon: FileText, + accent: 'text-blue-300', + }, + security: { + title: 'Security & privacy', + description: 'Dedicated security and privacy considerations.', + icon: ShieldCheck, + accent: 'text-emerald-300', + }, + companion: { + title: 'Companion specs', + description: 'Extensions, hardenings, and supporting RFCs.', + icon: Layers, + accent: 'text-purple-300', + }, + profile: { + title: 'Deployment profiles', + description: 'Profiles that constrain the protocol for specific assurance regimes.', + icon: Award, + accent: 'text-amber-300', + }, +} + +export function ProtocolReferences({ title, description, references }: ProtocolReferencesProps) { + if (references.length === 0) { + return null + } + + const grouped = CATEGORY_ORDER + .map((category) => ({ + category, + items: references.filter((r) => r.category === category), + })) + .filter((group) => group.items.length > 0) + + return ( +
+
+
+ +
+
+

{title}

+

{description}

+
+
+ +
+ {grouped.map(({ category, items }) => ( + + ))} +
+
+ ) +} + +function CategoryGroup({ + category, + items, +}: { + category: ProtocolReferenceCategory + items: ProtocolReference[] +}) { + const meta = CATEGORY_META[category] + const Icon = meta.icon + + return ( +
+
+ +

+ {meta.title} +

+ · {meta.description} +
+ +
+ ) +} + +export default ProtocolReferences diff --git a/frontend/src/protocols/explainers/ssf.ts b/frontend/src/protocols/explainers/ssf.ts index c59b64b..868c234 100644 --- a/frontend/src/protocols/explainers/ssf.ts +++ b/frontend/src/protocols/explainers/ssf.ts @@ -235,23 +235,23 @@ export const SSF_EXPLAINERS: Record = { references: [ { label: 'OpenID CAEP Spec', - href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + href: 'https://openid.net/specs/openid-caep-1_0-final.html', }, { label: 'OpenID RISC Event Types', - href: 'https://openid.net/specs/openid-risc-profile-1_0-ID1.html', + href: 'https://openid.net/specs/openid-risc-1_0.html', }, { label: 'RFC 8417 §5 (Security Considerations)', href: 'https://datatracker.ietf.org/doc/html/rfc8417#section-5', }, { - label: 'OpenID CAEP §6 (Security Considerations)', - href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + label: 'OpenID CAEP §4 (Security Considerations)', + href: 'https://openid.net/specs/openid-caep-1_0-final.html', }, { - label: 'OpenID RISC §6 (Security Considerations)', - href: 'https://openid.net/specs/openid-risc-profile-1_0-ID1.html', + label: 'OpenID RISC §4 (Security Considerations)', + href: 'https://openid.net/specs/openid-risc-1_0.html', }, ], }, @@ -593,11 +593,11 @@ export const SSF_EXPLAINERS: Record = { references: [ { label: 'OpenID CAEP §2 (Common Event Properties)', - href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + href: 'https://openid.net/specs/openid-caep-1_0-final.html', }, { - label: 'OpenID CAEP §6 (Security Considerations)', - href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + label: 'OpenID CAEP §4 (Security Considerations)', + href: 'https://openid.net/specs/openid-caep-1_0-final.html', }, ], }, @@ -651,12 +651,12 @@ export const SSF_EXPLAINERS: Record = { ], references: [ { - label: 'OpenID RISC Profile §2.2 (account-disabled)', - href: 'https://openid.net/specs/openid-risc-profile-1_0-ID1.html', + label: 'OpenID RISC Profile §2.3 (account-disabled)', + href: 'https://openid.net/specs/openid-risc-1_0.html', }, { - label: 'OpenID RISC §6 (Security Considerations)', - href: 'https://openid.net/specs/openid-risc-profile-1_0-ID1.html', + label: 'OpenID RISC §4 (Security Considerations)', + href: 'https://openid.net/specs/openid-risc-1_0.html', }, ], }, @@ -680,7 +680,7 @@ export const SSF_EXPLAINERS: Record = { 'method, the suspected attacker\'s IP, and internal ticket ID. ' + 'All of that lands in the SaaS\'s logs, accessible to the SaaS\'s ' + 'support staff and any subprocessor of the SaaS\'s logging ' + - 'pipeline. RISC §2.1 explicitly warns: "Do NOT include actual ' + + 'pipeline. RISC §2.7 explicitly warns: "Do NOT include actual ' + 'compromised credential values in the SET" — but the broader ' + 'principle (don\'t leak detection details) is often missed.', impact: @@ -713,11 +713,11 @@ export const SSF_EXPLAINERS: Record = { references: [ { label: 'OpenID CAEP §2 (reason_admin / reason_user)', - href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + href: 'https://openid.net/specs/openid-caep-1_0-final.html', }, { - label: 'OpenID CAEP §6 (Security Considerations) / RISC §2.1 Privacy Warning', - href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + label: 'OpenID CAEP §4 (Security Considerations) / RISC §2.7 Privacy Warning', + href: 'https://openid.net/specs/openid-caep-1_0-final.html', }, ], }, @@ -768,12 +768,12 @@ export const SSF_EXPLAINERS: Record = { ], references: [ { - label: 'OpenID CAEP §3.2 (credential-change)', - href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + label: 'OpenID CAEP §3.3 (credential-change)', + href: 'https://openid.net/specs/openid-caep-1_0-final.html', }, { - label: 'OpenID CAEP §6 (Security Considerations)', - href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + label: 'OpenID CAEP §4 (Security Considerations)', + href: 'https://openid.net/specs/openid-caep-1_0-final.html', }, ], }, @@ -839,12 +839,12 @@ export const SSF_EXPLAINERS: Record = { ], references: [ { - label: 'OpenID CAEP §3.2 (credential-change change_type)', - href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + label: 'OpenID CAEP §3.3 (credential-change change_type)', + href: 'https://openid.net/specs/openid-caep-1_0-final.html', }, { - label: 'OpenID CAEP §6 (Security Considerations)', - href: 'https://openid.net/specs/openid-caep-1_0-ID2.html', + label: 'OpenID CAEP §4 (Security Considerations)', + href: 'https://openid.net/specs/openid-caep-1_0-final.html', }, ], }, diff --git a/frontend/src/protocols/presentation/protocol-catalog-data.ts b/frontend/src/protocols/presentation/protocol-catalog-data.ts index 7404394..5599869 100644 --- a/frontend/src/protocols/presentation/protocol-catalog-data.ts +++ b/frontend/src/protocols/presentation/protocol-catalog-data.ts @@ -1,8 +1,18 @@ +export type ProtocolReferenceCategory = 'core' | 'security' | 'companion' | 'profile' + +export interface ProtocolReference { + category: ProtocolReferenceCategory + label: string + href: string + note?: string +} + export interface ProtocolFlowCatalogData { id: string name: string rfc: string backendId?: string + references?: ProtocolReference[] } export interface ProtocolCatalogDataItem { @@ -12,6 +22,7 @@ export interface ProtocolCatalogDataItem { spec: string specUrl: string flows: ProtocolFlowCatalogData[] + references: ProtocolReference[] } // Build-safe route and sitemap source: no UI component imports. @@ -23,12 +34,83 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ spec: 'RFC 6749', specUrl: 'https://datatracker.ietf.org/doc/html/rfc6749', flows: [ - { id: 'authorization-code', backendId: 'authorization_code', name: 'Authorization Code', rfc: '§4.1' }, - { id: 'authorization-code-pkce', backendId: 'authorization_code_pkce', name: 'Authorization Code + PKCE', rfc: 'RFC 7636' }, - { id: 'client-credentials', backendId: 'client_credentials', name: 'Client Credentials', rfc: '§4.4' }, - { id: 'refresh-token', backendId: 'refresh_token', name: 'Refresh Token', rfc: '§6' }, - { id: 'token-introspection', backendId: 'token_introspection', name: 'Token Introspection', rfc: 'RFC 7662' }, - { id: 'token-revocation', backendId: 'token_revocation', name: 'Token Revocation', rfc: 'RFC 7009' }, + { + id: 'authorization-code', + backendId: 'authorization_code', + name: 'Authorization Code', + rfc: '§4.1', + references: [ + { category: 'core', label: 'RFC 6749 §4.1 — Authorization Code Grant', href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-4.1' }, + { category: 'security', label: 'RFC 9700 §2.1 — Protecting Redirect-Based Flows', href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-2.1' }, + ], + }, + { + id: 'authorization-code-pkce', + backendId: 'authorization_code_pkce', + name: 'Authorization Code + PKCE', + rfc: 'RFC 7636', + references: [ + { category: 'core', label: 'RFC 6749 §4.1 — Authorization Code Grant', href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-4.1' }, + { category: 'core', label: 'RFC 7636 — Proof Key for Code Exchange', href: 'https://datatracker.ietf.org/doc/html/rfc7636' }, + { category: 'security', label: 'RFC 9700 §2.1.1 — PKCE for All Clients', href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-2.1.1' }, + ], + }, + { + id: 'client-credentials', + backendId: 'client_credentials', + name: 'Client Credentials', + rfc: '§4.4', + references: [ + { category: 'core', label: 'RFC 6749 §4.4 — Client Credentials Grant', href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-4.4' }, + { category: 'security', label: 'RFC 9700 §4.5 — Client Authentication', href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-4.5' }, + ], + }, + { + id: 'refresh-token', + backendId: 'refresh_token', + name: 'Refresh Token', + rfc: '§6', + references: [ + { category: 'core', label: 'RFC 6749 §6 — Refreshing an Access Token', href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-6' }, + { category: 'security', label: 'RFC 9700 §2.2.2 — Refresh Token Protection', href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-2.2.2' }, + ], + }, + { + id: 'token-introspection', + backendId: 'token_introspection', + name: 'Token Introspection', + rfc: 'RFC 7662', + references: [ + { category: 'core', label: 'RFC 7662 — Token Introspection', href: 'https://datatracker.ietf.org/doc/html/rfc7662' }, + { category: 'security', label: 'RFC 7662 §4 — Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc7662#section-4' }, + ], + }, + { + id: 'token-revocation', + backendId: 'token_revocation', + name: 'Token Revocation', + rfc: 'RFC 7009', + references: [ + { category: 'core', label: 'RFC 7009 — Token Revocation', href: 'https://datatracker.ietf.org/doc/html/rfc7009' }, + { category: 'security', label: 'RFC 7009 §5 — Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc7009#section-5' }, + ], + }, + ], + references: [ + { category: 'core', label: 'RFC 6749 — OAuth 2.0 Authorization Framework', href: 'https://datatracker.ietf.org/doc/html/rfc6749' }, + { category: 'core', label: 'RFC 6750 — Bearer Token Usage', href: 'https://datatracker.ietf.org/doc/html/rfc6750' }, + { category: 'security', label: 'RFC 9700 — OAuth 2.0 Security Best Current Practice (BCP 240)', href: 'https://datatracker.ietf.org/doc/html/rfc9700', note: 'The canonical security guidance for modern OAuth deployments.' }, + { category: 'security', label: 'RFC 6819 — OAuth 2.0 Threat Model and Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc6819', note: 'Original threat model; superseded in practice by RFC 9700.' }, + { category: 'companion', label: 'RFC 7636 — Proof Key for Code Exchange (PKCE)', href: 'https://datatracker.ietf.org/doc/html/rfc7636' }, + { category: 'companion', label: 'RFC 7662 — Token Introspection', href: 'https://datatracker.ietf.org/doc/html/rfc7662' }, + { category: 'companion', label: 'RFC 7009 — Token Revocation', href: 'https://datatracker.ietf.org/doc/html/rfc7009' }, + { category: 'companion', label: 'RFC 8414 — Authorization Server Metadata', href: 'https://datatracker.ietf.org/doc/html/rfc8414' }, + { category: 'companion', label: 'RFC 8693 — Token Exchange', href: 'https://datatracker.ietf.org/doc/html/rfc8693' }, + { category: 'companion', label: 'RFC 8707 — Resource Indicators', href: 'https://datatracker.ietf.org/doc/html/rfc8707' }, + { category: 'companion', label: 'RFC 9207 — Authorization Server Issuer Identification', href: 'https://datatracker.ietf.org/doc/html/rfc9207', note: 'AS Mix-Up defence.' }, + { category: 'companion', label: 'RFC 9449 — DPoP (Demonstrating Proof of Possession)', href: 'https://datatracker.ietf.org/doc/html/rfc9449' }, + { category: 'companion', label: 'RFC 9101 — JWT-Secured Authorization Request (JAR)', href: 'https://datatracker.ietf.org/doc/html/rfc9101' }, + { category: 'companion', label: 'RFC 9126 — Pushed Authorization Requests (PAR)', href: 'https://datatracker.ietf.org/doc/html/rfc9126' }, ], }, { @@ -38,11 +120,74 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ spec: 'OpenID Connect Core 1.0', specUrl: 'https://openid.net/specs/openid-connect-core-1_0.html', flows: [ - { id: 'oidc-authorization-code', backendId: 'oidc_authorization_code', name: 'Authorization Code Flow', rfc: '§3.1' }, - { id: 'oidc-implicit', backendId: 'oidc_implicit', name: 'Implicit Flow (Legacy)', rfc: '§3.2' }, - { id: 'hybrid', backendId: 'oidc_hybrid', name: 'Hybrid Flow', rfc: '§3.3' }, - { id: 'userinfo', backendId: 'oidc_userinfo', name: 'UserInfo Endpoint', rfc: '§5.3' }, - { id: 'discovery', backendId: 'oidc_discovery', name: 'Discovery', rfc: 'Discovery 1.0' }, + { + id: 'oidc-authorization-code', + backendId: 'oidc_authorization_code', + name: 'Authorization Code Flow', + rfc: '§3.1', + references: [ + { category: 'core', label: 'OIDC Core §3.1 — Authentication using the Authorization Code Flow', href: 'https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth' }, + { category: 'security', label: 'OIDC Core §16 — Security Considerations', href: 'https://openid.net/specs/openid-connect-core-1_0.html#Security' }, + { category: 'security', label: 'OIDC Core §15.5.2 — Nonce Implementation Notes', href: 'https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes' }, + ], + }, + { + id: 'oidc-implicit', + backendId: 'oidc_implicit', + name: 'Implicit Flow (Legacy)', + rfc: '§3.2', + references: [ + { category: 'core', label: 'OIDC Core §3.2 — Authentication using the Implicit Flow', href: 'https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth' }, + { category: 'security', label: 'RFC 9700 §2.1.2 — Implicit Grant SHOULD NOT Be Used', href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-2.1.2', note: 'Modern guidance: prefer Authorization Code + PKCE.' }, + ], + }, + { + id: 'hybrid', + backendId: 'oidc_hybrid', + name: 'Hybrid Flow', + rfc: '§3.3', + references: [ + { category: 'core', label: 'OIDC Core §3.3 — Authentication using the Hybrid Flow', href: 'https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth' }, + { category: 'security', label: 'OIDC Core §3.3.5 — Hybrid Flow Security', href: 'https://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken' }, + ], + }, + { + id: 'userinfo', + backendId: 'oidc_userinfo', + name: 'UserInfo Endpoint', + rfc: '§5.3', + references: [ + { category: 'core', label: 'OIDC Core §5.3 — UserInfo Endpoint', href: 'https://openid.net/specs/openid-connect-core-1_0.html#UserInfo' }, + { category: 'security', label: 'OIDC Core §16.11 — Token Substitution', href: 'https://openid.net/specs/openid-connect-core-1_0.html#TokenSubstitution' }, + ], + }, + { + id: 'discovery', + backendId: 'oidc_discovery', + name: 'Discovery', + rfc: 'Discovery 1.0', + references: [ + { category: 'core', label: 'OpenID Connect Discovery 1.0', href: 'https://openid.net/specs/openid-connect-discovery-1_0.html' }, + { category: 'security', label: 'Discovery 1.0 §7 — Security Considerations', href: 'https://openid.net/specs/openid-connect-discovery-1_0.html#Security' }, + ], + }, + ], + references: [ + { category: 'core', label: 'OpenID Connect Core 1.0', href: 'https://openid.net/specs/openid-connect-core-1_0.html' }, + { category: 'core', label: 'OpenID Connect Discovery 1.0', href: 'https://openid.net/specs/openid-connect-discovery-1_0.html' }, + { category: 'core', label: 'OpenID Connect Dynamic Client Registration 1.0', href: 'https://openid.net/specs/openid-connect-registration-1_0.html' }, + { category: 'core', label: 'OpenID Connect RP-Initiated Logout 1.0', href: 'https://openid.net/specs/openid-connect-rpinitiated-1_0.html' }, + { category: 'core', label: 'OpenID Connect Back-Channel Logout 1.0', href: 'https://openid.net/specs/openid-connect-backchannel-1_0.html' }, + { category: 'core', label: 'OpenID Connect Front-Channel Logout 1.0', href: 'https://openid.net/specs/openid-connect-frontchannel-1_0.html' }, + { category: 'security', label: 'OpenID Connect Core §16 — Security Considerations', href: 'https://openid.net/specs/openid-connect-core-1_0.html#Security', note: 'Section dedicated to OIDC-specific threats above OAuth 2.0.' }, + { category: 'companion', label: 'RFC 7519 — JSON Web Token (JWT)', href: 'https://datatracker.ietf.org/doc/html/rfc7519' }, + { category: 'companion', label: 'RFC 7515 — JSON Web Signature (JWS)', href: 'https://datatracker.ietf.org/doc/html/rfc7515' }, + { category: 'companion', label: 'RFC 7516 — JSON Web Encryption (JWE)', href: 'https://datatracker.ietf.org/doc/html/rfc7516' }, + { category: 'companion', label: 'RFC 7517 — JSON Web Key (JWK)', href: 'https://datatracker.ietf.org/doc/html/rfc7517' }, + { category: 'companion', label: 'RFC 7518 — JSON Web Algorithms (JWA)', href: 'https://datatracker.ietf.org/doc/html/rfc7518' }, + { category: 'companion', label: 'RFC 9493 — Subject Identifiers for SETs', href: 'https://datatracker.ietf.org/doc/html/rfc9493' }, + { category: 'profile', label: 'FAPI 2.0 Security Profile', href: 'https://openid.net/specs/fapi-2_0-security-profile.html', note: 'High-assurance profile for financial-grade APIs.' }, + { category: 'profile', label: 'FAPI 2.0 Message Signing', href: 'https://openid.net/specs/fapi-2_0-message-signing.html' }, ], }, { @@ -52,9 +197,47 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ spec: 'OpenID4VCI 1.0', specUrl: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html', flows: [ - { id: 'oid4vci-pre-authorized', name: 'Pre-Authorized Code', rfc: 'OID4VCI §4, §6.1, §8' }, - { id: 'oid4vci-pre-authorized-tx-code', name: 'Pre-Authorized + tx_code', rfc: 'OID4VCI §6.1' }, - { id: 'oid4vci-deferred-issuance', name: 'Deferred Issuance', rfc: 'OID4VCI Deferred Endpoint' }, + { + id: 'oid4vci-pre-authorized', + name: 'Pre-Authorized Code', + rfc: 'OID4VCI §4, §6.1, §8', + references: [ + { category: 'core', label: 'OID4VCI 1.0 §4 — Credential Offer', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-credential-offer' }, + { category: 'core', label: 'OID4VCI 1.0 §6.1 — Pre-Authorized Code Flow', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-pre-authorized-code-flow' }, + { category: 'core', label: 'OID4VCI 1.0 §8 — Credential Endpoint', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-credential-endpoint' }, + { category: 'security', label: 'OID4VCI 1.0 §11.2 — Pre-Authorized Code Flow Security', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-security-considerations' }, + ], + }, + { + id: 'oid4vci-pre-authorized-tx-code', + name: 'Pre-Authorized + tx_code', + rfc: 'OID4VCI §6.1', + references: [ + { category: 'core', label: 'OID4VCI 1.0 §6.1 — Pre-Authorized Code Flow', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-pre-authorized-code-flow' }, + { category: 'security', label: 'OID4VCI 1.0 §11.3 — PIN / tx_code Phishing', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-security-considerations' }, + ], + }, + { + id: 'oid4vci-deferred-issuance', + name: 'Deferred Issuance', + rfc: 'OID4VCI Deferred Endpoint', + references: [ + { category: 'core', label: 'OID4VCI 1.0 §9 — Deferred Credential Endpoint', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-deferred-credential-endpoin' }, + { category: 'security', label: 'OID4VCI 1.0 §11 — Security Considerations', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-security-considerations' }, + ], + }, + ], + references: [ + { category: 'core', label: 'OpenID for Verifiable Credential Issuance 1.0', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html' }, + { category: 'security', label: 'OID4VCI 1.0 §11 — Security Considerations', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-security-considerations' }, + { category: 'security', label: 'OpenID Foundation — Formal Security Analysis of OpenID for VCs', href: 'https://openid.net/formal-security-analysis-openid-verifiable-credentials/', note: 'Independent formal analysis covering OID4VCI and OID4VP.' }, + { category: 'companion', label: 'IETF SD-JWT (Selective Disclosure for JWTs)', href: 'https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/' }, + { category: 'companion', label: 'IETF SD-JWT VC (SD-JWT-based Verifiable Credentials)', href: 'https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/' }, + { category: 'companion', label: 'W3C Verifiable Credentials Data Model 2.0', href: 'https://www.w3.org/TR/vc-data-model-2.0/' }, + { category: 'companion', label: 'ISO/IEC 18013-5 — Mobile Driving Licence (mDL)', href: 'https://www.iso.org/standard/69084.html' }, + { category: 'companion', label: 'RFC 7800 — Proof-of-Possession Key Semantics for JWTs', href: 'https://datatracker.ietf.org/doc/html/rfc7800', note: 'cnf claim used for credential key binding.' }, + { category: 'companion', label: 'RFC 7636 — Proof Key for Code Exchange (PKCE)', href: 'https://datatracker.ietf.org/doc/html/rfc7636' }, + { category: 'profile', label: 'OpenID4VC High Assurance Interoperability Profile (HAIP)', href: 'https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0.html', note: 'Required-feature profile for eIDAS 2 and similar high-assurance regimes.' }, ], }, { @@ -64,8 +247,36 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ spec: 'OpenID4VP 1.0', specUrl: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html', flows: [ - { id: 'oid4vp-direct-post', name: 'DCQL + direct_post', rfc: 'OID4VP §5, §8.2' }, - { id: 'oid4vp-direct-post-jwt', name: 'DCQL + direct_post.jwt', rfc: 'OID4VP §8.3.1' }, + { + id: 'oid4vp-direct-post', + name: 'DCQL + direct_post', + rfc: 'OID4VP §5, §8.2', + references: [ + { category: 'core', label: 'OID4VP 1.0 §5 — Authorization Request', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-authorization-request' }, + { category: 'core', label: 'OID4VP 1.0 §6.1 — DCQL (Digital Credentials Query Language)', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-digital-credentials-query-l' }, + { category: 'core', label: 'OID4VP 1.0 §8.2 — direct_post Response Mode', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-response-modes' }, + { category: 'security', label: 'OID4VP 1.0 §11.1 — Verifier Impersonation', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-security-considerations' }, + ], + }, + { + id: 'oid4vp-direct-post-jwt', + name: 'DCQL + direct_post.jwt', + rfc: 'OID4VP §8.3.1', + references: [ + { category: 'core', label: 'OID4VP 1.0 §8.3 — direct_post.jwt Response Mode', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-response-modes' }, + { category: 'core', label: 'RFC 7516 — JSON Web Encryption (JWE)', href: 'https://datatracker.ietf.org/doc/html/rfc7516' }, + { category: 'security', label: 'OID4VP 1.0 §11.2 — Nonce Binding', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-security-considerations' }, + ], + }, + ], + references: [ + { category: 'core', label: 'OpenID for Verifiable Presentations 1.0', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html' }, + { category: 'security', label: 'OID4VP 1.0 §11 — Security Considerations', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-security-considerations' }, + { category: 'security', label: 'OpenID Foundation — Formal Security Analysis of OpenID for VCs', href: 'https://openid.net/formal-security-analysis-openid-verifiable-credentials/' }, + { category: 'companion', label: 'RFC 9101 — JWT-Secured Authorization Request (JAR)', href: 'https://datatracker.ietf.org/doc/html/rfc9101' }, + { category: 'companion', label: 'RFC 7516 — JSON Web Encryption (JWE)', href: 'https://datatracker.ietf.org/doc/html/rfc7516', note: 'Underlies direct_post.jwt response encryption.' }, + { category: 'companion', label: 'W3C Digital Credentials API', href: 'https://w3c-fedid.github.io/digital-credentials/', note: 'Browser API for presenting VCs to verifiers.' }, + { category: 'profile', label: 'OpenID4VC High Assurance Interoperability Profile (HAIP)', href: 'https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0.html' }, ], }, { @@ -75,10 +286,58 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ spec: 'SAML 2.0 Core', specUrl: 'https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf', flows: [ - { id: 'sp-initiated-sso', backendId: 'sp_initiated_sso', name: 'SP-Initiated SSO', rfc: 'Profiles §4.1' }, - { id: 'idp-initiated-sso', backendId: 'idp_initiated_sso', name: 'IdP-Initiated SSO', rfc: 'Profiles §4.1.5' }, - { id: 'single-logout', backendId: 'single_logout', name: 'Single Logout (SLO)', rfc: 'Profiles §4.4' }, - { id: 'metadata', name: 'Metadata Exchange', rfc: 'Metadata' }, + { + id: 'sp-initiated-sso', + backendId: 'sp_initiated_sso', + name: 'SP-Initiated SSO', + rfc: 'Profiles §4.1', + references: [ + { category: 'core', label: 'SAML 2.0 Profiles §4.1 — Web Browser SSO Profile', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf' }, + { category: 'core', label: 'SAML 2.0 Bindings — HTTP-POST and HTTP-Redirect', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf' }, + { category: 'security', label: 'SAML Security and Privacy Considerations §6.4 — Stolen Assertion / Replay', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-sec-consider-2.0-os.pdf' }, + ], + }, + { + id: 'idp-initiated-sso', + backendId: 'idp_initiated_sso', + name: 'IdP-Initiated SSO', + rfc: 'Profiles §4.1.5', + references: [ + { category: 'core', label: 'SAML 2.0 Profiles §4.1.5 — Unsolicited Responses', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf' }, + { category: 'security', label: 'SAML Security and Privacy Considerations §6.4 — Replay without InResponseTo', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-sec-consider-2.0-os.pdf', note: 'IdP-initiated has no AuthnRequest to bind against — assertion-ID cache is mandatory.' }, + ], + }, + { + id: 'single-logout', + backendId: 'single_logout', + name: 'Single Logout (SLO)', + rfc: 'Profiles §4.4', + references: [ + { category: 'core', label: 'SAML 2.0 Profiles §4.4 — Single Logout Profile', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf' }, + { category: 'security', label: 'SAML Security and Privacy Considerations §6.4.7 — Session-related issues', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-sec-consider-2.0-os.pdf' }, + ], + }, + { + id: 'metadata', + name: 'Metadata Exchange', + rfc: 'Metadata', + references: [ + { category: 'core', label: 'SAML 2.0 Metadata', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf' }, + { category: 'security', label: 'SAML Security and Privacy Considerations §6.5 — Trust Establishment', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-sec-consider-2.0-os.pdf' }, + ], + }, + ], + references: [ + { category: 'core', label: 'SAML 2.0 Core', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf' }, + { category: 'core', label: 'SAML 2.0 Bindings', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf' }, + { category: 'core', label: 'SAML 2.0 Profiles', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf' }, + { category: 'core', label: 'SAML 2.0 Metadata', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf' }, + { category: 'core', label: 'SAML 2.0 Authentication Context', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-authn-context-2.0-os.pdf' }, + { category: 'core', label: 'SAML 2.0 Conformance', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-conformance-2.0-os.pdf' }, + { category: 'security', label: 'SAML 2.0 Security and Privacy Considerations', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-sec-consider-2.0-os.pdf', note: 'OASIS-published threat model and countermeasures specific to SAML.' }, + { category: 'security', label: 'SAML 2.0 Approved Errata', href: 'https://docs.oasis-open.org/security/saml/v2.0/sstc-saml-approved-errata-2.0.html', note: 'Includes security-relevant clarifications.' }, + { category: 'companion', label: 'W3C XML Signature Syntax and Processing', href: 'https://www.w3.org/TR/xmldsig-core/' }, + { category: 'companion', label: 'W3C XML Encryption Syntax and Processing', href: 'https://www.w3.org/TR/xmlenc-core1/' }, ], }, { @@ -88,10 +347,60 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ spec: 'SPIFFE Specifications', specUrl: 'https://spiffe.io/docs/latest/spiffe-about/overview/', flows: [ - { id: 'x509-svid-issuance', name: 'X.509-SVID Acquisition', rfc: 'X.509-SVID' }, - { id: 'jwt-svid-issuance', name: 'JWT-SVID Acquisition', rfc: 'JWT-SVID' }, - { id: 'mtls-handshake', name: 'mTLS with X.509-SVIDs', rfc: 'RFC 8446' }, - { id: 'certificate-rotation', name: 'Certificate Rotation', rfc: 'Workload API' }, + { + id: 'x509-svid-issuance', + name: 'X.509-SVID Acquisition', + rfc: 'X.509-SVID', + references: [ + { category: 'core', label: 'SPIFFE X.509-SVID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md' }, + { category: 'core', label: 'SPIFFE Workload API §5.2 — FetchX509SVID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md' }, + { category: 'security', label: 'X.509-SVID §4 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#4-security-considerations' }, + ], + }, + { + id: 'jwt-svid-issuance', + name: 'JWT-SVID Acquisition', + rfc: 'JWT-SVID', + references: [ + { category: 'core', label: 'SPIFFE JWT-SVID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md' }, + { category: 'core', label: 'SPIFFE Workload API §5.4 — FetchJWTSVID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md' }, + { category: 'security', label: 'JWT-SVID §6 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#6-security-considerations' }, + ], + }, + { + id: 'mtls-handshake', + name: 'mTLS with X.509-SVIDs', + rfc: 'RFC 8446', + references: [ + { category: 'core', label: 'SPIFFE X.509-SVID §3 — Validation', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#3-validation' }, + { category: 'core', label: 'RFC 8446 — TLS 1.3', href: 'https://datatracker.ietf.org/doc/html/rfc8446' }, + { category: 'security', label: 'X.509-SVID §4 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#4-security-considerations' }, + ], + }, + { + id: 'certificate-rotation', + name: 'Certificate Rotation', + rfc: 'Workload API', + references: [ + { category: 'core', label: 'SPIFFE Workload API — streaming SVID updates', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md' }, + { category: 'security', label: 'Workload API §6 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md#6-security-considerations' }, + ], + }, + ], + references: [ + { category: 'core', label: 'SPIFFE-ID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md' }, + { category: 'core', label: 'SPIFFE X.509-SVID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md' }, + { category: 'core', label: 'SPIFFE JWT-SVID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md' }, + { category: 'core', label: 'SPIFFE Workload API', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md' }, + { category: 'core', label: 'SPIFFE Trust Domain and Bundle', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md' }, + { category: 'core', label: 'SPIFFE Federation', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md' }, + { category: 'security', label: 'X.509-SVID §4 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#4-security-considerations' }, + { category: 'security', label: 'JWT-SVID §6 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#6-security-considerations' }, + { category: 'security', label: 'Workload API §6 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md#6-security-considerations' }, + { category: 'security', label: 'Federation §6 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md#6-security-considerations' }, + { category: 'security', label: 'Trust Domain and Bundle §5 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#5-security-considerations' }, + { category: 'companion', label: 'RFC 8446 — TLS 1.3', href: 'https://datatracker.ietf.org/doc/html/rfc8446', note: 'Underlying transport for X.509-SVID mTLS.' }, + { category: 'companion', label: 'SPIRE Documentation', href: 'https://spiffe.io/docs/latest/spire-about/spire-concepts/', note: 'Reference implementation of SPIFFE.' }, ], }, { @@ -101,11 +410,63 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ spec: 'RFC 7642, 7643, 7644', specUrl: 'https://datatracker.ietf.org/doc/html/rfc7644', flows: [ - { id: 'user-lifecycle', name: 'User Lifecycle', rfc: 'RFC 7644 §3.2-3.6' }, - { id: 'group-management', backendId: 'group-membership', name: 'Group Management', rfc: 'RFC 7644 §3.2-3.6' }, - { id: 'filter-queries', backendId: 'user-discovery', name: 'Filter Queries', rfc: 'RFC 7644 §3.4.2' }, - { id: 'schema-discovery', name: 'Schema Discovery', rfc: 'RFC 7644 §4' }, - { id: 'bulk-operations', name: 'Bulk Operations', rfc: 'RFC 7644 §3.7' }, + { + id: 'user-lifecycle', + name: 'User Lifecycle', + rfc: 'RFC 7644 §3.2-3.6', + references: [ + { category: 'core', label: 'RFC 7644 §3.3 — Creating Resources', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-3.3' }, + { category: 'core', label: 'RFC 7644 §3.5 — Modifying with PATCH', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-3.5' }, + { category: 'core', label: 'RFC 7643 §4.1 — User Resource Schema', href: 'https://datatracker.ietf.org/doc/html/rfc7643#section-4.1' }, + { category: 'security', label: 'RFC 7644 §7 — Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-7' }, + ], + }, + { + id: 'group-management', + backendId: 'group-membership', + name: 'Group Management', + rfc: 'RFC 7644 §3.2-3.6', + references: [ + { category: 'core', label: 'RFC 7643 §4.2 — Group Resource Schema', href: 'https://datatracker.ietf.org/doc/html/rfc7643#section-4.2' }, + { category: 'core', label: 'RFC 7644 §3.5.2 — PATCH Operations', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-3.5.2' }, + { category: 'security', label: 'RFC 7644 §7 — Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-7' }, + ], + }, + { + id: 'filter-queries', + backendId: 'user-discovery', + name: 'Filter Queries', + rfc: 'RFC 7644 §3.4.2', + references: [ + { category: 'core', label: 'RFC 7644 §3.4.2 — Querying Resources', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2' }, + { category: 'core', label: 'RFC 7644 §3.4.2.2 — Filtering Grammar', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.2' }, + ], + }, + { + id: 'schema-discovery', + name: 'Schema Discovery', + rfc: 'RFC 7644 §4', + references: [ + { category: 'core', label: 'RFC 7644 §4 — Service Provider Configuration', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-4' }, + { category: 'core', label: 'RFC 7643 §7 — Schema Definition', href: 'https://datatracker.ietf.org/doc/html/rfc7643#section-7' }, + ], + }, + { + id: 'bulk-operations', + name: 'Bulk Operations', + rfc: 'RFC 7644 §3.7', + references: [ + { category: 'core', label: 'RFC 7644 §3.7 — Bulk Operations', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-3.7' }, + { category: 'security', label: 'RFC 7644 §7 — Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-7' }, + ], + }, + ], + references: [ + { category: 'core', label: 'RFC 7642 — SCIM Definitions, Overview, Concepts, Requirements', href: 'https://datatracker.ietf.org/doc/html/rfc7642' }, + { category: 'core', label: 'RFC 7643 — SCIM Core Schema', href: 'https://datatracker.ietf.org/doc/html/rfc7643' }, + { category: 'core', label: 'RFC 7644 — SCIM Protocol', href: 'https://datatracker.ietf.org/doc/html/rfc7644' }, + { category: 'security', label: 'RFC 7644 §7 — Security Considerations (Protocol)', href: 'https://datatracker.ietf.org/doc/html/rfc7644#section-7' }, + { category: 'security', label: 'RFC 7643 §9 — Security Considerations (Schema)', href: 'https://datatracker.ietf.org/doc/html/rfc7643#section-9' }, ], }, { @@ -115,13 +476,80 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ spec: 'SSF 1.0, CAEP 1.0, RISC 1.0, RFC 8417', specUrl: 'https://openid.net/specs/openid-sse-framework-1_0.html', flows: [ - { id: 'ssf-stream-configuration', name: 'Stream Configuration', rfc: 'SSF §4' }, - { id: 'ssf-push-delivery', name: 'Push Delivery', rfc: 'SSF §5.2.1' }, - { id: 'ssf-poll-delivery', name: 'Poll Delivery', rfc: 'SSF §5.2.2' }, - { id: 'caep-session-revoked', name: 'Session Revoked (CAEP)', rfc: 'CAEP §3.1' }, - { id: 'caep-credential-change', name: 'Credential Change (CAEP)', rfc: 'CAEP §3.2' }, - { id: 'risc-account-disabled', name: 'Account Disabled (RISC)', rfc: 'RISC §2.2' }, - { id: 'risc-credential-compromise', name: 'Credential Compromise (RISC)', rfc: 'RISC §2.1' }, + { + id: 'ssf-stream-configuration', + name: 'Stream Configuration', + rfc: 'SSF §4', + references: [ + { category: 'core', label: 'OpenID SSF §8 — Management API for SET Event Streams', href: 'https://openid.net/specs/openid-sharedsignals-framework-1_0-final.html' }, + ], + }, + { + id: 'ssf-push-delivery', + name: 'Push Delivery', + rfc: 'SSF §5.2.1', + references: [ + { category: 'core', label: 'RFC 8935 — SET Delivery via HTTP Push', href: 'https://datatracker.ietf.org/doc/html/rfc8935' }, + { category: 'security', label: 'RFC 8935 §5 — Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc8935#section-5' }, + ], + }, + { + id: 'ssf-poll-delivery', + name: 'Poll Delivery', + rfc: 'SSF §5.2.2', + references: [ + { category: 'core', label: 'RFC 8936 — SET Delivery via HTTP Polling', href: 'https://datatracker.ietf.org/doc/html/rfc8936' }, + { category: 'security', label: 'RFC 8936 §5 — Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc8936#section-5' }, + ], + }, + { + id: 'caep-session-revoked', + name: 'Session Revoked (CAEP)', + rfc: 'CAEP §3.1', + references: [ + { category: 'core', label: 'OpenID CAEP §3.1 — session-revoked', href: 'https://openid.net/specs/openid-caep-1_0-final.html' }, + { category: 'core', label: 'RFC 8417 — Security Event Token', href: 'https://datatracker.ietf.org/doc/html/rfc8417' }, + ], + }, + { + id: 'caep-credential-change', + name: 'Credential Change (CAEP)', + rfc: 'CAEP §3.2', + references: [ + { category: 'core', label: 'OpenID CAEP §3.3 — credential-change', href: 'https://openid.net/specs/openid-caep-1_0-final.html' }, + ], + }, + { + id: 'risc-account-disabled', + name: 'Account Disabled (RISC)', + rfc: 'RISC §2.2', + references: [ + { category: 'core', label: 'OpenID RISC §2.3 — account-disabled', href: 'https://openid.net/specs/openid-risc-1_0.html' }, + ], + }, + { + id: 'risc-credential-compromise', + name: 'Credential Compromise (RISC)', + rfc: 'RISC §2.1', + references: [ + { category: 'core', label: 'OpenID RISC §2.7 — credential-compromise', href: 'https://openid.net/specs/openid-risc-1_0.html' }, + { category: 'security', label: 'RISC §2.7 — Privacy Warning (do not include credential values)', href: 'https://openid.net/specs/openid-risc-1_0.html' }, + ], + }, + ], + references: [ + { category: 'core', label: 'OpenID Shared Signals Framework 1.0', href: 'https://openid.net/specs/openid-sharedsignals-framework-1_0-final.html' }, + { category: 'core', label: 'OpenID CAEP — Continuous Access Evaluation Profile', href: 'https://openid.net/specs/openid-caep-1_0-final.html' }, + { category: 'core', label: 'OpenID RISC Profile 1.0', href: 'https://openid.net/specs/openid-risc-1_0.html' }, + { category: 'core', label: 'RFC 8417 — Security Event Token (SET)', href: 'https://datatracker.ietf.org/doc/html/rfc8417' }, + { category: 'security', label: 'RFC 8417 §5 — Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc8417#section-5' }, + { category: 'security', label: 'RFC 8935 §5 — SET Push Delivery Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc8935#section-5' }, + { category: 'security', label: 'RFC 8936 §5 — SET Poll Delivery Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc8936#section-5' }, + { category: 'companion', label: 'RFC 8935 — SET Delivery Using HTTP Push (POST)', href: 'https://datatracker.ietf.org/doc/html/rfc8935' }, + { category: 'companion', label: 'RFC 8936 — SET Delivery Using HTTP Polling', href: 'https://datatracker.ietf.org/doc/html/rfc8936' }, + { category: 'companion', label: 'RFC 9493 — Subject Identifiers for SETs', href: 'https://datatracker.ietf.org/doc/html/rfc9493' }, + { category: 'companion', label: 'RFC 7519 — JSON Web Token (JWT)', href: 'https://datatracker.ietf.org/doc/html/rfc7519' }, + { category: 'companion', label: 'RFC 7515 — JSON Web Signature (JWS)', href: 'https://datatracker.ietf.org/doc/html/rfc7515' }, ], }, ] diff --git a/frontend/src/protocols/presentation/protocol-catalog.ts b/frontend/src/protocols/presentation/protocol-catalog.ts index 45fda26..48a2249 100644 --- a/frontend/src/protocols/presentation/protocol-catalog.ts +++ b/frontend/src/protocols/presentation/protocol-catalog.ts @@ -1,6 +1,6 @@ import type { ElementType } from 'react' import { Eye, Fingerprint, FileKey, Key, KeyRound, Radio, Shield, Users } from 'lucide-react' -import { PROTOCOL_CATALOG_DATA } from './protocol-catalog-data' +import { PROTOCOL_CATALOG_DATA, type ProtocolReference } from './protocol-catalog-data' export interface ProtocolFlowSummary { id: string @@ -17,6 +17,7 @@ export interface ProtocolCatalogItem { spec: string specUrl: string flows: ProtocolFlowSummary[] + references: ProtocolReference[] } export interface ComingSoonProtocol { @@ -55,6 +56,7 @@ export const PROTOCOL_CATALOG: ProtocolCatalogItem[] = PROTOCOL_CATALOG_DATA.map spec: item.spec, specUrl: item.specUrl, flows: item.flows, + references: item.references, })) export const COMING_SOON_PROTOCOLS: ComingSoonProtocol[] = [ diff --git a/frontend/src/views/FlowDetail.tsx b/frontend/src/views/FlowDetail.tsx index dd81f9e..a2f34e3 100644 --- a/frontend/src/views/FlowDetail.tsx +++ b/frontend/src/views/FlowDetail.tsx @@ -14,6 +14,8 @@ import { FlowDiagram } from '../lookingglass/components/FlowDiagram' import { FlowDefinition, FlowStep } from '../protocols/registry' import { CODE_EXAMPLES } from '../protocols/examples' import { ParameterExplainer } from '../components/ParameterExplainer' +import { ProtocolReferences } from '../components/ProtocolReferences' +import { getCatalogFlow, getFlowRouteId } from '../protocols/presentation/protocol-catalog-data' interface FlowDetailProps { protocolId: string @@ -43,6 +45,10 @@ export function FlowDetail({ const codeExample = CODE_EXAMPLES[currentFlowId] || CODE_EXAMPLES['_default'] const getCodeExample = () => codeExample?.code || '' + const flowRouteId = getFlowRouteId(protocolId, currentFlowId) + const catalogFlow = getCatalogFlow(protocolId, flowRouteId) + const flowReferences = catalogFlow?.references ?? [] + // Get flow badges — keyed by actual backend flow IDs const getBadges = () => { const badges = [] @@ -270,7 +276,7 @@ export function FlowDetail({

Click any step for details

- + {/* Specs that define this flow */} + {flowReferences.length > 0 && ( + + )} + {/* Navigation */}
FLOW_PRESENTATION_META[f.id]?.recommended) || flows[0] + const catalogEntry = getCatalogProtocol(protocolId) + return (
{/* Header */} @@ -151,6 +154,15 @@ export function ProtocolDemo({
+ {/* Specs & References */} + {catalogEntry && ( + + )} + {/* Protocol Features - from modular meta */}

From 048057101670a016456b94f9fc52abb353598f11 Mon Sep 17 00:00:00 2001 From: Ayoma Wijethunga Date: Wed, 6 May 2026 10:26:20 +0530 Subject: [PATCH 4/9] UPDATE ADDING_PROTOCOLS guide for explainers and references Signed-off-by: Ayoma Wijethunga --- docs/ADDING_PROTOCOLS.md | 101 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/docs/ADDING_PROTOCOLS.md b/docs/ADDING_PROTOCOLS.md index 0091f80..1421d14 100644 --- a/docs/ADDING_PROTOCOLS.md +++ b/docs/ADDING_PROTOCOLS.md @@ -224,7 +224,106 @@ async rewrites() { }, ``` -## 7. Optional: Looking Glass executor (for real execution) +## 7. Parameter explainers and references + +Two UI surfaces give learners security context for the new protocol: per-parameter explainer panels (clickable `(?)` icons beside flow parameters) and the "Specs & references" panels on the protocol overview and flow detail pages. Both are populated from data files; adding a new protocol means adding entries on both surfaces. + +### 7.1 Per-parameter explainers + +Create `frontend/src/protocols/explainers/.ts` exporting a map keyed by parameter name: + +```ts +import type { ParameterExplainer } from './index' + +export const NEWPROTOCOL_EXPLAINERS: Record = { + param_name: { + purpose: 'What this parameter does and why it exists.', + attacks: [ + { + id: 'named-attack', + name: 'Named attack pattern (CVE or research label)', + scenario: 'How an attacker exploits the parameter when it is absent or mishandled.', + impact: 'What the attacker achieves.', + }, + ], + mitigations: [ + { + action: 'Concrete mitigation step.', + rationale: 'Optional one-line reason this works.', + mitigates: ['named-attack'], + }, + ], + references: [ + { label: 'RFC/spec section', href: 'https://...' }, + ], + }, +} +``` + +Then register the map in `frontend/src/protocols/explainers/index.ts` by adding an import and spreading it into the `EXPLAINERS` record: + +```ts +import { NEWPROTOCOL_EXPLAINERS } from './newprotocol' + +const EXPLAINERS: Record = { + ...OAUTH2_EXPLAINERS, + // ... + ...NEWPROTOCOL_EXPLAINERS, +} +``` + +Notes: + +- Mitigations link back to attacks via `mitigates: string[]` of attack IDs. A single mitigation may cover several attacks. +- For parameters whose semantics differ across protocols (for example, `nonce` in OIDC versus OID4VP), use the override key form `${protocolId}:${name}` instead of the bare name. +- Source content from canonical specs and named real-world incidents (CVE numbers, research disclosures). Avoid universal-baseline mitigations such as "use HTTPS"; reviewers can be assumed to know those. +- See any existing protocol's explainer file (for example `oauth2.ts`) for a worked reference of tone, length, and structure. +- The map keys must match the parameter names emitted by the backend's `FlowStep.Parameters` map (see section 3). Mismatched names will silently fail to render an explainer. + +### 7.2 Per-protocol references panel + +Update `frontend/src/protocols/presentation/protocol-catalog-data.ts` to add a `references` array on the protocol's catalog entry: + +```ts +{ + id: 'newprotocol', + name: 'New Protocol', + // ...existing fields + references: [ + { category: 'core', label: 'Core spec name', href: 'https://...' }, + { category: 'security', label: 'Security & privacy considerations', href: 'https://...' }, + { category: 'companion', label: 'Companion RFC', href: 'https://...' }, + { category: 'profile', label: 'Deployment profile', href: 'https://...' }, + ], +} +``` + +The categories render as separate sections in the UI: + +- `core`: specs that normatively define the protocol. +- `security`: dedicated security and privacy considerations documents (or deep-anchored security sections of core specs). +- `companion`: extension RFCs and related standards the protocol composes with. +- `profile`: deployment profiles that constrain the protocol for specific assurance regimes (FAPI, HAIP, eIDAS, and so on). + +### 7.3 Per-flow references panel + +In the same file, add a `references` array to each flow definition. Prefer deep-anchored URLs to specific sections rather than spec landing pages: + +```ts +{ + id: 'flow-id', + name: 'Flow Name', + rfc: '§X.Y', + references: [ + { category: 'core', label: 'Spec §X.Y - Section title', href: 'https://...#anchor' }, + { category: 'security', label: 'Security considerations §Z', href: 'https://...#anchor' }, + ], +} +``` + +The same `ProtocolReferences` component renders both surfaces, so flow references should be focused (typically two to four per flow) and non-overlapping with the protocol-level reading list. + +## 8. Optional: Looking Glass executor (for real execution) If your flow is executable, implement a flow executor. From 5139414e89916db7b44c3e1420bd817c5ba757e2 Mon Sep 17 00:00:00 2001 From: Ayoma Wijethunga Date: Fri, 8 May 2026 23:49:54 +0530 Subject: [PATCH 5/9] PATCH spec references and section numbers per PR review Signed-off-by: Ayoma Wijethunga --- .../presentation/protocol-catalog-data.ts | 91 ++++++++++--------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/frontend/src/protocols/presentation/protocol-catalog-data.ts b/frontend/src/protocols/presentation/protocol-catalog-data.ts index 5599869..0c403b9 100644 --- a/frontend/src/protocols/presentation/protocol-catalog-data.ts +++ b/frontend/src/protocols/presentation/protocol-catalog-data.ts @@ -62,7 +62,7 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ rfc: '§4.4', references: [ { category: 'core', label: 'RFC 6749 §4.4 — Client Credentials Grant', href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-4.4' }, - { category: 'security', label: 'RFC 9700 §4.5 — Client Authentication', href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-4.5' }, + { category: 'security', label: 'RFC 9700 §2.5 — Client Authentication', href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-2.5' }, ], }, { @@ -72,7 +72,7 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ rfc: '§6', references: [ { category: 'core', label: 'RFC 6749 §6 — Refreshing an Access Token', href: 'https://datatracker.ietf.org/doc/html/rfc6749#section-6' }, - { category: 'security', label: 'RFC 9700 §2.2.2 — Refresh Token Protection', href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-2.2.2' }, + { category: 'security', label: 'RFC 9700 §4.14 — Refresh Token Protection', href: 'https://datatracker.ietf.org/doc/html/rfc9700#section-4.14' }, ], }, { @@ -148,7 +148,8 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ rfc: '§3.3', references: [ { category: 'core', label: 'OIDC Core §3.3 — Authentication using the Hybrid Flow', href: 'https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth' }, - { category: 'security', label: 'OIDC Core §3.3.5 — Hybrid Flow Security', href: 'https://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken' }, + { category: 'security', label: 'OIDC Core §3.3.2 — Hybrid Flow Authorization Endpoint', href: 'https://openid.net/specs/openid-connect-core-1_0.html#HybridAuthorizationEndpoint' }, + { category: 'security', label: 'OIDC Core §3.3.3 — Hybrid Flow Token Endpoint', href: 'https://openid.net/specs/openid-connect-core-1_0.html#HybridTokenEndpoint' }, ], }, { @@ -186,8 +187,8 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ { category: 'companion', label: 'RFC 7517 — JSON Web Key (JWK)', href: 'https://datatracker.ietf.org/doc/html/rfc7517' }, { category: 'companion', label: 'RFC 7518 — JSON Web Algorithms (JWA)', href: 'https://datatracker.ietf.org/doc/html/rfc7518' }, { category: 'companion', label: 'RFC 9493 — Subject Identifiers for SETs', href: 'https://datatracker.ietf.org/doc/html/rfc9493' }, - { category: 'profile', label: 'FAPI 2.0 Security Profile', href: 'https://openid.net/specs/fapi-2_0-security-profile.html', note: 'High-assurance profile for financial-grade APIs.' }, - { category: 'profile', label: 'FAPI 2.0 Message Signing', href: 'https://openid.net/specs/fapi-2_0-message-signing.html' }, + { category: 'profile', label: 'FAPI 2.0 Security Profile', href: 'https://openid.net/specs/fapi-security-profile-2_0.html', note: 'High-assurance profile for financial-grade APIs.' }, + { category: 'profile', label: 'FAPI 2.0 Message Signing', href: 'https://openid.net/specs/fapi-message-signing-2_0.html' }, ], }, { @@ -203,9 +204,9 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ rfc: 'OID4VCI §4, §6.1, §8', references: [ { category: 'core', label: 'OID4VCI 1.0 §4 — Credential Offer', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-credential-offer' }, - { category: 'core', label: 'OID4VCI 1.0 §6.1 — Pre-Authorized Code Flow', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-pre-authorized-code-flow' }, + { category: 'core', label: 'OID4VCI 1.0 §3.5 — Pre-Authorized Code Flow', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-pre-authorized-code-flow' }, { category: 'core', label: 'OID4VCI 1.0 §8 — Credential Endpoint', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-credential-endpoint' }, - { category: 'security', label: 'OID4VCI 1.0 §11.2 — Pre-Authorized Code Flow Security', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-security-considerations' }, + { category: 'security', label: 'OID4VCI 1.0 §13.6 — Pre-Authorized Code Flow Security Considerations', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-pre-authorized-code-flow-2' }, ], }, { @@ -213,8 +214,8 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ name: 'Pre-Authorized + tx_code', rfc: 'OID4VCI §6.1', references: [ - { category: 'core', label: 'OID4VCI 1.0 §6.1 — Pre-Authorized Code Flow', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-pre-authorized-code-flow' }, - { category: 'security', label: 'OID4VCI 1.0 §11.3 — PIN / tx_code Phishing', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-security-considerations' }, + { category: 'core', label: 'OID4VCI 1.0 §6.1 — Token Request', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-token-request' }, + { category: 'security', label: 'OID4VCI 1.0 §13.6.2 — Transaction Code Phishing', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-transaction-code-phishing' }, ], }, { @@ -223,13 +224,13 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ rfc: 'OID4VCI Deferred Endpoint', references: [ { category: 'core', label: 'OID4VCI 1.0 §9 — Deferred Credential Endpoint', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-deferred-credential-endpoin' }, - { category: 'security', label: 'OID4VCI 1.0 §11 — Security Considerations', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-security-considerations' }, + { category: 'security', label: 'OID4VCI 1.0 §13 — Security Considerations', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-security-considerations' }, ], }, ], references: [ { category: 'core', label: 'OpenID for Verifiable Credential Issuance 1.0', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html' }, - { category: 'security', label: 'OID4VCI 1.0 §11 — Security Considerations', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-security-considerations' }, + { category: 'security', label: 'OID4VCI 1.0 §13 — Security Considerations', href: 'https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-security-considerations' }, { category: 'security', label: 'OpenID Foundation — Formal Security Analysis of OpenID for VCs', href: 'https://openid.net/formal-security-analysis-openid-verifiable-credentials/', note: 'Independent formal analysis covering OID4VCI and OID4VP.' }, { category: 'companion', label: 'IETF SD-JWT (Selective Disclosure for JWTs)', href: 'https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/' }, { category: 'companion', label: 'IETF SD-JWT VC (SD-JWT-based Verifiable Credentials)', href: 'https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/' }, @@ -254,8 +255,8 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ references: [ { category: 'core', label: 'OID4VP 1.0 §5 — Authorization Request', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-authorization-request' }, { category: 'core', label: 'OID4VP 1.0 §6.1 — DCQL (Digital Credentials Query Language)', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-digital-credentials-query-l' }, - { category: 'core', label: 'OID4VP 1.0 §8.2 — direct_post Response Mode', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-response-modes' }, - { category: 'security', label: 'OID4VP 1.0 §11.1 — Verifier Impersonation', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-security-considerations' }, + { category: 'core', label: 'OID4VP 1.0 §8.2 — direct_post Response Mode', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-response-mode-direct_post' }, + { category: 'security', label: 'OID4VP 1.0 §14.1 — Verifier Impersonation (Preventing Replay of Verifiable Presentations)', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-preventing-replay-of-verifi' }, ], }, { @@ -263,19 +264,20 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ name: 'DCQL + direct_post.jwt', rfc: 'OID4VP §8.3.1', references: [ - { category: 'core', label: 'OID4VP 1.0 §8.3 — direct_post.jwt Response Mode', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-response-modes' }, + { category: 'core', label: 'OID4VP 1.0 §8.3.1 — direct_post.jwt Response Mode', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-response-mode-direct_postjw' }, { category: 'core', label: 'RFC 7516 — JSON Web Encryption (JWE)', href: 'https://datatracker.ietf.org/doc/html/rfc7516' }, - { category: 'security', label: 'OID4VP 1.0 §11.2 — Nonce Binding', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-security-considerations' }, + { category: 'security', label: 'OID4VP 1.0 §14.1.2 — Verifiable Presentations (Nonce Binding)', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-verifiable-presentations' }, ], }, ], references: [ { category: 'core', label: 'OpenID for Verifiable Presentations 1.0', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html' }, - { category: 'security', label: 'OID4VP 1.0 §11 — Security Considerations', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-security-considerations' }, + { category: 'security', label: 'OID4VP 1.0 §14 — Security Considerations', href: 'https://openid.net/specs/openid-4-verifiable-presentations-1_0-final.html#name-security-considerations' }, { category: 'security', label: 'OpenID Foundation — Formal Security Analysis of OpenID for VCs', href: 'https://openid.net/formal-security-analysis-openid-verifiable-credentials/' }, { category: 'companion', label: 'RFC 9101 — JWT-Secured Authorization Request (JAR)', href: 'https://datatracker.ietf.org/doc/html/rfc9101' }, { category: 'companion', label: 'RFC 7516 — JSON Web Encryption (JWE)', href: 'https://datatracker.ietf.org/doc/html/rfc7516', note: 'Underlies direct_post.jwt response encryption.' }, { category: 'companion', label: 'W3C Digital Credentials API', href: 'https://w3c-fedid.github.io/digital-credentials/', note: 'Browser API for presenting VCs to verifiers.' }, + { category: 'companion', label: 'digitalcredentials.dev', href: 'https://digitalcredentials.dev/', note: 'Experimental verifier and wallet playground for OID4VP and W3C credentials.' }, { category: 'profile', label: 'OpenID4VC High Assurance Interoperability Profile (HAIP)', href: 'https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0.html' }, ], }, @@ -314,7 +316,7 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ rfc: 'Profiles §4.4', references: [ { category: 'core', label: 'SAML 2.0 Profiles §4.4 — Single Logout Profile', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf' }, - { category: 'security', label: 'SAML Security and Privacy Considerations §6.4.7 — Session-related issues', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-sec-consider-2.0-os.pdf' }, + { category: 'security', label: 'SAML Security and Privacy Considerations §7.1.4 — Single Logout Profile', href: 'https://docs.oasis-open.org/security/saml/v2.0/saml-sec-consider-2.0-os.pdf' }, ], }, { @@ -352,9 +354,9 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ name: 'X.509-SVID Acquisition', rfc: 'X.509-SVID', references: [ - { category: 'core', label: 'SPIFFE X.509-SVID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md' }, - { category: 'core', label: 'SPIFFE Workload API §5.2 — FetchX509SVID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md' }, - { category: 'security', label: 'X.509-SVID §4 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#4-security-considerations' }, + { category: 'core', label: 'SPIFFE X.509-SVID', href: 'https://spiffe.io/docs/latest/spiffe-specs/x509-svid/' }, + { category: 'core', label: 'SPIFFE Workload API §5.2.1 — FetchX509SVID', href: 'https://spiffe.io/docs/latest/spiffe-specs/spiffe_workload_api/#521-fetchx509svid' }, + { category: 'security', label: 'X.509-SVID §4 — Constraints and Usage', href: 'https://spiffe.io/docs/latest/spiffe-specs/x509-svid/#4-constraints-and-usage' }, ], }, { @@ -362,9 +364,9 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ name: 'JWT-SVID Acquisition', rfc: 'JWT-SVID', references: [ - { category: 'core', label: 'SPIFFE JWT-SVID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md' }, - { category: 'core', label: 'SPIFFE Workload API §5.4 — FetchJWTSVID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md' }, - { category: 'security', label: 'JWT-SVID §6 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#6-security-considerations' }, + { category: 'core', label: 'SPIFFE JWT-SVID', href: 'https://spiffe.io/docs/latest/spiffe-specs/jwt-svid/' }, + { category: 'core', label: 'SPIFFE Workload API §6.2.1 — FetchJWTSVID', href: 'https://spiffe.io/docs/latest/spiffe-specs/spiffe_workload_api/#621-fetchjwtsvid' }, + { category: 'security', label: 'JWT-SVID §7 — Security Considerations', href: 'https://spiffe.io/docs/latest/spiffe-specs/jwt-svid/#7-security-considerations' }, ], }, { @@ -372,9 +374,9 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ name: 'mTLS with X.509-SVIDs', rfc: 'RFC 8446', references: [ - { category: 'core', label: 'SPIFFE X.509-SVID §3 — Validation', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#3-validation' }, + { category: 'core', label: 'SPIFFE X.509-SVID §5 — Validation', href: 'https://spiffe.io/docs/latest/spiffe-specs/x509-svid/#5-validation' }, { category: 'core', label: 'RFC 8446 — TLS 1.3', href: 'https://datatracker.ietf.org/doc/html/rfc8446' }, - { category: 'security', label: 'X.509-SVID §4 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#4-security-considerations' }, + { category: 'security', label: 'X.509-SVID §4 — Constraints and Usage', href: 'https://spiffe.io/docs/latest/spiffe-specs/x509-svid/#4-constraints-and-usage' }, ], }, { @@ -382,23 +384,22 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ name: 'Certificate Rotation', rfc: 'Workload API', references: [ - { category: 'core', label: 'SPIFFE Workload API — streaming SVID updates', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md' }, - { category: 'security', label: 'Workload API §6 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md#6-security-considerations' }, + { category: 'core', label: 'SPIFFE Workload API §4.2 — Connection Lifetime', href: 'https://spiffe.io/docs/latest/spiffe-specs/spiffe_workload_api/#42-connection-lifetime' }, + { category: 'security', label: 'X.509-SVID §4 — Constraints and Usage', href: 'https://spiffe.io/docs/latest/spiffe-specs/x509-svid/#4-constraints-and-usage' }, ], }, ], references: [ - { category: 'core', label: 'SPIFFE-ID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md' }, - { category: 'core', label: 'SPIFFE X.509-SVID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md' }, - { category: 'core', label: 'SPIFFE JWT-SVID', href: 'https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md' }, - { category: 'core', label: 'SPIFFE Workload API', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md' }, - { category: 'core', label: 'SPIFFE Trust Domain and Bundle', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md' }, - { category: 'core', label: 'SPIFFE Federation', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md' }, - { category: 'security', label: 'X.509-SVID §4 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#4-security-considerations' }, - { category: 'security', label: 'JWT-SVID §6 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#6-security-considerations' }, - { category: 'security', label: 'Workload API §6 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md#6-security-considerations' }, - { category: 'security', label: 'Federation §6 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md#6-security-considerations' }, - { category: 'security', label: 'Trust Domain and Bundle §5 — Security Considerations', href: 'https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#5-security-considerations' }, + { category: 'core', label: 'SPIFFE-ID', href: 'https://spiffe.io/docs/latest/spiffe-specs/spiffe-id/' }, + { category: 'core', label: 'SPIFFE X.509-SVID', href: 'https://spiffe.io/docs/latest/spiffe-specs/x509-svid/' }, + { category: 'core', label: 'SPIFFE JWT-SVID', href: 'https://spiffe.io/docs/latest/spiffe-specs/jwt-svid/' }, + { category: 'core', label: 'SPIFFE Workload API', href: 'https://spiffe.io/docs/latest/spiffe-specs/spiffe_workload_api/' }, + { category: 'core', label: 'SPIFFE Trust Domain and Bundle', href: 'https://spiffe.io/docs/latest/spiffe-specs/spiffe_trust_domain_and_bundle/' }, + { category: 'core', label: 'SPIFFE Federation', href: 'https://spiffe.io/docs/latest/spiffe-specs/spiffe_federation/' }, + { category: 'security', label: 'X.509-SVID §4 — Constraints and Usage', href: 'https://spiffe.io/docs/latest/spiffe-specs/x509-svid/#4-constraints-and-usage' }, + { category: 'security', label: 'JWT-SVID §7 — Security Considerations', href: 'https://spiffe.io/docs/latest/spiffe-specs/jwt-svid/#7-security-considerations' }, + { category: 'security', label: 'Federation §7 — Security Considerations', href: 'https://spiffe.io/docs/latest/spiffe-specs/spiffe_federation/#7-security-considerations' }, + { category: 'security', label: 'Trust Domain and Bundle §6 — Security Considerations', href: 'https://spiffe.io/docs/latest/spiffe-specs/spiffe_trust_domain_and_bundle/#6-security-considerations' }, { category: 'companion', label: 'RFC 8446 — TLS 1.3', href: 'https://datatracker.ietf.org/doc/html/rfc8446', note: 'Underlying transport for X.509-SVID mTLS.' }, { category: 'companion', label: 'SPIRE Documentation', href: 'https://spiffe.io/docs/latest/spire-about/spire-concepts/', note: 'Reference implementation of SPIFFE.' }, ], @@ -406,8 +407,8 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ { id: 'scim', name: 'SCIM 2.0', - description: 'System for Cross-domain Identity Management. Standards-based protocol for automating user provisioning and lifecycle management between identity providers and service providers.', - spec: 'RFC 7642, 7643, 7644', + description: 'System for Cross-domain Identity Management (SCIM). Standards-based protocol for automating user provisioning and lifecycle management between identity providers and service providers.', + spec: 'System for Cross-domain Identity Management (RFC 7642, 7643, 7644)', specUrl: 'https://datatracker.ietf.org/doc/html/rfc7644', flows: [ { @@ -474,14 +475,14 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ name: 'Shared Signals (SSF)', description: 'OpenID Shared Signals Framework for real-time security event sharing. Enables continuous access evaluation (CAEP) and risk incident coordination (RISC) between identity providers and relying parties.', spec: 'SSF 1.0, CAEP 1.0, RISC 1.0, RFC 8417', - specUrl: 'https://openid.net/specs/openid-sse-framework-1_0.html', + specUrl: 'https://openid.net/specs/openid-sharedsignals-framework-1_0.html', flows: [ { id: 'ssf-stream-configuration', name: 'Stream Configuration', rfc: 'SSF §4', references: [ - { category: 'core', label: 'OpenID SSF §8 — Management API for SET Event Streams', href: 'https://openid.net/specs/openid-sharedsignals-framework-1_0-final.html' }, + { category: 'core', label: 'OpenID SSF §8 — Management API for SET Event Streams', href: 'https://openid.net/specs/openid-sharedsignals-framework-1_0.html' }, ], }, { @@ -499,7 +500,7 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ rfc: 'SSF §5.2.2', references: [ { category: 'core', label: 'RFC 8936 — SET Delivery via HTTP Polling', href: 'https://datatracker.ietf.org/doc/html/rfc8936' }, - { category: 'security', label: 'RFC 8936 §5 — Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc8936#section-5' }, + { category: 'security', label: 'RFC 8936 §4 — Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc8936#section-4' }, ], }, { @@ -538,13 +539,13 @@ export const PROTOCOL_CATALOG_DATA: ProtocolCatalogDataItem[] = [ }, ], references: [ - { category: 'core', label: 'OpenID Shared Signals Framework 1.0', href: 'https://openid.net/specs/openid-sharedsignals-framework-1_0-final.html' }, + { category: 'core', label: 'OpenID Shared Signals Framework 1.0', href: 'https://openid.net/specs/openid-sharedsignals-framework-1_0.html' }, { category: 'core', label: 'OpenID CAEP — Continuous Access Evaluation Profile', href: 'https://openid.net/specs/openid-caep-1_0-final.html' }, { category: 'core', label: 'OpenID RISC Profile 1.0', href: 'https://openid.net/specs/openid-risc-1_0.html' }, { category: 'core', label: 'RFC 8417 — Security Event Token (SET)', href: 'https://datatracker.ietf.org/doc/html/rfc8417' }, { category: 'security', label: 'RFC 8417 §5 — Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc8417#section-5' }, { category: 'security', label: 'RFC 8935 §5 — SET Push Delivery Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc8935#section-5' }, - { category: 'security', label: 'RFC 8936 §5 — SET Poll Delivery Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc8936#section-5' }, + { category: 'security', label: 'RFC 8936 §4 — SET Poll Delivery Security Considerations', href: 'https://datatracker.ietf.org/doc/html/rfc8936#section-4' }, { category: 'companion', label: 'RFC 8935 — SET Delivery Using HTTP Push (POST)', href: 'https://datatracker.ietf.org/doc/html/rfc8935' }, { category: 'companion', label: 'RFC 8936 — SET Delivery Using HTTP Polling', href: 'https://datatracker.ietf.org/doc/html/rfc8936' }, { category: 'companion', label: 'RFC 9493 — Subject Identifiers for SETs', href: 'https://datatracker.ietf.org/doc/html/rfc9493' }, From e2774aeaa37a71589cbd8f3fb3130bd83107e818 Mon Sep 17 00:00:00 2001 From: Ayoma Wijethunga Date: Sat, 9 May 2026 00:00:49 +0530 Subject: [PATCH 6/9] CREATE spec reference verifier script Signed-off-by: Ayoma Wijethunga --- frontend/package.json | 1 + frontend/scripts/verify-references.mjs | 379 +++++++++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 frontend/scripts/verify-references.mjs diff --git a/frontend/package.json b/frontend/package.json index f612d52..3fb3d23 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "build": "next build", "start": "next start", "lint": "eslint src --report-unused-disable-directives --max-warnings 0", + "verify-refs": "node scripts/verify-references.mjs", "generate-og-image": "node scripts/generate-og-image.mjs" }, "dependencies": { diff --git a/frontend/scripts/verify-references.mjs b/frontend/scripts/verify-references.mjs new file mode 100644 index 0000000..23cd59d --- /dev/null +++ b/frontend/scripts/verify-references.mjs @@ -0,0 +1,379 @@ +#!/usr/bin/env node +// Verifies the {label, href} reference pairs we ship in protocol-catalog-data.ts +// and src/protocols/explainers/*.ts against the live spec documents. For each +// href: confirms the URL returns 200, then (when an anchor is present) fetches +// the page, locates the anchor, and fuzzy-matches the section number and title +// from our label against the heading text we find next to the anchor. +// +// Usage: npm run verify-refs +// npm run verify-refs -- --only=oid4vp filter by file basename +// npm run verify-refs -- --strict exit non-zero on warnings too + +import { readFile, readdir } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import path from 'node:path' + +const HERE = path.dirname(fileURLToPath(import.meta.url)) +const ROOT = path.resolve(HERE, '..') +const FILES_TO_SCAN = [ + path.join(ROOT, 'src/protocols/presentation/protocol-catalog-data.ts'), +] + +const args = process.argv.slice(2) +const onlyArg = args.find((a) => a.startsWith('--only=')) +const onlyFilter = onlyArg ? onlyArg.split('=')[1] : null +const strict = args.includes('--strict') + +const REFERENCE_REGEX = + /\{\s*category:\s*'(?[^']+)'\s*,\s*label:\s*'(?