From 745d610a0a8d1ecc8a7b22eb2d24452e933cd96e Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Wed, 25 Feb 2026 20:05:26 +0100 Subject: [PATCH] Rewrite CLAUDE.md for IPS project, remove Barycenter content Replace the incorrectly copied Barycenter (OIDC IdP) content with accurate IPS project documentation including workspace structure, build commands, error handling conventions, architecture overview, and a pkg5 legacy compatibility reference mapping our docs and implementation to the original pkg5 specifications. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 585 ++++++++++-------------------------------------------- 1 file changed, 102 insertions(+), 483 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ec2aee9..5bb2f2b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,492 +4,111 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Barycenter is an OpenID Connect Identity Provider (IdP) implementing OAuth 2.0 Authorization Code flow with PKCE. The project is written in Rust using axum for the web framework, SeaORM for database access (SQLite and PostgreSQL), and josekit for JOSE/JWT operations. +IPS (Image Packaging System) is a Rust reimplementation of the illumos/OpenIndiana package management system (replacing the Python-based pkg5). It handles software packaging, installation, updates, and dependency resolution for illumos distributions. + +**Workspace crates:** libips (core library), pkg6 (client CLI), pkg6repo (repo management CLI), pkg6depotd (HTTP depot server), pkg6recv, pkgtree, specfile, ports, userland, xtask. ## Build and Development Commands ```bash -# Build the project -cargo build - -# Run the application (defaults to config.toml) -cargo run - -# Run with custom config -cargo run -- --config path/to/config.toml - -# Run in release mode -cargo build --release -cargo run --release - -# Check code without building -cargo check - -# Run tests (IMPORTANT: use cargo nextest, not cargo test) -cargo nextest run - -# Run with logging (uses RUST_LOG environment variable) -RUST_LOG=debug cargo run -RUST_LOG=barycenter=trace cargo run +cargo build # Build all crates +cargo check # Check without building +cargo nextest run # Run tests (CRITICAL: use nextest, not cargo test) +cargo nextest run test_name # Run specific test +make release # Build release + copy to artifacts/ +RUST_LOG=libips=trace cargo run # Run with logging ``` -## Testing - -**CRITICAL: Always use `cargo nextest run` instead of `cargo test`.** - -This project uses [cargo-nextest](https://nexte.st/) for running tests because: -- Tests run in separate processes, preventing port conflicts in integration tests -- Better test isolation and reliability -- Cleaner output and better performance - -Install nextest if you don't have it: -```bash -cargo install cargo-nextest -``` - -Run tests: -```bash -# Run all tests -cargo nextest run - -# Run with verbose output -cargo nextest run --verbose - -# Run specific test -cargo nextest run test_name -``` - -## Configuration - -The application loads configuration from: -1. Default values (defined in `src/settings.rs`) -2. Configuration file (default: `config.toml`) -3. Environment variables with prefix `BARYCENTER__` (e.g., `BARYCENTER__SERVER__PORT=9090`) - -Environment variables use double underscores as separators for nested keys. - -### Database Configuration - -Barycenter supports both SQLite and PostgreSQL databases. The database backend is automatically detected from the connection string: - -**SQLite (default):** -```toml -[database] -url = "sqlite://barycenter.db?mode=rwc" -``` - -**PostgreSQL:** -```toml -[database] -url = "postgresql://user:password@localhost/barycenter" -``` - -Or via environment variable: -```bash -export BARYCENTER__DATABASE__URL="postgresql://user:password@localhost/barycenter" -``` - -## Architecture and Module Structure - -### Entry Point (`src/main.rs`) -The application initializes in this order: -1. Parse CLI arguments for config file path -2. Load settings from config file and environment -3. Initialize database connection and create tables via `storage::init()` -4. Initialize JWKS manager (generates or loads RSA keys) -5. Start web server with `web::serve()` - -### Settings (`src/settings.rs`) -Manages configuration with four main sections: -- `Server`: listen address and public base URL (issuer) -- `Database`: database connection string (SQLite or PostgreSQL) -- `Keys`: JWKS and private key paths, signing algorithm -- `Federation`: trust anchor URLs (future use) - -The `issuer()` method returns the OAuth issuer URL, preferring `public_base_url` or falling back to `http://{host}:{port}`. - -### Storage (`src/storage.rs`) -Database layer with raw SQL using SeaORM's `DatabaseConnection`. Supports both SQLite and PostgreSQL backends, automatically detected from the connection string. Tables: -- `clients`: OAuth client registrations (client_id, client_secret, redirect_uris) -- `auth_codes`: Authorization codes with PKCE challenge, subject, scope, nonce -- `access_tokens`: Bearer tokens with subject, scope, expiration -- `properties`: Key-value store for arbitrary user properties (owner, key, value) - -All IDs and tokens are generated via `random_id()` (24 random bytes, base64url-encoded). - -### JWKS Manager (`src/jwks.rs`) -Handles RSA key generation, persistence, and JWT signing: -- Generates 2048-bit RSA key on first run -- Persists private key as JSON to `private_key_path` -- Publishes public key set to `jwks_path` -- Provides `sign_jwt_rs256()` for ID Token signing with kid header - -### Web Endpoints (`src/web.rs`) -Implements OpenID Connect and OAuth 2.0 endpoints: - -**Discovery & Registration:** -- `GET /.well-known/openid-configuration` - OpenID Provider metadata -- `GET /.well-known/jwks.json` - Public signing keys -- `POST /connect/register` - Dynamic client registration - -**OAuth/OIDC Flow:** -- `GET /authorize` - Authorization endpoint (issues authorization code with PKCE) - - Validates client_id, redirect_uri, scope (must include "openid"), PKCE S256 - - Checks 2FA requirements (admin-enforced, high-value scopes, max_age) - - Redirects to /login or /login/2fa if authentication needed - - Returns redirect with code and state -- `POST /token` - Token endpoint (exchanges code for tokens) - - Supports `client_secret_basic` (Authorization header) and `client_secret_post` (form body) - - Validates PKCE S256 code_verifier - - Returns access_token, id_token (JWT with AMR/ACR claims), token_type, expires_in -- `GET /userinfo` - UserInfo endpoint (returns claims for Bearer token) - -**Authentication:** -- `GET /login` - Login page with passkey autofill and password fallback -- `POST /login` - Password authentication, checks 2FA requirements -- `GET /login/2fa` - Two-factor authentication page -- `POST /logout` - End user session - -**Passkey/WebAuthn Endpoints:** -- `POST /webauthn/register/start` - Start passkey registration (requires session) -- `POST /webauthn/register/finish` - Complete passkey registration -- `POST /webauthn/authenticate/start` - Start passkey authentication (public) -- `POST /webauthn/authenticate/finish` - Complete passkey authentication -- `POST /webauthn/2fa/start` - Start 2FA passkey verification (requires partial session) -- `POST /webauthn/2fa/finish` - Complete 2FA passkey verification - -**Passkey Management:** -- `GET /account/passkeys` - List user's registered passkeys -- `DELETE /account/passkeys/:credential_id` - Delete a passkey -- `PATCH /account/passkeys/:credential_id` - Update passkey name - -**Non-Standard:** -- `GET /properties/:owner/:key` - Get property value -- `PUT /properties/:owner/:key` - Set property value -- `GET /federation/trust-anchors` - List trust anchors - -### Error Handling (`src/errors.rs`) -Defines `CrabError` for internal error handling with conversions from common error types. - -## Key Implementation Details - -### PKCE Flow -- Only S256 code challenge method is supported (plain is rejected) -- Code challenge stored with auth code -- Code verifier validated at token endpoint by hashing and comparing - -### Client Authentication -Token endpoint accepts two methods: -1. `client_secret_basic`: HTTP Basic auth (client_id:client_secret base64-encoded) -2. `client_secret_post`: Form parameters (client_id and client_secret in body) - -### ID Token Claims -Generated ID tokens include: -- Standard claims: iss, sub, aud, exp, iat -- Optional: nonce (if provided in authorize request) -- at_hash: hash of access token per OIDC spec (left 128 bits of SHA-256, base64url) -- auth_time: timestamp of authentication (from session) -- amr: Authentication Method References array (e.g., ["pwd"], ["hwk"], ["pwd", "hwk"]) -- acr: Authentication Context Reference ("aal1" for single-factor, "aal2" for two-factor) -- Signed with RS256, includes kid header matching JWKS - -### State Management -- Authorization codes: 5 minute TTL, single-use (marked consumed) -- Access tokens: 1 hour TTL, checked for expiration and revoked flag -- Sessions: Track authentication methods (AMR), context (ACR), and MFA status -- WebAuthn challenges: 5 minute TTL, cleaned up every 5 minutes by background job -- All stored in database with timestamps - -### WebAuthn/Passkey Authentication - -Barycenter supports passwordless authentication using WebAuthn/FIDO2 passkeys with the following features: - -**Authentication Modes:** -- **Single-factor passkey login**: Passkeys as primary authentication method -- **Two-factor authentication**: Passkeys as second factor after password login -- **Password fallback**: Traditional password authentication remains available - -**Client Implementation:** -- Rust WASM module (`client-wasm/`) compiled with wasm-pack -- Browser-side WebAuthn API calls via wasm-bindgen -- Conditional UI support for autofill in Chrome 108+, Safari 16+ -- Progressive enhancement: falls back to explicit button if autofill unavailable - -**Passkey Storage:** -- Full `Passkey` object stored as JSON in database -- Tracks signature counter for clone detection -- Records backup state (cloud-synced vs hardware-bound) -- Supports friendly names for user management - -**AMR (Authentication Method References) Values:** -- `"pwd"`: Password authentication -- `"hwk"`: Hardware-bound passkey (YubiKey, security key) -- `"swk"`: Software/cloud-synced passkey (iCloud Keychain, password manager) -- Multiple values indicate multi-factor auth (e.g., `["pwd", "hwk"]`) - -**2FA Enforcement Modes:** - -1. **User-Optional 2FA**: Users can enable 2FA in account settings (future UI) -2. **Admin-Enforced 2FA**: Set `users.requires_2fa = 1` via GraphQL mutation -3. **Context-Based 2FA**: Triggered by: - - High-value scopes: "admin", "payment", "transfer", "delete" - - Fresh authentication required: `max_age < 300` seconds - - Can be configured per-scope or per-request - -**2FA Flow:** -1. User logs in with password → creates partial session (`mfa_verified=0`) -2. If 2FA required, redirect to `/login/2fa` -3. User verifies with passkey -4. Session upgraded: `mfa_verified=1`, `acr="aal2"`, `amr=["pwd", "hwk"]` -5. Authorization proceeds, ID token includes full authentication context - -## Current Implementation Status - -See `docs/oidc-conformance.md` for detailed OIDC compliance requirements. - -**Implemented:** -- Authorization Code flow with PKCE (S256) -- Dynamic client registration -- Token endpoint with client_secret_basic and client_secret_post -- ID Token signing (RS256) with at_hash, nonce, auth_time, AMR, and ACR claims -- UserInfo endpoint with Bearer token authentication -- Discovery and JWKS publication -- Property storage API -- User authentication with sessions -- Password authentication with argon2 hashing -- WebAuthn/passkey authentication (single-factor and two-factor) -- WASM client for browser-side WebAuthn operations -- Conditional UI/autofill for passkey login -- Three 2FA modes: user-optional, admin-enforced, context-based -- Background jobs for cleanup (sessions, tokens, challenges) -- Admin GraphQL API for user management and job triggering -- Refresh token grant with rotation -- Session-based AMR/ACR tracking - -**Pending:** -- Cache-Control headers on token endpoint -- Consent flow (currently auto-consents) -- Token revocation and introspection endpoints -- OpenID Federation trust chain validation -- User account management UI - -## Admin GraphQL API - -The admin API is served on a separate port (default: 9091) and provides GraphQL queries and mutations for management: - -**Mutations:** -```graphql -mutation { - # Trigger background jobs manually - triggerJob(jobName: "cleanup_expired_sessions") { - success - message - } - - # Enable 2FA requirement for a user - setUser2faRequired(username: "alice", required: true) { - success - message - requires2fa - } -} -``` - -**Queries:** -```graphql -query { - # Get job execution history - jobLogs(limit: 10, onlyFailures: false) { - id - jobName - startedAt - completedAt - success - recordsProcessed - } - - # Get user 2FA status - user2faStatus(username: "alice") { - username - requires2fa - passkeyEnrolled - passkeyCount - passkeyEnrolledAt - } - - # List available jobs - availableJobs { - name - description - schedule - } -} -``` - -Available job names: -- `cleanup_expired_sessions` (hourly at :00) -- `cleanup_expired_refresh_tokens` (hourly at :30) -- `cleanup_expired_challenges` (every 5 minutes) - -## Building the WASM Client - -The passkey authentication client is written in Rust and compiled to WebAssembly: - -```bash -# Install wasm-pack if not already installed -cargo install wasm-pack - -# Build the WASM module -cd client-wasm -wasm-pack build --target web --out-dir ../static/wasm - -# The built files will be in static/wasm/: -# - barycenter_webauthn_client_bg.wasm -# - barycenter_webauthn_client.js -# - TypeScript definitions (.d.ts files) -``` - -The WASM module is automatically loaded by the login page and provides: -- `supports_webauthn()`: Check if WebAuthn is available -- `supports_conditional_ui()`: Check for autofill support -- `register_passkey(options)`: Create a new passkey -- `authenticate_passkey(options, mediation)`: Authenticate with passkey - -## Testing and Validation - -### Manual Testing Flow - -**1. Test Password Login:** -```bash -# Navigate to http://localhost:9090/login -# Enter username: admin, password: password123 -# Should create session and redirect -``` - -**2. Test Passkey Registration:** -```bash -# After logging in with password -# Navigate to http://localhost:9090/account/passkeys -# (Future UI - currently use browser console) - -# Call via JavaScript console: -fetch('/webauthn/register/start', { method: 'POST' }) - .then(r => r.json()) - .then(data => { - // Use browser's navigator.credentials.create() with returned options - }); -``` - -**3. Test Passkey Authentication:** -- Navigate to `/login` -- Click on username field -- Browser should show passkey autofill (Chrome 108+, Safari 16+) -- Select a passkey to authenticate - -**4. Test Admin-Enforced 2FA:** -```graphql -# Via admin API (port 9091) -mutation { - setUser2faRequired(username: "admin", required: true) { - success - } -} -``` - -Then: -1. Log out -2. Log in with password -3. Should redirect to `/login/2fa` -4. Complete passkey verification -5. Should complete authorization with ACR="aal2" - -**5. Test Context-Based 2FA:** -```bash -# Request authorization with max_age < 300 -curl "http://localhost:9090/authorize?...&max_age=60" -# Should trigger 2FA even if not admin-enforced -``` - -### OIDC Flow Testing - -```bash -# 1. Register a client -curl -X POST http://localhost:9090/connect/register \ - -H "Content-Type: application/json" \ - -d '{ - "redirect_uris": ["http://localhost:8080/callback"], - "client_name": "Test Client" - }' - -# 2. Generate PKCE -verifier=$(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_') -challenge=$(echo -n "$verifier" | openssl dgst -binary -sha256 | base64 | tr -d '=' | tr '+/' '-_') - -# 3. Navigate to authorize endpoint (in browser) -http://localhost:9090/authorize?client_id=CLIENT_ID&redirect_uri=http://localhost:8080/callback&response_type=code&scope=openid&code_challenge=$challenge&code_challenge_method=S256&state=random - -# 4. After redirect, exchange code for tokens -curl -X POST http://localhost:9090/token \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "grant_type=authorization_code&code=CODE&redirect_uri=http://localhost:8080/callback&client_id=CLIENT_ID&client_secret=SECRET&code_verifier=$verifier" - -# 5. Decode ID token to verify AMR/ACR claims -# Use jwt.io or similar to inspect the token -``` - -### Expected ID Token Claims - -After passkey authentication: -```json -{ - "iss": "http://localhost:9090", - "sub": "user_subject_uuid", - "aud": "client_id", - "exp": 1234567890, - "iat": 1234564290, - "auth_time": 1234564290, - "amr": ["hwk"], // or ["swk"] for cloud-synced, ["pwd", "hwk"] for 2FA - "acr": "aal1", // or "aal2" for 2FA - "nonce": "optional_nonce" -} -``` - -## Migration Guide for Existing Deployments - -If you have an existing Barycenter deployment, the database will be automatically migrated when you update: - -1. **Backup your database** before upgrading -2. Run the application - migrations run automatically on startup -3. New tables will be created: - - `passkeys`: Stores registered passkeys - - `webauthn_challenges`: Temporary challenge storage -4. Existing tables will be extended: - - `sessions`: Added `amr`, `acr`, `mfa_verified` columns - - `users`: Added `requires_2fa`, `passkey_enrolled_at` columns - -**Post-Migration Steps:** - -1. Build the WASM client: - ```bash - cd client-wasm - wasm-pack build --target web --out-dir ../static/wasm - ``` - -2. Restart the application to serve static files - -3. Users can now register passkeys via `/account/passkeys` (future UI) - -4. Enable 2FA for specific users via admin API: - ```graphql - mutation { - setUser2faRequired(username: "admin", required: true) { - success - } - } - ``` - -**No Breaking Changes:** -- Password authentication continues to work -- Existing sessions remain valid -- ID tokens now include AMR/ACR claims (additive change) -- OIDC clients receiving new claims should handle gracefully \ No newline at end of file +**CRITICAL: Always use `cargo nextest run` instead of `cargo test`.** Tests need process isolation to avoid port conflicts. + +## Coding Conventions + +### Error Handling (miette + thiserror) + +- All error enums derive `#[derive(Debug, Error, Diagnostic)]` +- Each variant has `#[diagnostic(code(...), help(...))]` with actionable guidance +- Subsystem errors chain via `#[error(transparent)] #[diagnostic(transparent)]` +- Return `IpsResult` (alias for `Result`) from the top-level API +- Subsystem functions return their own result type (e.g., `RepositoryResult`, `ImageResult`, `FmriResult`) +- Each module defines: `pub type Result = std::result::Result;` +- Diagnostic codes follow the pattern `ips::module::variant` (e.g., `ips::image_error::io`) + +### Rust Style + +- Use strongly typed idiomatic Rust — express logic in datatypes +- Do error handling — express options for how to handle issues for calling functions +- Use miette's diagnostic pattern to inform the user thoroughly what they need to do +- Never use raw SQL — use rusqlite with proper abstractions +- Edition 2024 + +## Architecture + +### libips (core library) +- `fmri.rs` — FMRI parsing (`pkg://publisher/name@version`) +- `actions/` — Manifest action types (File, Dir, Link, User, Group, etc.) and filesystem executors +- `image/` — Image metadata, catalog queries, installed packages DB (SQLite) +- `repository/` — `ReadableRepository`/`WritableRepository` traits, FileBackend (local), RestBackend (HTTP) +- `solver/` — Dependency resolution via resolvo +- `digest/` — SHA1/SHA2/SHA3 hashing +- `payload/` — Package payload handling +- `publisher.rs` — Publisher configuration +- `transformer.rs` — Manifest transformation/linting +- `depend/` — Dependency generation and parsing + +### Key Types +- `Fmri` — Fault Management Resource Identifier (`pkg://publisher/name@release,branch-build:timestamp`) +- `Manifest` — Collection of actions describing a package +- `Image` — Full or Partial image with metadata, publishers, catalogs +- `FileBackend` / `RestBackend` — Repository implementations + +## pkg5 Legacy Documentation and Compatibility + +We carry the original pkg5 reference documentation in `doc/pkg5_docs/` to guide compatibility with the legacy Python client. Our own docs in `doc/` describe how we've implemented the compatibility layer. + +### Reference Specifications (from pkg5) + +| Topic | File | What it covers | +|-------|------|----------------| +| **Depot protocol** | `depot.txt`, `depot.rst` | HTTP REST API: `/file/0/{hash}`, `/catalog/0/`, `/manifest/0/{fmri}`, `/search/0/`, `/p5i/0/`, `/publisher/0/`, `/versions/0/` | +| **Retrieval protocol** | `guide-retrieval-protocol.rst` | Client-depot wire protocol, ETag/If-Modified-Since caching, content negotiation | +| **Publication protocol** | `guide-publication-protocol.rst` | Transaction-based publishing (`open`, `add`, `close`) | +| **Repository format** | `guide-repository-format.rst` | On-disk layout: `pkg/`, `file/` (two-level hash dirs), `catalog/`, `trans/`, `updatelog/` | +| **Catalog v1** | `catalog-v1.txt`, `catalog.txt` | Delta-encoded text catalog with `catalog.attrs`, `npkgs`, incremental updates | +| **Actions** | `actions.txt`, `actions.rst` | All 12 action types: file, dir, link, hardlink, depend, service, user, group, driver, license, legacy, set | +| **FMRI & versioning** | `versions.txt`, dev-guide `chpt3.txt` | `pkg://publisher/name@release,branch-build:timestamp`, component ordering | +| **Image types** | `image.txt`, `image.rst` | Full, Zone, User images and their metadata layout | +| **Server API versions** | `server_api_versions.txt`, `client_api_versions.txt` | Protocol version negotiation between client and depot | +| **Signed manifests** | `signed_manifests.txt` | Manifest signature format and verification | +| **Linked images** | `linked-images.txt`, `parallel-linked-images.txt` | Zone/parent-child image constraints | +| **Facets & variants** | `facets.txt` | Conditional action delivery (`variant.arch`, `facet.doc`) | +| **Mediated links** | `mediated-links.txt` | Conflict resolution for symlinks across packages | +| **On-disk format** | `on-disk-format.txt` | Container format proposal for packages | +| **Dev guide** | `dev-guide/chpt1-14.txt` | Full IPS developer guide chapters | + +### Our Compatibility Implementation + +| Feature | Our doc | Implementation | +|---------|---------|----------------| +| **p5i publisher files** | `doc/pub_p5i_implementation.md` | `FileBackend` generates `pub.p5i` JSON files per publisher for backward compat with `pkg set-publisher -p` | +| **Obsoleted packages** | `doc/obsoleted_packages.md` | Detects `pkg.obsolete=true` during pkg5 import; stores in `/obsoleted/` with structured metadata | +| **pkg5 import** | `pkg6repo/src/pkg5_import.rs` | Reads `pkg5.repository`, two-level hash dirs, gzip payloads, URL-encoded versions; converts to pkg6 format | +| **Depot REST API** | `pkg6depotd/src/http/` | Serves same HTTP paths as pkg5 depot (`/file/0/`, `/manifest/0/`, `/catalog/0/`, `/publisher/0/`) | +| **libips integration** | `doc/forge_docs/ips_integration.md` | Typed API replacing pkg5 CLI tools (`pkgsend`, `pkgmogrify`, `pkgdepend`, `pkglint`, `pkgrepo`) | +| **Error handling** | `doc/rust_docs/error_handling.md` | miette + thiserror patterns with `ips::module::variant` codes | + +### Key Compatibility Surfaces for Legacy Client + +The legacy `pkg(1)` Python client expects these from a depot server: +1. **`/versions/0/`** — lists supported operations and protocol versions +2. **`/publisher/0/`** — returns `application/vnd.pkg5.info` (p5i JSON) +3. **`/catalog/0/`** — catalog with `Last-Modified` header, incremental updates +4. **`/manifest/0/{fmri}`** — `text/plain` manifest with `ETag` +5. **`/file/0/{hash}`** — gzip-compressed file content with `ETag` and `Cache-Control` +6. **`/search/0/{token}`** — search returning `index action value package` tuples + +## Documentation Maintenance + +- Keep `docs/ai/architecture.md` updated when making structural changes. Bump the "last updated" date. +- Create a new timestamped plan in `docs/ai/plans/` before starting a new phase or significant feature. +- Create a new timestamped ADR in `docs/ai/decisions/` when making meaningful technology or design choices. Number sequentially from the last ADR. +- Never delete old plans or decisions. Mark superseded plans with status `Superseded` and link to the replacement.