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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
33 changes: 32 additions & 1 deletion crates/tinysecurity-approval/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]
Expand Down
6 changes: 1 addition & 5 deletions crates/tinysecurity-approval/src/engine_flow_trust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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()?;
Expand Down
38 changes: 22 additions & 16 deletions crates/tinysecurity-approval/src/engine_gaps_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand All @@ -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(())
}
Expand All @@ -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(())
}
4 changes: 1 addition & 3 deletions crates/tinysecurity-approval/src/engine_import_trust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()?;
Expand Down
19 changes: 17 additions & 2 deletions crates/tinysecurity-approval/src/engine_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
}
3 changes: 1 addition & 2 deletions crates/tinysecurity-approval/src/engine_tool_trust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()?;
Expand Down
14 changes: 13 additions & 1 deletion crates/tinysecurity-module/src/approval_callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 8 additions & 1 deletion crates/tinysecurity-module/src/approval_callbacks_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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);
}
Loading