| @@ -24,9 +24,15 @@ | ||
| copies the Python reference registry's manifests, beacons, watermarks, | ||
| events, and corpus rows into the Rust SQLite schema while preserving event | ||
| IDs, corpus metadata, and registry evidence relationships. | ||
| + | - **Rust policy test parity.** Fixed the `oversight-policy` crate's manifest | |
| + | fixture after the v0.4.11 `Recipient.p256_pub` schema addition so the full | |
| + | Rust workspace test suite compiles again. | |
| - **Deployment docs.** Added `docs/REGISTRY_DEPLOYMENT.md` covering the live | ||
| Compose/Caddy flow, route map, token headers, DNS bridge secret, and local | ||
| versus live conformance commands. | ||
| + | - **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`. | |
| ## v0.4.11 - 2026-05-08 Hardware-keys completion: Python parity, browser support, end-to-end seal | ||
| @@ -2,7 +2,7 @@ | ||
| **Open protocol for cryptographic data provenance, recipient attribution, and leak detection.** | ||
| - | Co-authored by Zion Boggan and Claude Opus 4.6/4.7 (Anthropic) and Codex ChatGPT-5-4 (OpenAI). | |
| + | Co-authored by Zion Boggan, Claude Opus 4.6/4.7 (Anthropic), and Codex (GPT-5.4, OpenAI). | |
| Format-agnostic. Post-quantum ready (ML-KEM-768 + ML-DSA-65). Layered watermarking with honest limits: L1/L2 are lightweight steganographic signals, L3 is opt-in semantic marking for prose, and content fingerprinting helps identify leaked copies even when fragile marks are destroyed. | ||
| @@ -80,6 +80,20 @@ oversight-registry --db rust-registry.sqlite \ | ||
| Remove `--migrate-dry-run` to copy manifests, beacons, watermarks, events, and | ||
| corpus rows into the Rust database. | ||
| + | ## Current main after v0.4.11 | |
| + | ||
| + | `main` is past the v0.4.11 hardware-key tag. The live registry deployment | |
| + | path now includes the Compose/Caddy `live` profile, `.env.example` operator | |
| + | secrets, and shared write-side token enforcement across the Python FastAPI | |
| + | and Rust Axum registries. The Rust registry also has Python-to-Rust SQLite | |
| + | migration tooling (`--migrate-from`, `--migrate-dry-run`) so operators can | |
| + | preflight and copy attribution rows without treating the Python reference as | |
| + | a permanent production dependency. | |
| + | ||
| + | The next Rust-registry gate is operational burn-in: longer-running deployment | |
| + | tests, migration validation against real operator databases, and a final | |
| + | wire-format stability declaration before v1.0. | |
| + | ||
| ## Quick start | ||
| ```bash | ||
| @@ -403,18 +417,19 @@ project does not backport fixes below the current stable line. | ||
| | Layer | Checks | Status | | ||
| |---|---|---| | ||
| - | | Python test_e2e | 11 | green | | |
| - | | Python test_e2e_v2 | 13 | green | | |
| - | | Python test_pq | 7 | green | | |
| - | | Rust oversight-crypto | 7 | green | | |
| - | | Rust oversight-manifest | 2 | green | | |
| - | | Rust oversight-container | 8 | green | | |
| - | | Rust oversight-watermark | 4 | green | | |
| - | | Rust oversight-tlog | 7 | green | | |
| - | | Rust oversight-policy | 6 | green | | |
| + | | Python pytest suite | 10 | green | | |
| + | | Rust oversight-container | 17 | green | | |
| + | | Rust oversight-crypto | 21 | green | | |
| + | | Rust oversight-formats | 35 | green | | |
| + | | Rust oversight-manifest | 3 | green | | |
| + | | Rust oversight-policy | 7 | green | | |
| + | | Rust oversight-registry | 8 | green | | |
| + | | Rust oversight-rekor | 10 | green | | |
| | Rust oversight-semantic | 8 | green | | ||
| + | | Rust oversight-tlog | 7 | green | | |
| + | | Rust oversight-watermark | 4 | green | | |
| | Cross-language conformance | 3 | green | | ||
| - | | Total | 76 | all green | | |
| + | | Total automated Rust unit tests | 120 | all green | | |
| ## Design principles (what Oversight never does) | ||
| @@ -432,7 +447,11 @@ project does not backport fixes below the current stable line. | ||
| Public-log interoperability is now via Rekor DSSE attestations; the local log remains | ||
| a lightweight registry integrity mechanism, not a drop-in replacement for Rekor. | ||
| - **No independent security audit yet.** Planned for 2027. Until then: user-beware, cryptographer-review welcome. Open an issue. | ||
| - | - **Rust port is core-only.** ~1,500 LOC ported. The remaining ~5,500 LOC (semantic dictionary, format adapters, registry server, integrations) is multi-release scope. Python is still the canonical reference. | |
| + | - **Rust port is broad but not final.** The Rust workspace now covers the | |
| + | cryptographic core, manifests, containers, policy checks, watermark | |
| + | detection, semantic helpers, format adapters, and the Axum/SQLx registry. | |
| + | Python remains the canonical reference until the Rust registry finishes | |
| + | deployment burn-in, migration validation, and v1.0 wire-format freeze. | |
| ## License | ||
| @@ -59,20 +59,20 @@ for new contributors. Git plus tag is the supported pin. | ||
| ```toml | ||
| [dependencies] | ||
| - | oversight-crypto = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" } | |
| - | oversight-manifest = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" } | |
| - | oversight-container = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" } | |
| - | oversight-tlog = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" } | |
| - | oversight-rekor = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" } | |
| - | oversight-watermark = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" } | |
| - | oversight-policy = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" } | |
| + | oversight-crypto = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.11" } | |
| + | oversight-manifest = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.11" } | |
| + | oversight-container = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.11" } | |
| + | oversight-tlog = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.11" } | |
| + | oversight-rekor = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.11" } | |
| + | oversight-watermark = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.11" } | |
| + | oversight-policy = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.11" } | |
| ``` | ||
| Cargo resolves all seven entries against the same git checkout, so the | ||
| fetch happens once and every crate is byte-identical to what the desktop | ||
| CLI shipped against. `Cargo.lock` records the resolved commit | ||
| - | (`af6f725c` for `v0.4.8`); a downstream consumer who commits their lock | |
| - | file will get reproducible resolution across machines. | |
| + | (`14547d9` for `v0.4.11`); a downstream consumer who commits their lock file | |
| + | will get reproducible resolution across machines. | |
| For a consumer that prefers a commit-sha pin over a tag pin, the same | ||
| pattern works with `rev` instead of `tag`. Tag is the recommended default |
| @@ -217,14 +217,14 @@ wrap/unwrap, `WrappedDekP256` envelope, and `SoftwareP256KeyProvider` | ||
| (in-memory P-256 reference impl) are in `oversight-crypto`. Cross-suite | ||
| envelopes are rejected explicitly. 21/21 tests in the crate pass. | ||
| - | The remaining work is the `PivKeyProvider` (PKCS#11 against a YubiKey / | |
| - | Nitrokey / OnlyKey PIV slot) - a different `KeyProvider` impl that calls | |
| - | into `cryptoki` instead of holding the scalar in process - plus the | |
| - | manifest / container plumbing that lets `OSGT-HW-P256-v1` ride the | |
| - | existing seal pipeline. The | |
| - | registry records whether each recipient pubkey is file-backed or | |
| - | hardware-backed so issuers can require hardware backing for sensitive | |
| - | material. | |
| + | **v0.4.11 closed the software reference path across Rust, Python, and the | |
| + | browser inspector.** `OSGT-HW-P256-v1` now has manifest/container plumbing, | |
| + | Python wrap/unwrap parity, and a public viewer sample fixture. The remaining | |
| + | hardware work is the `PivKeyProvider` (PKCS#11 against a YubiKey / Nitrokey / | |
| + | OnlyKey PIV slot), a different `KeyProvider` implementation that calls into | |
| + | `cryptoki` instead of holding the scalar in process. The registry records | |
| + | whether each recipient pubkey is file-backed or hardware-backed so issuers can | |
| + | require hardware backing for sensitive material. | |
| ### Registry in Rust | ||
| @@ -310,8 +310,8 @@ via VM and retype, hardware-key pull mid-open. | ||
| | 8 | Browser inspector, classic-suite decrypt, opsec scanner + CI | Shipped | | ||
| | 9 | Hybrid PQ decrypt in browser | Shipped (2026-05-03) | | ||
| | 10 | Outlook add-in | Next | | ||
| - | | 11 | Hardware KeyProvider in Rust | In progress | | |
| - | | 12 | Rust Axum registry, migration tooling | In progress | | |
| + | | 11 | Hardware KeyProvider in Rust | Suite shipped (v0.4.11); PIV provider next | | |
| + | | 12 | Rust Axum registry, migration tooling | Migration tooling shipped; deployment burn-in next | | |
| | 13 | arXiv preprint, threat-model repo document | Mid-term | | ||
| | 14 | IETF Internet-Draft, CFRG or equivalent BoF | Mid-term | | ||
| | 15 | USENIX Security Cycle 2, Black Hat EU 2026 | Mid-term | |
| @@ -214,7 +214,12 @@ fn run() -> Result<(), Box<dyn std::error::Error>> { | ||
| &recipient_pub_bytes, | ||
| )?; | ||
| std::fs::write(&output, &blob)?; | ||
| - | println!("✓ sealed {} -> {} ({} bytes)", input.display(), output.display(), blob.len()); | |
| + | println!( | |
| + | "✓ sealed {} -> {} ({} bytes)", | |
| + | input.display(), | |
| + | output.display(), | |
| + | blob.len() | |
| + | ); | |
| println!(" file_id: {}", manifest.file_id); | ||
| } | ||
| @@ -227,8 +232,12 @@ fn run() -> Result<(), Box<dyn std::error::Error>> { | ||
| let recipient_id = load_identity(&recipient)?; | ||
| let blob = std::fs::read(&input)?; | ||
| let policy_ctx = PolicyContext::local_only(policy_state_dir)?; | ||
| - | let (plaintext, manifest) = | |
| - | open_sealed(&blob, recipient_id.x25519_priv.as_ref(), None, Some(&policy_ctx))?; | |
| + | let (plaintext, manifest) = open_sealed( | |
| + | &blob, | |
| + | recipient_id.x25519_priv.as_ref(), | |
| + | None, | |
| + | Some(&policy_ctx), | |
| + | )?; | |
| if output.as_os_str() == "-" { | ||
| use std::io::Write; | ||
| std::io::stdout().write_all(&plaintext)?; | ||
| @@ -251,7 +260,10 @@ fn run() -> Result<(), Box<dyn std::error::Error>> { | ||
| println!(" suite_id: {}", sf.suite_id); | ||
| println!(" ciphertext_len: {} bytes", sf.ciphertext.len()); | ||
| println!(" aead_nonce: {}", hex::encode(sf.aead_nonce)); | ||
| - | println!(" signature valid: {}", sf.manifest.verify().unwrap_or(false)); | |
| + | println!( | |
| + | " signature valid: {}", | |
| + | sf.manifest.verify().unwrap_or(false) | |
| + | ); | |
| } | ||
| Commands::Watermark { | ||
| @@ -273,7 +285,8 @@ fn run() -> Result<(), Box<dyn std::error::Error>> { | ||
| } | ||
| }; | ||
| - | let marked = adapter.embed_watermark(&data, &mark_bytes) | |
| + | let marked = adapter | |
| + | .embed_watermark(&data, &mark_bytes) | |
| .map_err(|e| format!("embed failed: {}", e))?; | ||
| std::fs::write(&output, &marked)?; | ||
| println!( | ||
| @@ -291,7 +304,8 @@ fn run() -> Result<(), Box<dyn std::error::Error>> { | ||
| let registry = FormatRegistry::default(); | ||
| let adapter = resolve_adapter(®istry, &data, format.as_deref(), &input)?; | ||
| - | let candidates = adapter.extract_watermark(&data) | |
| + | let candidates = adapter | |
| + | .extract_watermark(&data) | |
| .map_err(|e| format!("extract failed: {}", e))?; | ||
| println!( | ||
| @@ -350,9 +364,14 @@ fn resolve_adapter<'a>( | ||
| path: &PathBuf, | ||
| ) -> Result<&'a dyn FormatAdapter, Box<dyn std::error::Error>> { | ||
| if let Some(name) = format_override { | ||
| - | return registry | |
| - | .by_name(name) | |
| - | .ok_or_else(|| format!("unknown format: '{}'. available: {:?}", name, registry.adapter_names()).into()); | |
| + | return registry.by_name(name).ok_or_else(|| { | |
| + | format!( | |
| + | "unknown format: '{}'. available: {:?}", | |
| + | name, | |
| + | registry.adapter_names() | |
| + | ) | |
| + | .into() | |
| + | }); | |
| } | ||
| // Try content-based detection first |
| @@ -104,7 +104,12 @@ pub struct SealedFile { | ||
| pub suite_id: u8, | ||
| } | ||
| - | fn read_exact<'a>(buf: &'a [u8], at: &mut usize, n: usize, field: &'static str) -> Result<&'a [u8], ContainerError> { | |
| + | fn read_exact<'a>( | |
| + | buf: &'a [u8], | |
| + | at: &mut usize, | |
| + | n: usize, | |
| + | field: &'static str, | |
| + | ) -> Result<&'a [u8], ContainerError> { | |
| if buf.len() < *at + n { | ||
| return Err(ContainerError::Truncated { | ||
| wanted: n, | ||
| @@ -148,7 +153,9 @@ impl SealedFile { | ||
| let mut at = 0usize; | ||
| let magic = read_exact(data, &mut at, 6, "magic")?; | ||
| if magic != MAGIC { | ||
| - | return Err(ContainerError::BadMagic { got: magic.to_vec() }); | |
| + | return Err(ContainerError::BadMagic { | |
| + | got: magic.to_vec(), | |
| + | }); | |
| } | ||
| let hdr = read_exact(data, &mut at, 2, "version/suite")?; | ||
| let fmt_ver = hdr[0]; | ||
| @@ -244,10 +251,14 @@ pub fn seal( | ||
| )); | ||
| } | ||
| if recipient_x25519_pub.len() != 32 { | ||
| - | return Err(ContainerError::Precondition("recipient pubkey must be 32 bytes")); | |
| + | return Err(ContainerError::Precondition( | |
| + | "recipient pubkey must be 32 bytes", | |
| + | )); | |
| } | ||
| if issuer_ed25519_priv.len() != 32 { | ||
| - | return Err(ContainerError::Precondition("issuer priv key must be 32 bytes")); | |
| + | return Err(ContainerError::Precondition( | |
| + | "issuer priv key must be 32 bytes", | |
| + | )); | |
| } | ||
| manifest.sign(issuer_ed25519_priv)?; | ||
| @@ -275,7 +286,9 @@ pub fn open_sealed( | ||
| policy_ctx: Option<&PolicyContext>, | ||
| ) -> Result<(Vec<u8>, Manifest), ContainerError> { | ||
| if recipient_x25519_priv.len() != 32 { | ||
| - | return Err(ContainerError::Precondition("recipient priv key must be 32 bytes")); | |
| + | return Err(ContainerError::Precondition( | |
| + | "recipient priv key must be 32 bytes", | |
| + | )); | |
| } | ||
| let sf = SealedFile::from_bytes(blob)?; | ||
| @@ -299,9 +312,16 @@ pub fn open_sealed( | ||
| return Err(ContainerError::Precondition("file expired (not_after)")); | ||
| } | ||
| } | ||
| - | if let Some(nb) = sf.manifest.policy.get("not_before").and_then(|v| v.as_i64()) { | |
| + | if let Some(nb) = sf | |
| + | .manifest | |
| + | .policy | |
| + | .get("not_before") | |
| + | .and_then(|v| v.as_i64()) | |
| + | { | |
| if now < nb { | ||
| - | return Err(ContainerError::Precondition("file not yet released (not_before)")); | |
| + | return Err(ContainerError::Precondition( | |
| + | "file not yet released (not_before)", | |
| + | )); | |
| } | ||
| } | ||
| @@ -384,7 +404,9 @@ pub fn seal_hw_p256( | ||
| )); | ||
| } | ||
| if issuer_ed25519_priv.len() != 32 { | ||
| - | return Err(ContainerError::Precondition("issuer priv key must be 32 bytes")); | |
| + | return Err(ContainerError::Precondition( | |
| + | "issuer priv key must be 32 bytes", | |
| + | )); | |
| } | ||
| manifest.sign(issuer_ed25519_priv)?; | ||
| @@ -443,9 +465,16 @@ pub fn open_sealed_with_provider( | ||
| return Err(ContainerError::Precondition("file expired (not_after)")); | ||
| } | ||
| } | ||
| - | if let Some(nb) = sf.manifest.policy.get("not_before").and_then(|v| v.as_i64()) { | |
| + | if let Some(nb) = sf | |
| + | .manifest | |
| + | .policy | |
| + | .get("not_before") | |
| + | .and_then(|v| v.as_i64()) | |
| + | { | |
| if now < nb { | ||
| - | return Err(ContainerError::Precondition("file not yet released (not_before)")); | |
| + | return Err(ContainerError::Precondition( | |
| + | "file not yet released (not_before)", | |
| + | )); | |
| } | ||
| } | ||
| @@ -493,7 +522,12 @@ pub fn seal_multi( | ||
| issuer_ed25519_priv: &[u8], | ||
| recipient_x25519_pubs: &[&[u8]], | ||
| ) -> Result<Vec<u8>, ContainerError> { | ||
| - | let _ = (plaintext, manifest, issuer_ed25519_priv, recipient_x25519_pubs); | |
| + | let _ = ( | |
| + | plaintext, | |
| + | manifest, | |
| + | issuer_ed25519_priv, | |
| + | recipient_x25519_pubs, | |
| + | ); | |
| Err(ContainerError::Precondition( | ||
| "seal_multi disabled until manifests can bind all recipients", | ||
| )) | ||
| @@ -502,10 +536,16 @@ pub fn seal_multi( | ||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| - | use oversight_crypto::{ClassicIdentity, FileKeyProvider, SoftwareP256Identity, SoftwareP256KeyProvider}; | |
| + | use oversight_crypto::{ | |
| + | ClassicIdentity, FileKeyProvider, SoftwareP256Identity, SoftwareP256KeyProvider, | |
| + | }; | |
| use oversight_manifest::Recipient; | ||
| - | fn make_manifest(issuer: &ClassicIdentity, recipient: &ClassicIdentity, plaintext: &[u8]) -> Manifest { | |
| + | fn make_manifest( | |
| + | issuer: &ClassicIdentity, | |
| + | recipient: &ClassicIdentity, | |
| + | plaintext: &[u8], | |
| + | ) -> Manifest { | |
| Manifest::new( | ||
| "doc.txt", | ||
| crypto::content_hash(plaintext), | ||
| @@ -559,8 +599,15 @@ mod tests { | ||
| let recipient = ClassicIdentity::generate(); | ||
| let plaintext = b"This is my secret document."; | ||
| let mut m = make_manifest(&issuer, &recipient, plaintext); | ||
| - | let blob = seal(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &recipient.x25519_pub).unwrap(); | |
| - | let (pt, manifest) = open_sealed(&blob, recipient.x25519_priv.as_ref(), None, None).unwrap(); | |
| + | let blob = seal( | |
| + | plaintext, | |
| + | &mut m, | |
| + | issuer.ed25519_priv.as_ref(), | |
| + | &recipient.x25519_pub, | |
| + | ) | |
| + | .unwrap(); | |
| + | let (pt, manifest) = | |
| + | open_sealed(&blob, recipient.x25519_priv.as_ref(), None, None).unwrap(); | |
| assert_eq!(pt, plaintext); | ||
| assert_eq!(manifest.file_id, m.file_id); | ||
| } | ||
| @@ -572,7 +619,13 @@ mod tests { | ||
| let bob = ClassicIdentity::generate(); | ||
| let plaintext = b"secret"; | ||
| let mut m = make_manifest(&issuer, &alice, plaintext); | ||
| - | let blob = seal(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &alice.x25519_pub).unwrap(); | |
| + | let blob = seal( | |
| + | plaintext, | |
| + | &mut m, | |
| + | issuer.ed25519_priv.as_ref(), | |
| + | &alice.x25519_pub, | |
| + | ) | |
| + | .unwrap(); | |
| // Bob tries to open - should fail at AEAD stage | ||
| assert!(open_sealed(&blob, bob.x25519_priv.as_ref(), None, None).is_err()); | ||
| } | ||
| @@ -583,7 +636,13 @@ mod tests { | ||
| let alice = ClassicIdentity::generate(); | ||
| let plaintext = b"secret"; | ||
| let mut m = make_manifest(&issuer, &alice, plaintext); | ||
| - | let mut blob = seal(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &alice.x25519_pub).unwrap(); | |
| + | let mut blob = seal( | |
| + | plaintext, | |
| + | &mut m, | |
| + | issuer.ed25519_priv.as_ref(), | |
| + | &alice.x25519_pub, | |
| + | ) | |
| + | .unwrap(); | |
| let len = blob.len(); | ||
| blob[len - 1] ^= 0x01; | ||
| assert!(open_sealed(&blob, alice.x25519_priv.as_ref(), None, None).is_err()); | ||
| @@ -606,7 +665,9 @@ mod tests { | ||
| blob.extend_from_slice(&(5u32 * 1024 * 1024).to_be_bytes()); | ||
| blob.resize(100, 0); | ||
| match SealedFile::from_bytes(&blob) { | ||
| - | Err(ContainerError::Oversized { field: "manifest", .. }) => (), | |
| + | Err(ContainerError::Oversized { | |
| + | field: "manifest", .. | |
| + | }) => (), | |
| other => panic!("expected Oversized manifest error, got {:?}", other), | ||
| } | ||
| } | ||
| @@ -628,7 +689,13 @@ mod tests { | ||
| let recipient = ClassicIdentity::generate(); | ||
| let plaintext = b"classic via provider path"; | ||
| let mut m = make_manifest(&issuer, &recipient, plaintext); | ||
| - | let blob = seal(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &recipient.x25519_pub).unwrap(); | |
| + | let blob = seal( | |
| + | plaintext, | |
| + | &mut m, | |
| + | issuer.ed25519_priv.as_ref(), | |
| + | &recipient.x25519_pub, | |
| + | ) | |
| + | .unwrap(); | |
| let provider = FileKeyProvider::new(recipient); | ||
| let (pt, manifest) = open_sealed_with_provider(&blob, &provider, None, None).unwrap(); | ||
| @@ -670,7 +737,8 @@ mod tests { | ||
| let plaintext = b"for alice only"; | ||
| let mut m = make_hw_manifest(&issuer, &alice_pub, plaintext); | ||
| - | let blob = seal_hw_p256(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &alice_pub).unwrap(); | |
| + | let blob = | |
| + | seal_hw_p256(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &alice_pub).unwrap(); | |
| let bob_provider = SoftwareP256KeyProvider::new(bob); | ||
| assert!( | ||
| @@ -688,11 +756,15 @@ mod tests { | ||
| let plaintext = b"hw envelope"; | ||
| let mut m = make_hw_manifest(&issuer, &alice_pub, plaintext); | ||
| - | let blob = seal_hw_p256(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &alice_pub).unwrap(); | |
| + | let blob = | |
| + | seal_hw_p256(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &alice_pub).unwrap(); | |
| let wrong_alg = FileKeyProvider::new(ClassicIdentity::generate()); | ||
| let res = open_sealed_with_provider(&blob, &wrong_alg, None, None); | ||
| - | assert!(res.is_err(), "X25519 provider must not be accepted for an OSGT-HW-P256-v1 container"); | |
| + | assert!( | |
| + | res.is_err(), | |
| + | "X25519 provider must not be accepted for an OSGT-HW-P256-v1 container" | |
| + | ); | |
| } | ||
| #[test] | ||
| @@ -707,7 +779,10 @@ mod tests { | ||
| let mut m = make_hw_manifest(&issuer, &alice_pub, plaintext); | ||
| m.suite = crypto::SUITE_CLASSIC_V1.to_string(); | ||
| let res = seal_hw_p256(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &alice_pub); | ||
| - | assert!(res.is_err(), "seal_hw_p256 must require manifest.suite == OSGT-HW-P256-v1"); | |
| + | assert!( | |
| + | res.is_err(), | |
| + | "seal_hw_p256 must require manifest.suite == OSGT-HW-P256-v1" | |
| + | ); | |
| } | ||
| #[test] | ||
| @@ -715,9 +790,18 @@ mod tests { | ||
| // Each manifest suite must map to a unique container header byte; | ||
| // adding a new suite without updating this match would otherwise | ||
| // silently shape-shift into an UnsupportedManifestSuite at seal time. | ||
| - | assert_eq!(suite_id_for_manifest(crypto::SUITE_CLASSIC_V1), Some(SUITE_CLASSIC_V1_ID)); | |
| - | assert_eq!(suite_id_for_manifest(crypto::SUITE_HYBRID_V1), Some(SUITE_HYBRID_V1_ID)); | |
| - | assert_eq!(suite_id_for_manifest(crypto::SUITE_HW_P256_V1), Some(SUITE_HW_P256_V1_ID)); | |
| + | assert_eq!( | |
| + | suite_id_for_manifest(crypto::SUITE_CLASSIC_V1), | |
| + | Some(SUITE_CLASSIC_V1_ID) | |
| + | ); | |
| + | assert_eq!( | |
| + | suite_id_for_manifest(crypto::SUITE_HYBRID_V1), | |
| + | Some(SUITE_HYBRID_V1_ID) | |
| + | ); | |
| + | assert_eq!( | |
| + | suite_id_for_manifest(crypto::SUITE_HW_P256_V1), | |
| + | Some(SUITE_HW_P256_V1_ID) | |
| + | ); | |
| assert_eq!(suite_id_for_manifest("OSGT-UNKNOWN"), None); | ||
| } | ||
| @@ -727,10 +811,19 @@ mod tests { | ||
| let alice = ClassicIdentity::generate(); | ||
| let plaintext = b"secret"; | ||
| let mut m = make_manifest(&issuer, &alice, plaintext); | ||
| - | let mut blob = seal(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &alice.x25519_pub).unwrap(); | |
| + | let mut blob = seal( | |
| + | plaintext, | |
| + | &mut m, | |
| + | issuer.ed25519_priv.as_ref(), | |
| + | &alice.x25519_pub, | |
| + | ) | |
| + | .unwrap(); | |
| blob[7] ^= 0x01; | ||
| match SealedFile::from_bytes(&blob) { | ||
| - | Err(ContainerError::SuiteMismatch { header, manifest_suite }) => { | |
| + | Err(ContainerError::SuiteMismatch { | |
| + | header, | |
| + | manifest_suite, | |
| + | }) => { | |
| assert_eq!(header, 0); | ||
| assert_eq!(manifest_suite, crypto::SUITE_CLASSIC_V1); | ||
| } | ||
| @@ -744,7 +837,13 @@ mod tests { | ||
| let alice = ClassicIdentity::generate(); | ||
| let plaintext = b"secret"; | ||
| let mut m = make_manifest(&issuer, &alice, plaintext); | ||
| - | let mut blob = seal(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &alice.x25519_pub).unwrap(); | |
| + | let mut blob = seal( | |
| + | plaintext, | |
| + | &mut m, | |
| + | issuer.ed25519_priv.as_ref(), | |
| + | &alice.x25519_pub, | |
| + | ) | |
| + | .unwrap(); | |
| blob.extend_from_slice(b"junk"); | ||
| match SealedFile::from_bytes(&blob) { | ||
| Err(ContainerError::TrailingBytes(4)) => (), | ||
| @@ -758,8 +857,14 @@ mod tests { | ||
| let alice = ClassicIdentity::generate(); | ||
| let plaintext = b"secret"; | ||
| let mut m = make_manifest(&issuer, &alice, plaintext); | ||
| - | m.policy["not_after"] = serde_json::json!(1000); // long ago | |
| - | let blob = seal(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &alice.x25519_pub).unwrap(); | |
| + | m.policy["not_after"] = serde_json::json!(1000); // long ago | |
| + | let blob = seal( | |
| + | plaintext, | |
| + | &mut m, | |
| + | issuer.ed25519_priv.as_ref(), | |
| + | &alice.x25519_pub, | |
| + | ) | |
| + | .unwrap(); | |
| match open_sealed(&blob, alice.x25519_priv.as_ref(), None, None) { | ||
| Err(ContainerError::Precondition("file expired (not_after)")) => (), | ||
| other => panic!("expected expiry error, got {:?}", other.is_ok()), | ||
| @@ -774,12 +879,16 @@ mod tests { | ||
| let plaintext = b"limited"; | ||
| let mut m = make_manifest(&issuer, &alice, plaintext); | ||
| m.policy["max_opens"] = serde_json::json!(1); | ||
| - | let blob = seal(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &alice.x25519_pub).unwrap(); | |
| + | let blob = seal( | |
| + | plaintext, | |
| + | &mut m, | |
| + | issuer.ed25519_priv.as_ref(), | |
| + | &alice.x25519_pub, | |
| + | ) | |
| + | .unwrap(); | |
| - | let dir = std::env::temp_dir().join(format!( | |
| - | "oversight-container-policy-{}", | |
| - | std::process::id() | |
| - | )); | |
| + | let dir = | |
| + | std::env::temp_dir().join(format!("oversight-container-policy-{}", std::process::id())); | |
| let _ = std::fs::remove_dir_all(&dir); | ||
| let ctx = oversight_policy::PolicyContext::local_only(&dir).unwrap(); | ||
| @@ -818,7 +927,10 @@ mod tests { | ||
| let recipients: Vec<&[u8]> = vec![&alice.x25519_pub, &bob.x25519_pub, &carol.x25519_pub]; | ||
| match seal_multi(plaintext, &mut m, issuer.ed25519_priv.as_ref(), &recipients) { | ||
| Err(ContainerError::Precondition(msg)) => assert!(msg.contains("seal_multi disabled")), | ||
| - | other => panic!("expected seal_multi to fail closed, got {:?}", other.is_ok()), | |
| + | other => panic!( | |
| + | "expected seal_multi to fail closed, got {:?}", | |
| + | other.is_ok() | |
| + | ), | |
| } | ||
| } | ||
| } |
| @@ -33,16 +33,15 @@ use ed25519_dalek::{ | ||
| VerifyingKey as EdVerifyingKey, | ||
| }; | ||
| use hkdf::Hkdf; | ||
| + | use p256::{ | |
| + | ecdh::diffie_hellman as p256_diffie_hellman, elliptic_curve::sec1::ToEncodedPoint, | |
| + | PublicKey as P256PublicKey, SecretKey as P256SecretKey, | |
| + | }; | |
| use rand_core::{OsRng, RngCore}; | ||
| use sha2::{Digest, Sha256}; | ||
| use thiserror::Error; | ||
| use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519StaticSecret}; | ||
| use zeroize::{Zeroize, Zeroizing}; | ||
| - | use p256::{ | |
| - | ecdh::diffie_hellman as p256_diffie_hellman, | |
| - | elliptic_curve::sec1::ToEncodedPoint, | |
| - | PublicKey as P256PublicKey, SecretKey as P256SecretKey, | |
| - | }; | |
| pub const XCHACHA_KEY_LEN: usize = 32; | ||
| pub const XCHACHA_NONCE_LEN: usize = 24; | ||
| @@ -147,7 +146,13 @@ pub fn aead_encrypt( | ||
| let cipher = XChaCha20Poly1305::new(key.into()); | ||
| let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); | ||
| let ct = cipher | ||
| - | .encrypt(&nonce, Payload { msg: plaintext, aad }) | |
| + | .encrypt( | |
| + | &nonce, | |
| + | Payload { | |
| + | msg: plaintext, | |
| + | aad, | |
| + | }, | |
| + | ) | |
| .map_err(|_| CryptoError::AeadFailed)?; | ||
| let mut nonce_arr = [0u8; XCHACHA_NONCE_LEN]; | ||
| nonce_arr.copy_from_slice(&nonce); | ||
| @@ -174,7 +179,13 @@ pub fn aead_decrypt( | ||
| } | ||
| let cipher = XChaCha20Poly1305::new(key.into()); | ||
| cipher | ||
| - | .decrypt(nonce.into(), Payload { msg: ciphertext, aad }) | |
| + | .decrypt( | |
| + | nonce.into(), | |
| + | Payload { | |
| + | msg: ciphertext, | |
| + | aad, | |
| + | }, | |
| + | ) | |
| .map_err(|_| CryptoError::AeadFailed) | ||
| } | ||
| @@ -225,7 +236,11 @@ impl WrappedDek { | ||
| eph.copy_from_slice(&eph_bytes); | ||
| let mut nonce = [0u8; XCHACHA_NONCE_LEN]; | ||
| nonce.copy_from_slice(&nonce_bytes); | ||
| - | Ok(WrappedDek { ephemeral_pub: eph, nonce, wrapped_dek: wrapped }) | |
| + | Ok(WrappedDek { | |
| + | ephemeral_pub: eph, | |
| + | nonce, | |
| + | wrapped_dek: wrapped, | |
| + | }) | |
| } | ||
| } | ||
| @@ -256,7 +271,11 @@ pub fn wrap_dek_for_recipient( | ||
| .map_err(|_| CryptoError::Hkdf)?; | ||
| let (nonce, wrapped) = aead_encrypt(kek.as_ref(), dek, b"oversight-dek")?; | ||
| - | Ok(WrappedDek { ephemeral_pub: eph_pub.to_bytes(), nonce, wrapped_dek: wrapped }) | |
| + | Ok(WrappedDek { | |
| + | ephemeral_pub: eph_pub.to_bytes(), | |
| + | nonce, | |
| + | wrapped_dek: wrapped, | |
| + | }) | |
| } | ||
| pub fn unwrap_dek( | ||
| @@ -359,12 +378,18 @@ pub struct FileKeyProvider { | ||
| impl FileKeyProvider { | ||
| /// Wrap an existing [`ClassicIdentity`] without a label. | ||
| pub fn new(identity: ClassicIdentity) -> Self { | ||
| - | Self { inner: identity, label: None } | |
| + | Self { | |
| + | inner: identity, | |
| + | label: None, | |
| + | } | |
| } | ||
| /// Wrap with a label (e.g., the recipient_id from the identity JSON). | ||
| pub fn with_label(identity: ClassicIdentity, label: impl Into<String>) -> Self { | ||
| - | Self { inner: identity, label: Some(label.into()) } | |
| + | Self { | |
| + | inner: identity, | |
| + | label: Some(label.into()), | |
| + | } | |
| } | ||
| /// Borrow the underlying classic identity. Hardware providers won't be | ||
| @@ -464,7 +489,10 @@ impl SoftwareP256Identity { | ||
| debug_assert_eq!(bytes.len(), P256_PUBLIC_KEY_LEN); | ||
| let mut public_sec1 = [0u8; P256_PUBLIC_KEY_LEN]; | ||
| public_sec1.copy_from_slice(bytes); | ||
| - | Self { secret, public_sec1 } | |
| + | Self { | |
| + | secret, | |
| + | public_sec1, | |
| + | } | |
| } | ||
| pub fn public_key_sec1(&self) -> &[u8; P256_PUBLIC_KEY_LEN] { | ||
| @@ -518,7 +546,11 @@ impl WrappedDekP256 { | ||
| eph.copy_from_slice(&eph_bytes); | ||
| let mut nonce = [0u8; XCHACHA_NONCE_LEN]; | ||
| nonce.copy_from_slice(&nonce_bytes); | ||
| - | Ok(WrappedDekP256 { ephemeral_pub: eph, nonce, wrapped_dek: wrapped }) | |
| + | Ok(WrappedDekP256 { | |
| + | ephemeral_pub: eph, | |
| + | nonce, | |
| + | wrapped_dek: wrapped, | |
| + | }) | |
| } | ||
| } | ||
| @@ -535,11 +567,12 @@ pub fn wrap_dek_for_recipient_p256( | ||
| got: recipient_p256_pub_sec1.len(), | ||
| }); | ||
| } | ||
| - | let recipient_pub = P256PublicKey::from_sec1_bytes(recipient_p256_pub_sec1) | |
| - | .map_err(|_| CryptoError::InvalidKeyLength { | |
| + | let recipient_pub = P256PublicKey::from_sec1_bytes(recipient_p256_pub_sec1).map_err(|_| { | |
| + | CryptoError::InvalidKeyLength { | |
| expected: P256_PUBLIC_KEY_LEN, | ||
| got: recipient_p256_pub_sec1.len(), | ||
| - | })?; | |
| + | } | |
| + | })?; | |
| let eph_secret = P256SecretKey::random(&mut OsRng); | ||
| let eph_pub = eph_secret.public_key(); | ||
| @@ -563,7 +596,11 @@ pub fn wrap_dek_for_recipient_p256( | ||
| .map_err(|_| CryptoError::Hkdf)?; | ||
| let (nonce, wrapped) = aead_encrypt(kek.as_ref(), dek, b"oversight-hw-p256-dek")?; | ||
| - | Ok(WrappedDekP256 { ephemeral_pub: eph_pub_arr, nonce, wrapped_dek: wrapped }) | |
| + | Ok(WrappedDekP256 { | |
| + | ephemeral_pub: eph_pub_arr, | |
| + | nonce, | |
| + | wrapped_dek: wrapped, | |
| + | }) | |
| } | ||
| /// Software-backed P-256 [`KeyProvider`]. Useful for tests and as a fallback | ||
| @@ -576,11 +613,17 @@ pub struct SoftwareP256KeyProvider { | ||
| impl SoftwareP256KeyProvider { | ||
| pub fn new(identity: SoftwareP256Identity) -> Self { | ||
| - | Self { inner: identity, label: None } | |
| + | Self { | |
| + | inner: identity, | |
| + | label: None, | |
| + | } | |
| } | ||
| pub fn with_label(identity: SoftwareP256Identity, label: impl Into<String>) -> Self { | ||
| - | Self { inner: identity, label: Some(label.into()) } | |
| + | Self { | |
| + | inner: identity, | |
| + | label: Some(label.into()), | |
| + | } | |
| } | ||
| pub fn identity(&self) -> &SoftwareP256Identity { | ||
| @@ -792,7 +835,9 @@ mod tests { | ||
| // Raw x25519_dalek for comparison. | ||
| let bob_sk = X25519StaticSecret::from(bob_priv_copy); | ||
| - | let raw = bob_sk.diffie_hellman(&X25519PublicKey::from(alice_pub_copy)).to_bytes(); | |
| + | let raw = bob_sk | |
| + | .diffie_hellman(&X25519PublicKey::from(alice_pub_copy)) | |
| + | .to_bytes(); | |
| assert_eq!(via_provider.as_slice(), &raw[..]); | ||
| } | ||
| @@ -843,7 +888,10 @@ mod tests { | ||
| // Bob's provider should fail to recover the DEK. | ||
| let res = unwrap_dek_with_provider(&wrapped, &provider_bob); | ||
| - | assert!(res.is_err(), "Bob's provider must not unwrap a DEK addressed to Alice"); | |
| + | assert!( | |
| + | res.is_err(), | |
| + | "Bob's provider must not unwrap a DEK addressed to Alice" | |
| + | ); | |
| } | ||
| // ------- P-256 (OSGT-HW-P256-v1) ---------------------------------------- | ||
| @@ -888,7 +936,10 @@ mod tests { | ||
| let wrapped = wrap_dek_for_recipient_p256(dek.as_ref(), &alice_pub).unwrap(); | ||
| let res = unwrap_dek_with_provider_p256(&wrapped, &provider_bob); | ||
| - | assert!(res.is_err(), "Bob's P-256 provider must not unwrap a DEK addressed to Alice"); | |
| + | assert!( | |
| + | res.is_err(), | |
| + | "Bob's P-256 provider must not unwrap a DEK addressed to Alice" | |
| + | ); | |
| } | ||
| #[test] | ||
| @@ -903,7 +954,10 @@ mod tests { | ||
| let bob_x25519 = FileKeyProvider::new(ClassicIdentity::generate()); | ||
| let res = unwrap_dek_with_provider_p256(&wrapped, &bob_x25519); | ||
| - | assert!(res.is_err(), "X25519 provider must not be accepted for a P-256 envelope"); | |
| + | assert!( | |
| + | res.is_err(), | |
| + | "X25519 provider must not be accepted for a P-256 envelope" | |
| + | ); | |
| } | ||
| #[test] |
| @@ -134,8 +134,8 @@ pub fn embed_docx_metadata( | ||
| .map_err(|e| FormatError::Internal(format!("ZIP entry error: {}", e)))?; | ||
| let name = entry.name().to_string(); | ||
| - | let options = SimpleFileOptions::default() | |
| - | .compression_method(zip::CompressionMethod::Deflated); | |
| + | let options = | |
| + | SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated); | |
| writer | ||
| .start_file(&name, options) | ||
| .map_err(|e| FormatError::Internal(format!("ZIP write error: {}", e)))?; | ||
| @@ -146,22 +146,20 @@ pub fn embed_docx_metadata( | ||
| std::io::Read::read_to_end(&mut entry, &mut contents) | ||
| .map_err(|e| FormatError::Io(e))?; | ||
| let modified = inject_keywords_into_core_xml(&contents, &tag)?; | ||
| - | std::io::Write::write_all(&mut writer, &modified) | |
| - | .map_err(|e| FormatError::Io(e))?; | |
| + | std::io::Write::write_all(&mut writer, &modified).map_err(|e| FormatError::Io(e))?; | |
| } else { | ||
| // Copy entry unchanged | ||
| let mut contents = Vec::new(); | ||
| std::io::Read::read_to_end(&mut entry, &mut contents) | ||
| .map_err(|e| FormatError::Io(e))?; | ||
| - | std::io::Write::write_all(&mut writer, &contents) | |
| - | .map_err(|e| FormatError::Io(e))?; | |
| + | std::io::Write::write_all(&mut writer, &contents).map_err(|e| FormatError::Io(e))?; | |
| } | ||
| } | ||
| // If there was no docProps/core.xml, create one | ||
| if !found_core { | ||
| - | let options = SimpleFileOptions::default() | |
| - | .compression_method(zip::CompressionMethod::Deflated); | |
| + | let options = | |
| + | SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated); | |
| writer | ||
| .start_file("docProps/core.xml", options) | ||
| .map_err(|e| FormatError::Internal(format!("ZIP write error: {}", e)))?; | ||
| @@ -192,8 +190,7 @@ pub fn extract_docx_metadata(docx_bytes: &[u8]) -> Result<DocxOversightMeta, For | ||
| // Try to read docProps/core.xml | ||
| if let Ok(mut entry) = archive.by_name("docProps/core.xml") { | ||
| let mut contents = Vec::new(); | ||
| - | std::io::Read::read_to_end(&mut entry, &mut contents) | |
| - | .map_err(|e| FormatError::Io(e))?; | |
| + | std::io::Read::read_to_end(&mut entry, &mut contents).map_err(|e| FormatError::Io(e))?; | |
| let keywords = extract_keywords_from_core_xml(&contents)?; | ||
| if let Some(kw) = keywords { | ||
| parse_oversight_tag(&kw, &mut meta); | ||
| @@ -217,8 +214,7 @@ pub fn extract_body_text(docx_bytes: &[u8]) -> Result<String, FormatError> { | ||
| .map_err(|e| FormatError::Malformed(format!("missing word/document.xml: {}", e)))?; | ||
| let mut contents = Vec::new(); | ||
| - | std::io::Read::read_to_end(&mut entry, &mut contents) | |
| - | .map_err(|e| FormatError::Io(e))?; | |
| + | std::io::Read::read_to_end(&mut entry, &mut contents).map_err(|e| FormatError::Io(e))?; | |
| extract_text_elements(&contents) | ||
| } | ||
| @@ -324,10 +320,7 @@ fn extract_keywords_from_core_xml(xml_bytes: &[u8]) -> Result<Option<String>, Fo | ||
| in_keywords = true; | ||
| } | ||
| Ok(Event::Text(ref t)) if in_keywords => { | ||
| - | let text = t | |
| - | .unescape() | |
| - | .unwrap_or_default() | |
| - | .to_string(); | |
| + | let text = t.unescape().unwrap_or_default().to_string(); | |
| return Ok(Some(text)); | ||
| } | ||
| Ok(Event::End(ref e)) if e.name().as_ref() == b"cp:keywords" => { | ||
| @@ -366,10 +359,7 @@ fn extract_text_elements(xml_bytes: &[u8]) -> Result<String, FormatError> { | ||
| } | ||
| } | ||
| Ok(Event::Text(ref t)) if in_text => { | ||
| - | let text = t | |
| - | .unescape() | |
| - | .unwrap_or_default() | |
| - | .to_string(); | |
| + | let text = t.unescape().unwrap_or_default().to_string(); | |
| paragraph_texts.push(text); | ||
| } | ||
| Ok(Event::End(ref e)) => { | ||
| @@ -486,7 +476,10 @@ mod tests { | ||
| fn sanitize_field_code_strips_dangerous() { | ||
| assert_eq!(sanitize_field_code("normal text"), "normal text"); | ||
| assert_eq!(sanitize_field_code("{FIELD \\s}"), "FIELD s"); | ||
| - | assert_eq!(sanitize_field_code("<script>alert('x')</script>"), "scriptalert(x)/script"); | |
| + | assert_eq!( | |
| + | sanitize_field_code("<script>alert('x')</script>"), | |
| + | "scriptalert(x)/script" | |
| + | ); | |
| assert_eq!(sanitize_field_code("hello&world"), "helloworld"); | ||
| } | ||
| @@ -502,7 +495,10 @@ mod tests { | ||
| #[test] | ||
| fn parse_oversight_tag_with_other_keywords() { | ||
| let mut meta = DocxOversightMeta::default(); | ||
| - | parse_oversight_tag("finance report oversight:cafebabe;issuer:alice quarterly", &mut meta); | |
| + | parse_oversight_tag( | |
| + | "finance report oversight:cafebabe;issuer:alice quarterly", | |
| + | &mut meta, | |
| + | ); | |
| assert_eq!(meta.mark_id.as_deref(), Some("cafebabe")); | ||
| assert_eq!(meta.issuer_id.as_deref(), Some("alice")); | ||
| } |
| @@ -292,12 +292,10 @@ fn sanitize_pdf_string(s: &str) -> String { | ||
| /// Helper to get a string value from a PDF dictionary. | ||
| fn get_string_from_dict(dict: &Dictionary, key: &str) -> Option<String> { | ||
| - | dict.get(key.as_bytes()) | |
| - | .ok() | |
| - | .and_then(|obj| match obj { | |
| - | Object::String(bytes, _) => String::from_utf8(bytes.clone()).ok(), | |
| - | _ => None, | |
| - | }) | |
| + | dict.get(key.as_bytes()).ok().and_then(|obj| match obj { | |
| + | Object::String(bytes, _) => String::from_utf8(bytes.clone()).ok(), | |
| + | _ => None, | |
| + | }) | |
| } | ||
| /// Simplified text extraction from a PDF content stream. |
| @@ -112,19 +112,13 @@ fn sanitize_file_id(file_id: &str) -> Result<()> { | ||
| fn counter_path(ctx: &PolicyContext, file_id: &str) -> Result<PathBuf> { | ||
| sanitize_file_id(file_id)?; | ||
| - | let dir = ctx | |
| - | .state_dir | |
| - | .as_ref() | |
| - | .ok_or(PolicyError::ContextRequired)?; | |
| + | let dir = ctx.state_dir.as_ref().ok_or(PolicyError::ContextRequired)?; | |
| Ok(dir.join(format!("{}.opens.json", file_id))) | ||
| } | ||
| fn lock_path(ctx: &PolicyContext, file_id: &str) -> Result<PathBuf> { | ||
| sanitize_file_id(file_id)?; | ||
| - | let dir = ctx | |
| - | .state_dir | |
| - | .as_ref() | |
| - | .ok_or(PolicyError::ContextRequired)?; | |
| + | let dir = ctx.state_dir.as_ref().ok_or(PolicyError::ContextRequired)?; | |
| Ok(dir.join(format!("{}.opens.lock", file_id))) | ||
| } | ||
| @@ -255,7 +249,8 @@ pub fn record_open(manifest: &Manifest, ctx: Option<&PolicyContext>) -> Result<u | ||
| "registry max_opens enforcement is not implemented; refusing local fallback".into(), | ||
| )), | ||
| Mode::Hybrid => Err(PolicyError::Violation( | ||
| - | "hybrid max_opens enforcement is not implemented; refusing silent local fallback".into(), | |
| + | "hybrid max_opens enforcement is not implemented; refusing silent local fallback" | |
| + | .into(), | |
| )), | ||
| } | ||
| } | ||
| @@ -281,6 +276,7 @@ mod tests { | ||
| recipient_id: "alice".into(), | ||
| x25519_pub: "00".repeat(32), | ||
| ed25519_pub: None, | ||
| + | p256_pub: None, | |
| }, | ||
| "https://registry", | ||
| "text/plain", | ||
| @@ -317,7 +313,9 @@ mod tests { | ||
| "jurisdiction": "EU", | ||
| })); | ||
| let dir = TempDir::new().unwrap(); | ||
| - | let ctx = PolicyContext::local_only(dir.path()).unwrap().with_jurisdiction("US"); | |
| + | let ctx = PolicyContext::local_only(dir.path()) | |
| + | .unwrap() | |
| + | .with_jurisdiction("US"); | |
| assert!(check_policy(&m, Some(&ctx)).is_err()); | ||
| } | ||
| @@ -339,7 +337,7 @@ mod tests { | ||
| })); | ||
| assert_eq!(record_open(&m, Some(&ctx)).unwrap(), 1); | ||
| assert_eq!(record_open(&m, Some(&ctx)).unwrap(), 2); | ||
| - | assert!(record_open(&m, Some(&ctx)).is_err()); // 3rd exceeds | |
| + | assert!(record_open(&m, Some(&ctx)).is_err()); // 3rd exceeds | |
| } | ||
| #[test] |
| @@ -27,11 +27,17 @@ fn main() { | ||
| } | ||
| match args[1].as_str() { | ||
| "pae" => { | ||
| - | let payload_type = args.get(2).cloned().unwrap_or_else(|| DSSE_PAYLOAD_TYPE.into()); | |
| + | let payload_type = args | |
| + | .get(2) | |
| + | .cloned() | |
| + | .unwrap_or_else(|| DSSE_PAYLOAD_TYPE.into()); | |
| let payload = read_stdin(); | ||
| let out = pae(&payload_type, &payload); | ||
| let stdout = io::stdout(); | ||
| - | stdout.lock().write_all(hex::encode(out).as_bytes()).unwrap(); | |
| + | stdout | |
| + | .lock() | |
| + | .write_all(hex::encode(out).as_bytes()) | |
| + | .unwrap(); | |
| } | ||
| "verify" => { | ||
| let pub_hex = args.get(2).expect("pub hex required"); | ||
| @@ -50,8 +56,7 @@ fn main() { | ||
| let priv_hex = args.get(2).expect("priv hex required"); | ||
| let priv_bytes = hex::decode(priv_hex).expect("valid hex priv"); | ||
| let raw = read_stdin(); | ||
| - | let stmt: serde_json::Value = | |
| - | serde_json::from_slice(&raw).expect("parse statement"); | |
| + | let stmt: serde_json::Value = serde_json::from_slice(&raw).expect("parse statement"); | |
| let env = sign_dsse(&stmt, &priv_bytes, "").expect("sign"); | ||
| let canon = env.to_canonical_json().expect("canonicalize"); | ||
| print!("{}", canon); | ||
| @@ -59,15 +64,13 @@ fn main() { | ||
| "payload_b64" => { | ||
| // Read envelope from stdin, print the base64 payload. | ||
| let raw = read_stdin(); | ||
| - | let env: DsseEnvelope = | |
| - | serde_json::from_slice(&raw).expect("parse envelope"); | |
| + | let env: DsseEnvelope = serde_json::from_slice(&raw).expect("parse envelope"); | |
| print!("{}", env.payload_b64); | ||
| } | ||
| "decode_payload" => { | ||
| // Read envelope from stdin, print decoded payload (the canonical statement bytes). | ||
| let raw = read_stdin(); | ||
| - | let env: DsseEnvelope = | |
| - | serde_json::from_slice(&raw).expect("parse envelope"); | |
| + | let env: DsseEnvelope = serde_json::from_slice(&raw).expect("parse envelope"); | |
| let bytes = B64.decode(env.payload_b64.as_bytes()).expect("b64"); | ||
| io::stdout().lock().write_all(&bytes).unwrap(); | ||
| } |
| @@ -15,8 +15,7 @@ use std::collections::BTreeMap; | ||
| use base64::{engine::general_purpose::STANDARD as B64, Engine as _}; | ||
| use ed25519_dalek::{ | ||
| - | Signature, Signer, SigningKey, Verifier, VerifyingKey, SECRET_KEY_LENGTH, | |
| - | SIGNATURE_LENGTH, | |
| + | Signature, Signer, SigningKey, Verifier, VerifyingKey, SECRET_KEY_LENGTH, SIGNATURE_LENGTH, | |
| }; | ||
| use serde::{Deserialize, Serialize}; | ||
| use serde_json::Value; | ||
| @@ -98,7 +97,10 @@ impl OversightRegistrationPredicate { | ||
| "issuer_pubkey_ed25519".into(), | ||
| Value::from(self.issuer_pubkey_ed25519.clone()), | ||
| ); | ||
| - | m.insert("recipient_id".into(), Value::from(self.recipient_id.clone())); | |
| + | m.insert( | |
| + | "recipient_id".into(), | |
| + | Value::from(self.recipient_id.clone()), | |
| + | ); | |
| m.insert( | ||
| "recipient_pubkey_sha256".into(), | ||
| Value::from(self.recipient_pubkey_sha256.clone()), | ||
| @@ -131,8 +133,7 @@ impl OversightRegistrationPredicate { | ||
| /// Compute `recipient_pubkey_sha256` from the raw X25519 public key (hex). | ||
| pub fn hash_recipient_pubkey(x25519_pub_hex: &str) -> Result<String, RekorError> { | ||
| - | let raw = hex::decode(x25519_pub_hex) | |
| - | .map_err(|_| RekorError::KeyLength("x25519 pub hex"))?; | |
| + | let raw = hex::decode(x25519_pub_hex).map_err(|_| RekorError::KeyLength("x25519 pub hex"))?; | |
| let h = Sha256::digest(&raw); | ||
| Ok(hex::encode(h)) | ||
| } | ||
| @@ -324,7 +325,10 @@ pub fn verify_inclusion_offline( | ||
| return (false, "dsse payload missing subject digest"); | ||
| } | ||
| if subject_digest != Some(expected_content_hash_sha256_hex) { | ||
| - | return (false, "dsse subject digest does not match expected content hash"); | |
| + | return ( | |
| + | false, | |
| + | "dsse subject digest does not match expected content hash", | |
| + | ); | |
| } | ||
| let tle = match bundle_rekor_field.get("transparency_log_entry") { | ||
| Some(v) if v.is_object() => v, | ||
| @@ -334,7 +338,10 @@ pub fn verify_inclusion_offline( | ||
| .iter() | ||
| .any(|k| tle.get(*k).is_some()); | ||
| if !has_proof { | ||
| - | return (false, "transparency_log_entry has no inclusion proof or logEntry shape"); | |
| + | return ( | |
| + | false, | |
| + | "transparency_log_entry has no inclusion proof or logEntry shape", | |
| + | ); | |
| } | ||
| (true, "ok") | ||
| } | ||
| @@ -511,8 +518,7 @@ mod tests { | ||
| // Python: hashlib.sha256(bytes.fromhex("42"*32)).hexdigest() | ||
| let h = hash_recipient_pubkey(&"42".repeat(32)).unwrap(); | ||
| // Pre-computed reference value. | ||
| - | let expected = | |
| - | "bcdfe2c5b3b1c6c4f0d2b3f9c2c95dc6c0f9b1e6f6f9e60c7e75c5f37e80f1d4"; | |
| + | let expected = "bcdfe2c5b3b1c6c4f0d2b3f9c2c95dc6c0f9b1e6f6f9e60c7e75c5f37e80f1d4"; | |
| // We don't hard-code the exact digest here (would brittle-tie to a | ||
| // specific byte pattern); instead just check length + determinism. | ||
| assert_eq!(h.len(), 64); | ||
| @@ -561,7 +567,8 @@ mod tests { | ||
| let stmt = build_statement("a", &"b".repeat(64), &pred); | ||
| let env = sign_dsse(&stmt, &sk.to_bytes(), "").unwrap(); | ||
| let bundle_rekor = serde_json::json!({}); | ||
| - | let (ok, reason) = verify_inclusion_offline(&bundle_rekor, &env, pk.as_bytes(), &"b".repeat(64)); | |
| + | let (ok, reason) = | |
| + | verify_inclusion_offline(&bundle_rekor, &env, pk.as_bytes(), &"b".repeat(64)); | |
| assert!(!ok); | ||
| assert!(reason.contains("transparency_log_entry")); | ||
| } | ||
| @@ -589,7 +596,8 @@ mod tests { | ||
| let bundle_rekor = serde_json::json!({ | ||
| "transparency_log_entry": {"inclusionProof": {}} | ||
| }); | ||
| - | let (ok, reason) = verify_inclusion_offline(&bundle_rekor, &env, pk.as_bytes(), &"c".repeat(64)); | |
| + | let (ok, reason) = | |
| + | verify_inclusion_offline(&bundle_rekor, &env, pk.as_bytes(), &"c".repeat(64)); | |
| assert!(!ok); | ||
| assert!(reason.contains("subject digest")); | ||
| } |
| @@ -1,4 +1,4 @@ | ||
| - | use oversight_semantic::{embed_synonyms, verify_synonyms, iter_matchable_words}; | |
| + | use oversight_semantic::{embed_synonyms, iter_matchable_words, verify_synonyms}; | |
| const TEXT: &str = "Q3 revenue performance exceeded expectations across all business units. \ | ||
| The team plans to continue the expansion strategy outlined in our report at \ | ||
| @@ -15,14 +15,22 @@ fn main() { | ||
| let matches_before = iter_matchable_words(TEXT).len(); | ||
| let matches_after = iter_matchable_words(&marked).len(); | ||
| let (ok, score) = verify_synonyms(&marked, mark, 0.70); | ||
| - | println!("mark {}: matches before={} after={}, verify ok={} score={:.3}", | |
| - | name, matches_before, matches_after, ok, score); | |
| + | println!( | |
| + | "mark {}: matches before={} after={}, verify ok={} score={:.3}", | |
| + | name, matches_before, matches_after, ok, score | |
| + | ); | |
| // Print the first few matches before/after | ||
| - | let before: Vec<_> = iter_matchable_words(TEXT).iter().take(10) | |
| - | .map(|m| (m.orig_word.clone(), m.class_index, m.variant_index)).collect(); | |
| - | let after: Vec<_> = iter_matchable_words(&marked).iter().take(10) | |
| - | .map(|m| (m.orig_word.clone(), m.class_index, m.variant_index)).collect(); | |
| + | let before: Vec<_> = iter_matchable_words(TEXT) | |
| + | .iter() | |
| + | .take(10) | |
| + | .map(|m| (m.orig_word.clone(), m.class_index, m.variant_index)) | |
| + | .collect(); | |
| + | let after: Vec<_> = iter_matchable_words(&marked) | |
| + | .iter() | |
| + | .take(10) | |
| + | .map(|m| (m.orig_word.clone(), m.class_index, m.variant_index)) | |
| + | .collect(); | |
| println!(" first 10 before: {:?}", before); | ||
| println!(" first 10 after: {:?}", after); | ||
| } |
| @@ -73,8 +73,7 @@ static URL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://\S+").unwrap()); | ||
| static EMAIL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b[\w.+-]+@[\w.-]+\.\w+\b").unwrap()); | ||
| static INLINE_CODE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"`[^`]+`").unwrap()); | ||
| static CODE_BLOCK_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?s)```.*?```").unwrap()); | ||
| - | static UNIX_PATH_RE: Lazy<Regex> = | |
| - | Lazy::new(|| Regex::new(r"(?:^|\s)(?:/|~/|\./)[^\s]+").unwrap()); | |
| + | static UNIX_PATH_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?:^|\s)(?:/|~/|\./)[^\s]+").unwrap()); | |
| static HEX_BLOB_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b[A-Fa-f0-9]{16,}\b").unwrap()); | ||
| static BASE64_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b[A-Za-z0-9+/]{32,}={0,2}\b").unwrap()); | ||
| static WORD_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Za-z]+)\b").unwrap()); | ||
| @@ -83,8 +82,13 @@ static WORD_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Za-z]+)\b").unwrap | ||
| fn skip_mask(text: &str) -> Vec<bool> { | ||
| let mut mask = vec![false; text.len()]; | ||
| let patterns: &[&Lazy<Regex>] = &[ | ||
| - | &URL_RE, &EMAIL_RE, &INLINE_CODE_RE, &CODE_BLOCK_RE, &UNIX_PATH_RE, | |
| - | &HEX_BLOB_RE, &BASE64_RE, | |
| + | &URL_RE, | |
| + | &EMAIL_RE, | |
| + | &INLINE_CODE_RE, | |
| + | &CODE_BLOCK_RE, | |
| + | &UNIX_PATH_RE, | |
| + | &HEX_BLOB_RE, | |
| + | &BASE64_RE, | |
| ]; | ||
| for pat in patterns { | ||
| for m in pat.find_iter(text) { | ||
| @@ -179,8 +183,15 @@ fn case_preserve(replacement: &str, orig: &str) -> String { | ||
| if orig.chars().all(|c| c.is_uppercase() || !c.is_alphabetic()) && orig.len() > 1 { | ||
| return replacement.to_uppercase(); | ||
| } | ||
| - | let first_upper = orig.chars().next().map(|c| c.is_uppercase()).unwrap_or(false); | |
| - | let rest_lower = orig.chars().skip(1).all(|c| c.is_lowercase() || !c.is_alphabetic()); | |
| + | let first_upper = orig | |
| + | .chars() | |
| + | .next() | |
| + | .map(|c| c.is_uppercase()) | |
| + | .unwrap_or(false); | |
| + | let rest_lower = orig | |
| + | .chars() | |
| + | .skip(1) | |
| + | .all(|c| c.is_lowercase() || !c.is_alphabetic()); | |
| if first_upper && rest_lower { | ||
| let mut s = String::new(); | ||
| for (i, c) in replacement.chars().enumerate() { | ||
| @@ -281,7 +292,8 @@ pub fn verify_synonyms(text: &str, candidate_mark_id: &[u8], threshold: f64) -> | ||
| mod tests { | ||
| use super::*; | ||
| - | const TEST_TEXT: &str = "Q3 revenue performance exceeded expectations across all business units. \ | |
| + | const TEST_TEXT: &str = | |
| + | "Q3 revenue performance exceeded expectations across all business units. \ | |
| The team plans to continue the expansion strategy outlined in our report at \ | ||
| https://internal.example.com/q3-2026.pdf and will begin hiring in \ | ||
| /home/claude/hiring_plan.docx this month. However, there are important risks \ | ||
| @@ -312,10 +324,14 @@ Overall the results show clear momentum and a strong basis for continued growth. | ||
| fn url_and_path_preserved_through_embed() { | ||
| let mark = b"\x01\x23\x45\x67\x89\xab\xcd\xef"; | ||
| let marked = embed_synonyms(TEST_TEXT, mark, 5); | ||
| - | assert!(marked.contains("https://internal.example.com/q3-2026.pdf"), | |
| - | "URL was munged"); | |
| - | assert!(marked.contains("/home/claude/hiring_plan.docx"), | |
| - | "path was munged"); | |
| + | assert!( | |
| + | marked.contains("https://internal.example.com/q3-2026.pdf"), | |
| + | "URL was munged" | |
| + | ); | |
| + | assert!( | |
| + | marked.contains("/home/claude/hiring_plan.docx"), | |
| + | "path was munged" | |
| + | ); | |
| } | ||
| #[test] | ||
| @@ -334,7 +350,11 @@ Overall the results show clear momentum and a strong basis for continued growth. | ||
| let marked = embed_synonyms(TEST_TEXT, good, 5); | ||
| let (ok, score) = verify_synonyms(&marked, bad, 0.70); | ||
| assert!(!ok, "wrong mark verified (score={})", score); | ||
| - | assert!(score < 0.70, "wrong-mark score suspiciously high: {}", score); | |
| + | assert!( | |
| + | score < 0.70, | |
| + | "wrong-mark score suspiciously high: {}", | |
| + | score | |
| + | ); | |
| } | ||
| #[test] |
| @@ -116,14 +116,13 @@ pub fn verify_inclusion_proof( | ||
| return false; | ||
| } | ||
| - | fn rec( | |
| - | h_in: [u8; 32], | |
| - | m: usize, | |
| - | remaining: &[[u8; 32]], | |
| - | n: usize, | |
| - | ) -> Option<[u8; 32]> { | |
| + | fn rec(h_in: [u8; 32], m: usize, remaining: &[[u8; 32]], n: usize) -> Option<[u8; 32]> { | |
| if n == 1 { | ||
| - | return if remaining.is_empty() { Some(h_in) } else { None }; | |
| + | return if remaining.is_empty() { | |
| + | Some(h_in) | |
| + | } else { | |
| + | None | |
| + | }; | |
| } | ||
| if remaining.is_empty() { | ||
| return None; | ||
| @@ -278,9 +277,8 @@ impl TransparencyLog { | ||
| /// Append a JSON event. Helper that canonicalizes and calls append(). | ||
| pub fn append_event(&self, event: &serde_json::Value) -> Result<usize> { | ||
| - | let bytes = serde_jcs::to_vec(event).map_err(|_| { | |
| - | TlogError::Json(serde_json::Error::custom("canonicalization failed")) | |
| - | })?; | |
| + | let bytes = serde_jcs::to_vec(event) | |
| + | .map_err(|_| TlogError::Json(serde_json::Error::custom("canonicalization failed")))?; | |
| self.append(&bytes) | ||
| } | ||
| @@ -335,7 +333,7 @@ impl TransparencyLog { | ||
| let leaves_copy: Vec<[u8; 32]> = leaves.clone(); | ||
| let leaf_hash_hex = hex::encode(leaves[index]); | ||
| let tree_size = leaves.len(); | ||
| - | drop(leaves); // release before calling root() which also locks | |
| + | drop(leaves); // release before calling root() which also locks | |
| let path = audit_path(&leaves_copy, index); | ||
| let root = self.root(); |