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:
rust/crates/kit-keystorecrate, public API (Keystore,KeySigner,KeyGenerator,KeyImporter,KeyOpener,KeyLister,KeyExporter,KeyDeleter), structuredErrorenum, typed wrappers (KeyLabel,KeyRefLabel,KeyId,PublicKeyBytes,KeyRef,KeySelector).Capabilitiesreporting structurally tied to operation-trait availability, with capability-honesty tests in each backend module.- Software backend with encrypted-at-rest records (
software:<label>, XChaCha20-Poly1305 + length-prefixed AAD over backend/label/algorithm / public key/key ID/attrs/protector ID, wrapped DEK in an OS protector). - Software-raw backend (
software-raw:<label>), reusingkit_core::sign::{load_raw_32, save_raw_32}hardening. - OS-native backends behind feature flags:
macos-keychain,linux-secret-service,systemd-creds. (Windows is not a supported target — see §6.4.) YubiKeybackend behindbackend-yubikey— OpenPGP signing slot (Ed25519) and PIV signing slot (P-256). secp256k1 is reportedUnsupportedAlgorithm; FIDO2/CTAP keys are not handled in this backend and remain on the external signer path.kit key {generate,list,import,export,delete}CLI with the flags and defaults specified in §9.- Keystore-backed commit signing (
signer = keystore) and attestation signing (attest.signer = keystore). - Repo-forbidden config gating for every selector in §8 and the raw-file selectors in §8.3.
Not yet shipped against this spec:
- In-memory backend (
BackendKind::Memoryexists in the enum, but noMemoryKeystoreis implemented in this build). - External signer keystore bridge (
BackendKind::External) —open_backendreturnsBackendUnavailable; external signing continues to live inkit-rpcper §12. - Cloud KMS backend (
BackendKind::Cloud) — same as above. - Secure Enclave, Windows TPM/CNG provider-backed keys, TPM/PCR-bound
systemd-credssemantics, bounded PIN/touch prompt providers for YubiKey, hardware ECDSA verification-equivalence CI for every required platform.
2. Non-negotiable design decisions
kit-coremust remain lean. It must not depend on platform keychain, credential-manager, D-Bus, TPM, Secure Enclave, YubiKey, or cloud KMS dependencies.- Platform and vault dependencies live in a new crate,
rust/crates/kit-keystore, behind feature flags where appropriate. - Raw key files are supported. The
kit keygencommand creates them. - New keystore UX is exposed through
kit key ..., not by changing the meaning ofkit keygen. - Repo-controlled config must never select a backend, label, key reference, signer binary, or signing algorithm. All such selectors are user-scoped.
- No signing command may silently generate or rotate a key. Key creation is
explicit through
kit key generateorkit keygen. - 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.
- 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.
- 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.
software:<label>is the encrypted-at-rest software backend. Productionkit-binbuilds must enable the target-appropriate OS protector feature, whilekit-keystoredefault features remain empty. Raw-file storage is exposed only through the explicitsoftware-raw:<label>backend token and must not be the secure default.- 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
- Cross-platform persistence of long-term signing keys.
- Algorithms: Ed25519, secp256k1, and P-256.
- Generation, import, export where allowed, enumeration, opening, signing, metadata lookup, and deletion.
- A uniform signer handle abstraction that hides whether the key is software, OS-native, hardware-bound, or external.
kit-binintegration throughkit key {generate,list,import,export,delete}.kit-attestintegration through a keystore-backed implementation of its existingSignertrait.- Ed25519 commit signing through a keystore signer handle.
- Test vectors proving equivalence with raw-file software signing semantics.
- Threat-model documentation updates.
3.2 Out of scope
- Protocol-level enrollment, revocation, expiry, authorization policy, account mapping, or chain/project scoping.
- Secret sharing, threshold signing, social recovery, or Shamir schemes.
- Generic secrets management, environment variable injection, or arbitrary secret storage.
- Arbitrary blob encryption or decryption APIs.
- PKCS#11/HSM abstraction beyond the narrow signing handle model.
- Network-aware key semantics such as
this key signs for chain X. - Cloud KMS support.
- PKCS#11/HSM support.
- Secure Enclave, Windows TPM/CNG provider keys, or TPM/PCR-bound
systemd-credsbehavior unless implemented and capability-tested. - Replacing existing raw-key workflows (
kit keygen,signing_key) — they remain supported alongside the keystore.
4. Crate boundary
The new crate is:
rust/crates/kit-keystore
Workspace integration:
- Add
crates/kit-keystoretorust/Cargo.tomlworkspace members. - The crate must use workspace package metadata, lints, edition, MSRV, and license policy.
- Default features must build on Linux and macOS CI without native service daemons, TPM libraries, smartcard libraries, or OS-specific SDK setup beyond what the target already provides.
kit-keystoredefault features remain empty/lean. Productionkit-binbuilds, packages, and CI release gates must enable the platform-appropriate encrypted software protector feature so the configuredsoftwaredefault is usable on supported targets while the library crate remains feature-gated.
Allowed dependencies:
- Pure-Rust crypto dependencies already used by the workspace may be reused.
- New direct dependencies must satisfy
docs/RELEASE.mdandrust/deny.toml. - OS-specific dependencies must be optional and feature-gated.
Forbidden dependencies:
kit-coremust not depend onkit-keystore.- A wasm32 build must not depend on native
kit-keystorebackends. - Default workspace builds must not require Linux Secret Service, systemd, TPM libraries, YubiKey libraries, Windows APIs, or macOS GUI keychain access.
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:
Algorithmmust convert to and fromkit_attest::Algorithmwhen theattestintegration feature is enabled.- Canonical string forms are exactly
ed25519,secp256k1andp256. - The enum must not include backend-specific variants.
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:
extractable = truemeansexportmay return secret material.extractable = falsemeans export must fail with a typed non-extractable error.require_user_presence = truerequests a per-operation user-presence gate such as Touch ID, PIN, biometric prompt, or hardware touch.device_bound = truerequests that a key cannot be restored onto another machine through backup or cloud sync.
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:
- Operation booleans must match operation-specific trait availability on the
Keystoreregistry. They describe structural backend support, not a guarantee that the current session, daemon, hardware token, or protector is available. - A backend compiled in but unavailable at runtime must return an unavailable error from construction or operations, and must not silently fall back to a weaker backend.
- Capability checks must be testable without performing a signing operation.
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:
- Ed25519: 32-byte raw public key.
- secp256k1: 33-byte compressed SEC1 public key by default.
- P-256: 33-byte compressed SEC1 public key by default.
Canonical key IDs:
- Ed25519 new canonical form:
ed25519:<64 lowercase hex raw pubkey>. - Ed25519 repo-key attestation form:
blake3:<64 lowercase hex blake3(pubkey)>. - secp256k1:
secp256k1:<66 lowercase hex compressed SEC1 pubkey>. - P-256:
p256:<66 lowercase hex compressed SEC1 pubkey>.
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:
- Ed25519 bytes are the raw 32-byte seed, matching current
.kit/keys/default.keysemantics. - secp256k1 bytes are a raw 32-byte scalar accepted by the existing k256 signer.
- P-256 bytes are a raw 32-byte scalar accepted by the existing p256 signer.
- Invalid scalar values must be rejected during import or signer construction.
- Secret material must use
zeroizeor equivalent workspace-approved zeroization. - Secret bytes must not be public fields.
Debugmust be manually implemented and must never include secret bytes.- Any accessor exposing secret bytes must be named to make the risk explicit,
for example
expose_secretorinto_bytes.
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:
signtakes&mut selfto support backends with sessions, prompts, retry counters, cached public-key state, or external connections.- The trait is
Send, notSend + Sync, intentionally. The implementation needs&mut selfsigning because many real backends have session state, prompt state, retry counters, cached handles, or child-process state. A concrete signer may still beSync, but callers must not rely on concurrent signing through one handle. signsigns the supplied message according to the selected algorithm's existing kit semantics. It must not invent additional protocol-level domain separation.- Ed25519 signing signs the supplied bytes directly. DSSE passes the PAE bytes.
Commit signing passes the 32-byte
kit-corecommit signing hash described in section 10. - secp256k1/P-256 signing follows existing
kit-attestsemantics: ECDSA over SHA-256 of the supplied bytes, compactr || s, low-S canonical. DSSE passes the PAE bytes.
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:
- The caller supplies the finished 32-byte digest. The signer returns
r || s || vwiths <= n / 2andvof 27 or 28 (SPEC-WRITE-GRANTS §4.4). The digest is signed as given; the signer adds no hashing or domain separation. - Only a secp256k1 key held by the
softwareorsoftware-rawbackend implements it. Every other signer — the OS-native backends andYubiKey, where secp256k1 is anywayUnsupportedAlgorithm— keeps the default, which fails closed withUnsupportedOperation. A non-secp256k1 software key returnsUnsupportedAlgorithm. - It never exports secret material, and callers must not fall back to
KeyExporterwhen it is unsupported: the caller reports that the key cannot sign natively and offers the import path (an external wallet signature) instead. - It signs an arbitrary digest, so it MUST NOT be reachable from a repository configuration or from any surface that lets remote content choose the digest. Its only caller is the owner-signing module of the CLI, which hashes a statement the user has just built.
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:
- Labels are backend-local names and must be valid UTF-8.
- Labels must reject empty strings,
:, path separators, NUL, control characters, and leading/trailing whitespace. - Backends may either enforce label uniqueness across algorithms or allow the
same label for multiple algorithms. If they allow duplicate labels, callers
must use
KeySelector.algorithm = Some(...)for open/export/delete. - The canonical key identity for create/import conflict checks is
(label, algorithm). generateandimportmust refuse to overwrite an existing(label, algorithm)unlessoptions.overwrite = true.- If
options.overwrite = true, replacement semantics are backend-defined but must never delete or replace any key other than the exact(label, algorithm)being created/imported. Backends should make replacement atomic where the underlying vault supports it; otherwise they must document the non-atomic behavior in their backend notes and tests. - Encrypted software backends must prove the existing record decrypts before an overwrite replaces it. If the protector is permanently unavailable, recovery requires manual record removal and wrapped-DEK cleanup rather than silent data loss.
openmust fail if the selector is missing or ambiguous.listmust return deterministic ordering by(backend, label, algorithm)where the backend can list keys.deletemust delete only the selected key and must not delete by prefix.exportmust fail for non-extractable keys.- Operation support must be represented by operation-specific traits available
through
Keystore; non-exportable backends must not implementKeyExporter. - Public APIs that accept labels, key references, key IDs, or public key bytes
must use typed wrappers rather than unconstrained
Stringvalues.
5.8 Error taxonomy
kit-keystore::Error must distinguish at least:
- unavailable backend
- unsupported algorithm
- unsupported operation
- unsupported attributes
- invalid label
- key already exists
- key not found
- ambiguous key selector
- key is not extractable
- invalid key material
- authentication required
- user declined
- operation timed out
- backend I/O failure
- backend access denied
- serialization or encoding failure
- internal error
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:
- Provide deterministic tests and a complete cross-platform baseline.
- Provide import/export semantics for golden vectors.
- Allow CLI and integration work to run before all OS-native backends are available.
- Preserve current raw-key workflows through an abstraction without claiming they satisfy the encrypted-at-rest vault requirement.
Storage:
- Software keystore storage is user-scoped, not repo-scoped.
- Default storage root is
$XDG_DATA_HOME/kit/keys/on Unix-like platforms, falling back to~/.local/share/kit/keys/whenXDG_DATA_HOMEis unset. Non-Unix platforms must use the platform's per-user application data directory or a documented equivalent. software:<label>maps to exactly one encrypted key record under that user-scoped storage root. It must not resolve relative to the current repo.software-raw:<label>maps to exactly one raw key record under a raw-specific storage subtree. It exists for deterministic tests only. It must never be selected by default..kit/keys/*files are used only through the raw-file flows such askit keygen,signing_key, andrepo-key.- The software backend reuses the existing hardened raw-file functions,
behind the
kit-keystoreabstraction. - If it writes files, it must preserve or improve current hardening:
- Unix
0600key files. - Unix
0700parent directories. - effective-uid owner checks.
- no symlink final component.
- symlink ancestor rejection.
- atomic tmp + fsync + rename + parent fsync writes.
- Unix
- It must not weaken current
kit_core::sign::{load_raw_32, save_raw_32}security properties.
Storage-security modes:
- Raw-file mode (
software-raw) preserves the hardened0600raw-key behavior ofkit keygen. It does not satisfy the encrypted-at-rest software-backend acceptance criterion, and must never be the default. - Encrypted software-file mode owns the
softwarebackend token. It must encrypt key material at rest using OS-protected envelope encryption:- Generate a fresh random data-encryption key (DEK) per stored key record.
- Encrypt the 32-byte secret with an AEAD approved by workspace supply-chain review.
- Bind record version, backend token, label, algorithm, public key, key ID, and key attributes as AEAD associated data.
- Protect or wrap the DEK with an OS-native protection mechanism for the
current platform: macOS Keychain, Linux Secret Service, or
systemd-credsfor headless/server Linux. - On Linux, the
softwarebackend may auto-select Secret Service only when a desktop session is detected and the protector opens cleanly. Secret Service errors must fail closed rather than silently falling back to a weaker protector. The backend may selectsystemd-credsfor headless/server use when no desktop Secret Service session is detected, and must fail closed if no usable protector is available. - Fail closed if no configured OS protection mechanism is available.
- Never derive the encryption key from a kit-managed password, hidden passphrase, repo data, or environment variable.
- The software backend must be clearly reported as
BackendKind::Softwareand documented as software-only, not hardware-bound. - The software backend must not claim
supports_device_bound,supports_non_extractable, or encrypted-at-rest properties unless implemented truthfully. - The raw-file backend must be clearly reported as
BackendKind::SoftwareRawand must not claim encrypted-at-rest protection.
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:
- It must never be selected by default in the native CLI.
- It must not persist keys.
- It must be clearly identified as
BackendKind::Memory.
6.3 macOS Keychain backend
Feature name: backend-macos-keychain.
Default target: macOS only.
Requirements:
- Store extractable software keys as Keychain generic password or keychain key items under a stable service/account scheme.
- Implement deterministic listing of keys created by the kit service/account
scheme so
kit key list --backend macos-keychainworks. - create/import refuses existing
(label, algorithm)values in the normal sequential case. Concurrent same-label create/import atomicity depends on the native Keychain primitive exposed by the selected dependency and is not a cross-process lock guarantee. - Stores extractable signing seeds in the user Keychain and must not claim Secure Enclave, non-extractable, or device-bound semantics.
- Device-bound Keychain storage using
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnlyandkSecAttrSynchronizable = falseis out of scope unless this spec is amended and the property is covered by capability tests. - P-256 Secure Enclave support is optional, but must report non-extractable and user-presence capabilities accurately if implemented.
- Ed25519 must not be advertised as Secure Enclave-backed. Apple Secure Enclave does not provide Ed25519 signing.
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:
- Use the Secret Service API through a reviewed dependency or a small adapter.
- Implement deterministic listing of keys created by the kit service/attribute
scheme so
kit key list --backend linux-secret-serviceworks. - Must fail clearly when no service is available or the session is locked.
- create/import refuses existing
(label, algorithm)values in the normal sequential case. Concurrent same-attribute create/import atomicity depends on the Secret Service implementation and dependency surface and is not a cross-process lock guarantee. - Must not be selected by default for headless/server mode.
6.6 systemd-creds backend
Feature name: backend-systemd-creds.
Default target: Linux headless/server.
Requirements:
- The
systemd-credsbackend may shell out tosystemd-credsrather than link TPM2 libraries. - Shelling out must avoid shell interpolation. Use
Commandwith argv tokens. - Backend must clearly report when
systemd-credsis not available. - Treats
systemd-credsas encrypted credential storage. It does not claim TPM/PCR sealing or device binding unless the backend explicitly requests and verifies those properties.
6.7 YubiKey backend
Feature name: backend-yubikey.
Requirements:
- Support may be split across OpenPGP, PIV, and FIDO2/CTAP implementations.
- Backend must advertise the exact applet and algorithm support it can use.
- FIDO2/WebAuthn signing must preserve the existing WebAuthn wrapping
semantics already used by
kit-attest. Fails closed through the keystore API for FIDO2/CTAP labels and routes users to an external WebAuthn signer unless a future keystore signer API can return the full WebAuthn assertion metadata. - User presence/PIN prompts must be represented through typed errors or an explicit prompt flow. Do not block indefinitely without timeout handling.
- Does not accept YubiKey PINs through environment variables. PIN- or touch-required signing fails closed with typed authentication errors until a bounded prompt provider is implemented. FIDO2/CTAP labels continue to route users to the external signer path.
Current implementation status:
- The shipped YubiKey backend covers OpenPGP signing slots (Ed25519) and PIV
signing slots (P-256). secp256k1 is reported as
UnsupportedAlgorithm. - The backend is read-only over the keystore API:
can_generate,can_import,can_export, andcan_deleteare all false; provisioning is done out of band withykman/gpg --card-edit. Capabilities::supports_user_presenceis derived from the discovered slot policies (PinPolicy != NeverorTouchPolicy ∈ { Always, Cached }).supports_device_boundandsupports_non_extractableare true iff at least one signing key was discovered.- FIDO2/CTAP slots are not exposed by this backend at all. They remain on the
external signer path. The "fail closed through the keystore API for
FIDO2/CTAP labels" requirement is enforced explicitly: a
fido2-/ctap-prefixed label hits a dedicated guard that returnsUnsupportedOperation(directing callers to the CTAP external signer), while any other label that does not resolve to an OpenPGP or PIV signing slot returnsKeyNotFound. Both fail closed.
6.8 External and cloud backends
External signer and cloud KMS support are out of scope unless this spec is amended.
If implemented:
- External signing should reuse
kit-rpcsigner protocol where practical. - Cloud KMS backends must clearly identify non-extractability, algorithm support, and signature encoding.
- Cloud key refs must never be repo-controlled.
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:
- Backend names are lowercase ASCII tokens — see
BackendKind::as_strfor the authoritative list (software,software-raw,macos-keychain,linux-secret-service,systemd-creds,yubikey,external,cloud,memory). - Labels are backend-local but must pass the label validation in section 5.7.
- A key ref must not contain paths, URLs, shell metacharacter semantics, or implicit environment expansion.
- A key ref must not be interpreted relative to the repo.
- Repo config must not set key refs.
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:
signerselects the commit-signing source. Built-in default isfile, which uses the raw-filesigning_key. Settingsigner = keystoreopts commit signing intokit-keystore.key.backendis the default backend forkit keycommands when no backend is provided and no full key ref is being used.key.default_refis the default key ref for generic commands.- Per-algorithm refs override generic refs.
attest.signer = repo-keyremains the built-in default. Settingattest.signer = keystoreopts attestation signing into the keystore-backed attestation signer.- Existing
attest.signer = repo-keyandattest.signer = externalremain supported.
Precedence:
- Commit signing always uses Ed25519. If
signer = fileor empty, commit signing uses thesigning_keyraw-file. Ifsigner = keystore, commit signing useskey.ed25519_ref; if empty, it useskey.default_ref; if both are empty, it fails with a config error rather than silently falling back to raw-file signing. - Keystore-backed attestation signing uses the ref for the selected algorithm:
key.ed25519_ref,key.secp256k1_ref, orkey.p256_ref. If the selected per-algorithm ref is empty, it useskey.default_ref. - Commit signing and attestation signing intentionally share the same default per-algorithm key refs. Separate attestation-only key refs are out of scope unless this spec is amended.
- CLI flags override config for the command being run because they are explicit user input.
- A configured full key ref's backend must not be ignored. For example,
key.default_ref = yubikey:mainselects the YubiKey backend for signing; it must not be silently reinterpreted assoftware:mainbecausekey.backend = softwareis also set.
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:
- A malicious repo must not choose which private key signs attacker-controlled content.
- A malicious repo must not switch a user from file-backed signing to a hardware/cloud/backend signer.
- A malicious repo must not trigger user-presence prompts or confused-deputy through a user-trusted key.
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:
signing_keydrives raw-file Ed25519 commit signing.attest.*_key_pathdrives repo-key attestation signing.- Keystore refs do not replace existing path keys; they are a parallel mechanism.
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:
--backend:key.backend, fallbacksoftware.--label: parsed from the algorithm-specific configured key ref; if no ref is configured, defaults todefaultfor Ed25519,default-secp256k1for secp256k1, anddefault-p256for P-256.--algorithm:ed25519.extractable: backend default. Software backend defaults to extractable.device_bound: false unless requested.require_user_presence: false unless requested.
Behavior:
- Must call
Keystore::generatewithGenerateOptions { overwrite: false }by default. - Must call
Keystore::generatewithGenerateOptions { overwrite: true }when--forceis provided. - Must refuse overwrite unless
--forceis provided. - Must print backend, label, algorithm, public key, key ID, and capabilities.
- If
--print-pubkeyis provided, must print the canonical key ID on a stable final line suitable for scripting. - Must not create repo config.
9.2 kit key list
Usage:
kit key list [--backend <backend>] [--json]
Behavior:
- Lists keys visible to the selected backend or default backend.
- Output order must be deterministic.
- Human output must include backend, label, algorithm, key ID, and capability flags.
- JSON output must be stable enough for downstream tooling and covered by tests if added.
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:
- Import accepts exactly 32 bytes of secret material.
- Hex input must be 64 lowercase or uppercase hex characters; output remains lowercase.
- File input must be read with the same security posture as current raw-key loading where applicable.
- Invalid curve scalars must be rejected.
- Must call
Keystore::importwithImportOptions { overwrite: false }by default. - Must call
Keystore::importwithImportOptions { overwrite: true }when--forceis provided. - Must refuse overwrite unless
--forceis provided. - Must zeroize local secret buffers.
9.4 kit key export
Usage:
kit key export [--backend <backend>] [--label <label>]
[--algorithm ed25519|secp256k1|p256]
--unsafe-print-secret
Behavior:
- Export is intentionally noisy and must require
--unsafe-print-secretwhen printing secret material to stdout. - Export must fail for non-extractable keys.
- Export must print only secret hex when scripting mode is requested. Human warnings must go to stderr.
- Supports stdout export only. Exporting directly to a file is out of scope until a hardened file-output contract is added to this spec.
- Export must never happen implicitly as part of signing, listing, or metadata lookup.
9.5 kit key delete
Usage:
kit key delete [--backend <backend>] [--label <label>]
[--algorithm ed25519|secp256k1|p256]
--yes
Behavior:
- Must require
--yes. No interactive prompt is required. - Must delete exactly one selected key.
- Must report key-not-found distinctly from successful deletion.
- Must not delete raw key files unless the selected backend is the software backend and the selected label maps to that exact key.
- For encrypted software storage, deletion intentionally fails if the selected record cannot be decrypted with its recorded protector. Manual recovery from a permanently lost protector requires removing the exact stored record plus its corresponding OS-protected wrapped DEK.
10. Commit signing integration
Current commit signing lives in kit-core::sign and signs Ed25519 over the
current domain-separated commit signing hash.
Requirements:
kit-corecontinues to own canonical commit/remix signing bytes and verification.kit-coremust expose enough public helpers to support keystore signing without depending onkit-keystore.- The exact value handed to an Ed25519 keystore signer for commit signing is
kit_core::sign::commit_signing_hash(commit), a 32-byte digest currently defined asBLAKE3(len_le16(COMMIT_DOMAIN) || COMMIT_DOMAIN || commit_signing_bytes(commit)). - Ed25519 keystore commit signing must produce the same commit signature as
current
KeyPairsigning for the same seed and same commit object. - Commit objects continue to embed the Ed25519 public key in
Commit.signer. - Verification remains unchanged.
- Missing keystore keys must be hard errors. Do not auto-generate during commit.
Implementation options:
- Preferred: add a small adapter in
kit-binthat obtains the commit signing hash throughkit_core::sign::commit_signing_hash, asks the Ed25519 keystore signer to sign exactly those 32 bytes, then stores the resulting 64-byte signature inCommit.signature. - Do not duplicate
domain_digestconstruction inkit-binorkit-keystore.
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:
- Add a keystore-backed attestation signer adapter.
- The adapter must implement the existing
kit_attest::Signertrait. - It must sign DSSE PAE bytes using existing per-algorithm semantics:
- Ed25519 signs PAE directly.
- secp256k1 signs SHA-256(PAE), compact low-S ECDSA.
- P-256 signs SHA-256(PAE), compact low-S ECDSA.
- Existing
repo-keyandexternalsigners remain supported. attest_factorymust acceptattest.signer = keystoreonly from user-scoped config or CLI arguments.- Keystore-backed signatures must verify through the existing
kit_attest::verifyregistry when the trust root contains the emitted public key.
Key IDs:
- Keystore Ed25519 key IDs should be canonical
ed25519:<pubkey>. - The repo-key signer uses
blake3:<hash(pubkey)>. - The verifier accepts
blake3:for Ed25519.
12. External signer relationship
kit-rpc already defines a signer protocol with KEY_FORM_OPAQUE_HANDLE.
kit-keystore does not replace this protocol.
Rules:
- External signers remain valid for TPM, Secure Enclave, CTAP, and custom integrations.
kit-keystoremay later provide an external signer backend or bridge using the same RPC protocol.- Keystore work must not mutate the v1 RPC wire schema unless this spec is updated with an analysis of the change.
- If key refs are passed to external signers in the future, use
KEY_FORM_OPAQUE_HANDLEand explicit key_ref bytes.
13. Security requirements
13.1 Config and confused deputy
- Repo config must not select keystore backend, key ref, label, signer kind, external signer path, external signer args, or default signing algorithm.
- User-scoped config may select those values.
- CLI flags may select those values because the user explicitly invoked the command.
- Any new config key that can influence private-key selection or signing policy
must be added to
REPO_FORBIDDEN_KEYS.
13.2 No silent key creation
commit,attest,merge,cherry-pick, or any future signing command must not generate a key as a side effect.- Missing key errors must point users to
kit key generateorkit keygen.
13.3 Secret handling
- Secret material must be zeroized on drop.
- Debug output must redact secrets.
- Tests must not log generated secrets unless the test is explicitly verifying export behavior.
- Exported secrets are allowed only through explicit export commands/APIs.
13.4 Backend honesty
- A backend must fail closed when requested attributes cannot be honored.
- A backend must not label a key as hardware-bound unless the private key is actually non-extractable from hardware or OS-protected storage.
- A backend must not label a key as device-bound if cloud sync or backup can restore it on another machine.
13.5 User presence
- User-presence prompts must be opt-in unless the backend inherently requires them.
- CLI must surface enough context for the user to understand why a prompt is happening.
- Timeouts and user-declined outcomes must be distinct errors.
13.6 Audit logging
Audit logging is not required.
If implemented:
- It must be opt-in.
- It must not log secret material, signatures, raw payloads, or private key bytes.
- It may log timestamp, backend, label, algorithm, key ID, operation, and success/failure.
14. Testing requirements
14.1 Unit tests
kit-keystore must test:
- label validation
- key ref parsing
- capabilities reporting
- generate/open/list/delete
- import/export for extractable software keys
- non-extractable export failure where supported by a test backend
- invalid scalar rejection
- key-not-found and key-already-exists errors
- deterministic ordering of list output
14.2 Integration tests
kit-bin must test:
kit key generatecreates a key through the software backend.kit key listreports generated keys.kit key importimports a known Ed25519 seed.kit key export --unsafe-print-secretround-trips an imported extractable key.kit key delete --yesdeletes exactly the selected key.- Repo config cannot set keystore selectors.
- User config can set keystore selectors.
- Missing keys fail without generation.
14.3 Golden equivalence tests
Required golden tests:
- Imported Ed25519 seed through software keystore produces byte-identical
commit signatures to current
kit_core::sign::KeyPairfor the same commit. - Imported Ed25519 seed through software keystore produces byte-identical DSSE signatures to existing Ed25519 software signer for the same PAE, where key ID differences are accounted for separately.
- Imported secp256k1 scalar through software keystore produces byte-identical
DSSE signature to existing
Secp256k1Signerfor the same PAE. - Imported P-256 scalar through software keystore produces byte-identical DSSE
signature to existing
P256Signerfor the same PAE.
Clarification:
- Byte-identical ECDSA signatures are required only for software deterministic signers.
- OS-native or hardware ECDSA backends must produce signatures that verify, but they are not required to be byte-identical unless the backend guarantees deterministic RFC 6979 behavior.
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
- Add
kit-keystorecrate. Shipped. - Add core API, errors, label/key-ref parsing, capabilities. Shipped.
- Add memory backend for tests. Not shipped —
BackendKind::Memoryis defined but noMemoryKeystoreimplementation exists. Software-raw with atempfile-backed root is used in the test suite instead. - Add software backend reusing existing hardened raw-key behavior where
practical. Shipped as
SoftwareRawKeystore, which delegates tokit_core::sign::{load_raw_32, save_raw_32, save_raw_32_create_new}. - Add unit tests. Shipped.
CLI surface
- Add
commands/key.rs. Shipped atrust/crates/kit-bin/src/commands/key.rs. - Wire
kit keyinkit-bin/src/lib.rs. Shipped. - Preserve
kit keygen. Shipped. - Add config keys and repo-forbidden tests. Shipped — see
KeyConfigandREPO_FORBIDDEN_KEYSinlibkit/src/config.rs. - Add CLI integration tests. Shipped.
Attestation integration
- Add keystore-backed
kit_attest::Signeradapter. Shipped asKeystoreAttestSignerincommands/attest_factory.rs. - Extend
attest_factoryforattest.signer = keystore. Shipped. - Add DSSE signing and verification tests. Shipped.
Commit integration
- Add or expose the minimal
kit-corehelper needed for keystore commit signing without creating a dependency cycle. Shipped —kit_core::sign::commit_signing_hashis exposed and used byCommitSigner::Keystoreincommands/commit.rs. - Teach
kit committo use a configured Ed25519 keystore key ref. Shipped behindsigner = keystore. - Add golden equivalence tests. Shipped — see
keystore_commit_signature_matches_file_keypair_signatureincommands/commit.rstests.
Backend matrix
- Make
softwarethe encrypted-at-rest software backend using OS-protected envelope encryption. Shipped —SoftwareKeystore+EncryptedKeyRecord(XChaCha20-Poly1305, length-prefixed AAD, OS protector wrapping the DEK). - Move raw-file persistence to
software-raw. Shipped asSoftwareRawKeystore. - Implement macOS Keychain, Linux Secret Service,
systemd-creds, and YubiKey OpenPGP/PIV/FIDO2 backends behind feature flags with honest capabilities. Shipped for macOS Keychain (macos-keychain), Linux Secret Service (linux-secret-service),systemd-creds(systemd-creds), and YubiKey OpenPGP + PIV (backend-yubikey). Not shipped: a dedicated YubiKey FIDO2/CTAP routing in this backend — FIDO2 stays on the external signer path per §6.7. Removed: the Windows Credential Manager backend (§6.4) — Windows is not a supported target. - Add backend factory/resolution so CLI, commit signing, and attestation signing
route by full key ref. Shipped —
open_backendplusselection_forincommands/key.rs, mirrored in commit and attest paths. - Add platform-gated tests and capability honesty tests. Shipped.
CI and documentation
- Cross-platform CI for Linux desktop-compatible paths, Linux
headless/server-compatible paths, and macOS (Windows is not a
supported target): live OS-native backend tests are
gated by
KIT_RUN_NATIVE_KEYSTORE_TESTS=1per §14.4; not every CI runner currently exercises every backend. - Golden vectors and verification-equivalence tests: shipped for deterministic software/software-raw signers; hardware/OS ECDSA verification-equivalence coverage does not yet cover every backend.
- Threat model docs:
docs/THREAT-MODEL.mddoes not yet document per-backend malware, disk-extraction, backup-exfiltration, and side-channel assumptions for every backend in §6.
16. Design decisions and defaults
kit-keystoreincludes macOS Keychain, Linux Secret Service,systemd-creds, and YubiKey backends with capability reports limited to what they actually implement.- The encrypted software-file design is OS-protected envelope encryption. The
softwarebackend is encrypted-at-rest; raw-file storage is explicit viasoftware-raw. - YubiKey support includes OpenPGP/PIV discovery and FIDO2/CTAP fail-closed routing; bounded PIN/touch prompt signing is out of scope unless this spec is amended.
- Commit signing and attestation signing deliberately share the same
per-algorithm
key.*_refdefaults; attestation-only key refs are out of scope unless a future need is identified and this spec is amended.
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:
- default backend: encrypted-at-rest software backend (
software) - raw-file backend: explicit
software-raw - commit signing source: raw-file
signing_key - attestation signing source: existing
repo-key - new Ed25519 keystore key IDs:
ed25519:<pubkey> - repo-key attestation key IDs:
blake3:<hash(pubkey)> - export mode: stdout only with
--unsafe-print-secret - commit and attestation refs: shared per-algorithm
key.*_refvalues - no silent import of raw files
- no implicit export
- no repo-controlled key refs
- no hardware capability claims without runtime proof
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) |