KIT

Reference snapshot · source revision 7a2e36f2b250934a095e542ae20e62dcfb0d5f70

THREAT-MODEL — kit security boundaries

Status: Informative. Companion document to SECURITY.md and the SPEC-* normative specs. Audience: integrators, auditors, and contributors changing crypto- or key-handling code.

This document states what kit defends, what it does not, and where the lines are drawn. It is the source of truth a reviewer consults when a change touches signing, key files, configuration, transport auth, or the release pipeline.


1. Scope

kit is a content-addressed VCS with cryptographic signing (specs/SPEC-SIGNING.md) and an attestation subsystem (specs/SPEC-ATTESTATIONS.md, specs/SPEC-EXTERNAL-SIGNER.md). kit defends:

kit does NOT defend:


2. Trust boundaries

The following boundaries cross trust domains. Code that moves data across one of these lines MUST treat the input as untrusted.

Boundary Trusted side Untrusted side
Local user ↔ remote repo author the running user files inside a cloned tree, including
the repo's .kit/config
Local user ↔ same-host other UID the running user other UIDs on the host
Transport peer (network) the peer the user every other host on the path
configured
Release pipeline runner the workflow YAML any artifact a third-party action emits

The "remote repo author" line is load-bearing. A user who runs kit clone accepts the remote's content into the working tree; they do NOT thereby accept the remote's choice of signing key, key file path, external signer binary, or trust-roots file.


3. Attacker models

For each attacker, this document enumerates what kit claims to defend and what it does not. "Defend" means the design intends a security property and the implementation has tests or fuzz coverage backing it.

3.1 Hostile remote repo author

Attacker controls every file in the cloned tree, including .kit/config, working-tree contents, ref files, packs, and any attestation envelope they push.

kit defends:

kit does NOT defend:

3.1a Hostile git upstream (git-bridge import)

Attacker controls a git repository that the user imports or fetches with kit git import/fetch/pull (feature git-bridge): every object byte, ref name, tag chain, and the transport-level ref advertisement.

kit defends:

kit does NOT defend:

3.2 Local same-host attacker, different UID

Attacker has a shell on the same host under a different UID.

kit defends:

kit does NOT defend against:

3.3 Local same-host attacker who later gains code execution as the user

Attacker runs as the same UID as kit, either by tricking the user into running their code or by exploiting an unrelated process.

kit does NOT defend against this case. Once an attacker runs as the same UID, they can read the key file, spawn arbitrary external signers, edit .kit/config, and so on. kit's threat model assumes the local UID is trusted.

The mitigations in §4 and §5 are about preventing a remote repo from escalating into this position via on-disk config.

3.4 MITM on SSH or HTTPS transport

Attacker is on the network path between the client and the remote.

kit defends:

kit does NOT defend:

3.5 Compromised release pipeline runner

Attacker has code execution on the GitHub Actions runner that produces release artifacts.

kit defends:

kit does NOT defend:

3.6 Hostile download path for the installer

Attacker controls the network path or the host that serves release archives. The attacker does not control the host that serves install.sh.

install.sh defends:

install.sh does NOT defend:


4. Configuration scope split

kit reads two configuration files. Their scope is partitioned by attacker model: a hostile clone can write <repo>/.kit/config but it cannot write the user-scoped file at $XDG_CONFIG_HOME/kit/config (~/.config/kit/config by default).

Security-sensitive keys — anything that selects a key path or an external process — live only in user scope. A repo config that attempts to set them is rejected with a warning; the value is ignored.

Key Scope Rationale
signer User Selects raw-file vs keystore commit signing.
key.backend User Selects the keystore backend family.
key.default_ref User Selects a private signing key reference.
key.ed25519_ref User Selects the Ed25519 private signing key reference.
key.secp256k1_ref User Selects the secp256k1 private signing key reference.
key.p256_ref User Selects the P-256 private signing key reference.
signing_key User Selects which key file is read for commit signing.
attest.signer User Selects repo-key / external / keystore.
attest.default_algorithm User Selects the attestation signing algorithm.
attest.external_signer_path User Selects which binary is spawned as a signer.
attest.external_signer_args User Argv for the signer subprocess.
attest.secp256k1_key_path User Selects which secp256k1 key file is read.
attest.p256_key_path User Selects which P-256 key file is read.
ssh.strict_host_key_checking User Could weaken host-key verification.
ssh.user_known_hosts_file User Selects which file is the source of trust.
ssh.identity_file User Selects which private key SSH presents.
user.identity User Author identity; cannot be repo-selected for signed data.
pull.require_signed User Disables post-fetch signature verification (§3.1).
default_branch Repo UX default. No security weight.
remote_endpoint Repo Address; trust is on the user's transport config.
remote_bucket Repo Address.
remote_type Repo Dispatch hint to the transport layer.

Repo-safe keys are applied after the user file and may override user defaults. The security fence is narrower and stricter: private-key, signer, executable, trust-root, and host-key selectors are user-only and are ignored when they appear in repo config.

The per-key classification, the read-site fence, the runtime credential gate on remote_endpoint, and the rules for adding a new config key are codified in docs/specs/SPEC-CONFIG-SECURITY.md. That spec is the authoritative reference when reviewing a change that touches libkit/src/config.rs or a transport that consumes a config-derived endpoint, key path, or credential.

remote_endpoint, remote_bucket, and remote_type are repo-safe to read (a clone needs to be able to nominate a default mirror), but kit push, kit pull, and kit fetch will refuse to attach ambient KIT_API_TOKEN or KIT_S3_* credentials to a repo-configured endpoint unless the user has explicitly listed that endpoint in user-scoped trusted_remote_endpoint. This closes the hostile-clone credential-exfiltration channel without breaking safe portable defaults for unauthenticated mirrors and SSH-bearing remotes.

The gate is enforced at a single transport-dispatch choke point (remote_dispatch::open_trusted), which runs the per-endpoint check before it constructs any credential-bearing transport. The check is keyed on the resolved endpoint plus its provenance — repo_chosen (selected by repo-scoped config: the flat remote_endpoint or a remote.<name>.url entry) versus user-chosen (user-scoped config or an explicit CLI argument) — never on a remote name. This means the same fence applies uniformly whether the credential-bearing endpoint comes from the single-remote field or from a named remote: a hostile clone cannot smuggle ambient credentials to a new host by hiding the endpoint behind a named remote, and a user-chosen endpoint with the user's own credentials is never second-guessed. Unauthenticated and SSH/file flows carry no ambient HTTP/S3 credentials and pass the gate unchanged.


5. Trust-roots scope

kit verify-attest and kit verify --trusted both load trust roots from $XDG_CONFIG_HOME/kit/trust-roots.toml by default. The path is not repo-local for the same reason as §4: a hostile clone must not choose its own verifier. kit trust add/list/remove manages the same file.

A user can override the path on the command line (--trust-roots <path>) for ad-hoc verification, but the override is per-invocation; there is no repo config knob that sets it. Both commands additionally refuse an in-repo trust-roots path unless --trust-roots was passed explicitly (warn_if_unsafe_trust_roots in rust/crates/kit-bin/src/commands/trust_roots.rs) — a hostile clone that ships .kit/trust-roots.toml listing attacker keys cannot get it selected implicitly.

Without --trusted, kit verify proves only that the object's embedded signer produced its signature — it does not consult trust roots at all (SPEC-SIGNING §6.1). A signature from an unlisted, attacker-controlled key is indistinguishable from a trusted one unless the caller opts into --trusted. kit clone/pull/fetch do not run any verification by default; that remains a separate, tracked gap.


6. Key file format and protections

Property Value
Format raw 32-byte Ed25519 seed (no PEM, no DER, no password wrap in v1)
Permissions mode 0600, MUST be set on creation
Owner euid of the running process; mismatch is a hard failure
Open flag O_NOFOLLOW — symlink in the path is a hard failure
Parent directory 0700, owner-checked
Write strategy tempfile in same directory, fsync, atomic rename, fsync of parent
Zeroization seed buffers scrubbed at generation and file-I/O boundaries

KeyPair::generate scrubs its local seed buffer after constructing the keypair. KeyPair::from_seed takes [u8;32]; callers that own long-lived secret buffers must use a zeroizing owner before and after the call.

The same protections apply to the secp256k1 and P-256 key files selected via attest.secp256k1_key_path and attest.p256_key_path.

6.1 Keystore backends

Issue-complete V1 adds user-scoped software, OS-native, Linux desktop/headless, and YubiKey-backed keystore backends selected by kit key ..., signer = keystore, and attest.signer = keystore.

Security assumptions:

Keystore non-goals in V1:


7. Out of scope

The following are explicitly out of scope. kit makes no claim and takes no defensive posture against them.


8. Verification gates

The following tests, fuzz targets, and CI gates keep this document honest. A change that weakens any of these requires a matching update here.


9. Reporting issues

See SECURITY.md. Use GitHub Security Advisories. Do not file public issues for vulnerabilities.