| @@ -33,6 +33,9 @@ | ||
| - **Public description refresh.** Updated README/roadmap/embedding copy to | ||
| describe v0.4.11 as the current stable line and the post-tag Rust registry | ||
| deployment and migration work on `main`. | ||
| + | - **Code comment style.** Added `CONTRIBUTING.md` guidance to prefer | |
| + | self-explanatory code and tests over prose-style inline comments, then | |
| + | removed noisy implementation comments from the Rust registry path. | |
| ## v0.4.11 - 2026-05-08 Hardware-keys completion: Python parity, browser support, end-to-end seal | ||
| @@ -0,0 +1,18 @@ | ||
| + | # Contributing | |
| + | ||
| + | ## Code style | |
| + | ||
| + | Oversight is security protocol code, so implementation files should read like | |
| + | code first. | |
| + | ||
| + | - Prefer clear names, small functions, and tests over explanatory inline | |
| + | comments. | |
| + | - Do not add prose comments for ordinary control flow, configuration loading, | |
| + | route setup, or obvious validation. | |
| + | - Keep comments only when they document a non-obvious protocol invariant, | |
| + | external wire-format requirement, compatibility constraint, or security | |
| + | boundary that code alone cannot make explicit. | |
| + | - When a comment is necessary, keep it short and factual. Avoid conversational | |
| + | sentences and implementation diary notes. | |
| + | ||
| + | Public documentation belongs in `docs/`, not in source-file commentary. |
| @@ -29,11 +29,8 @@ use thiserror::Error; | ||
| pub const MAGIC: [u8; 6] = *b"OSGT\x01\x00"; | ||
| pub const SUITE_CLASSIC_V1_ID: u8 = 1; | ||
| pub const SUITE_HYBRID_V1_ID: u8 = 2; | ||
| - | /// Hardware-backed P-256 ECDH suite for PIV-compatible tokens. | |
| - | /// See `docs/HARDWARE_KEYS.md` and `oversight_crypto::SUITE_HW_P256_V1`. | |
| pub const SUITE_HW_P256_V1_ID: u8 = 3; | ||
| - | // Hard caps to prevent DoS via attacker-controlled length fields. | |
| pub const MAX_MANIFEST_BYTES: usize = 4 * 1024 * 1024; | ||
| pub const MAX_WRAPPED_DEK_BYTES: usize = 1 * 1024 * 1024; | ||
| // 4 GiB on 64-bit; usize::MAX on 32-bit (which is just under 4 GiB anyway). | ||
| @@ -223,14 +220,12 @@ impl SealedFile { | ||
| // -------------------------- High-level API -------------------------- | ||
| - | /// Seal plaintext for a single recipient. | |
| pub fn seal( | ||
| plaintext: &[u8], | ||
| manifest: &mut Manifest, | ||
| issuer_ed25519_priv: &[u8], | ||
| recipient_x25519_pub: &[u8], | ||
| ) -> Result<Vec<u8>, ContainerError> { | ||
| - | // Preconditions as explicit checks (not asserts - python -O safety parity). | |
| if manifest.content_hash != crypto::content_hash(plaintext) { | ||
| return Err(ContainerError::Precondition( | ||
| "manifest.content_hash != sha256(plaintext)", | ||
| @@ -278,7 +273,6 @@ pub fn seal( | ||
| sf.to_bytes() | ||
| } | ||
| - | /// Open a sealed blob. Returns (plaintext, manifest). | |
| pub fn open_sealed( | ||
| blob: &[u8], | ||
| recipient_x25519_priv: &[u8], | ||
| @@ -302,7 +296,6 @@ pub fn open_sealed( | ||
| } | ||
| } | ||
| - | // Policy enforcement (time-based) - expanded version in oversight-policy crate later | |
| let now = std::time::SystemTime::now() | ||
| .duration_since(std::time::UNIX_EPOCH) | ||
| .map(|d| d.as_secs() as i64) | ||
| @@ -325,7 +318,6 @@ pub fn open_sealed( | ||
| } | ||
| } | ||
| - | // DEK unwrap: try slots if present, else single wrap | |
| let dek = if let Some(slots) = sf.wrapped_dek.get("slots").and_then(|v| v.as_array()) { | ||
| let mut recovered = None; | ||
| for slot in slots { | ||
| @@ -348,20 +340,11 @@ pub fn open_sealed( | ||
| return Err(ContainerError::HashMismatch); | ||
| } | ||
| - | // Count only successful recipient decryptions. Failed key guesses cannot | |
| - | // burn max_opens, but a policy failure still prevents plaintext release. | |
| oversight_policy::record_open(&sf.manifest, policy_ctx)?; | ||
| Ok((plaintext, sf.manifest)) | ||
| } | ||
| - | /// Seal `plaintext` for a hardware-backed P-256 recipient (`OSGT-HW-P256-v1`). | |
| - | /// | |
| - | /// Mirrors [`seal`] but consumes the recipient's P-256 SEC1 uncompressed | |
| - | /// public key (65 bytes) instead of an X25519 public key. The manifest's | |
| - | /// `suite` field must already be set to `oversight_crypto::SUITE_HW_P256_V1` | |
| - | /// and the recipient's `p256_pub` field must hex-match `recipient_p256_sec1_pub`. | |
| - | /// All other invariants (content_hash, size_bytes, signature) match [`seal`]. | |
| pub fn seal_hw_p256( | ||
| plaintext: &[u8], | ||
| manifest: &mut Manifest, | ||
| @@ -515,7 +498,6 @@ pub fn open_sealed_with_provider( | ||
| Ok((plaintext, sf.manifest)) | ||
| } | ||
| - | /// Fail closed until the manifest schema can explicitly bind every recipient. | |
| pub fn seal_multi( | ||
| plaintext: &[u8], | ||
| manifest: &mut Manifest, | ||
| @@ -719,7 +701,6 @@ mod tests { | ||
| ) | ||
| .unwrap(); | ||
| - | // Container header must carry the hardware suite id. | |
| assert_eq!(blob[7], SUITE_HW_P256_V1_ID); | ||
| let provider = SoftwareP256KeyProvider::new(recipient); |
| @@ -1,15 +1,8 @@ | ||
| - | //! Ed25519 signature verification for /register. | |
| - | //! | |
| - | //! Uses the workspace `oversight-manifest` crate to parse and verify manifests | |
| - | //! in canonical JSON form. The issuer's Ed25519 public key is embedded in the | |
| - | //! manifest itself - verification proves the issuer signed the exact bytes. | |
| - | ||
| use axum::http::{header, HeaderMap}; | ||
| use oversight_manifest::Manifest; | ||
| use crate::error::{RegistryError, Result as RegistryResult}; | ||
| - | /// Extract a token from either `Authorization: Bearer ...` or a named header. | |
| pub fn bearer_or_header_token(headers: &HeaderMap, header_name: &'static str) -> Option<String> { | ||
| if let Some(auth) = headers | ||
| .get(header::AUTHORIZATION) | ||
| @@ -31,7 +24,6 @@ pub fn bearer_or_header_token(headers: &HeaderMap, header_name: &'static str) -> | ||
| .map(str::to_string) | ||
| } | ||
| - | /// Require an optional deployment token. Empty config preserves local/dev mode. | |
| pub fn require_optional_token( | ||
| configured_token: Option<&str>, | ||
| headers: &HeaderMap, | ||
| @@ -68,14 +60,7 @@ pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { | ||
| diff == 0 | ||
| } | ||
| - | /// Parse a manifest JSON value, canonicalize it, and verify the embedded | |
| - | /// Ed25519 signature. | |
| - | /// | |
| - | /// Returns `(signature_valid, issuer_ed25519_pub_hex)`. | |
| - | /// If parsing fails, returns `(false, "")`. | |
| pub fn verify_manifest_signature(manifest_value: &serde_json::Value) -> (bool, String) { | ||
| - | // Serialize to canonical JSON bytes (sorted keys, no whitespace) the same | |
| - | // way the Python server does: json.dumps(m, sort_keys=True, separators=(",",":")) | |
| let canonical = match serde_jcs::to_vec(manifest_value) { | ||
| Ok(b) => b, | ||
| Err(_) => return (false, String::new()), | ||
| @@ -94,12 +79,6 @@ pub fn verify_manifest_signature(manifest_value: &serde_json::Value) -> (bool, S | ||
| } | ||
| } | ||
| - | /// Normalize a list of sidecar items (beacons or watermarks) to sorted | |
| - | /// canonical JSON strings for exact comparison against the signed manifest. | |
| - | /// | |
| - | /// This mirrors the Python `_canonical_items()` function that sorts the | |
| - | /// JSON-serialized forms to detect any mismatch between the request sidecars | |
| - | /// and the manifest's signed copies. | |
| pub fn canonical_items(items: &[serde_json::Value]) -> Vec<String> { | ||
| let mut result: Vec<String> = items | ||
| .iter() | ||
| @@ -109,12 +88,6 @@ pub fn canonical_items(items: &[serde_json::Value]) -> Vec<String> { | ||
| result | ||
| } | ||
| - | /// Validate that the request beacons/watermarks exactly match the signed | |
| - | /// manifest's beacons/watermarks. Returns the signed copies on success. | |
| - | /// | |
| - | /// This is the v0.4.4 hardening check: the registry uses the manifest's | |
| - | /// embedded copies as the source of truth. If the request sidecars differ | |
| - | /// from what was signed, the registration is rejected. | |
| pub fn validate_signed_artifacts( | ||
| manifest_value: &serde_json::Value, | ||
| req_beacons: &[serde_json::Value], | ||
| @@ -152,7 +125,6 @@ mod tests { | ||
| fn canonical_items_sorts_deterministically() { | ||
| let a = serde_json::json!({"z": 1, "a": 2}); | ||
| let b = serde_json::json!({"a": 2, "z": 1}); | ||
| - | // Same logical object, different key order: canonical form should match. | |
| let ca = canonical_items(&[a]); | ||
| let cb = canonical_items(&[b]); | ||
| assert_eq!(ca, cb); |
| @@ -1,8 +1,3 @@ | ||
| - | //! SQLite database setup, migrations, and query functions. | |
| - | //! | |
| - | //! Uses SQLx with WAL mode for concurrent read/write access. | |
| - | //! All queries use parameterized bindings - no string interpolation. | |
| - | ||
| use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; | ||
| use std::path::Path; | ||
| use std::str::FromStr; | ||
| @@ -23,9 +18,7 @@ pub struct MigrationReport { | ||
| pub corpus: i64, | ||
| } | ||
| - | /// Create a SQLite connection pool with WAL mode and sensible defaults. | |
| pub async fn create_pool(db_path: &Path) -> Result<SqlitePool> { | ||
| - | // Ensure parent directory exists. | |
| if let Some(parent) = db_path.parent() { | ||
| std::fs::create_dir_all(parent) | ||
| .map_err(|e| RegistryError::Internal(format!("cannot create db directory: {e}")))?; | ||
| @@ -36,7 +29,6 @@ pub async fn create_pool(db_path: &Path) -> Result<SqlitePool> { | ||
| .map_err(|e| RegistryError::Internal(format!("bad db url: {e}")))? | ||
| .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) | ||
| .synchronous(sqlx::sqlite::SqliteSynchronous::Normal) | ||
| - | // Busy timeout so concurrent writers don't fail immediately. | |
| .busy_timeout(std::time::Duration::from_secs(5)); | ||
| let pool = SqlitePoolOptions::new() | ||
| @@ -47,7 +39,6 @@ pub async fn create_pool(db_path: &Path) -> Result<SqlitePool> { | ||
| Ok(pool) | ||
| } | ||
| - | /// Run schema migrations (CREATE TABLE IF NOT EXISTS). | |
| pub async fn run_migrations(pool: &SqlitePool) -> Result<()> { | ||
| sqlx::query( | ||
| r#" | ||
| @@ -131,7 +122,6 @@ pub async fn run_migrations(pool: &SqlitePool) -> Result<()> { | ||
| .execute(pool) | ||
| .await?; | ||
| - | // Indices | |
| sqlx::query("CREATE INDEX IF NOT EXISTS idx_events_token ON events(token_id);") | ||
| .execute(pool) | ||
| .await?; | ||
| @@ -145,8 +135,6 @@ pub async fn run_migrations(pool: &SqlitePool) -> Result<()> { | ||
| Ok(()) | ||
| } | ||
| - | /// Copy rows from the Python reference registry SQLite database into the Rust | |
| - | /// registry schema. The schemas are intentionally identical for these tables. | |
| pub async fn migrate_from_sqlite( | ||
| pool: &SqlitePool, | ||
| source_path: &Path, | ||
| @@ -308,9 +296,6 @@ async fn copy_attached_source(conn: &mut sqlx::pool::PoolConnection<sqlx::Sqlite | ||
| Ok(()) | ||
| } | ||
| - | // ---- Manifest queries --------------------------------------------------- | |
| - | ||
| - | /// Look up the issuer pubkey for an existing file_id. Returns None if not found. | |
| pub async fn get_manifest_issuer_pub(pool: &SqlitePool, file_id: &str) -> Result<Option<String>> { | ||
| let row: Option<(String,)> = | ||
| sqlx::query_as("SELECT issuer_ed25519_pub FROM manifests WHERE file_id = ?") | ||
| @@ -320,7 +305,6 @@ pub async fn get_manifest_issuer_pub(pool: &SqlitePool, file_id: &str) -> Result | ||
| Ok(row.map(|r| r.0)) | ||
| } | ||
| - | /// Insert or replace a manifest row. | |
| pub async fn upsert_manifest( | ||
| pool: &SqlitePool, | ||
| file_id: &str, | ||
| @@ -344,7 +328,6 @@ pub async fn upsert_manifest( | ||
| Ok(()) | ||
| } | ||
| - | /// Get manifest JSON by file_id. | |
| pub async fn get_manifest(pool: &SqlitePool, file_id: &str) -> Result<Option<ManifestRow>> { | ||
| let row = sqlx::query_as::<_, ManifestRow>( | ||
| "SELECT file_id, recipient_id, issuer_id, issuer_ed25519_pub, manifest_json, registered_at FROM manifests WHERE file_id = ?", | ||
| @@ -355,9 +338,6 @@ pub async fn get_manifest(pool: &SqlitePool, file_id: &str) -> Result<Option<Man | ||
| Ok(row) | ||
| } | ||
| - | // ---- Beacon queries ----------------------------------------------------- | |
| - | ||
| - | /// Insert or replace a beacon row. | |
| pub async fn upsert_beacon( | ||
| pool: &SqlitePool, | ||
| token_id: &str, | ||
| @@ -381,7 +361,6 @@ pub async fn upsert_beacon( | ||
| Ok(()) | ||
| } | ||
| - | /// Look up a beacon by token_id. | |
| pub async fn get_beacon(pool: &SqlitePool, token_id: &str) -> Result<Option<BeaconRow>> { | ||
| let row = sqlx::query_as::<_, BeaconRow>( | ||
| "SELECT token_id, file_id, recipient_id, issuer_id, kind, registered_at FROM beacons WHERE token_id = ?", | ||
| @@ -392,7 +371,6 @@ pub async fn get_beacon(pool: &SqlitePool, token_id: &str) -> Result<Option<Beac | ||
| Ok(row) | ||
| } | ||
| - | /// Get all beacons for a file_id. | |
| pub async fn get_beacons_by_file(pool: &SqlitePool, file_id: &str) -> Result<Vec<BeaconRow>> { | ||
| let rows = sqlx::query_as::<_, BeaconRow>( | ||
| "SELECT token_id, file_id, recipient_id, issuer_id, kind, registered_at FROM beacons WHERE file_id = ?", | ||
| @@ -403,9 +381,6 @@ pub async fn get_beacons_by_file(pool: &SqlitePool, file_id: &str) -> Result<Vec | ||
| Ok(rows) | ||
| } | ||
| - | // ---- Watermark queries -------------------------------------------------- | |
| - | ||
| - | /// Insert or replace a watermark row. | |
| pub async fn upsert_watermark( | ||
| pool: &SqlitePool, | ||
| mark_id: &str, | ||
| @@ -429,7 +404,6 @@ pub async fn upsert_watermark( | ||
| Ok(()) | ||
| } | ||
| - | /// Look up a watermark by mark_id (optionally filtered by layer). | |
| pub async fn get_watermark( | ||
| pool: &SqlitePool, | ||
| mark_id: &str, | ||
| @@ -457,7 +431,6 @@ pub async fn get_watermark( | ||
| Ok(row) | ||
| } | ||
| - | /// Get all watermarks for a file_id. | |
| pub async fn get_watermarks_by_file(pool: &SqlitePool, file_id: &str) -> Result<Vec<WatermarkRow>> { | ||
| let rows = sqlx::query_as::<_, WatermarkRow>( | ||
| "SELECT mark_id, layer, file_id, recipient_id, issuer_id, registered_at FROM watermarks WHERE file_id = ?", | ||
| @@ -468,9 +441,6 @@ pub async fn get_watermarks_by_file(pool: &SqlitePool, file_id: &str) -> Result< | ||
| Ok(rows) | ||
| } | ||
| - | // ---- Event queries ------------------------------------------------------ | |
| - | ||
| - | /// Insert a beacon callback event. | |
| pub async fn insert_event( | ||
| pool: &SqlitePool, | ||
| token_id: &str, | ||
| @@ -504,7 +474,6 @@ pub async fn insert_event( | ||
| Ok(()) | ||
| } | ||
| - | /// Get recent events for a file_id, most recent first. | |
| pub async fn get_recent_events( | ||
| pool: &SqlitePool, | ||
| file_id: &str, | ||
| @@ -520,7 +489,6 @@ pub async fn get_recent_events( | ||
| Ok(rows) | ||
| } | ||
| - | /// Get all events for a file_id, oldest first. | |
| pub async fn get_events_by_file(pool: &SqlitePool, file_id: &str) -> Result<Vec<EventRow>> { | ||
| let rows = sqlx::query_as::<_, EventRow>( | ||
| "SELECT id, token_id, file_id, recipient_id, issuer_id, kind, source_ip, user_agent, extra, timestamp, qualified_timestamp, tlog_index FROM events WHERE file_id = ? ORDER BY timestamp ASC", | ||
| @@ -531,9 +499,6 @@ pub async fn get_events_by_file(pool: &SqlitePool, file_id: &str) -> Result<Vec< | ||
| Ok(rows) | ||
| } | ||
| - | // ---- Corpus queries ----------------------------------------------------- | |
| - | ||
| - | /// Insert or replace a corpus hash entry. | |
| pub async fn upsert_corpus( | ||
| pool: &SqlitePool, | ||
| file_id: &str, | ||
| @@ -553,7 +518,6 @@ pub async fn upsert_corpus( | ||
| Ok(()) | ||
| } | ||
| - | /// Look up a corpus entry by perceptual hash, joining with beacons for ownership. | |
| pub async fn lookup_by_perceptual_hash( | ||
| pool: &SqlitePool, | ||
| hash_value: &str, | ||
| @@ -567,7 +531,6 @@ pub async fn lookup_by_perceptual_hash( | ||
| Ok(row) | ||
| } | ||
| - | /// Return recent L3 semantic watermark candidates for verifier scrapers. | |
| pub async fn get_semantic_candidates( | ||
| pool: &SqlitePool, | ||
| limit: i64, |
| @@ -1,5 +1,3 @@ | ||
| - | //! Unified error types and Axum error responses for the registry. | |
| - | ||
| use axum::http::StatusCode; | ||
| use axum::response::{IntoResponse, Response}; | ||
| use axum::Json; | ||
| @@ -45,7 +43,6 @@ impl IntoResponse for RegistryError { | ||
| RegistryError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()), | ||
| }; | ||
| - | // Log server-side errors at error level; client errors at debug. | |
| match status.as_u16() { | ||
| 400..=499 => tracing::debug!(%status, %message, "client error"), | ||
| _ => tracing::error!(%status, %message, "server error"), |
| @@ -1,15 +1,3 @@ | ||
| - | //! Oversight v1.0 Registry Server - Axum + SQLx | |
| - | //! | |
| - | //! Rust port of the Python FastAPI registry (`registry/server.py`). | |
| - | //! | |
| - | //! Features: | |
| - | //! - SQLite with WAL mode (via SQLx) for concurrent access | |
| - | //! - Ed25519 manifest signature verification on /register | |
| - | //! - Token bucket rate limiting with X-Forwarded-For support | |
| - | //! - RFC 6962 Merkle transparency log | |
| - | //! - Optional Rekor v2 attestation | |
| - | //! - Registry Ed25519 identity for signing log entries | |
| - | ||
| #![forbid(unsafe_code)] | ||
| mod auth; | ||
| @@ -39,38 +27,42 @@ use tower_http::trace::TraceLayer; | ||
| pub const VERSION: &str = "1.0.0"; | ||
| - | // ---- CLI args ----------------------------------------------------------- | |
| - | ||
| #[derive(Parser, Debug)] | ||
| #[command(name = "oversight-registry", version = VERSION, about = "Oversight attribution registry server")] | ||
| struct Args { | ||
| - | /// Host to bind to (overridden by OVERSIGHT_HOST env) | |
| - | #[arg(long, default_value = "127.0.0.1")] | |
| + | #[arg( | |
| + | long, | |
| + | default_value = "127.0.0.1", | |
| + | help = "Host to bind to (overridden by OVERSIGHT_HOST env)" | |
| + | )] | |
| host: String, | ||
| - | /// Port to bind to (overridden by OVERSIGHT_PORT env) | |
| - | #[arg(long, default_value = "8080")] | |
| + | #[arg( | |
| + | long, | |
| + | default_value = "8080", | |
| + | help = "Port to bind to (overridden by OVERSIGHT_PORT env)" | |
| + | )] | |
| port: u16, | ||
| - | /// SQLite database path (overridden by OVERSIGHT_DB env) | |
| - | #[arg(long)] | |
| + | #[arg(long, help = "SQLite database path (overridden by OVERSIGHT_DB env)")] | |
| db: Option<String>, | ||
| - | /// Data directory for tlog and identity key (overridden by OVERSIGHT_DATA env) | |
| - | #[arg(long)] | |
| + | #[arg( | |
| + | long, | |
| + | help = "Data directory for tlog and identity key (overridden by OVERSIGHT_DATA env)" | |
| + | )] | |
| data_dir: Option<String>, | ||
| - | /// Copy rows from a Python registry SQLite database into --db and exit | |
| - | #[arg(long)] | |
| + | #[arg( | |
| + | long, | |
| + | help = "Copy rows from a Python registry SQLite database into --db and exit" | |
| + | )] | |
| migrate_from: Option<String>, | ||
| - | /// Report migration row counts without writing to --db | |
| - | #[arg(long)] | |
| + | #[arg(long, help = "Report migration row counts without writing to --db")] | |
| migrate_dry_run: bool, | ||
| } | ||
| - | // ---- Application state -------------------------------------------------- | |
| - | ||
| pub struct AppState { | ||
| pub db: SqlitePool, | ||
| pub tlog: TransparencyLog, | ||
| @@ -83,19 +75,15 @@ pub struct AppState { | ||
| pub rekor_url: String, | ||
| } | ||
| - | /// Registry's Ed25519 identity keypair (hex-encoded). | |
| pub struct RegistryIdentity { | ||
| pub ed25519_priv: String, | ||
| pub ed25519_pub: String, | ||
| } | ||
| - | // ---- Rate limiter (token bucket with LRU eviction) ---------------------- | |
| - | ||
| pub struct RateLimiter { | ||
| rate: f64, | ||
| burst: f64, | ||
| max_keys: usize, | ||
| - | /// Map from client key -> (tokens, last_time) | |
| state: Mutex<HashMap<String, (f64, Instant)>>, | ||
| } | ||
| @@ -129,9 +117,7 @@ impl RateLimiter { | ||
| } | ||
| fn evict_if_needed(&self, state: &mut HashMap<String, (f64, Instant)>) { | ||
| - | // Simple eviction: if over capacity, remove oldest entries. | |
| while state.len() > self.max_keys { | ||
| - | // Find the oldest entry | |
| if let Some(oldest_key) = state | ||
| .iter() | ||
| .min_by_key(|(_, (_, t))| *t) | ||
| @@ -145,14 +131,10 @@ impl RateLimiter { | ||
| } | ||
| } | ||
| - | // ---- Helpers ------------------------------------------------------------ | |
| - | ||
| - | /// ISO 8601 UTC timestamp. | |
| pub fn timestamp_stub() -> String { | ||
| chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string() | ||
| } | ||
| - | /// Extract the client key for rate limiting. | |
| fn client_key(headers: &HeaderMap, addr: Option<&SocketAddr>, trusted_proxy: bool) -> String { | ||
| if trusted_proxy { | ||
| if let Some(xff) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()) { | ||
| @@ -175,7 +157,6 @@ fn same_file_path(a: &std::path::Path, b: &std::path::Path) -> bool { | ||
| } | ||
| } | ||
| - | /// Load or create the registry Ed25519 identity keypair. | |
| fn load_or_create_identity(data_dir: &PathBuf) -> Option<RegistryIdentity> { | ||
| let identity_path = data_dir.join("registry-identity.json"); | ||
| @@ -198,7 +179,6 @@ fn load_or_create_identity(data_dir: &PathBuf) -> Option<RegistryIdentity> { | ||
| } | ||
| } | ||
| - | // Generate new identity | |
| use ed25519_dalek::SigningKey; | ||
| use rand_core::OsRng; | ||
| @@ -217,8 +197,6 @@ fn load_or_create_identity(data_dir: &PathBuf) -> Option<RegistryIdentity> { | ||
| .as_secs(), | ||
| }); | ||
| - | // Write with restrictive permissions. On Unix we'd use mode 0o600; | |
| - | // on Windows we write normally (ACLs handle access control). | |
| #[cfg(unix)] | ||
| { | ||
| use std::os::unix::fs::OpenOptionsExt; | ||
| @@ -301,8 +279,6 @@ fn cors_layer() -> CorsLayer { | ||
| .max_age(std::time::Duration::from_secs(3600)) | ||
| } | ||
| - | // ---- Rate-limit middleware ---------------------------------------------- | |
| - | ||
| async fn rate_limit_middleware( | ||
| State(state): State<Arc<AppState>>, | ||
| ConnectInfo(addr): ConnectInfo<SocketAddr>, | ||
| @@ -317,11 +293,8 @@ async fn rate_limit_middleware( | ||
| Ok(next.run(req).await) | ||
| } | ||
| - | // ---- Server entry point ------------------------------------------------- | |
| - | ||
| #[tokio::main] | ||
| async fn main() -> anyhow::Result<()> { | ||
| - | // Initialize tracing (structured logging). | |
| tracing_subscriber::fmt() | ||
| .with_env_filter( | ||
| tracing_subscriber::EnvFilter::try_from_default_env() | ||
| @@ -331,7 +304,6 @@ async fn main() -> anyhow::Result<()> { | ||
| let args = Args::parse(); | ||
| - | // Resolve config from env vars (higher priority) or CLI args. | |
| let host = std::env::var("OVERSIGHT_HOST").unwrap_or(args.host); | ||
| let port: u16 = std::env::var("OVERSIGHT_PORT") | ||
| .ok() | ||
| @@ -386,10 +358,8 @@ async fn main() -> anyhow::Result<()> { | ||
| let rekor_url = std::env::var("OVERSIGHT_REKOR_URL") | ||
| .unwrap_or_else(|_| oversight_rekor::DEFAULT_REKOR_URL.to_string()); | ||
| - | // Ensure data directory exists. | |
| fs::create_dir_all(&data_dir)?; | ||
| - | // Initialize database. | |
| tracing::info!(path = %db_path.display(), "opening database"); | ||
| let pool = db::create_pool(&db_path).await?; | ||
| db::run_migrations(&pool).await?; | ||
| @@ -408,7 +378,6 @@ async fn main() -> anyhow::Result<()> { | ||
| return Ok(()); | ||
| } | ||
| - | // Initialize transparency log. | |
| let tlog_dir = data_dir.join("tlog"); | ||
| let identity = load_or_create_identity(&data_dir); | ||
| let tlog = TransparencyLog::open_with_signer( | ||
| @@ -436,7 +405,6 @@ async fn main() -> anyhow::Result<()> { | ||
| rekor_url, | ||
| }); | ||
| - | // Build router. | |
| let app = Router::new() | ||
| .route("/health", get(routes::health::health)) | ||
| .route( | ||
| @@ -474,7 +442,6 @@ async fn main() -> anyhow::Result<()> { | ||
| .layer(TraceLayer::new_for_http()) | ||
| .with_state(state); | ||
| - | // Bind and serve. | |
| let addr: SocketAddr = format!("{host}:{port}") | ||
| .parse() | ||
| .map_err(|e| anyhow::anyhow!("invalid bind address: {e}"))?; | ||
| @@ -493,7 +460,6 @@ async fn main() -> anyhow::Result<()> { | ||
| Ok(()) | ||
| } | ||
| - | /// Wait for SIGINT / SIGTERM (Unix) or Ctrl+C (all platforms). | |
| async fn shutdown_signal() { | ||
| let ctrl_c = async { | ||
| tokio::signal::ctrl_c() |
| @@ -1,22 +1,11 @@ | ||
| - | //! Request/response types and database row types for the registry. | |
| - | ||
| use serde::{Deserialize, Serialize}; | ||
| - | // ---- Input size limits (reject oversized payloads) ---------------------- | |
| - | ||
| - | /// Maximum length of a file_id, token_id, mark_id, or similar identifier. | |
| pub const MAX_ID_LEN: usize = 256; | ||
| - | /// Maximum length of a canonical manifest JSON blob. | |
| pub const MAX_MANIFEST_JSON_LEN: usize = 256 * 1024; // 256 KiB | ||
| - | /// Maximum number of beacons in a single registration. | |
| pub const MAX_BEACONS: usize = 500; | ||
| - | /// Maximum number of watermarks in a single registration. | |
| pub const MAX_WATERMARKS: usize = 500; | ||
| - | /// Maximum number of corpus hash entries in a single registration. | |
| pub const MAX_CORPUS_ENTRIES: usize = 64; | ||
| - | // ---- Request types ------------------------------------------------------ | |
| - | ||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| pub struct RegistrationRequest { | ||
| pub manifest: serde_json::Value, | ||
| @@ -48,8 +37,6 @@ pub struct QueryParams { | ||
| pub file_id: String, | ||
| } | ||
| - | // ---- Response types ----------------------------------------------------- | |
| - | ||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| pub struct RegistrationResponse { | ||
| pub ok: bool, | ||
| @@ -104,8 +91,6 @@ pub struct QueryResponse { | ||
| pub beacons: Option<Vec<BeaconRow>>, | ||
| } | ||
| - | // ---- DB row types ------------------------------------------------------- | |
| - | ||
| #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] | ||
| pub struct BeaconRow { | ||
| pub token_id: String, |
| @@ -1,5 +1,3 @@ | ||
| - | //! POST /attribute - attribution lookup by token_id, mark_id, or perceptual_hash. | |
| - | ||
| use axum::extract::State; | ||
| use axum::http::HeaderMap; | ||
| use axum::Json; | ||
| @@ -23,7 +21,6 @@ pub async fn attribute( | ||
| "operator", | ||
| )?; | ||
| - | // Validate input sizes | |
| if let Some(ref id) = q.token_id { | ||
| if id.len() > MAX_ID_LEN { | ||
| return Err(RegistryError::BadRequest("token_id too long".into())); | ||
| @@ -45,7 +42,6 @@ pub async fn attribute( | ||
| } | ||
| } | ||
| - | // Determine lookup strategy (same priority as Python server) | |
| let (file_id, recipient_id, issuer_id) = if let Some(ref token_id) = q.token_id { | ||
| match db::get_beacon(&state.db, token_id).await? { | ||
| Some(row) => (row.file_id, row.recipient_id, row.issuer_id), | ||
| @@ -99,13 +95,11 @@ pub async fn attribute( | ||
| )); | ||
| }; | ||
| - | // Fetch manifest | |
| let manifest = match db::get_manifest(&state.db, &file_id).await? { | ||
| Some(row) => serde_json::from_str(&row.manifest_json).ok(), | ||
| None => None, | ||
| }; | ||
| - | // Fetch recent events | |
| let events = db::get_recent_events(&state.db, &file_id, 50).await?; | ||
| let event_values: Vec<serde_json::Value> = events | ||
| .iter() |
| @@ -1,5 +1,3 @@ | ||
| - | //! Beacon callback endpoints: HTTP image, OCSP-style, and license checks. | |
| - | ||
| use axum::extract::{ConnectInfo, Path, State}; | ||
| use axum::http::{header, HeaderMap, StatusCode}; | ||
| use axum::response::{IntoResponse, Response}; |
| @@ -1,5 +1,3 @@ | ||
| - | //! POST /dns_event - authenticated beacon callback logging from the DNS server. | |
| - | ||
| use axum::extract::{ConnectInfo, State}; | ||
| use axum::http::HeaderMap; | ||
| use axum::Json; | ||
| @@ -21,7 +19,6 @@ pub async fn dns_event( | ||
| ) -> Result<Json<DnsEventResponse>> { | ||
| verify_dns_event_auth(&state, &headers, &addr)?; | ||
| - | // Validate input sizes | |
| if evt.token_id.is_empty() || evt.token_id.len() > MAX_ID_LEN { | ||
| return Err(RegistryError::BadRequest("invalid token_id".into())); | ||
| } | ||
| @@ -35,13 +32,11 @@ pub async fn dns_event( | ||
| return Err(RegistryError::BadRequest("dns event field too long".into())); | ||
| } | ||
| - | // Look up beacon ownership | |
| let beacon = db::get_beacon(&state.db, &evt.token_id).await?; | ||
| let file_id = beacon.as_ref().map(|b| b.file_id.as_str()); | ||
| let recipient_id = beacon.as_ref().map(|b| b.recipient_id.as_str()); | ||
| let issuer_id = beacon.as_ref().map(|b| b.issuer_id.as_str()); | ||
| - | // Append to transparency log | |
| let timestamp_str = crate::timestamp_stub(); | ||
| let tlog_event = serde_json::json!({ | ||
| "event": "beacon", | ||
| @@ -60,7 +55,6 @@ pub async fn dns_event( | ||
| .map(|idx| idx as i64) | ||
| .unwrap_or(-1); | ||
| - | // Record event in DB | |
| let now = SystemTime::now() | ||
| .duration_since(UNIX_EPOCH) | ||
| .unwrap_or_default() |
| @@ -1,5 +1,3 @@ | ||
| - | //! GET /evidence/{file_id} - signed provenance bundle for a registered file. | |
| - | ||
| use axum::extract::{Path, State}; | ||
| use axum::Json; | ||
| use ed25519_dalek::{Signer, SigningKey}; |
| @@ -1,5 +1,3 @@ | ||
| - | //! GET /health - liveness/readiness probe. | |
| - | ||
| use axum::extract::State; | ||
| use axum::Json; | ||
| use std::sync::Arc; |
| @@ -1,5 +1,3 @@ | ||
| - | //! Route modules for the Oversight registry. | |
| - | ||
| pub mod attribute; | ||
| pub mod beacon; | ||
| pub mod dns_event; |
| @@ -1,5 +1,3 @@ | ||
| - | //! GET /query/{file_id} - watermark/beacon ownership lookup by file_id. | |
| - | ||
| use axum::extract::{Path, State}; | ||
| use axum::Json; | ||
| use std::sync::Arc; | ||
| @@ -17,7 +15,6 @@ pub async fn query_file( | ||
| return Err(RegistryError::BadRequest("file_id too long".into())); | ||
| } | ||
| - | // Check manifest exists | |
| let manifest_row = db::get_manifest(&state.db, &file_id).await?; | ||
| if manifest_row.is_none() { | ||
| return Ok(Json(QueryResponse { | ||
| @@ -31,7 +28,6 @@ pub async fn query_file( | ||
| } | ||
| let manifest_row = manifest_row.unwrap(); | ||
| - | // Fetch watermarks and beacons for this file | |
| let watermarks = db::get_watermarks_by_file(&state.db, &file_id).await?; | ||
| let beacons = db::get_beacons_by_file(&state.db, &file_id).await?; | ||
| @@ -1,11 +1,3 @@ | ||
| - | //! POST /register - store manifest, beacons, watermarks with signature verification. | |
| - | //! | |
| - | //! Security invariants: | |
| - | //! 1. The manifest's Ed25519 signature MUST verify. | |
| - | //! 2. Request sidecars must exactly match the signed manifest's copies. | |
| - | //! 3. Re-registration of a file_id requires the same issuer pubkey. | |
| - | //! 4. All inputs are size-validated before processing. | |
| - | ||
| use axum::extract::State; | ||
| use axum::http::HeaderMap; | ||
| use axum::Json; | ||
| @@ -30,7 +22,6 @@ pub async fn register( | ||
| "operator", | ||
| )?; | ||
| - | // ---- Input validation ---- | |
| let manifest = &req.manifest; | ||
| let file_id = manifest | ||
| @@ -55,7 +46,6 @@ pub async fn register( | ||
| ))); | ||
| } | ||
| - | // Validate manifest JSON size | |
| let manifest_json = serde_json::to_string(manifest) | ||
| .map_err(|e| RegistryError::BadRequest(format!("manifest serialization: {e}")))?; | ||
| if manifest_json.len() > MAX_MANIFEST_JSON_LEN { | ||
| @@ -76,7 +66,6 @@ pub async fn register( | ||
| return Err(RegistryError::BadRequest("identifier too long".into())); | ||
| } | ||
| - | // ---- Signature verification ---- | |
| let (sig_ok, issuer_pub) = verify_manifest_signature(manifest); | ||
| if !sig_ok { | ||
| return Err(RegistryError::BadRequest( | ||
| @@ -89,12 +78,10 @@ pub async fn register( | ||
| )); | ||
| } | ||
| - | // ---- v0.4.4 hardening: validate sidecars match signed manifest ---- | |
| let (signed_beacons, signed_watermarks) = | ||
| validate_signed_artifacts(manifest, &req.beacons, &req.watermarks) | ||
| .map_err(RegistryError::BadRequest)?; | ||
| - | // ---- Check existing issuer ---- | |
| let existing_pub = db::get_manifest_issuer_pub(&state.db, file_id).await?; | ||
| if let Some(ref existing) = existing_pub { | ||
| if existing != &issuer_pub { | ||
| @@ -106,7 +93,6 @@ pub async fn register( | ||
| } | ||
| } | ||
| - | // ---- Persist ---- | |
| let now = SystemTime::now() | ||
| .duration_since(UNIX_EPOCH) | ||
| .unwrap_or_default() | ||
| @@ -185,7 +171,6 @@ pub async fn register( | ||
| .await?; | ||
| } | ||
| - | // Corpus hashes (optional) | |
| if let Some(ref corpus) = req.corpus { | ||
| if corpus.len() > MAX_CORPUS_ENTRIES { | ||
| return Err(RegistryError::BadRequest(format!( | ||
| @@ -202,7 +187,6 @@ pub async fn register( | ||
| } | ||
| } | ||
| - | // ---- Transparency log ---- | |
| let timestamp_str = crate::timestamp_stub(); | ||
| let tlog_event = serde_json::json!({ | ||
| "event": "register", | ||
| @@ -220,7 +204,6 @@ pub async fn register( | ||
| .map(|idx| idx as i64) | ||
| .unwrap_or(-1); | ||
| - | // ---- Optional Rekor attestation ---- | |
| let rekor_result = if state.rekor_enabled { | ||
| attest_to_rekor( | ||
| &state, | ||
| @@ -251,8 +234,6 @@ pub async fn register( | ||
| })) | ||
| } | ||
| - | /// Sign a registration predicate and submit to a Rekor v2 transparency log. | |
| - | /// Non-fatal: returns None on any error so the registry remains usable. | |
| fn attest_to_rekor( | ||
| state: &AppState, | ||
| file_id: &str, | ||
| @@ -325,13 +306,11 @@ fn attest_to_rekor( | ||
| match oversight_rekor::sign_dsse(&statement, &priv_bytes, "") { | ||
| Ok(envelope) => { | ||
| let pub_bytes = hex::decode(&identity.ed25519_pub).ok()?; | ||
| - | // Rekor v2 expects DER-encoded SubjectPublicKeyInfo. For Ed25519 this is | |
| - | // a fixed 12-byte prefix + 32-byte raw key. | |
| - | let der_prefix: [u8; 12] = [ | |
| + | let ed25519_spki_der_prefix: [u8; 12] = [ | |
| 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, | ||
| ]; | ||
| let mut der = Vec::with_capacity(44); | ||
| - | der.extend_from_slice(&der_prefix); | |
| + | der.extend_from_slice(&ed25519_spki_der_prefix); | |
| der.extend_from_slice(&pub_bytes); | ||
| match oversight_rekor::upload::upload_dsse(&envelope, &der, &state.rekor_url) { |
| @@ -1,5 +1,3 @@ | ||
| - | //! GET /candidates/semantic - recent L3 semantic mark IDs for scrapers. | |
| - | ||
| use axum::extract::{Query, State}; | ||
| use axum::Json; | ||
| use serde::Deserialize; |
| @@ -1,5 +1,3 @@ | ||
| - | //! Transparency-log read endpoints for federated verifiers. | |
| - | ||
| use axum::extract::{Path, Query, State}; | ||
| use axum::Json; | ||
| use serde::Deserialize; |
| @@ -1,5 +1,3 @@ | ||
| - | //! GET /.well-known/oversight-registry - registry identity advertisement. | |
| - | ||
| use axum::extract::State; | ||
| use axum::Json; | ||
| use std::sync::Arc; |