SPEC-KEYSTORE.md

This Kit feature is not available yet.

In docs/specs/

Preview

Text · 64.1 KiB

spec: 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-keystore crate, public API (Keystore, KeySigner, KeyGenerator, KeyImporter, KeyOpener, KeyLister, KeyExporter, KeyDeleter), structured Error enum, typed wrappers (KeyLabel, KeyRefLabel, KeyId, PublicKeyBytes, KeyRef, KeySelector).
  • Capabilities reporting 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>), reusing mkit_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.)
  • YubiKey backend behind backend-yubikey — OpenPGP signing slot (Ed25519) and PIV signing slot (P-256). secp256k1 is reported UnsupportedAlgorithm; 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::Memory exists in the enum, but no MemoryKeystore is implemented in this build).
  • External signer keystore bridge (BackendKind::External) — open_backend returns BackendUnavailable; external signing continues to live in mkit-rpc per §12.
  • Cloud KMS backend (BackendKind::Cloud) — same as above.
  • Secure Enclave, Windows TPM/CNG provider-backed keys, TPM/PCR-bound systemd-creds semantics, bounded PIN/touch prompt providers for YubiKey, hardware ECDSA verification-equivalence CI for every required platform.

2. Non-negotiable design decisions

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

3. Scope

3.1 In scope

  • 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-cli integration through mkit key {generate,list,import,export,delete}.
  • mkit-attest integration through a keystore-backed implementation of its existing Signer trait.
  • 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-creds behavior 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-keystore

Workspace integration:

  • Add crates/mkit-keystore to rust/Cargo.toml workspace 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-keystore default features remain empty/lean. Production mkit-cli builds, packages, and CI release gates must enable the platform-appropriate encrypted software protector feature so the configured software default 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.md and rust/deny.toml.
  • OS-specific dependencies must be optional and feature-gated.

Forbidden dependencies:

  • mkit-core must not depend on mkit-keystore.
  • mkit-wasm must not depend on native mkit-keystore backends.
  • 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:

  • Algorithm must convert to and from mkit_attest::Algorithm when the attest integration feature is enabled.
  • Canonical string forms are exactly ed25519, secp256k1, p256, and bls12381-thr (the BLS variant matches the same prefix as mkit_attest::Algorithm::Bls12381Threshold).
  • The enum must not include backend-specific variants.
  • Bls12381Threshold is feature-gated behind bls-threshold. The generic KeyImporter / KeyExporter traits MUST reject this variant with UnsupportedAlgorithm — BLS shares are variable-length (≈52 bytes for MinSig) and the 32-byte SecretKey type 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 = true means export may return secret material.
  • extractable = false means export must fail with a typed non-extractable error.
  • require_user_presence = true requests a per-operation user-presence gate such as Touch ID, PIN, biometric prompt, or hardware touch.
  • device_bound = true requests 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 Keystore registry. 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.key semantics.
  • 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 zeroize or equivalent workspace-approved zeroization.
  • Secret bytes must not be public fields.
  • Debug must 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_secret or into_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:

  • sign takes &mut self to support backends with sessions, prompts, retry counters, cached public-key state, or external connections.
  • The trait is Send, not Send + Sync, intentionally. Issue #104 sketched Send + Sync, but the implementation needs &mut self signing because many real backends have session state, prompt state, retry counters, cached handles, or child-process state. A concrete signer may still be Sync, but callers must not rely on concurrent signing through one handle.
  • sign signs 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-core commit signing hash described in section 10.
  • secp256k1/P-256 signing follows existing mkit-attest semantics: ECDSA over SHA-256 of the supplied bytes, compact r || 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 || v with s <= n / 2 and v of 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 software or software-raw backend implements it. Every other signer — the OS-native backends and YubiKey, where secp256k1 is anyway UnsupportedAlgorithm — keeps the default, which fails closed with UnsupportedOperation. A non-secp256k1 software key returns UnsupportedAlgorithm.
  • It never exports secret material, and callers must not fall back to KeyExporter when 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).
  • generate and import must refuse to overwrite an existing (label, algorithm) unless options.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.
  • open must fail if the selector is missing or ambiguous.
  • list must return deterministic ordering by (backend, label, algorithm) where the backend can list keys.
  • delete must delete only the selected key and must not delete by prefix.
  • export must fail for non-extractable keys.
  • Operation support must be represented by operation-specific traits available through Keystore; non-exportable backends must not implement KeyExporter.
  • Public APIs that accept labels, key references, key IDs, or public key bytes must use typed wrappers rather than unconstrained String values.

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/ when XDG_DATA_HOME is 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 as mkit keygen, signing_key, and repo-key compatibility.
  • The software backend reuses the existing hardened raw-file functions, behind the mkit-keystore abstraction.
  • If it writes files, it must preserve or improve current hardening:
    • Unix 0600 key files.
    • Unix 0700 parent directories.
    • effective-uid owner checks.
    • no symlink final component.
    • symlink ancestor rejection.
    • atomic tmp + fsync + rename + parent fsync writes.
  • 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 hardened 0600 raw-key behavior of legacy mkit 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 software backend 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-creds for headless/server Linux.
    • On Linux, the software backend 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 select systemd-creds for 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::Software and 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::SoftwareRaw and 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 tag

algorithm_id registry:

ValueAlgorithm
0x01Ed25519
0x02Secp256k1
0x03P256
0x04Reserved — 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):

BitMeaning
0extractable
1require_user_presence
2device_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 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.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 MinSig variant — the wire-encoded commonware_cryptography::bls12381::primitives::group::Share) so they cannot ride the generic KeyImporter / KeyExporter paths (which are pinned at 32 bytes). The keystore exposes a dedicated BLS-share API on SoftwareKeystore:
    • store_bls_share(label, share_bytes, cohort_public_key, share_index, threshold, total, keyid, overwrite)
    • load_bls_share(label) -> LoadedBlsShare
    • delete_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 to EncryptedKeyRecord with magic MKITKSB1 (canonical EncryptedKeyRecord uses MKITKSV1). 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 the software backend when the bls-threshold feature is on. The software-raw backend MUST NOT advertise BLS — raw storage cannot bind AAD.
  • Generic KeyImporter / KeyExporter / KeyGenerator calls with algorithm == Bls12381Threshold MUST fail closed with UnsupportedAlgorithm or UnsupportedOperation. Generation of BLS shares flows through a separate trusted-dealer ceremony — see SPEC-RELEASE-THRESHOLD.md §2.1 and mkit-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 reject Bls12381Threshold with UnsupportedAlgorithm. 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-keychain works.
  • 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 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly and kSecAttrSynchronizable = false is 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-service works.
  • 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-creds backend may shell out to systemd-creds rather than link TPM2 libraries.
  • Shelling out must avoid shell interpolation. Use Command with argv tokens.
  • Backend must clearly report when systemd-creds is not available.
  • Treats systemd-creds as 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-attest and 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, and can_delete are all false; provisioning is done out of band with ykman / gpg --card-edit.
  • Capabilities::supports_user_presence is derived from the discovered slot policies (PinPolicy != Never or TouchPolicy ∈ { Always, Cached }). supports_device_bound and supports_non_extractable are 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 returns UnsupportedOperation (directing callers to the CTAP external signer), while any other label that does not resolve to an OpenPGP or PIV signing slot returns KeyNotFound. 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-rpc signer 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_str for 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-key

Interpretation:

  • signer selects the commit-signing source. Built-in default is legacy, which preserves existing raw-file signing_key behavior. Setting signer = keystore opts commit signing into mkit-keystore.
  • key.backend is the default backend for mkit key commands when no backend is provided and no full key ref is being used.
  • key.default_ref is the default key ref for generic commands.
  • Per-algorithm refs override generic refs.
  • attest.signer = repo-key remains the built-in default. Setting attest.signer = keystore opts attestation signing into the keystore-backed attestation signer.
  • Existing attest.signer = repo-key and attest.signer = external remain supported.

Precedence:

  • Commit signing always uses Ed25519. If signer = legacy or empty, commit signing uses the existing signing_key raw-file behavior. If signer = keystore, commit signing uses key.ed25519_ref; if empty, it uses key.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, or key.p256_ref. If the selected per-algorithm ref is empty, it uses key.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:main selects the YubiKey backend for signing; it must not be silently reinterpreted as software:main because key.backend = software is 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_args

Compatibility behavior:

  • signing_key continues to drive legacy raw-file Ed25519 commit signing.
  • attest.*_key_path continues 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, fallback software.
  • --label: parsed from the algorithm-specific configured key ref; if no ref is configured, defaults to default for Ed25519, default-secp256k1 for secp256k1, and default-p256 for 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::generate with GenerateOptions { overwrite: false } by default.
  • Must call Keystore::generate with GenerateOptions { overwrite: true } when --force is provided.
  • Must refuse overwrite unless --force is provided.
  • Must print backend, label, algorithm, public key, key ID, and capabilities.
  • If --print-pubkey is 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::import with ImportOptions { overwrite: false } by default.
  • Must call Keystore::import with ImportOptions { overwrite: true } when --force is provided.
  • Must refuse overwrite unless --force is 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-secret

Behavior:

  • Export is intentionally noisy and must require --unsafe-print-secret when 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]
                --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 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-core continues to own canonical commit/remix signing bytes and verification.
  • mkit-core must expose enough public helpers to support keystore signing without depending on mkit-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 as BLAKE3(len_le16(COMMIT_DOMAIN) || COMMIT_DOMAIN || commit_signing_bytes(commit)).
  • Ed25519 keystore commit signing must produce the same commit signature as current KeyPair signing 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-cli that obtains the commit signing hash through mkit_core::sign::commit_signing_hash, asks the Ed25519 keystore signer to sign exactly those 32 bytes, then stores the resulting 64-byte signature in Commit.signature.
  • Do not duplicate domain_digest construction in mkit-cli or mkit-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::Signer trait.
  • 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-key and external signers remain supported.
  • attest_factory must accept attest.signer = keystore only from user-scoped config or CLI arguments.
  • Keystore-backed signatures must verify through the existing mkit_attest::verify registry 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-keystore may 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_HANDLE and 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 generate or legacy mkit 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 generate creates a key through the software backend.
  • mkit key list reports generated keys.
  • mkit key import imports a known Ed25519 seed.
  • mkit key export --unsafe-print-secret round-trips an imported extractable key.
  • mkit key delete --yes deletes 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::KeyPair for 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 Secp256k1Signer for the same PAE.
  • Imported P-256 scalar through software keystore produces byte-identical DSSE signature to existing P256Signer for 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 --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 mkit-keystore crate. Shipped.
  • Add core API, errors, label/key-ref parsing, capabilities. Shipped.
  • Add memory backend for tests. Not shipped — BackendKind::Memory is defined but no MemoryKeystore implementation exists. Software-raw with a tempfile-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 to mkit_core::sign::{load_raw_32, save_raw_32, save_raw_32_create_new}.
  • Add unit tests. Shipped.

CLI surface

  • Add commands/key.rs. Shipped at rust/crates/mkit-cli/src/commands/key.rs.
  • Wire mkit key in mkit-cli/src/lib.rs. Shipped.
  • Preserve mkit keygen. Shipped.
  • Add config keys and repo-forbidden tests. Shipped — see KeyConfig and REPO_FORBIDDEN_KEYS in mkit-cli/src/config.rs.
  • Add CLI integration tests. Shipped.

Attestation integration

  • Add keystore-backed mkit_attest::Signer adapter. Shipped as KeystoreAttestSigner in commands/attest_factory.rs.
  • Extend attest_factory for attest.signer = keystore. Shipped.
  • Add DSSE signing and verification tests. Shipped.

Commit integration

  • Add or expose the minimal mkit-core helper needed for keystore commit signing without creating a dependency cycle. Shipped — mkit_core::sign::commit_signing_hash is exposed and used by CommitSigner::Keystore in commands/commit.rs.
  • Teach mkit commit to use a configured Ed25519 keystore key ref. Shipped behind signer = keystore.
  • Add golden equivalence tests. Shipped — see keystore_commit_signature_matches_legacy_keypair_signature in commands/commit.rs tests.

Backend matrix

  • Make software the 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 as SoftwareRawKeystore.
  • 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_backend plus selection_for in commands/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=1 per §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.md does 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-keystore includes 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 software backend is encrypted-at-rest; raw compatibility is explicit via software-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.*_ref defaults; 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.*_ref values
  • 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.

InvariantEnforced by
A repo can never choose the key, backend, label, signer, or algorithm that signsevery §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 keyexplicit 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 oneconstruction/operations return unavailable errors; Linux protector selection fails closed (§5.3, §6.1)
Capabilities never overstate what a backend can dooperation booleans tied to trait availability; per-backend honesty rules (§2.7, §5.3, §13.4)
Requested key attributes are honored or refused, never silently weakenedbackends reject unsupported attribute combinations (§5.2, §13.4)
Secret bytes never leak via Debug, logs, or implicit pathsZeroizing 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 passwordper-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 metadataAAD 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 keyoverwrite requires --force / overwrite: true; no prefix deletes (§5.7, §9.1, §9.5)
A configured full key ref's backend is authoritativethe ref routes to its backend; never reinterpreted via key.backend (§7, §8.1)
Keystore signing is byte-equal to legacy signing where determinism allowsgolden equivalence vectors for deterministic software backends; verification-equivalence otherwise (§2.8, §14.3)
The keystore never invents protocol-level domain separationsign 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 APIdedicated label guard returns UnsupportedOperation, routing to the external signer path (§6.7)

Files