diff --git a/Cargo.toml b/Cargo.toml index 4b32a343..0b85407f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -141,6 +141,57 @@ must_use_candidate = "warn" broken_intra_doc_links = "warn" private_intra_doc_links = "warn" +# Debug and test builds (which `cargo test` and `cargo llvm-cov` inherit) run the +# crypto dependencies optimized. Unoptimized Argon2id at the production 64 MiB / 3 +# passes takes seconds per derivation, and every password round-trip test pays it, +# several times over under coverage instrumentation. Only these third-party crates +# are optimized, so the workspace's own code keeps full debug assertions and its +# coverage counters; what each test checks is unchanged. +[profile.dev.package.argon2] +opt-level = 3 + +[profile.dev.package.blake2] +opt-level = 3 + +[profile.dev.package.aes] +opt-level = 3 + +[profile.dev.package."aes-gcm"] +opt-level = 3 + +[profile.dev.package.ghash] +opt-level = 3 + +[profile.dev.package.polyval] +opt-level = 3 + +[profile.dev.package.chacha20] +opt-level = 3 + +[profile.dev.package.chacha20poly1305] +opt-level = 3 + +[profile.dev.package.poly1305] +opt-level = 3 + +[profile.dev.package.sha2] +opt-level = 3 + +[profile.dev.package.hmac] +opt-level = 3 + +[profile.dev.package.hkdf] +opt-level = 3 + +[profile.dev.package."curve25519-dalek"] +opt-level = 3 + +[profile.dev.package."x25519-dalek"] +opt-level = 3 + +[profile.dev.package.zeroize] +opt-level = 3 + [profile.release] # Cross-crate optimization and smaller, faster binaries for release builds. lto = "thin" diff --git a/crates/tinysecurity-approval/src/engine.rs b/crates/tinysecurity-approval/src/engine.rs index 459c97a1..7f396618 100644 --- a/crates/tinysecurity-approval/src/engine.rs +++ b/crates/tinysecurity-approval/src/engine.rs @@ -824,7 +824,9 @@ impl Engine { if native && intents.len() >= 32 && !intents.contains_key(&row.binding.call_sha256) { return Err(Error::Unavailable); } - if !creations.contains_key(&row.binding.call_sha256) && creations.len() >= 1024 { + if !creations.contains_key(&row.binding.call_sha256) + && creations.len() >= MAX_PENDING_CREATIONS + { return Err(Error::Unavailable); } let row = creations @@ -2133,6 +2135,35 @@ fn reusable_grant<'a>( }) } +/// Rows a scope may hold before a new tool, flow or import grant is refused: +/// below the 1024 the module's load contract accepts, so ordinary approvals keep +/// a margin. Shared by the tool, flow-trust and import-trust grant paths. +pub(crate) const PRODUCTION_MAX_SCOPE_ROWS: usize = 960; + +/// Pending creations the retry memo holds before a new call identity is refused. +pub(crate) const PRODUCTION_MAX_PENDING_CREATIONS: usize = 1024; + +/// The memo cap the engine enforces; small in unit tests for the same reason as +/// `MAX_SCOPE_ROWS`. Pinned by `engine::tests::the_production_creation_memo_cap_is_1024`. +pub(crate) const MAX_PENDING_CREATIONS: usize = if cfg!(test) { + 16 +} else { + PRODUCTION_MAX_PENDING_CREATIONS +}; +const _: () = assert!(MAX_PENDING_CREATIONS <= PRODUCTION_MAX_PENDING_CREATIONS); + +/// The cap the engine enforces. Unit tests run against a small cap so filling a +/// scope to its limit takes a handful of grants rather than 960; the refusal +/// logic under test is the same comparison either way, and the production value +/// is pinned by `engine::tests::the_production_scope_row_cap_is_960`. +pub(crate) const MAX_SCOPE_ROWS: usize = if cfg!(test) { + 8 +} else { + PRODUCTION_MAX_SCOPE_ROWS +}; +const _: () = assert!(PRODUCTION_MAX_SCOPE_ROWS < 1024); +const _: () = assert!(MAX_SCOPE_ROWS <= PRODUCTION_MAX_SCOPE_ROWS); + #[path = "engine_flow_trust.rs"] mod flow_trust; #[path = "engine_import_trust.rs"] diff --git a/crates/tinysecurity-approval/src/engine_flow_trust.rs b/crates/tinysecurity-approval/src/engine_flow_trust.rs index 74ba8c97..ee0c5337 100644 --- a/crates/tinysecurity-approval/src/engine_flow_trust.rs +++ b/crates/tinysecurity-approval/src/engine_flow_trust.rs @@ -20,10 +20,6 @@ use tinysecurity_bus::{ const ATTRIBUTION: &str = "flow-trust-grant"; pub(super) const IMPORT_ATTRIBUTION: &str = "flow-trust-import"; -/// Rows a scope may hold before a new grant is refused, below the 1024 the module's -/// load contract accepts so ordinary approvals keep a margin. -const MAX_SCOPE_ROWS: usize = 960; - /// Digest of the exact `(flow, tool)` pair, domain-separated from every call digest. pub(super) fn trust_key(flow_id: &str, tool: &str) -> String { let mut bytes = Vec::with_capacity(flow_id.len() + tool.len() + 8); @@ -134,7 +130,7 @@ impl Engine { } // Revoked and expired grants still occupy durable rows. Reissuing them would // append another row and eventually exceed the module's load bound. - if rows.len() >= MAX_SCOPE_ROWS { + if rows.len() >= super::MAX_SCOPE_ROWS { return Err(Error::Unavailable); } let redactor = self.redactor()?; diff --git a/crates/tinysecurity-approval/src/engine_gaps_tests.rs b/crates/tinysecurity-approval/src/engine_gaps_tests.rs index b793c448..a1395803 100644 --- a/crates/tinysecurity-approval/src/engine_gaps_tests.rs +++ b/crates/tinysecurity-approval/src/engine_gaps_tests.rs @@ -1589,30 +1589,33 @@ async fn a_flow_identity_may_be_as_long_as_a_callers_but_a_tool_name_keeps_its_b Ok(()) } +/// The scope row cap the engine enforces in unit tests, as a grant counter. +const CAP: u64 = crate::engine::MAX_SCOPE_ROWS as u64; + #[tokio::test] async fn a_full_scope_takes_no_new_flow_trust_but_still_answers_for_a_live_one() -> Result<()> { let (_, owner) = owner()?; owner.register_scope(register(7)).await?; - for n in 0..960u64 { + for n in 0..CAP { owner .grant_flow_tool(grant(7, &format!("flow-{n}"), "shell", n + 1)) .await?; } assert_eq!( owner - .grant_flow_tool(grant(7, "one-more", "shell", 961)) + .grant_flow_tool(grant(7, "one-more", "shell", CAP + 1)) .await .err(), Some(Error::Unavailable) ); let again = owner - .grant_flow_tool(grant(7, "flow-0", "shell", 962)) + .grant_flow_tool(grant(7, "flow-0", "shell", CAP + 2)) .await?; assert_eq!(again.outcome, FlowTrustOutcome2::Unchanged); // The scope still loads. assert_eq!( owner.list_flow_trust(listing(7, None)).await?.entries.len(), - 960 + crate::engine::MAX_SCOPE_ROWS ); Ok(()) } @@ -1621,33 +1624,33 @@ async fn a_full_scope_takes_no_new_flow_trust_but_still_answers_for_a_live_one() async fn a_revoked_pair_cannot_cross_the_scope_row_cap_when_regranted() -> Result<()> { let (fixture, owner) = owner()?; owner.register_scope(register(7)).await?; - for n in 0..960u64 { + for n in 0..CAP { owner .grant_flow_tool(grant(7, &format!("flow-{n}"), "shell", n + 1)) .await?; } assert_eq!( fixture.rows.lock().map_err(|_| Error::Unavailable)?.len(), - 960 + crate::engine::MAX_SCOPE_ROWS ); let revoked = owner - .revoke_flow_tool(revoke(7, "flow-0", "shell", 961)) + .revoke_flow_tool(revoke(7, "flow-0", "shell", CAP + 1)) .await?; assert_eq!(revoked.outcome, FlowTrustOutcome2::Revoked); assert_eq!( fixture.rows.lock().map_err(|_| Error::Unavailable)?.len(), - 960 + crate::engine::MAX_SCOPE_ROWS ); assert_eq!( owner - .grant_flow_tool(grant(7, "flow-0", "shell", 962)) + .grant_flow_tool(grant(7, "flow-0", "shell", CAP + 2)) .await .err(), Some(Error::Unavailable) ); assert_eq!( fixture.rows.lock().map_err(|_| Error::Unavailable)?.len(), - 960 + crate::engine::MAX_SCOPE_ROWS ); Ok(()) } @@ -1656,28 +1659,31 @@ async fn a_revoked_pair_cannot_cross_the_scope_row_cap_when_regranted() -> Resul async fn a_revoked_tool_cannot_cross_the_scope_row_cap_when_regranted() -> Result<()> { let (fixture, owner) = owner()?; owner.register_scope(register(7)).await?; - for n in 0..960u64 { + for n in 0..CAP { owner .grant_tool(tool_grant(7, &format!("tool-{n}"), n + 1)) .await?; } assert_eq!( fixture.rows.lock().map_err(|_| Error::Unavailable)?.len(), - 960 + crate::engine::MAX_SCOPE_ROWS ); - let revoked = owner.revoke_tool(tool_revoke(7, "tool-0", 961)).await?; + let revoked = owner.revoke_tool(tool_revoke(7, "tool-0", CAP + 1)).await?; assert_eq!(revoked.outcome, ToolTrustOutcome2::Revoked); assert_eq!( fixture.rows.lock().map_err(|_| Error::Unavailable)?.len(), - 960 + crate::engine::MAX_SCOPE_ROWS ); assert_eq!( - owner.grant_tool(tool_grant(7, "tool-0", 962)).await.err(), + owner + .grant_tool(tool_grant(7, "tool-0", CAP + 2)) + .await + .err(), Some(Error::Unavailable) ); assert_eq!( fixture.rows.lock().map_err(|_| Error::Unavailable)?.len(), - 960 + crate::engine::MAX_SCOPE_ROWS ); Ok(()) } diff --git a/crates/tinysecurity-approval/src/engine_import_trust.rs b/crates/tinysecurity-approval/src/engine_import_trust.rs index f2cd4670..81b2e371 100644 --- a/crates/tinysecurity-approval/src/engine_import_trust.rs +++ b/crates/tinysecurity-approval/src/engine_import_trust.rs @@ -12,8 +12,6 @@ use tinysecurity_bus::{ ImportTrustSourceRequest2, TOOL_TRUST_LIFETIME_MS2, }; -const MAX_SCOPE_ROWS: usize = 960; - struct ImportPlan { attribution: &'static str, choice: ApprovalChoice2, @@ -215,7 +213,7 @@ impl Engine { request_id: existing.request_id.clone(), }); } - if rows.len() >= MAX_SCOPE_ROWS { + if rows.len() >= super::MAX_SCOPE_ROWS { return Err(Error::Unavailable); } let now = self.now()?; diff --git a/crates/tinysecurity-approval/src/engine_tests.rs b/crates/tinysecurity-approval/src/engine_tests.rs index 5a2cbed6..cb258c82 100644 --- a/crates/tinysecurity-approval/src/engine_tests.rs +++ b/crates/tinysecurity-approval/src/engine_tests.rs @@ -1046,7 +1046,7 @@ async fn completed_executions_do_not_exhaust_the_creation_retry_window() -> Resu Arc::new(TestClock::default()), Redactor::new(&RedactorConfig::default()).map_err(|_| Error::Invalid)?, )?; - for index in 0..1026 { + for index in 0..crate::engine::MAX_PENDING_CREATIONS + 2 { let mut input = request(); input.request.caller.call_id = format!("completed-{index}"); let row = owner.begin(input.clone()).await?; @@ -1139,7 +1139,7 @@ async fn indeterminate_creation_identities_survive_capacity_pressure_and_retry() let f = Arc::new(Fixture::default()); let owner = engine(f.clone(), Arc::new(TestClock::default()))?; f.fail.store(1, Ordering::SeqCst); - for index in 0..1024 { + for index in 0..crate::engine::MAX_PENDING_CREATIONS { let mut input = request(); input.request.caller.call_id = format!("unacknowledged-{index}"); assert!(owner.begin(input).await.is_err()); @@ -2532,3 +2532,18 @@ async fn app_io_rows_cannot_supply_reusable_or_flow_authority() -> Result<()> { } #[path = "engine_scopes_tests.rs"] mod engine_scopes_tests; + +#[test] +fn the_production_scope_row_cap_is_960() { + // Unit tests enforce a small cap (`MAX_SCOPE_ROWS`) so the cap tests stay + // fast; this pins the value every non-test build enforces. The bound against + // the 1024-row load contract is a compile-time assertion beside the constant. + assert_eq!(crate::engine::PRODUCTION_MAX_SCOPE_ROWS, 960); +} + +#[test] +fn the_production_creation_memo_cap_is_1024() { + // Unit tests enforce a small `MAX_PENDING_CREATIONS`; this pins the value every + // non-test build enforces. + assert_eq!(crate::engine::PRODUCTION_MAX_PENDING_CREATIONS, 1024); +} diff --git a/crates/tinysecurity-approval/src/engine_tool_trust.rs b/crates/tinysecurity-approval/src/engine_tool_trust.rs index f2bef415..cdfa1104 100644 --- a/crates/tinysecurity-approval/src/engine_tool_trust.rs +++ b/crates/tinysecurity-approval/src/engine_tool_trust.rs @@ -14,7 +14,6 @@ use tinysecurity_bus::{ const ATTRIBUTION: &str = "tool-trust-grant"; pub(super) const IMPORT_ATTRIBUTION: &str = "tool-trust-import"; -const MAX_SCOPE_ROWS: usize = 960; /// Digest of an exact tool name, disjoint from call and flow trust bindings. pub(super) fn trust_key(tool: &str) -> String { @@ -121,7 +120,7 @@ impl Engine { } // Revoked and expired grants still occupy durable rows. Reissuing them would // append another row and eventually exceed the module's load bound. - if rows.len() >= MAX_SCOPE_ROWS { + if rows.len() >= super::MAX_SCOPE_ROWS { return Err(Error::Unavailable); } let redactor = self.redactor()?; diff --git a/crates/tinysecurity-module/src/approval_callbacks.rs b/crates/tinysecurity-module/src/approval_callbacks.rs index 8ce36918..0a71a020 100644 --- a/crates/tinysecurity-module/src/approval_callbacks.rs +++ b/crates/tinysecurity-module/src/approval_callbacks.rs @@ -10,6 +10,18 @@ use tinysecurity_bus::{ AuditConfig2, AuditEvent2, AuditKind2, AuditSinkRequest2, AuditSinkResponse2, HumanDecisionReceipt2, SubjectScope, VerifyHumanRequest2, callback_names2, }; + +/// Runtime subjects one store client may hold: the engine's own scope cap +/// (`MAX_APPROVAL_SCOPES2`). Unit tests use a small cap so filling the client to +/// its limit takes a handful of admits rather than 65,535; the refusal is the same +/// comparison either way. Non-test builds use the bus constant directly, and +/// `tests::the_bus_approval_scope_cap_is_65536` pins its contract value. +pub(crate) const MAX_RUNTIME_SCOPES: usize = if cfg!(test) { + 16 +} else { + tinysecurity_bus::MAX_APPROVAL_SCOPES2 +}; +const _: () = assert!(MAX_RUNTIME_SCOPES <= tinysecurity_bus::MAX_APPROVAL_SCOPES2); /// Captured registered callback identity; no model-provided destination or scope. pub(crate) struct ApprovalStoreClient { connection: tinybus::Connection, @@ -82,7 +94,7 @@ impl ApprovalStoreClient { fn admit(&self, handle: SubjectScope) -> Result<()> { let mut scopes = self.scopes.write().map_err(|_| Error::Unavailable)?; if !scopes.contains(&handle) { - if scopes.len() >= tinysecurity_bus::MAX_APPROVAL_SCOPES2 { + if scopes.len() >= MAX_RUNTIME_SCOPES { return Err(Error::Unavailable); } scopes.push(handle); diff --git a/crates/tinysecurity-module/src/approval_callbacks_tests.rs b/crates/tinysecurity-module/src/approval_callbacks_tests.rs index 6ca995db..d560e88e 100644 --- a/crates/tinysecurity-module/src/approval_callbacks_tests.rs +++ b/crates/tinysecurity-module/src/approval_callbacks_tests.rs @@ -979,7 +979,7 @@ async fn store_client_admits_releases_and_bounds_runtime_subjects() client.release(&handle)?; assert!(client.scope(&handle).is_err()); // Filling to the cap refuses the next new subject, and re-admitting a held one stays fine. - for n in 0..(tinysecurity_bus::MAX_APPROVAL_SCOPES2 - 1) { + for n in 0..(super::MAX_RUNTIME_SCOPES - 1) { client.admit(tinysecurity::approval::scope_handle(&SubjectScope { user: format!("u{n}"), workspace: "w".into(), @@ -1116,3 +1116,10 @@ async fn released_subjects_keep_their_stream_only_for_the_retention_window_and_p broker.abort(); Ok(()) } + +#[test] +fn the_bus_approval_scope_cap_is_65536() { + // Unit tests enforce a small `MAX_RUNTIME_SCOPES`; every other build uses + // `MAX_APPROVAL_SCOPES2` itself, whose contract value this pins. + assert_eq!(tinysecurity_bus::MAX_APPROVAL_SCOPES2, 65_536); +}