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
27 changes: 22 additions & 5 deletions .github/scripts/check-native-coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,11 @@ def production_source(name):


def changed_lines(diff):
"""Extract added/modified production line ranges from git's unified=0 diff."""
"""Extract changed executable production lines from git's unified=0 diff."""
files = {}
current = None
new_line = None
remaining = 0
for line in diff.splitlines():
if line.startswith('+++ '):
name = line[4:]
Expand All @@ -123,14 +125,29 @@ def changed_lines(diff):
raise ValueError('invalid git diff source path')
if current is not None and not production_source(current):
current = None
new_line = None
remaining = 0
elif current is not None and line.startswith('@@ '):
match = re.match(r'@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@', line)
if match is None:
raise ValueError('invalid git diff hunk')
first = int(match[1])
count = int(match[2]) if match[2] is not None else 1
if count:
files.setdefault(current, set()).update(range(first, first + count))
new_line = int(match[1])
remaining = int(match[2]) if match[2] is not None else 1
elif current is not None and new_line is not None and remaining:
if line.startswith('+') and not line.startswith('+++'):
source = line[1:].lstrip()
# Rustdoc comments do not compile to executable lines. In
# particular, documentation-only changes must not fail the
# changed-line gate for having no measured executable lines.
if not source.startswith(('///', '//!')):
files.setdefault(current, set()).add(new_line)
new_line += 1
remaining -= 1
elif line.startswith('-') and not line.startswith('---'):
continue
elif line.startswith(' '):
new_line += 1
remaining -= 1
return files


Expand Down
9 changes: 9 additions & 0 deletions .github/scripts/check-native-coverage_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ def test_utf8_git_diff_is_measured_under_windows_charmap_locale(self):
self.assertIn('Changed executable lines: 1/1 (100.00%)', output.getvalue())
self.assertNotIn('No changed production', output.getvalue())

def test_rustdoc_only_changes_skip_changed_executable_line_gate(self):
output = io.StringIO()
with self.git_coverage_fixture(b'/// updated documentation\n') as argv, \
mock.patch.object(sys, 'argv', argv), \
contextlib.redirect_stdout(output):
gate.main()
self.assertIn('No changed compiled executable sources', output.getvalue())
self.assertNotIn('Changed executable lines:', output.getvalue())

def test_malformed_utf8_git_diff_is_rejected_instead_of_skipping_changes(self):
output, errors = io.StringIO(), io.StringIO()
with self.git_coverage_fixture(b'// invalid \xff\n') as argv, \
Expand Down
235 changes: 15 additions & 220 deletions MODULE.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ scores meet local thresholds before durable `auto:jev` one-shot approval;
all uncertain/error/budget paths keep the authenticated human flow. Required
CAS/audit receipts and fresh policy floors still gate permission.

Contract 1.10 adds [confidential injected master resolution](MODULE.md#confidential-injected-master-resolution) with opaque native aliases. Configured-secret reinit is conservatively denied pending a coherent owner transaction. This is an injected-store prerequisite, not production keyring or password migration.
Contract 1.10 adds [confidential injected master resolution](docs/module-crypto.md#confidential-injected-master-resolution) with opaque native aliases. Configured-secret reinit is conservatively denied pending a coherent owner transaction. This is an injected-store prerequisite, not production keyring or password migration.

Contract 1.9 adds [scoped declarative sandbox planning](crates/tinysecurity-sandbox/README.md).
`Plan2` uses immutable host-registered source provenance, ceilings and executor
Expand Down Expand Up @@ -89,7 +89,8 @@ Use `.dylib` on macOS and `tinysecurity_module.dll` on Windows. The loader
example proves allow/deny, malformed/unsupported invocation rejection,
reconfiguration retention and generation, then prints real bus latency.

See [module boundary](MODULE.md), [accepted specification](docs/specs/security-module.md),
See [module boundary](MODULE.md), [confidential crypto members](docs/module-crypto.md),
[registered subject policies](docs/module-subject-policies.md), [accepted specification](docs/specs/security-module.md),
[implementation sequence](docs/plans/security-module.md), and [roadmap](ROADMAP.md).

`ConsumeSandboxPlan2` carries the original typed `Plan2` envelope as well as the
Expand Down
9 changes: 6 additions & 3 deletions crates/tinysecurity-bus/src/path_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ pub struct PathBatchResponse2 {
/// Query the unconditional protected-path floor for paths outside any scope.
///
/// The floor is lexical and needs no registered policy: credential stores, core
/// system roots, parent-directory traversal (`..` and the forms Windows normalizes to it,
/// such as `.. ` and `...`) and NUL bytes. It holds whether or not the host's
/// discretionary policy is enabled.
/// system roots, parent-directory traversal (`..` and conservative cross-platform
/// forms such as `.. ` and `...`) and NUL bytes. Windows treats exact `..` as a
/// parent component; some Windows path APIs separately normalize trailing spaces
/// or periods, while `...` can remain a distinct name. The policy rejects these
/// forms on every OS. The floor holds whether or not the host's discretionary
/// policy is enabled.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProtectedPathRequest2 {
Expand Down
6 changes: 4 additions & 2 deletions crates/tinysecurity-policy/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ pub(super) const WINDOWS_SYSTEM_ROOTS: &[&str] = &[
"program files (x86)",
"programdata",
];
/// A parent-directory segment, including the forms Windows normalizes to `..`
/// (trailing spaces and dots, as in `.. ` or `...`). Checked lexically on every OS.
/// A parent-directory segment, including conservative cross-platform forms with
/// trailing spaces or dots (such as `.. ` or `...`). Windows treats exact `..`
/// as a parent component; some Windows path APIs separately normalize trailing
/// spaces or periods, while `...` can remain a distinct name. Checked on every OS.
pub(super) fn parent_segment(segment: &str) -> bool {
segment.starts_with("..") && segment.trim_end_matches([' ', '.']).is_empty()
}
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ docs/

- **[`specs/`](specs/README.md)** — one file per feature, module, or subsystem,
describing its behavior, public surface, invariants, and acceptance criteria.
- **[`module-crypto.md`](module-crypto.md)** — confidential device, master, cipher, password, keyring and pairing members of the TinySecurity module contract.
- **[`module-subject-policies.md`](module-subject-policies.md)** — registered subject policy members and their generation-bound authorization rules.
- **[`plans/`](plans/README.md)** — implementation-ordered, test-first steps for
delivering an approved specification. Plans name exact files and verification
commands, and are updated as the work progresses.
Expand Down
173 changes: 173 additions & 0 deletions docs/module-crypto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Confidential crypto and secret members

## Confidential device handles

Optional `ModuleConfig.device: DeviceConfig2` adds `EvaluateDevice2` and
`ExecuteDevice2` to the native interface. Existing omitted-device payloads and
v2/v3/v4 wire frames keep their serialized forms. These members require TinyBus
confidential transport and the immutable verified `trusted_host` peer. Missing
configuration, another sender, public identity claims, unconfidential calls,
missing callbacks and required audit failures deny native authority.

Each configured device scope references a subject already registered in
`approvals`, captures its verified origin, and optionally consumes one static
X25519 credential during SDK init. Absent credentials generate native static
keys. Neither variant exports private material. Device calls select only an
existing scope and a bounded operation: issue a static/ephemeral key handle,
derive a role-bound tunnel, seal/open v2, seal/open v3 handshake or v4 ack,
revoke a handle, or cancel the authenticated scope. Public keys retain the
canonical 32-byte URL-safe unpadded base64 format.

Evaluate builds the exact content digest internally and uses the existing
verified approval callbacks. Execute requires the resulting exact human Once
receipt and fresh required audit correlation; automatic, durable-tool or
caller-asserted decisions cannot issue native authority. Every callback/worker
await is followed by live generation/scope/deadline checks. Plaintext, keys,
frames and handles do not reach approval/audit payloads, errors, Debug or
`PolicyInfo`.

Explicit init bounds cover registrations, handles, concurrent reservations,
per-scope rate, TTL, message bytes and retained bootstrap nonces. Reinit may
change bounds without reimporting credentials or changing immutable subjects.
It invalidates all old handles; successful new-generation receipts issue new
handles. Revoke removes dependent tunnels; scoped Cancel fences pending work.
Bootstrap replay evidence survives alias issuance, Cancel and reinit and fails
closed at capacity. Clock rollback/unavailability faults device authority for
that process. Shutdown closes owned credentials even when an SDK observer
retains the connection. See the [owned cleanup and replay limits](../crates/tinysecurity-crypto/README.md#scoped-device-handles).

`device_native_contract` loads the hashed shared library and exercises actual
confidential dispatch, verified approval/audit callback continuity, v2/v3/v4,
replay/revoke, valid generation-two receipts, old-handle denial and SDK shutdown.
Run the retained runner with the absolute library path from both owner and
non-owner working directories. Linux proves Linux behavior; macOS and Windows
require their native runners. This does not release artifacts or migrate a host.

## Confidential injected master resolution

Contract 1.10 implements optional `ModuleConfig.secret` and confidential
`EvaluateSecret2` / `ExecuteSecret2`. The only operations are
`ResolveConfigMaster` and `ResolveAppMaster`. Init registers metadata: an exact
existing approval scope, purpose, opaque backend descriptor, target named slot
and at most three compatible historical readers. App authority is separately
registered; calls cannot override subjects, origins, backends, slots or paths.

Evaluation retains the same bounded native reservation, private challenge and
keyed safe correlation through execution. Registered auto approval, reusable
choices and judges cannot issue this grant. Actual confidential `VerifyHuman`,
Human Once decision and reservation CAS, and the exact required audit sink
receipt finish before backend load or mint. A canceled begin holds its admitted
reservation until expiry; retry requires a fresh original call id. There is no
second wait queue.

The fixed `SecretBackend2` callback has confidential `Load` and `Set` only.
Every reply echoes backend, registration, generation, operation id, phase and
slot before status interpretation. Denial is not absence. Encodings are bounded
to 4096 bytes. A valid target skips all historical readers; compatible sources
must parse and agree before mutation. Their original uppercase/whitespace
encoding is copied unchanged and verified by exact target readback. No source
is deleted. File is only a named injected store entry, not physical file access.

One resolver and exclusive callback context persist for each immutable binding.
Live native authority is checked before and after each await and before mint
and publication. Cancellation after Set begins, timeout, wrong readback and
indeterminate mutation permanently fence that resolver. The host callback broker
must independently serialize each actual target slot, retain detached provider
workers and block fresh reads/mint/Set until trusted quiescence and exact readback.
Operation ids and Stored acknowledgements do not provide generic CAS or durability.
Recreating a resolver, restarting a process or dropping a service tree is not
recovery authority. This checkpoint provides no recovery member.

Results contain a bounded random alias, purpose and generation only. The master
and underlying native handle remain internal, with no alias-use/read/export
member. Aliases are pruned on expiry or revocation and cleared at close. Owned
successfully decoded callback values and unpublished output are wiped; SDK
serialization, frame copies and partially decoded serde values are outside that
cleanup guarantee.

Configured-secret SDK reinit keeps the same process owner, store brokers and
master resolvers. Native and device transitions retain their state locks through
policy publication; valid-clock publication rejection preserves old authority.
Accepted generations invalidate pending native work and clear aliases, while
persistent resolver uncertainty remains unavailable. Native clock faults still
close authority permanently. Device replay capacity and every scope epoch are
checked before publication; bootstrap evidence survives accepted reinit. Physical files, plaintext delivery and routine native consumers remain outside
this subset; password, OS keyring and pairing members are in contract 1.14 below
(`PasswordCrypt2`, `KeyringAccess2`, `PairingControl2`).

## Confidential native config cipher (contract 1.11)

`EvaluateSecretCipher2` and `ExecuteSecretCipher2` are confidential routine
operations over an actual `ExecuteSecret2` ConfigUserMaster acquisition. They
never prompt for routine fields or create master authority from a receipt.
`secret_cipher` SDK metadata fixes one existing registration, consumer identity,
enabled setting, callback timeout and physical slot count. It cannot change on
reinit. Older 1.10 initialization omits it and retains its original behavior.
An older decoder does not know the additive `native_routine` audit kind; hosts
using this capability must negotiate the new contract vocabulary.

Evaluate owns the real private crypto pending reservation, then commits required
sanitized NativeRoutine intent through the existing AuditClient. Execute removes
it once, requires unchanged metadata and confidential input, and checks actual
execution, output-stage and publication audit acknowledgements. Audit arguments
contain keyed operation/output correlations, Engine-derived durable scope/policy
and fixed classifications. Caller-supplied audit event identities reserve stream
order; they never construct a private routine acknowledgement or cipher grant.

Plaintext, disabled/empty passthrough and disabled migration replacements go only
to the fixed confidential `SecretCipherConsumer2.Deliver` port. A real trusted
receiver authenticates the pinned loaded module and stages zeroizing output in
an exclusive bounded physical slot. It refuses overwrite and releases locally
only after the matching successful owner response. A staged callback reply is
not product plaintext publication. The owner retains its original reservation
through final required audit and live validation; only true ciphertext appears
in ordinary result fields. A failed/canceled/late/invalid stage leaves its sender
slot permanently occupied across generation changes and reinit. Operation-ID
or status echoes cannot clear uncertainty. A successful response permits the
receiver to consume/wipe its exact slot before accepting another stage there.

The broker currently requires every confidential destination to be an admitted
native module. Ordinary unattested host callbacks are denied, including master
HumanOnly prompt and secret backend delivery. The tracked native fixture loads
the real hashed TinySecurity cdylib and a separately admitted host-trusted native
callback fixture. This proves module-to-admitted-native-consumer composition;
it does not prove ordinary OpenHuman host callback admission. That TinyBus
integration gap remains open. SDK serde/frame/partial-decode copies and process
abort are outside the owned-buffer wiping guarantee. App document storage remains a separate unfinished capability.

## Confidential password, keyring and pairing members (contract 1.14)

Optional `ModuleConfig.crypto: CryptoConfig2` adds three confidential members,
`PasswordCrypt2`, `KeyringAccess2` and `PairingControl2`. Like the device and
secret members they require TinyBus confidential transport and the immutable
verified `trusted_host` peer; an unconfigured module denies all three. Config is
init-only and immutable across reinit. It bounds payload bytes (at most 1 MiB)
and operations per monotonic minute (at most 4096).

`PasswordCrypt2` preserves the existing Argon2id v0x13 (64 MiB, 3 passes, 1 lane)
plus AES-256-GCM format. The payload keeps the legacy byte-array shape
(`ciphertext`, `nonce`, `salt`); encrypt generates a sixteen-byte salt unless the
caller supplies one for the legacy separate-salt layout. Frozen host fixtures
decrypt unchanged. At most two derivations run at once and there is no wait queue.

`KeyringAccess2` reads, writes or deletes a named secret on a backend chosen at
init: platform default (Keychain, Credential Manager, Linux kernel keyring),
Linux kernel keyring, or a protected encrypted file in an absolute directory.
Linux Secret Service is not compiled into this module and fails closed. Every
operation first calls the host's `ai.tinyhumans.tinysecurity.KeyringConsent2`
`Consent` callback with the verified caller, purpose, access kind and secret name
(never the value), and proceeds only on an explicit allow. Invalid names or
purposes, oversized values and denied or timed-out consent never reach the backend.

`PairingControl2` exposes `Status`, `Authenticate` (constant-time bearer check
against legacy tokens), `Invalidate` and `Revoke` for accounts registered at init.
Challenge issuance and numeric verification (`Begin`/`Verify`) are not exposed:
they need confidential delivery to an admitted native destination, the same open
TinyBus gap as the config cipher consumer. Their ports fail closed.

Passwords, plaintext, secret values and bearers are wiped after use and never
appear in `Debug`, errors, `PolicyInfo` (only `callbacks.crypto`) or audit rows.
The X25519/HKDF device tunnel remains the `EvaluateDevice2`/`ExecuteDevice2`
surface above. The OS keyring test runs on every platform and is required to
use the real credential store when `TINYSECURITY_REQUIRE_OS_KEYRING=1`; it
always removes its entry.
Loading
Loading