Attestation Format
Signatures use three nested layers, following the same standards as SLSA provenance.DSSE Envelope
The outermost layer is a DSSE (Dead Simple Signing Envelope):In-Toto Statement
The DSSE payload is an in-toto v1 attestation statement:subject binds the statement to a specific file by name and SHA-256 digest. The predicateType identifies this as a nono file attestation. Trust policy signatures use a separate predicate type: https://nono.sh/attestation/trust-policy/v1.
For keyless signing, the predicate carries OIDC provenance:
Sigstore Bundle
The DSSE envelope is wrapped in a Sigstore bundle v0.3:verificationMaterial contains a publicKey hint instead of a certificate. For keyless signing, it contains the Fulcio certificate chain and Rekor transparency log inclusion proof, making the bundle fully self-contained for offline verification.
Verification Pipeline
When nono encounters an instruction file (pre-exec scan or runtime interception), verification proceeds through these stages:
Keyless Verification
For keyless bundles, cryptographic verification includes:- Fulcio certificate chain — The signing certificate was issued by the Sigstore Fulcio CA
- Certificate validity — The Rekor timestamp proves the signature was created while the short-lived certificate was valid (certificates typically expire after 10-20 minutes)
- Rekor inclusion proof — The signature was logged in the Rekor transparency log
- ECDSA signature — The signature over the DSSE PAE is valid under the certificate’s public key
| OID | Field |
|---|---|
1.3.6.1.4.1.57264.1.1 | OIDC Issuer |
1.3.6.1.4.1.57264.1.8 | Source Repository |
1.3.6.1.4.1.57264.1.9 | Source Repository Ref |
1.3.6.1.4.1.57264.1.11 | Build Config URI (workflow) |
Keyed Verification
For keyed bundles, the public key is loaded from the system keystore and the ECDSA P-256 signature is verified directly. No Fulcio or Rekor is involved. Signer identity is extracted from the predicate’skey_id field.
Interception Points
Pre-exec Scan
Before fork/exec, nono scans the working directory for files matchingincludes. File discovery intentionally does not respect .gitignore — adding a file to .gitignore cannot bypass trust verification. To avoid pathological startup cost in large repos, the walker skips a fixed set of heavy directories such as .git, node_modules, target, dist, and common caches, plus any user-configured --skip-dir / skipdirs entries. Other hidden directories remain visible to the scan. This is the baseline — it catches files present at session start. If any file fails verification and enforcement is deny, the process never starts.
The pre-exec scan also verifies the trust policy’s own signature before trusting it.
Seccomp-Notify (Linux Runtime)
On Linux in Supervised mode, the seccomp-notify supervisor already trapsopenat/openat2 syscalls for capability expansion. When the target path matches an include pattern, the trust interceptor runs verification before the normal approval flow.
The interceptor:
- Reads the file content from disk (the supervisor has unrestricted access)
- Computes the SHA-256 digest and checks the blocklist
- Locates and verifies the
.bundlefile - On success: injects the file descriptor via
SECCOMP_IOCTL_NOTIF_ADDFD - On failure: returns
EPERMto the sandboxed process
Seatbelt Write-Protection (macOS)
On macOS, verified files are write-protected at the kernel level using literal Seatbelt deny rules. During sandbox profile generation:- The pre-exec trust scan verifies all files matching
includesin the trust policy - For each file that passed verification, a
(deny file-write-data (literal "/full/path"))rule is emitted - If the path involves a symlink (e.g.,
/tmp→/private/tmp), rules are emitted for both the original and canonical paths
openat() at runtime), macOS trust verification is startup-only. Files matching includes that appear after sandbox application are not verified before the agent reads them. The macOS enforcement boundary is: integrity of files verified at startup, not runtime interception of all file opens.
Verification Cache
To avoid re-computing signatures on every file access:Signed Trust Policy
The trust policy itself must be signed to establish a root of trust. Without this, an attacker who can write to the project directory can modifytrust-policy.json to add themselves as a publisher or weaken enforcement.
Policy bundles use the predicate type https://nono.sh/attestation/trust-policy/v1 to distinguish them from file attestation bundles.
During pre-exec scan, policy signature verification runs first:
- Locate
trust-policy.jsonin the scan root - Check for
.bundlesidecar - Verify the bundle (keyed or keyless)
- Extract and validate the policy content
deny, the sandbox refuses to start.
Trust Model
Policy signature proves provenance and tamper-resistance, not signer allowlisting. There is no higher-level document that defines who may author trust policy — the policy itself is that document. Operator acceptance of the initial policy is the trust bootstrap step, analogous to SSH’sknown_hosts or TLS’s root CA store.
Policy Merging
Multiple trust policies merge with additive-only semantics:| Field | Merge Strategy |
|---|---|
includes | Union |
publishers | Union |
blocklist.digests | Union |
enforcement | Strictest wins (deny > warn > audit) |
trust-policy.json can add publishers but cannot remove blocklist entries, remove include patterns, or downgrade enforcement.
Codebase Layout
Library (crates/nono/src/trust/)
Attestation primitives reusable by all language bindings.
| File | Contents |
|---|---|
types.rs | TrustPolicy, Publisher, Blocklist, Enforcement, SignerIdentity, VerificationOutcome |
digest.rs | SHA-256 digest computation (file and bytes) |
dsse.rs | DSSE envelope parsing/construction, PAE, in-toto statements |
bundle.rs | Sigstore bundle verification, Fulcio cert identity extraction |
policy.rs | Trust policy loading, merging, evaluation, file discovery |
signing.rs | ECDSA P-256 key generation, keyed signing, bundle construction |
CLI (crates/nono-cli/src/)
| File | Contents |
|---|---|
trust_cmd.rs | nono trust subcommand handlers |
trust_scan.rs | Pre-exec scan, policy signature verification |
trust_intercept.rs | Runtime trust interceptor (cache, verification dispatch) |
instruction_deny.rs | macOS Seatbelt write-protection for verified files |
Security Properties
Verification before ingestion. On Linux, the pre-exec scan and seccomp-notify runtime interception together ensure the agent never sees content that failed verification — files present at startup and files that appear mid-session are both verified before the agent can read them. On macOS, verification is startup-only: files present at launch are verified, and verified files are write-protected, but files that appear mid-session are not intercepted (see Seatbelt Write-Protection above). To mitigate this on macOS, literal patterns in the trust policy that have no matching file at startup cause a hard failure indeny mode.
No trust-on-first-use (Linux). On Linux, a file must have a valid signature from a trusted publisher on first encounter — seccomp-notify intercepts the openat() syscall before the read completes. On macOS, this guarantee applies only to files present at startup.
Blocklist before crypto. A known-malicious file is denied even if it has a valid signature. This handles the case of a legitimately-signed file that is later discovered to be malicious.
Private key protection. For keyed signing, the private key lives in the system keystore (macOS Keychain / Linux Secret Service). It is never written to disk in plaintext.
Short-lived certificates. Keyless signatures use short-lived Fulcio certificates (typically 10-20 minutes). The Rekor timestamp proves the signature was created while the certificate was valid, not against the current time.