Skip to main content
This page covers the internal design of nono’s attestation system. For usage instructions, see Trust & Attestation.

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):
{
  "payloadType": "application/vnd.in-toto+json",
  "payload": "<base64url of Statement>",
  "signatures": [
    {
      "keyid": "",
      "sig": "<base64url signature over PAE(payloadType, payload)>"
    }
  ]
}
The signature is computed over the Pre-Authentication Encoding (PAE) of the payload type and payload, preventing type confusion attacks.

In-Toto Statement

The DSSE payload is an in-toto v1 attestation statement:
{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "SKILLS.md",
      "digest": {
        "sha256": "a1b2c3d4e5f6..."
      }
    }
  ],
  "predicateType": "https://nono.sh/attestation/instruction-file/v1",
  "predicate": {
    "version": 1,
    "signer": {
      "kind": "keyed",
      "key_id": "nono-keystore:default"
    }
  }
}
The 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:
{
  "signer": {
    "kind": "keyless",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:org/repo:ref:refs/heads/main",
    "repository": "org/repo",
    "workflow_ref": ".github/workflows/sign-skills.yml@refs/heads/main"
  }
}

Sigstore Bundle

The DSSE envelope is wrapped in a Sigstore bundle v0.3:
{
  "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
  "verificationMaterial": {
    "certificate": { "rawBytes": "<base64 DER>" },
    "tlogEntries": [{ ... }]
  },
  "dsseEnvelope": { ... }
}
For keyed signing, 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: pre-exec scan or runtime interception flow chart- verification proceeds through these stages attest-flow.png

Keyless Verification

For keyless bundles, cryptographic verification includes:
  1. Fulcio certificate chain — The signing certificate was issued by the Sigstore Fulcio CA
  2. Certificate validity — The Rekor timestamp proves the signature was created while the short-lived certificate was valid (certificates typically expire after 10-20 minutes)
  3. Rekor inclusion proof — The signature was logged in the Rekor transparency log
  4. ECDSA signature — The signature over the DSSE PAE is valid under the certificate’s public key
Signer identity is extracted from Fulcio certificate extensions:
OIDField
1.3.6.1.4.1.57264.1.1OIDC Issuer
1.3.6.1.4.1.57264.1.8Source Repository
1.3.6.1.4.1.57264.1.9Source Repository Ref
1.3.6.1.4.1.57264.1.11Build 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’s key_id field.

Interception Points

Pre-exec Scan

Before fork/exec, nono scans the working directory for files matching includes. 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 traps openat/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:
  1. Reads the file content from disk (the supervisor has unrestricted access)
  2. Computes the SHA-256 digest and checks the blocklist
  3. Locates and verifies the .bundle file
  4. On success: injects the file descriptor via SECCOMP_IOCTL_NOTIF_ADDFD
  5. On failure: returns EPERM to the sandboxed process
A TOCTOU re-verification step ensures the digest of the opened file descriptor matches the digest verified during the trust check. This prevents a race where the file is swapped between verification and the fd injection.

Seatbelt Write-Protection (macOS)

On macOS, verified files are write-protected at the kernel level using literal Seatbelt deny rules. During sandbox profile generation:
  1. The pre-exec trust scan verifies all files matching includes in the trust policy
  2. For each file that passed verification, a (deny file-write-data (literal "/full/path")) rule is emitted
  3. If the path involves a symlink (e.g., /tmp/private/tmp), rules are emitted for both the original and canonical paths
This makes verified files structurally immutable — the sandboxed process cannot modify them even though the parent directory may have write access granted. The pre-exec scan gates execution, aborting if any existing file fails verification. Unlike Linux (where seccomp-notify intercepts every 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:
Cache key:   (canonical_path, inode, mtime, file_size)
Cache value: (sha256_digest, verification_result, publisher_name)
TTL:         Session lifetime (cleared on nono exit)
If any component of the cache key changes (file modified, replaced, moved), the cache entry is invalidated and verification re-runs.

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 modify trust-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:
  1. Locate trust-policy.json in the scan root
  2. Check for .bundle sidecar
  3. Verify the bundle (keyed or keyless)
  4. Extract and validate the policy content
If policy verification fails and enforcement is 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’s known_hosts or TLS’s root CA store.

Policy Merging

Multiple trust policies merge with additive-only semantics:
FieldMerge Strategy
includesUnion
publishersUnion
blocklist.digestsUnion
enforcementStrictest wins (deny > warn > audit)
Project-level policy cannot weaken user-level or embedded policy. A malicious project’s 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.
FileContents
types.rsTrustPolicy, Publisher, Blocklist, Enforcement, SignerIdentity, VerificationOutcome
digest.rsSHA-256 digest computation (file and bytes)
dsse.rsDSSE envelope parsing/construction, PAE, in-toto statements
bundle.rsSigstore bundle verification, Fulcio cert identity extraction
policy.rsTrust policy loading, merging, evaluation, file discovery
signing.rsECDSA P-256 key generation, keyed signing, bundle construction

CLI (crates/nono-cli/src/)

FileContents
trust_cmd.rsnono trust subcommand handlers
trust_scan.rsPre-exec scan, policy signature verification
trust_intercept.rsRuntime trust interceptor (cache, verification dispatch)
instruction_deny.rsmacOS 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 in deny 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.