Security
This section documents the controls that protect a distribution (a signed,
released build) and the trust decisions dk0 makes when it imports or builds one.
The underlying model is in the Specification "Attestations" and "OpenBSD signify
keys" sections; the value-store protections are in SECURITY.md.
Controls and entry points
Section titled “Controls and entry points”| Control | Entry point | Enforced on consumption |
|---|---|---|
| GitHub SLSA Level 2 attestation | import github-l2, inspect github-l2, restore github-l2 | Yes. The release values.json is verified with gh attestation verify against the Sigstore trusted root, scoped to -R OWNER/REPO; a failed verification is fatal. |
| OpenBSD signify signing of a distribution | prepare-version (key generation); distribute and combine (signing); every import (verification) | Yes. distribute signs the canonical build payload (ThunkDist.canonical_build_payload_id) when the build key is the distribution producer key (the dk-distribute CI action passes it with --keys-env), and combine re-signs the combined distribution the same way. On import github-l2, import local, restore github-l2 and remote-result import, the signed continuations must verify against the producer.openbsd_signify public key, and the build.attestation.openbsd_signify signature (when present) must verify over the canonical build payload. A present-but-invalid signature is fatal (SecConsumerTrust). |
Key rotation via signed continuations, monotonic per MAJOR.MINOR | prepare-version, distribute (author); every import (consumer) | Yes. SecPackageRegistry.characterize runs on both sides. A producer key is imported once and never overwritten (no key may reclaim an established MAJOR.MINOR); a new MAJOR.MINOR must carry the continuation key a trusted prior release signed; a release below the latest imported release is rejected (restore falls back to a cold build). |
| Vendor-key trust root and deny-by-default acceptance | every import | Yes. Trust anchors, in order: the built-in dk signify key for CommonsBase_Std, locally prepared keys in etc/dk/d, previously imported releases (the import directory etc/dk/i plus the local trust records in etc/dk/t), and the documented --trust-local-package escape hatch. Any other producer key gets an interactive accept/deny prompt that defaults to deny and denies at end of input, so CI fails closed. Transitive distributions recovered from a directly imported release are verified (signatures and rotation consistency) before their content-pinned acceptance, and never anchor a directly imported release's rotation. |
| Value-store integrity of Marshal-ed ASTs | build / get-object path | Yes. A SHA-256 prefix guards each Marshal-ed AST and is signify-signed with a per-workspace build key (SECURITY.md). |
Rule-capability consent for spawn / capture / writefile, keyed by signify provenance | request.ui.* (BuildRequestUi, SecUiCapability); trust list/grant/revoke | Yes. Deny-by-default prompt before a rule first exercises a capability — run (running a program: request.ui.spawn and request.ui.capture, gated identically so capture cannot bypass a spawn denial) or write (request.ui.writefile); fails closed with no TTY, naming dk0 trust grant. The prompt identifies the rule by its signify provenance: the producer key fingerprint and package@version for a rule from an imported, signature-verified distribution, or the values-file SHA-256 for a host script or local rule. Answering [a]lways (or dk0 trust grant) persists the grant in etc/dk/t/capabilities.json, keyed by the full producer public key (never the fingerprint, which is the attacker-choosable keynum) or by content hash; [y]es allows once. A producer-key grant is honored only for values content whose SHA-256 is recorded in the etc/dk/t/imports.json ledger against that producer — the producer signs each exported script module's content hash into build_to_sign.build_script_module_sha256s, and import records it after verifying the signature — so content tampered after import (or a local squatter) falls back to the content-hash prompt. Key acceptance at import never confers a capability. The process-wide --dangerously-trust-all is a separate command-line escape hatch; the isolated dk0 remote path additionally allows the single, producer-trusted orchestration rule it runs without a prompt. The prompt warns when a program is a bare name resolved through PATH. |
| Windows executable-search hardening | dk0 process startup (Shell.ml) | Yes. On Windows dk0 sets NoDefaultCurrentDirectoryInExePath, removing the current directory from the executable search for every program it spawns — rule spawns, precommands, function commands and subshells — so a program named by a bare name is found only through PATH, never from an executable dropped into the working directory. |
| Build-state exclusion from globs | request.ui.glob (BuildRequestUi) | Yes. The signify keys directory (holding build.sec), the data directory and the cache directory are never enumerated, so a rule cannot route build.sec or other build state into a content-addressed bundle. |
signify primitive (keygen / sign / verify / checksum lists) | signify -G / -S / -V / -C | The OpenBSD signify implementation (MlFront_Signify), including -C verification of a signed SHA256/SHA512 checksum list against its files. |
Trust model
Section titled “Trust model”- Attestation is required.
dk0rejects assets and objects produced without a trusted attestation. Two sources are recognized: a human OpenBSD signify signature, or GitHub Actions SLSA Level 2/3. import github-l2verifies two anchors. The release must carry a valid GitHub/Sigstore attestation for theOWNER/REPOon the command line, and its producer signify key must anchor to the built-in dk vendor root, a locally prepared key, a trusted continuation chain,--trust-local-package, or an interactive acceptance (deny by default).import localhas no transport attestation by design (the user names a local file); the signify signature, rotation, and acceptance controls above are its distribution-integrity control.inspectruns the same verification asimport(SLSA Level 2 forgithub-l2, then the signify / rotation / deny-by-default acceptance controls) but never imports the package payload: it extracts the exported script modules (for audit) and the sealed distribution scripts (for verified manifest rendering) from the small distmeta only. It is the verified-release source for catalog and manifest rendering.- Rule capabilities are deny-by-default and keyed by signify provenance.
A rule action that runs a program (
request.ui.spawn,request.ui.capture— oneruncapability, so capture cannot bypass a spawn denial) or writes a file (request.ui.writefile, thewritecapability) is prompted the first time; answering[a]lwayspersists a workspace grant (etc/dk/t/capabilities.json) for the rule's producer signify key — bound to the full public key — or, for unsigned local content, for the values-file SHA-256 (editing the content asks again). A rule from an imported distribution is attributed to the producer key when itsvalues.luacontent SHA-256 is in the import ledger (etc/dk/t/imports.json) against that key: the producer signs each exported script module's content SHA into the distribution (build_to_sign.build_script_module_sha256s), import records it after verifying the signature, and the run-time content is bound by matching that hash. Content tampered after import (or a local file squatting on the module id) is not in the ledger and safely falls back to the content-hash prompt.dk0 trust grant|revoke|listmanages the grants non-interactively (CI). Accepting a producer key at import time never grants a capability: authenticity and authorization are separate decisions. A program given as a bare name is flagged asPATH-resolved (with the current directory excluded from the search on Windows). dk0 remotecomposes producer trust into capability. The isolated remote orchestration path imports and verifies the explicitly named producer, then (only for the single orchestration rule it runs, in the ephemeral isolated workspace) allows that producer key's rule to run its own capture/spawn/writefile without a per-action prompt — sodk0 remote GitHub@Xno longer needs--dangerously-trust-all. The allowance is scoped to the named producer's key and the isolated run; it is empty in every other process.
Built-in trust root
Section titled “Built-in trust root”dk0 embeds the OpenBSD signify public key for exactly one package:
CommonsBase_Std, in SecConsumerTrust.builtin_root_keys (the current 2.6 line
key, fingerprint f012f39422d61ed2). This implements the Specification's
trust store, which names the dk signify key for the CommonsBase_Std packages
as the only trusted entity by default.
The keys are embedded because deny-by-default enforcement needs a starting
anchor. Nearly every dk workspace bootstraps by importing CommonsBase_Std
(coreutils, 7zip, toybox, ...), and a fresh consumer - a first-time user or a
CI runner - has no previously imported releases, no locally prepared keys, and
no terminal to answer an accept/deny prompt. Without the embedded root, every
first import would fail closed or teach users to reach for
--trust-local-package, hollowing out the deny-by-default model. Only this
one package's keys are embedded - not every Commons* key - to keep the
curated, code-reviewed trust surface minimal: every other producer anchors
through the signed continuation chains of imported releases, an explicit
--trust-local-package, or an interactive acceptance.
Newer CommonsBase_Std lines chain from the embedded keys through the signed
continuations of imported releases (2.5 signs the 2.6 and 3.0 keys, and so
on), so rotation within the reachable chain needs no code change. A release
only carries the chain it links transitively, though, so a fresh import of the
latest release can outrun an old root (this is how the 2.6 key earned its
entry). The network-gated trust-root.t test imports the latest
dkpkg/CommonsBase_Std release into an anchorless workspace; when it fails,
builtin_root_keys must gain the current line's key, taken from the signed
continuation of an attested prior release.
The consumer-side gaps that the controls above close were found by Opus 4.8 with Claude Code on 2026-07-11, and were closed the same day. Still tracked:
- GitHub SLSA Level 3 is not verified. A release carrying a non-empty
github_slsa_v1_l3attestation document is explicitly rejected on import instead of being accepted unverified. query manifestperforms no verification by design: it reads the local working tree for authoring and preview. A consumer that renders its output (for example a package catalog) should render from verified release data instead:inspect github-l2extracts the sealed distribution scripts, andquery manifest -f <outdir>/LIBRARY.VERSION.dist-scripts/dk.urenders them. Releases produced before dk0 sealed manifest inputs cannot be rendered this way until re-released.- A release produced without the distribution key in the build environment (for
example a local
distributewith the auto-generated workspace key) publishes an emptybuild.attestation.openbsd_signifyanchor; consumers accept the absence and rely on the other controls. Requiring the signature is a possible future hardening once every producer signs.