KIT

Reference snapshot · source revision 7a2e36f2b250934a095e542ae20e62dcfb0d5f70

SPEC-KEYSTORE — kit signing-key keystore

Status: Normative for kit-keystore behavior. This file does not specify wire formats; it specifies the keystore vault abstraction inside kit. ("Vault" throughout this document refers to the kit keystore vault abstraction, not HashiCorp Vault or any external secret manager.)

Authority: this file is the source of truth for kit-keystore behavior. If code, docs, or tests disagree with this file, update the implementation or amend this specification in the same change.

Scope reminder: this spec covers the crate rust/crates/kit-keystore, the kit key CLI command, the keystore-backed kit_attest::Signer adapter, and the keystore-backed commit signer adapter. External signers are specified by SPEC-EXTERNAL-SIGNER, not here.

1. Purpose

kit treats signatures as trust anchors for commits, remix objects, and DSSE-wrapped in-toto attestations. Today, long-term signing keys are raw 32-byte files under .kit/keys/, protected by strict local filesystem hardening. That is a useful baseline, but it is not a cross-platform vault policy.

kit-keystore provides a generic signing-key vault abstraction over software, OS-native, and hardware-backed key stores. It is deliberately a signing keystore, not a protocol lifecycle system. It returns signer handles; callers decide what those signers are allowed to sign.

kit-keystore provides the crate, API, CLI, config, a software backend (both a deterministic raw-file mode and an encrypted-at-rest mode), OS-native extractable storage backends where implemented, and keystore-backed commit/attestation signing, with honest capability reporting throughout. It does not provide Secure Enclave, Windows TPM/CNG provider keys, TPM/PCR-bound systemd credentials, cloud KMS, PKCS#11/HSM support, or FIDO2/WebAuthn keystore signing — those remain out of scope unless this spec is amended (§3.2).

1.1 Implementation status

This section summarizes what is implemented in the crate today. It is a reading aid; the per-section requirements below remain the normative contract.

Shipped against this spec:

Not yet shipped against this spec:

2. Non-negotiable design decisions

  1. kit-core must remain lean. It must not depend on platform keychain, credential-manager, D-Bus, TPM, Secure Enclave, YubiKey, or cloud KMS dependencies.
  2. Platform and vault dependencies live in a new crate, rust/crates/kit-keystore, behind feature flags where appropriate.
  3. Raw key files are supported. The kit keygen command creates them.
  4. New keystore UX is exposed through kit key ..., not by changing the meaning of kit keygen.
  5. Repo-controlled config must never select a backend, label, key reference, signer binary, or signing algorithm. All such selectors are user-scoped.
  6. No signing command may silently generate or rotate a key. Key creation is explicit through kit key generate or kit keygen.
  7. Backend capabilities are explicit. A backend must not pretend to support export, import, user presence, hardware binding, or algorithm support that it cannot actually provide.
  8. Byte-equal signature equivalence is required only where the backend is deterministic for the algorithm and imported key material. Hardware and OS ECDSA backends may produce valid but non-byte-equal signatures.
  9. WASM builds must not depend on native keystore code. Browser integrations use an in-memory backend or a JS-side signer bridge, not yet implemented.
  10. software:<label> is the encrypted-at-rest software backend. Production kit-bin builds must enable the target-appropriate OS protector feature, while kit-keystore default features remain empty. Raw-file storage is exposed only through the explicit software-raw:<label> backend token and must not be the secure default.
  11. The encrypted software backend must use OS-protected envelope encryption, not a password-derived key or an unaudited local encryption scheme.

3. Scope

3.1 In scope

3.2 Out of scope

4. Crate boundary

The new crate is:

rust/crates/kit-keystore

Workspace integration:

Allowed dependencies:

Forbidden dependencies:

5. Public Rust API

The API below is normative. Exact module names may change only if this file is updated in the same change.

5.1 Algorithm

kit-keystore must expose an algorithm enum equivalent to kit_attest::Algorithm:

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Algorithm {
    Ed25519,
    Secp256k1,
    P256,
}

Requirements:

5.2 Key attributes

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct KeyAttrs {
    pub extractable: bool,
    pub require_user_presence: bool,
    pub device_bound: bool,
}

Semantics:

KeyAttrs::default() is { extractable: true, require_user_presence: false, device_bound: false } because software-style storage decrypts into process memory. Stronger backends that hold non-extractable keys advertise that through Capabilities::supports_non_extractable and must reject extractable = true import paths they cannot honor.

Backends may reject unsupported attribute combinations. They must not silently weaken requested attributes.

5.3 Capabilities

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Capabilities {
    pub backend: BackendKind,
    pub algorithms: Vec<Algorithm>,
    pub can_generate: bool,
    pub can_import: bool,
    pub can_export: bool,
    pub can_delete: bool,
    pub supports_listing: bool,
    pub supports_user_presence: bool,
    pub supports_device_bound: bool,
    pub supports_non_extractable: bool,
}

BackendKind must identify the backend family without exposing internal implementation details:

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BackendKind {
    Software,
    SoftwareRaw,
    MacosKeychain,
    LinuxSecretService,
    SystemdCreds,
    YubiKey,
    External,
    Cloud,
    Memory,
}

Requirements:

5.4 Key metadata

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct KeyMetadata {
    label: KeyLabel,
    pub backend: BackendKind,
    pub algorithm: Algorithm,
    public_key: PublicKeyBytes,
    keyid: KeyId,
    pub extractable: bool,
    pub require_user_presence: bool,
    pub device_bound: bool,
}

impl KeyMetadata {
    pub fn label(&self) -> &str;
    pub fn label_id(&self) -> &KeyLabel;
    pub fn backend(&self) -> BackendKind;
    pub fn algorithm(&self) -> Algorithm;
    pub fn keyid(&self) -> &str;
    pub fn key_id(&self) -> &KeyId;
    pub fn public_key(&self) -> &[u8];
    pub fn public_key_bytes(&self) -> &PublicKeyBytes;
}

Public key encodings:

Canonical key IDs:

The keystore must expose canonical key IDs. The repo-key signer emits the blake3: form. Public KeyId values must be non-empty, must not contain control characters, and must be at most 256 bytes.

5.5 Secret key material

pub struct SecretKey {
    algorithm: Algorithm,
    bytes: zeroize::Zeroizing<[u8; 32]>,
}

impl SecretKey {
    pub fn new(algorithm: Algorithm, bytes: [u8; 32]) -> Self;
    pub fn from_zeroizing(
        algorithm: Algorithm,
        bytes: zeroize::Zeroizing<[u8; 32]>,
    ) -> Self;
    pub fn algorithm(&self) -> Algorithm;
    pub fn expose_secret(&self) -> &[u8; 32];
    pub fn into_bytes(self) -> zeroize::Zeroizing<[u8; 32]>;
}

impl core::fmt::Debug for SecretKey {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result;
}

Requirements:

5.6 Signer handle

pub trait KeySigner: Send {
    fn algorithm(&self) -> Algorithm;
    fn label(&self) -> &KeyLabel;
    fn metadata(&self) -> Result<KeyMetadata, Error>;
    fn public_key(&self) -> Result<PublicKeyBytes, Error>;
    fn keyid(&self) -> Result<KeyId, Error>;
    fn sign(&mut self, msg: &[u8]) -> Result<Vec<u8>, Error>;
    // Defaulted: returns `Error::UnsupportedOperation`. See "Recoverable
    // prehash signing" below.
    fn sign_prehash_recoverable_secp256k1(
        &mut self,
        prehash: &[u8; 32],
    ) -> Result<[u8; 65], Error>;
}

Requirements:

Recoverable prehash signing. sign cannot produce the SPEC-WRITE-GRANTS secp256k1-eip191 owner signature: it hashes with SHA-256 and returns no recovery byte, while that scheme signs a Keccak-256 digest and needs v. sign_prehash_recoverable_secp256k1 is the one narrow addition for it:

5.7 Keystore

Selectors identify keys for read, export, and deletion operations:

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct KeySelector {
    label: KeyLabel,
    pub algorithm: Option<Algorithm>,
}

impl KeySelector {
    pub fn new(label: impl Into<String>, algorithm: Option<Algorithm>) -> Result<Self, Error>;
    pub fn label(&self) -> &str;
    pub fn label_id(&self) -> &KeyLabel;
    pub fn algorithm(&self) -> Option<Algorithm>;
}

algorithm = None means "select by label only" and is valid only when the backend has exactly one key with that label. If a backend has multiple keys with the same label across algorithms, label-only selection must fail with an ambiguous-key error.

pub trait Keystore: Send + Sync {
    fn capabilities(&self) -> Capabilities;
    fn generator(&self) -> Option<&dyn KeyGenerator>;
    fn importer(&self) -> Option<&dyn KeyImporter>;
    fn opener(&self) -> Option<&dyn KeyOpener>;
    fn lister(&self) -> Option<&dyn KeyLister>;
    fn exporter(&self) -> Option<&dyn KeyExporter>;
    fn deleter(&self) -> Option<&dyn KeyDeleter>;
}

pub trait KeyGenerator: Send + Sync {
    fn generate(
        &self,
        label: &KeyLabel,
        algorithm: Algorithm,
        attrs: KeyAttrs,
        options: GenerateOptions,
    ) -> Result<Box<dyn KeySigner>, Error>;
}

pub trait KeyImporter: Send + Sync {
    fn import(
        &self,
        label: &KeyLabel,
        secret: SecretKey,
        attrs: KeyAttrs,
        options: ImportOptions,
    ) -> Result<Box<dyn KeySigner>, Error>;
}

pub trait KeyOpener: Send + Sync {
    fn open(&self, selector: &KeySelector) -> Result<Box<dyn KeySigner>, Error>;
}

pub trait KeyLister: Send + Sync {
    fn list(&self) -> Result<Vec<KeyMetadata>, Error>;
}

pub trait KeyExporter: Send + Sync {
    fn export(&self, selector: &KeySelector) -> Result<SecretKey, Error>;
}

pub trait KeyDeleter: Send + Sync {
    fn delete(&self, selector: &KeySelector) -> Result<(), Error>;
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GenerateOptions {
    pub overwrite: bool,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ImportOptions {
    pub overwrite: bool,
}

Requirements:

5.8 Error taxonomy

kit-keystore::Error must distinguish at least:

Errors must be structured enums, not stringly typed. CLI commands may render human-friendly messages from them.

User-facing Display output must redact backend-local labels, selectors, filesystem paths, and arbitrary backend payloads. Full diagnostics remain available through structured fields and developer/debug output.

6. Backend requirements

6.1 Software backend

kit-keystore includes a software backend with two storage-security modes, described below.

Purpose:

Storage:

Storage-security modes:

6.1.1 EncryptedKeyRecord wire format

This is the byte-exact on-disk layout of the encrypted software-key record referenced by the bullet list above (fields bound as AAD; XChaCha20-Poly1305; OS-protector-wrapped DEK). Every multi-byte integer is little-endian per SPEC-CONVENTIONS §3. A "length-prefixed field" below means [u32 LE length][length bytes] — this applies even to the fixed-24-byte nonce, which still carries its own length prefix rather than a bare 24-byte slice, for uniformity with every other field.

offset  size     field
0       8        magic              "KITKSV1\0"
8       1        version            0x01
9       1        algorithm_id       registry below
10      1        attrs_bits         key-attribute bitflags (extractable, etc.)
11      …        protector          length-prefixed UTF-8 (for example, "macos-keychain")
…       …        public_key         length-prefixed raw bytes
…       …        keyid              length-prefixed UTF-8 (canonical keyid string)
…       4+24     nonce              length-prefixed; length is always 24
…       …        wrapped_dek        length-prefixed; OS-protector output over the 32-byte DEK
…       …        ciphertext         length-prefixed; XChaCha20-Poly1305 ciphertext + 16-byte tag

algorithm_id registry:

Value Algorithm
0x01 Ed25519
0x02 Secp256k1
0x03 P256
0x04 Reserved — never appears in a canonical EncryptedKeyRecord. A decoder MUST reject id 0x04 inside a KITKSV1\0 record.

attrs_bits registry (bit 0 = LSB):

Bit Meaning
0 extractable
1 require_user_presence
2 device_bound

Any bit outside 0–2 set MUST be rejected as a decoding error.

Readers MUST reject any version byte other than 0x01 (no dual-version read — see SPEC-CONVENTIONS §2.2) and MUST reject any trailing bytes after the declared field list, the same discipline SPEC-INDEX applies to the staging-area file.

Nonce sourcing (normative): the 24-byte nonce is generated fresh, from a CSPRNG, for every encrypt call — never derived, never a counter. This is safe without any uniqueness-tracking mechanism because XChaCha20's 192-bit nonce space makes an accidental collision astronomically unlikely across any realistic number of key records; a 96-bit (regular ChaCha20/AES-GCM-sized) nonce would not have this property and would need a counter or record-count cap instead.

AAD construction (normative): the Additional Authenticated Data bound into the AEAD tag — and separately supplied to the OS-native protector wrapping the DEK, though some protectors ignore it (§6.1) — is the concatenation, in this exact order, of:

magic (8 bytes) || version (1 byte)
|| length-prefixed literal "software"
|| length-prefixed label
|| algorithm_id (1 byte) || attrs_bits (1 byte)
|| length-prefixed protector
|| length-prefixed public_key
|| length-prefixed keyid

Every variable-length field is length-prefixed exactly as in the record encoding above, so the AAD has no field-boundary ambiguity — the same canonicalization discipline SPEC-SIGNING §2.1 uses for its domain digests. This AAD binding is what defeats a substitution attack (an adversary swapping one key's ciphertext/wrapped-DEK into another key's record slot, or changing the claimed algorithm/attrs after the fact) — decryption fails closed if any bound field doesn't match what was encrypted.

6.2 Memory backend

An in-memory backend may be implemented for tests and WASM-like integrations.

Requirements:

6.3 macOS Keychain backend

Feature name: backend-macos-keychain.

Default target: macOS only.

Requirements:

6.4 Windows backend (removed)

Windows is not a supported kit target (see docs/INVARIANTS.md): commonware-runtime 2026.9.0's non-Linux storage-sync path calls libc::sync(), which does not exist on x86_64-pc-windows-msvc, so the workspace and its test suite no longer build there. The backend-windows-credential feature, the WindowsCredentialManager BackendKind variant, and the Windows Credential Manager implementation described in this section were removed rather than shipped untested. "windows-credential" is now an unrecognized backend name (Error::BackendUnavailable, not a fail-closed platform check). This section number is kept unused, rather than renumbered, so cross-references to §6.5+ stay stable.

6.5 Linux Secret Service backend

Feature name: backend-linux-secret-service.

Default target: Linux desktop only.

Requirements:

6.6 systemd-creds backend

Feature name: backend-systemd-creds.

Default target: Linux headless/server.

Requirements:

6.7 YubiKey backend

Feature name: backend-yubikey.

Requirements:

Current implementation status:

6.8 External and cloud backends

External signer and cloud KMS support are out of scope unless this spec is amended.

If implemented:

7. Canonical key reference scheme

CLI and config must use a compact key reference syntax:

<backend>:<label>

Examples:

software:default
software-raw:default
macos-keychain:default
linux-secret-service:default
systemd-creds:release
yubikey:main

Rules:

Implementation note: the label component of a parsed KeyRef is a KeyRefLabel, a typed wrapper that re-checks the general label rules in §5.7 plus an extra reject list ($ ~ * ? [ ] { } ; & | ' "). This is strictly stronger than the §5.7 validation: labels coming from a key-ref string cannot contain shell or environment-expansion characters even if a future backend would accept them in a non-ref context.

The syntax covers the full backend matrix in §6.

A full key ref includes the backend. For signing integrations, key.<algorithm>_ref = <backend>:<label> is authoritative and must route to that backend. key.backend is only the default backend for kit key commands when no explicit backend or configured key ref supplies one.

8. Config specification

8.1 New user-scoped keys

kit-keystore introduces these user-scoped config keys for keystore selection. Exact names are normative unless amended here:

signer = file
key.backend = software
key.default_ref = software:default
key.ed25519_ref = software:default
key.secp256k1_ref = software:default-secp256k1
key.p256_ref = software:default-p256
attest.signer = repo-key

Interpretation:

Precedence:

8.2 Repo-forbidden keys

Every config key in section 8.1 must be present in REPO_FORBIDDEN_KEYS in rust/crates/libkit/src/config.rs. The REPO_FORBIDDEN_KEYS list may include additional keys (for example signing_key, attest.external_signer_path, attest.external_signer_args, attest.secp256k1_key_path, attest.p256_key_path, attest.default_algorithm) covering raw-file signer selectors and other private-key-influencing settings. Such extras must not be removed because they enforce the same confused-deputy posture for the raw-file code path.

Reason:

8.3 Raw-file keys

These keys select raw-file signing:

signing_key
attest.secp256k1_key_path
attest.p256_key_path
attest.external_signer_path
attest.external_signer_args

Behavior:

9. CLI specification

The new command namespace is:

kit key <subcommand>

kit keygen creates raw-file keys.

9.1 kit key generate

Usage:

kit key generate [--backend <backend>] [--label <label>]
                  [--algorithm ed25519|secp256k1|p256]
                  [--extractable|--non-extractable]
                  [--device-bound]
                  [--require-user-presence]
                  [--force]
                  [--print-pubkey]

Defaults:

Behavior:

9.2 kit key list

Usage:

kit key list [--backend <backend>] [--json]

Behavior:

9.3 kit key import

Usage:

kit key import --algorithm ed25519|secp256k1|p256
                [--backend <backend>] [--label <label>]
                (--hex <64-hex> | --file <path>)
                [--extractable|--non-extractable]
                [--device-bound]
                [--require-user-presence]
                [--force]

Behavior:

9.4 kit key export

Usage:

kit key export [--backend <backend>] [--label <label>]
                [--algorithm ed25519|secp256k1|p256]
                --unsafe-print-secret

Behavior:

9.5 kit key delete

Usage:

kit key delete [--backend <backend>] [--label <label>]
                [--algorithm ed25519|secp256k1|p256]
                --yes

Behavior:

10. Commit signing integration

Current commit signing lives in kit-core::sign and signs Ed25519 over the current domain-separated commit signing hash.

Requirements:

Implementation options:

docs/specs/SPEC-SIGNING.md describes BLAKE3(len_le16(domain) || domain || signing_bytes), matching the implementation — see SPEC-SIGNING §3 for the canonical formula and kit_core::sign::domain_digest for the implementation.

11. Attestation integration

Current attestation signing uses kit_attest::Signer:

pub trait Signer {
    fn algorithm(&self) -> Algorithm;
    fn keyid(&self) -> Result<String, Error>;
    fn sign(&mut self, pae: &[u8]) -> Result<Vec<u8>, Error>;
}

Requirements:

Key IDs:

12. External signer relationship

kit-rpc already defines a signer protocol with KEY_FORM_OPAQUE_HANDLE. kit-keystore does not replace this protocol.

Rules:

13. Security requirements

13.1 Config and confused deputy

13.2 No silent key creation

13.3 Secret handling

13.4 Backend honesty

13.5 User presence

13.6 Audit logging

Audit logging is not required.

If implemented:

14. Testing requirements

14.1 Unit tests

kit-keystore must test:

14.2 Integration tests

kit-bin must test:

14.3 Golden equivalence tests

Required golden tests:

Clarification:

14.4 Platform-gated backend tests

Each OS-native backend must include tests gated by target OS and feature flag.

Tests must not require developer-specific keychain state. They must create and delete unique test labels.

Live OS-native backend tests are ignored by default and must fail loudly when explicitly invoked without KIT_RUN_NATIVE_KEYSTORE_TESTS=1. CI jobs that claim native backend coverage must run those ignored tests with the required environment gate set.

Hardware tests may be ignored by default and documented as manual tests.

14.5 CI gates

kit-keystore and its integrations must pass:

cd rust
cargo fmt --check
cargo clippy --all-targets --all-features --workspace -- -D warnings
cargo build --locked --workspace
cargo nextest run --locked --workspace --all-features
cargo test --locked --workspace --doc

If all-features cannot include OS-native backends on every CI OS, features must be structured so unsupported target-specific code is cfg-gated correctly.

15. Implementation stages

Status tags below describe what is in the current build. They are advisory; the per-section requirements above remain normative.

Crate and core API

CLI surface

Attestation integration

Commit integration

Backend matrix

CI and documentation

16. Design decisions and defaults

Open question: whether a future kit key export --file <path> mode is worth adding. Out of scope for now.

kit-keystore uses these fixed conservative defaults:

17. Invariants

§2 fixes the design-level invariants and §13 the security requirements; this table indexes them (plus the backend rules of §5–§6) by the property that MUST always hold. Rows cite the normative text; nothing here adds a new requirement.

Invariant Enforced by
A repo can never choose the key, backend, label, signer, or algorithm that signs every §8.1 selector (and the §8.3 raw-file ones) is in REPO_FORBIDDEN_KEYS (§2.5, §8.2, §13.1)
No signing command ever creates or rotates a key explicit kit key generate / kit keygen only; missing keys are hard errors (§2.6, §10, §13.2)
An unavailable backend never silently degrades to a weaker one construction/operations return unavailable errors; Linux protector selection fails closed (§5.3, §6.1)
Capabilities never overstate what a backend can do operation booleans tied to trait availability; per-backend honesty rules (§2.7, §5.3, §13.4)
Requested key attributes are honored or refused, never silently weakened backends reject unsupported attribute combinations (§5.2, §13.4)
Secret bytes never leak via Debug, logs, or implicit paths Zeroizing storage, manual Debug, expose_secret naming, redacting Display (§5.5, §5.8, §13.3)
Export happens only explicitly and only for extractable keys --unsafe-print-secret gate; typed non-extractable error; non-exportable backends omit KeyExporter (§5.7, §9.4, §13.3)
software keys are encrypted at rest under an OS protector, never a password per-record DEK + AEAD, OS-wrapped, fail closed with no usable protector (§2.10–2.11, §6.1)
An encrypted record cannot be re-bound to different metadata AAD binds version/backend/label/algorithm/pubkey/keyid/attrs (§6.1, §6.1.1)
(label, algorithm) is never silently overwritten, and delete removes exactly one key overwrite requires --force / overwrite: true; no prefix deletes (§5.7, §9.1, §9.5)
A configured full key ref's backend is authoritative the ref routes to its backend; never reinterpreted via key.backend (§7, §8.1)
Keystore signing is byte-equal to raw-file signing where determinism allows golden equivalence vectors for deterministic software backends; verification-equivalence otherwise (§2.8, §14.3)
The keystore never invents protocol-level domain separation sign signs the supplied bytes under existing kit semantics (commit hash per §10, DSSE PAE per §11) (§5.6)
FIDO2/CTAP labels fail closed through the keystore API dedicated label guard returns UnsupportedOperation, routing to the external signer path (§6.7)