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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SourceOS Continuum — lifecycle entry points.
# Control-plane targets delegate to Makefile.porter (the rehomed Porter control plane).
.PHONY: validate onboard dev-up dev-down shim-test test tools-test rollout promotion-gate portal compute mesh-demo grant commons mcp spine run loop verify lease sphere push push-webhook rollback edge login provision deploy inference availability
.PHONY: validate onboard dev-up dev-down shim-test test tools-test rollout promotion-gate portal compute mesh-demo grant commons mcp spine run loop verify lease sphere push push-webhook rollback edge login sso provision deploy inference availability

validate: ## repo hygiene + CapD validity
python3 tools/validate.py
Expand Down Expand Up @@ -60,6 +60,9 @@ edge: ## edge-worker demo: register the agent-machine into a cloud pool + evolve
login: ## login/session demo: authenticate the front door (fail-closed)
cd tools && python3 login.py

sso: ## SSO front door demo: OIDC + PKCE + WebAuthn/FIDO2 verification (fail-closed, stdlib-only)
cd tools && python3 sso.py

provision: ## provisioning demo: tenant broker + entitlement tier + BSS meter + /me
cd tools && python3 provisioning.py

Expand Down
31 changes: 31 additions & 0 deletions capd/sso-front-door.mesh.capd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"capability_id": "caps.dev.sso-front-door@0.1.0",
"kind": "dev.identity-sso",
"status": "experimental",
"name": "SSO front door — real OIDC + WebAuthn/FIDO2 verification, fail-closed, stdlib-only",
"description": "The 'who is this' layer above the login.py session core: the two real SSO mechanisms the cloud-shell-fog spec names, OIDC and WebAuthn/FIDO2, verified honestly with only the standard library. OIDC ID-token validation is strict and fail-closed (exact iss; aud/azp; exp/nbf/iat; nonce binding; alg 'none' always refused; HS256 verified via hmac). PKCE (S256) binds the authorization code to the client, with state (CSRF) and nonce (replay) checks. WebAuthn assertion verification checks ceremony type, challenge, origin, rpIdHash, user-present/user-verified flags, and signature-counter clone detection. Honest boundary: verifying an ASYMMETRIC signature (RS256/ES256 JWTs, WebAuthn credential keys) needs crypto the stdlib does not ship and must never be hand-rolled, so that one step is a clearly-marked injection point (verify_signature=…) wired in production to a JWKS/cryptography verifier — asymmetric algs are refused, never silently trusted, without it. A verified identity mints a login.py session; the same zero-trust, fail-closed posture the webhook gives machines, now for humans.",
"links": {
"engine": "tools/sso.py",
"session_core": "tools/login.py",
"machine_analogue": "tools/push_webhook.py (HMAC-verified machine door)",
"grant_binding": "tools/mcp_a2a_grant.py",
"spec_witness": "docs/SSO_FRONT_DOOR.md",
"reference_pattern": "OpenID Connect (auth-code + PKCE, ID-token claim validation) + W3C WebAuthn/FIDO2 (assertion verification) — met sovereign+stdlib: real claim/ceremony validation, alg:none refused, asymmetric signature verification as an injected JWKS/crypto step, never hand-rolled"
},
"composes_with": {
"session": "caps.dev.login-session@0.1.0",
"provisioning": "caps.dev.provisioning@0.1.0",
"control_plane": "caps.infra.paas.continuum-local@0.1.0",
"scales_up_to": "caps.infra.cluster-scaleup.hyperswarm@0.1.0"
},
"policy": {
"availability": "needs-work",
"fail_closed": true,
"alg_none_refused": true,
"pkce_required": true,
"nonce_replay_bound": true,
"webauthn_clone_detection": true,
"asymmetric_verify_injected_not_hand_rolled": true,
"evidence_emitting": false
}
}
18 changes: 13 additions & 5 deletions docs/PAAS_GAP_REGISTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ diagrams feel seamless is mostly missing — that's what this register makes exp
| **Nocalhost-Web** (admin web console) | portal (read-only dev console) | ◑ (no admin console) |
| **Nocalhost-API** (REST) | MCP ops surface (agent JSON-RPC) | ◑ (no REST admin API) |
| **Nocalhost-Dep** (cluster-side dep, `nocalhost-reserved` ns) | — | ○ **GAP: no cluster-side controller/operator** |
| **Login** / auth | — | ○ **GAP: no login/session/SSO** |
| **Login** / auth | `login.py` (session core) + `sso.py` (real OIDC + WebAuthn/FIDO2 verify) | ◑ (verify core done; asymmetric-sig verifier injected, not yet wired to a live IdP) |
| Admin: **Create User** | — | ○ **GAP** (this change closes it) |
| Admin: **Configure Cluster** | CapDs (static) | ◑ |
| Admin: **Configure Application** | buildpack + workload specs | ◑ |
Expand All @@ -35,7 +35,7 @@ diagrams feel seamless is mostly missing — that's what this register makes exp
|---|---|---|
| **Load Balancer** | — | ○ GAP (no ingress LB in continuum) |
| DataWorks **UI App Server** | portal | ◑ |
| **BlueMix SSO service** | — | ○ **GAP: no SSO** |
| **BlueMix SSO service** | `sso.py` — OIDC (auth-code + PKCE, ID-token validation) + WebAuthn/FIDO2 | ◑ (fail-closed verify core, stdlib; asymmetric-signature step injected) |
| **BlueMix Entitlement service** | admission **tiers** | ◑ (tier data, no entitlement *service*/token) |
| **Watson Token Server** (per-tenant) + **/me API** | grants (session tokens) | ◑ (no token server / user-profile `/me`) — this change adds `/me` |
| **Service Broker** (add tenant / provision new VM) | `devspace` manifests | ○ **GAP** (this change closes it) |
Expand Down Expand Up @@ -68,9 +68,17 @@ BlueMix calls it SSO+Entitlement+Broker+Token+BSS. It's the same server. `tools/
(this change) builds the governed core of it — provision/bind/deprovision a tenant (DevSpace + tier +
session), meter usage, and `/me` — and the portal exposes `/api/me` + `/api/provision`.

**Still open after this change** (the honest ranked backlog): IDE plugin · running remote terminal ·
real login/SSO · cluster-side dep operator · load-balancer + RM-A/B failover HA · ELK/Grafana
dashboards · landing page. Named here so nothing hides.
**Shipped since (front door + deploy).** The developer/human front door is now real and governed:
`login.py` session core + **`sso.py`** (OIDC auth-code+PKCE + WebAuthn/FIDO2, fail-closed, stdlib) →
the git-push-to-deploy ergonomic is complete end-to-end: **`push_webhook.py`** (HMAC-verified trigger)
→ `deploy_flow.py` preview → promotion gate → **`release_ledger.py`** (instant, reproducible,
fail-closed rollback). The machine door (webhook, HMAC) and the human door (SSO, OIDC/WebAuthn) share
one posture: a zero-trust gate, not an open hook.

**Still open** (the honest ranked backlog): IDE plugin · running remote terminal · **live IdP wiring**
for `sso.py`'s injected asymmetric-signature verifier (JWKS/`cryptography`) · cluster-side dep
operator · load-balancer + RM-A/B failover HA · ELK/Grafana dashboards · landing page. Named here so
nothing hides.

## Cloud Foundry / BlueMix — similar, different, and why ours is best-of-all

Expand Down
54 changes: 54 additions & 0 deletions docs/SSO_FRONT_DOOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# SSO front door — real OIDC + WebAuthn/FIDO2, fail-closed, stdlib-only

`login.py` is the **session core** — it issues and verifies an HMAC-bound, expiring session. But a
session should only be minted once you actually know *who is this*. That decision is SSO, and
`tools/sso.py` does it with the two real mechanisms the cloud-shell-fog spec names — **OIDC** and
**WebAuthn/FIDO2** — using only the standard library.

It is the human analogue of the push webhook: the webhook is the **machine** door (an HMAC-verified
`git push`); this is the **human** door (an OIDC/WebAuthn-verified login). Same posture — a
zero-trust, fail-closed gate, not an open one.

## The honest boundary

Verifying an **asymmetric** signature — RS256/ES256 ID tokens, WebAuthn credential public keys —
needs public-key crypto the Python stdlib does not ship, and hand-rolling RSA/ECDSA is exactly the
thing you must never do. So that one step is a **clearly-marked injection point**
(`verify_signature=…`), wired in production to a JWKS / `cryptography` verifier. Without it,
asymmetric algorithms are **refused, never silently trusted**.

Everything *else* — and it is the majority of what these protocols get wrong in the field — is real
here and stdlib-doable. That's the point: we build the security-critical validation honestly and
delegate exactly one crypto primitive, rather than fake the whole thing or pull a heavy dep into a
dependency-free stack.

## What's verified (all fail-closed)

**OIDC ID-token** (`verify_id_token`) — signature first, then every required claim:
- `alg: none` (or missing) is **always refused** — an unsigned token is never trusted.
- `HS256` verified with `hmac` (stdlib); asymmetric algs require the injected verifier.
- `iss` exact match · `aud` contains us (and `azp` is us when there are multiple audiences) ·
`exp`/`nbf`/`iat` with small leeway · `nonce` binds the token to *this* login (replay/CSRF).

**PKCE + auth-code** (`pkce_pair` / `build_authorization_request` / `verify_callback` /
`build_token_exchange`) — S256 challenge/verifier binding, `state` (CSRF) and `nonce` (replay) checks,
and the token-exchange body that sends the `code_verifier` as proof. All stdlib, no network I/O.

**WebAuthn assertion** (`verify_assertion`) — per WebAuthn §7.2:
- ceremony `type == webauthn.get` · `challenge` equals the one we issued (constant-time) ·
`origin` exact (anti-phishing) · `rpIdHash == sha256(rp_id)` · **UP** (user-present) required,
**UV** enforced when asked · **signature-counter clone detection** (a stored/reported 0 disables it,
else it must strictly increase). The credential signature over
`authenticatorData ‖ sha256(clientDataJSON)` is the injected step.

A valid verification → `session_from_identity()` mints a `login.py` session. An invalid one mints
nothing.

## Try it

```bash
make sso # PKCE ok/tampered · OIDC valid/alg-none-refused/bad-nonce-refused · WebAuthn valid/clone-refused · session
```

`sso.py` is a pure verification core (25 unit tests, every rejection path covered). Production wires
`verify_signature` to a JWKS fetcher for the real IdP; the claim/ceremony logic does not change.
Loading
Loading