---
title: Attestation Internals
description: Sigstore-based cryptographic attestation format and verification pipeline
---

This page covers the internal design of nono's attestation system. For usage instructions, see [Trust & Attestation](/cli/features/trust).

## Attestation Format

Signatures use three nested layers, following the same standards as SLSA provenance.

### DSSE Envelope

The outermost layer is a [DSSE](https://github.com/secure-systems-lab/dsse) (Dead Simple Signing Envelope):

```json
{
  "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](https://github.com/in-toto/attestation):

```json
{
  "_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:

```json
{
  "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](https://github.com/sigstore/protobuf-specs):

```json
{
  "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](/assets/attest-flow.png)

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:

| 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'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](/cli/features/supervisor), 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:

| Field | Merge Strategy |
|-------|---------------|
| `includes` | Union |
| `publishers` | Union |
| `blocklist.digests` | Union |
| `enforcement` | Strictest 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.

| 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](#seatbelt-write-protection-macos) 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.
