SPEC-KEYSTORE.md
In docs/specs/
Preview
Text · 64.1 KiBspec: SPEC-KEYSTORE
version: 1
status: stable-normative
audience: implementers and reviewers of the mkit-keystore crate, its CLI surface (mkit key), and the keystore-backed commit and attestation signers
SPEC-KEYSTORE — mkit signing-key keystore
Status: Normative for mkit-keystore behavior. This file does not
specify wire formats; it specifies the keystore vault abstraction inside
mkit. ("Vault" throughout this document refers to the mkit keystore vault
abstraction, not HashiCorp Vault or any external secret manager.)
Authority: this file is the source of truth for mkit-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/mkit-keystore, the
mkit key CLI command, the keystore-backed mkit_attest::Signer adapter,
and the keystore-backed commit signer adapter. External signers are specified
by SPEC-EXTERNAL-SIGNER, not here.
1. Purpose
mkit 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 .mkit/keys/, protected by strict local filesystem
hardening. That is a useful compatibility baseline, but it is not a
cross-platform vault policy.
mkit-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.
mkit-keystore provides the crate, API, CLI, config, a software backend (both
a deterministic raw-compatibility 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/mkit-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 compatibility backend (
software-raw:<label>), reusingmkit_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 — MKIT-6; 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.mkit 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 legacy 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 inmkit-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
mkit-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/mkit-keystore, behind feature flags where appropriate. - Existing raw key files remain supported for compatibility. The current
mkit keygencommand remains as a legacy compatibility surface unless a future spec explicitly removes it. - New keystore UX is exposed through
mkit key ..., not by changing the meaning ofmkit keygenin-place. - 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
mkit key generateor legacymkit 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. Productionmkit-clibuilds must enable the target-appropriate OS protector feature, whilemkit-keystoredefault features remain empty. Raw-file compatibility 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.
mkit-cliintegration throughmkit key {generate,list,import,export,delete}.mkit-attestintegration through a keystore-backed implementation of its existingSignertrait.- Ed25519 commit signing through a keystore signer handle.
- Test vectors proving compatibility with current 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 (
mkit keygen,signing_key) — they remain supported alongside the keystore.
4. Crate boundary
The new crate is:
rust/crates/mkit-keystoreWorkspace integration:
- Add
crates/mkit-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.
mkit-keystoredefault features remain empty/lean. Productionmkit-clibuilds, 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:
mkit-coremust not depend onmkit-keystore.mkit-wasmmust not depend on nativemkit-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
mkit-keystore must expose an algorithm enum equivalent to
mkit_attest::Algorithm:
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Algorithm {
Ed25519,
Secp256k1,
P256,
/// Feature-gated behind `bls-threshold`. See §6.1.1 below.
#[cfg(feature = "bls-threshold")]
Bls12381Threshold,
}Requirements:
Algorithmmust convert to and frommkit_attest::Algorithmwhen theattestintegration feature is enabled.- Canonical string forms are exactly
ed25519,secp256k1,p256, andbls12381-thr(the BLS variant matches the same prefix asmkit_attest::Algorithm::Bls12381Threshold). - The enum must not include backend-specific variants.
Bls12381Thresholdis feature-gated behindbls-threshold. The genericKeyImporter/KeyExportertraits MUST reject this variant withUnsupportedAlgorithm— BLS shares are variable-length (≈52 bytes forMinSig) and the 32-byteSecretKeytype cannot represent them. See §6.1.1 for the dedicated BLS share storage API.
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 legacy attestation form remains accepted where already supported:
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. Compatibility adapters may emit
legacy key IDs only where existing verifier contracts require them. 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
.mkit/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. Issue #104 sketchedSend + Sync, but 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 mkit 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
mkit-corecommit signing hash described in section 10. - secp256k1/P-256 signing follows existing
mkit-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
mkit-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
mkit-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/mkit/keys/on Unix-like platforms, falling back to~/.local/share/mkit/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 compatibility key record under a raw-specific storage subtree. It exists for deterministic tests, compatibility, and explicit migration work only. It must never be selected by default.- Legacy
.mkit/keys/*files remain supported only through legacy raw-file flows such asmkit keygen,signing_key, andrepo-keycompatibility. - The software backend reuses the existing hardened raw-file functions,
behind the
mkit-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
mkit_core::sign::{load_raw_32, save_raw_32}security properties.
Storage-security modes:
- Compatibility raw-file mode (
software-raw) preserves the hardened0600raw-key behavior of legacymkit 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 an mkit-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 compatibility 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 "MKITKSV1"
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 tagalgorithm_id registry:
| Value | Algorithm |
|---|---|
0x01 | Ed25519 |
0x02 | Secp256k1 |
0x03 | P256 |
0x04 | Reserved — never appears in a canonical EncryptedKeyRecord. BLS12-381 threshold shares share this algorithm-id space conceptually but are encoded through the distinct BlsShareRecord format (magic MKITKSB1, §6.1.2), since a share's plaintext is a variable-length commonware-encoded structure, not a 32-byte scalar. A decoder MUST reject id 0x04 inside a MKITKSV1 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
compatibility — see SPEC-CONVENTIONS §2's note on versioning; this is
local, unreleased state with no installed base to protect) 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 keyidEvery 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.1.2 BLS12-381 threshold share storage
The encrypted software backend stores BLS12-381 threshold shares (issue #160;
see also §6 of SPEC-RELEASE-THRESHOLD.md). The contract:
- BLS shares are variable-length (≈52 bytes for the
MinSigvariant — the wire-encodedcommonware_cryptography::bls12381::primitives::group::Share) so they cannot ride the genericKeyImporter/KeyExporterpaths (which are pinned at 32 bytes). The keystore exposes a dedicated BLS-share API onSoftwareKeystore:store_bls_share(label, share_bytes, cohort_public_key, share_index, threshold, total, keyid, overwrite)load_bls_share(label) -> LoadedBlsSharedelete_bls_share(label)list_bls_shares() -> Vec<(KeyLabel, BlsShareMetadata)>bls_share_metadata(label) -> BlsShareMetadata(public metadata only, no decryption)
- The on-disk record is
BlsShareRecord, a parallel format toEncryptedKeyRecordwith magicMKITKSB1(canonicalEncryptedKeyRecordusesMKITKSV1). It is XChaCha20-Poly1305 AEAD-encrypted with a DEK wrapped by the same OS-native protector the canonical record uses. - The record's AAD binds all of:
- magic + version
- backend tag (
bls12381-thr) - label
- protector id
- cohort public key (the 96-byte G2 compressed aggregated pubkey)
- share index (
u32) - quorum threshold M (
u32) - cohort total N (
u32) - canonical keyid
- The AAD binding defeats a substitution attack where an adversary swaps a 1-of-N share for a 3-of-N share, or routes a share for one cohort into the storage slot of another cohort.
- Directory layout:
<root>/bls12381-thr/<hex(label)>.share. - BLS shares are advertised in
Capabilities { algorithms, .. }of thesoftwarebackend when thebls-thresholdfeature is on. Thesoftware-rawbackend MUST NOT advertise BLS — raw storage cannot bind AAD. - Generic
KeyImporter/KeyExporter/KeyGeneratorcalls withalgorithm == Bls12381ThresholdMUST fail closed withUnsupportedAlgorithmorUnsupportedOperation. Generation of BLS shares flows through a separate trusted-dealer ceremony — seeSPEC-RELEASE-THRESHOLD.md§2.1 andmkit-cli's `mkit key generate --algorithm bls12381-thr --threshold M --total N --label<base>` surface. - Hostile backends (
yubikey,macos-keychain,linux-secret-service,systemd-creds) MUST rejectBls12381ThresholdwithUnsupportedAlgorithm. BLS share storage is software-backend-only; extending it to native backends with their own threshold-aware wire formats is out of scope unless this spec is amended.
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 mkit service/account
scheme so
mkit 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 mkit target (MKIT-6; 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 mkit service/attribute
scheme so
mkit 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
mkit-attestand contrib CTAP signer. Fails closed through the keystore API for FIDO2/CTAP labels and routes users to the external CTAP 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
mkit-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:mainRules:
- 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 mkit key commands
when no explicit backend or configured key ref supplies one.
8. Config specification
8.1 New user-scoped keys
mkit-keystore introduces these user-scoped config keys for keystore
selection. Exact names are normative unless amended here:
signer = legacy
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-keyInterpretation:
signerselects the commit-signing source. Built-in default islegacy, which preserves existing raw-filesigning_keybehavior. Settingsigner = keystoreopts commit signing intomkit-keystore.key.backendis the default backend formkit 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 = legacyor empty, commit signing uses the existingsigning_keyraw-file behavior. 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 legacy 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/mkit-cli/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 legacy signer selectors and other
private-key-influencing settings. Such extras must not be removed because
they enforce the same confused-deputy posture for the pre-keystore 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 Existing keys
Existing keys remain valid:
signing_key
attest.secp256k1_key_path
attest.p256_key_path
attest.external_signer_path
attest.external_signer_argsCompatibility behavior:
signing_keycontinues to drive legacy raw-file Ed25519 commit signing.attest.*_key_pathcontinues to drive legacy repo-key attestation signing.- Existing security scope rules remain unchanged.
- Keystore refs do not replace existing path keys; they are a parallel mechanism.
9. CLI specification
The new command namespace is:
mkit key <subcommand>mkit keygen remains as a legacy compatibility command and may internally
delegate to the old raw-file implementation.
9.1 mkit key generate
Usage:
mkit 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 mkit key list
Usage:
mkit 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 mkit key import
Usage:
mkit 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 mkit key export
Usage:
mkit key export [--backend <backend>] [--label <label>]
[--algorithm ed25519|secp256k1|p256]
--unsafe-print-secretBehavior:
- 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 mkit key delete
Usage:
mkit key delete [--backend <backend>] [--label <label>]
[--algorithm ed25519|secp256k1|p256]
--yesBehavior:
- 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 legacy 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 mkit-core::sign and signs Ed25519 over the
current domain-separated commit signing hash.
Requirements:
mkit-corecontinues to own canonical commit/remix signing bytes and verification.mkit-coremust expose enough public helpers to support keystore signing without depending onmkit-keystore.- The exact value handed to an Ed25519 keystore signer for commit signing is
mkit_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
mkit-clithat obtains the commit signing hash throughmkit_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 inmkit-cliormkit-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 mkit_core::sign::domain_digest for the implementation.
11. Attestation integration
Current attestation signing uses mkit_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
mkit_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
mkit_attest::verifyregistry when the trust root contains the emitted public key.
Key ID compatibility:
- New keystore Ed25519 key IDs should be canonical
ed25519:<pubkey>. - Existing repo-key signer may continue using legacy
blake3:<hash(pubkey)>. - Verifier must continue accepting legacy
blake3:for Ed25519.
12. External signer relationship
mkit-rpc already defines a signer protocol with KEY_FORM_OPAQUE_HANDLE.
mkit-keystore does not replace this protocol.
Rules:
- External signers remain valid for TPM, Secure Enclave, CTAP, and custom integrations.
mkit-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 a compatibility analysis.
- 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
mkit key generateor legacymkit 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
mkit-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
mkit-cli must test:
mkit key generatecreates a key through the software backend.mkit key listreports generated keys.mkit key importimports a known Ed25519 seed.mkit key export --unsafe-print-secretround-trips an imported extractable key.mkit 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 compatibility tests
Required golden tests:
- Imported Ed25519 seed through software keystore produces byte-identical
commit signatures to current
mkit_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 MKIT_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
mkit-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 --docIf 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
mkit-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 tomkit_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/mkit-cli/src/commands/key.rs. - Wire
mkit keyinmkit-cli/src/lib.rs. Shipped. - Preserve
mkit keygen. Shipped. - Add config keys and repo-forbidden tests. Shipped — see
KeyConfigandREPO_FORBIDDEN_KEYSinmkit-cli/src/config.rs. - Add CLI integration tests. Shipped.
Attestation integration
- Add keystore-backed
mkit_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
mkit-corehelper needed for keystore commit signing without creating a dependency cycle. Shipped —mkit_core::sign::commit_signing_hashis exposed and used byCommitSigner::Keystoreincommands/commit.rs. - Teach
mkit committo use a configured Ed25519 keystore key ref. Shipped behindsigner = keystore. - Add golden equivalence tests. Shipped — see
keystore_commit_signature_matches_legacy_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 compatibility 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 (MKIT-6). - 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 — MKIT-6): live OS-native backend tests are
gated by
MKIT_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
mkit-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 compatibility 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 mkit key export --file <path> mode is
worth adding. Out of scope for now.
mkit-keystore uses these fixed conservative defaults:
- default backend: encrypted-at-rest software backend (
software) - raw compatibility backend: explicit
software-raw - commit signing source: legacy raw-file
signing_key - attestation signing source: existing
repo-key - new Ed25519 keystore key IDs:
ed25519:<pubkey> - repo-key attestation legacy key IDs:
blake3:<hash(pubkey)> - export mode: stdout only with
--unsafe-print-secret - commit and attestation refs: shared per-algorithm
key.*_refvalues - no silent migration from 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 legacy ones) is in REPO_FORBIDDEN_KEYS (§2.5, §8.2, §13.1) |
| No signing command ever creates or rotates a key | explicit mkit key generate / legacy mkit 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; BLS AAD additionally binds cohort pubkey + share index + M + N (§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 legacy 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 mkit 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) |