Repository navigation
feat(keyring): opt-in read grants for repeated keyring reads - #65
Conversation
Every keyring operation asked the host for a fresh consent, so a host
that reads the same entry often prompted again and again, or cached the
plaintext itself where the module could not see or revoke it.
A host can now set KeyringConfig2.read_grant { max_ttl_ms } (at most
24 h). A read's consent request then carries an offer, and an Allowed
reply may carry grant { ttl_ms }. The module clamps it and lets later
reads of the same entry by the same agent, origin and tier skip the
prompt. Grants live only in module memory, at most 64 with the oldest
dropped first, and reads under one are still rate-admitted. Writes and
deletes are never offered a grant and always ask.
A write or delete of the entry, a reinit, the new RevokeGrants
operation (one entry, or all) and process exit each end a grant before
any backend access. Without read_grant nothing changes on the wire.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
Tiny Sweeper reviewReviewed head: FindingsNo active actionable findings. Advisory notes
Verdict: Ready for maintainer review · Priority: medium Diagnostics and supporting detailsFull summaryPull request purpose (title): feat(keyring): opt-in read grants for repeated keyring reads. Purpose (whole-pull-request title and description): feat(keyring): opt-in read grants for repeated keyring reads, closing #55. Today every keyring operation asks the host for fresh consent; a host that reads the same entry often either re-prompts or caches the plaintext where the module cannot see or revoke it. This pull request lets a host opt in to module-enforced read grants: `KeyringConfig2.read_grant { max_ttl_ms }` (ceiling 24 h) adds `offer` to a read's consent request and lets an `Allowed` reply carry `grant { ttl_ms }`. The module clamps the lifetime to the ceiling and lets later reads of the same entry by the same agent, origin and tier skip the prompt; purpose is deliberately not part of the grant key. Grants live only in module memory, at most 64 at a time, oldest dropped first, so a restart asks again. Granted reads are still rate-admitted; writes and deletes are never offered a grant and always ask. A write or delete of the entry (including one the human then refuses), a reinit, the new `RevokeGrants` operation (one entry or all), and process exit each end the grant before any backend access. The change is described as additive and opt-in: every new field is omitted when absent, so a host that does not configure `read_grant` sees no wire change, and `CONTRACT_VERSION` is unchanged. Review snapshot
Completeness: Complete What changedThe pull request touches six files: the bus wire types and their tests (`crates/tinysecurity-bus/src/crypto_ops.rs`, `crates/tinysecurity-bus/src/crypto_ops_tests.rs`), the module adapter and its tests (`crates/tinysecurity-module/src/adapter_crypto.rs`, `crates/tinysecurity-module/src/adapter_crypto_tests.rs`), the native-contract example (`crates/tinysecurity-module/examples/keyring_native_contract.rs`), and the module crypto documentation (`docs/module-crypto.md`). On the bus, `KeyringConfig2` gains an opt-in read-grant section, `KeyringConsentRequest2` gains `offer`, the consent response gains `grant`, and `KeyringOperation2` gains `RevokeGrants { name: Option<String> }`. Configuration validation bounds the ceiling, and reinit ends all grants. A latest-push-delta comparison is unavailable, so no statement is made about what changed since the prior reviewed head. Features
Tests
Before mergeNone. How this fits togetherflowchart LR
n0["KeyringBackend2<br/>changed"]:::changed
n1["KeyringConfig2<br/>changed"]:::changed
n2["KeyringOperation2<br/>changed"]:::changed
n3["config_round_trips_and_omits_absent_sections<br/>changed"]:::changed
n4["Consenter<br/>changed"]:::changed
n5["main<br/>changed"]:::changed
n6["standalone"]:::impacted
n7["CryptoConfig2"]:::impacted
n8["CryptoState"]:::impacted
n9["...que_authenticated_peer_and_valid_backends"]:::impacted
n10["keyring_request"]:::impacted
n1 -->|uses| n0
n3 -->|uses| n1
n3 -->|uses| n7
n5 -->|uses| n1
n5 -->|uses| n4
n5 -->|uses| n7
n6 -->|uses| n7
n7 -->|uses| n1
n8 -->|uses| n7
n9 -->|calls| n6
n9 -->|tests| n6
n10 -->|uses| n2
classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Agent review detailscritique
security
tests
commits
description
Evidence and run details
|
Summary
Every keyring operation asked the host for a fresh consent. A host that reads the same entry often, such as a signed-in session token, either prompted again and again, or cached the plaintext itself, where the module could neither see nor revoke it.
A host can now opt in to read grants, which the module enforces:
KeyringConfig2.read_grant { max_ttl_ms }, at most 24 hours. Once set, a read's consent request carriesoffer, and anAllowedreply may carrygrant { ttl_ms }.RevokeGrantsoperation, for one entry or all of them;Related issue
Closes #55. The scope follows the maintainer's decision on the issue: no purpose binding, a 24 h ceiling, memory only.
API or behavior changes
These are additive and opt-in. Without
read_grant, nothing changes on the wire, and consent stays one per operation.KeyringConfig2.read_grant: Option<KeyringReadGrant2>KeyringConsentRequest2.offer: Option<KeyringReadGrant2>KeyringConsentResponse2.grant: Option<KeyringGrant2>KeyringOperation2::RevokeGrants { name: Option<String> }All four are omitted when absent. Both consent structs keep
deny_unknown_fields, so an old host never seesoffer, and an old module refuses aread_grantconfig at load.CONTRACT_VERSIONstays (2, 1). A bump would make every 2.1 host incompatible, and nothing changes for a host that doesn't opt in. Maintainers may bump it at release.The module doesn't audit keyring operations today; the host's consent prompt is the record. This change adds no audit, and a granted read is logged nowhere new.
Validation
Commands actually run, on our Linux CI runner (rustc 1.99.1,
--locked, RUSTFLAGS unset so the repo'spolyval_force_softapplies) at a34ae0a:cargo fmt --all -- --check: cleancargo clippy --all-targets --all-features -- -D warnings: 0 warnings (also with--keep-going)cargo test -p tinysecurity-bus --all-features crypto_ops: 6 passed, 0 failedcargo test -p tinysecurity-module --all-features adapter::crypto::tests: 26 passed, 0 failedcargo build -p tinysecurity-module --examples --all-features: okcargo test --all-features: 1371 passed, 0 failedwrites_and_deletes_always_ask_and_end_the_entrys_grantfails. With the reinit revoke removed, onlyreinit_ends_every_read_grantfailsTests
a_read_grant_covers_repeat_reads_until_its_clamped_expiry: a 5 s grant clamped to the 1 s ceiling, covered at 999 ms and asked again at 1000 ms.a_read_grant_is_bound_to_the_entry_agent_origin_and_tier: another entry, agent, origin or tier asks again; another purpose does not.writes_and_deletes_always_ask_and_end_the_entrys_grant: only reads get an offer, and a refused write still ends the grant.revoke_grants_ends_one_entry_or_all_without_consent: by name, all, an unknown name, and a bad name refused.reinit_ends_every_read_grantread_grants_are_capped_and_the_oldest_is_dropped_firstwithout_read_grant_a_host_grant_is_ignored_and_nothing_is_offered: noofferkey on the wire, andRevokeGrantsrefused.a_zero_grant_grants_nothingmax_ttl_ms0 and 86 400 001 are refused, 86 400 000 is accepted.offer,grant,RevokeGrantsandread_grant.Access::Revokearm in the backend match. It's unreachable because revocation returns earlier, and it denies if ever reached.Documentation
docs/module-crypto.mddescribes read grants, and the module doc comment ofadapter_crypto.rsis updated.Checklist
#[allow(...)],#[ignore], or relaxed lints.envcontents in the diff or the description