styrene-identity 0.3.2

Deterministic key hierarchy for Styrene mesh nodes — one root secret derives SSH, git signing, age, WireGuard, and agent delegation keys via HKDF-SHA256
Documentation

styrene-identity

Deterministic key hierarchy for Styrene mesh nodes. One root secret derives all protocol-specific keys — RNS, Yggdrasil, WireGuard, SSH, age, git signing, and per-agent delegation keys — via HKDF-SHA256 with domain separation.

Published on crates.io.

Quick start

[dependencies]
styrene-identity = "0.1"

Generate an identity

use styrene_identity::file_signer::{ClosurePassphraseProvider, FileSigner};
use styrene_identity::signer::IdentitySigner;

let provider = Box::new(ClosurePassphraseProvider::new(|| {
    Ok(b"my-passphrase".to_vec())
}));
let signer = FileSigner::new("~/.config/styrene/identity.key", provider);
signer.generate(b"my-passphrase").expect("generate identity");

Derive keys from a root secret

use styrene_identity::derive::{KeyDeriver, KeyPurpose};

let root_secret = [0x42u8; 32]; // in practice, from a signer
let deriver = KeyDeriver::new(&root_secret);

// Flat-purpose keys
let git_seed = deriver.derive(KeyPurpose::GitSigning);   // Ed25519 seed
let age_key  = deriver.derive(KeyPurpose::Age);           // X25519 private key
let ssh_seed = deriver.derive(KeyPurpose::SshHost);       // Ed25519 seed

// Parameterized keys (two-level HKDF — structurally collision-free)
let github_ssh = deriver.derive_ssh_user_key("github").unwrap();
let agent_key  = deriver.derive_agent_key("omegon-primary").unwrap();

// All keys are deterministic: same root → same keys, always.

Public key derivation

use styrene_identity::derive::{KeyDeriver, KeyPurpose};
use styrene_identity::pubkey::{ed25519_verifying_key, x25519_public_key};

let deriver = KeyDeriver::new(&[0x42u8; 32]);

let git_vk = ed25519_verifying_key(&deriver.derive(KeyPurpose::GitSigning));
let age_pk = x25519_public_key(&deriver.derive(KeyPurpose::Age));

Lifecycle management with IdentityVault

use styrene_identity::vault::IdentityVault;
use styrene_identity::file_signer::ClosurePassphraseProvider;

let provider = Box::new(ClosurePassphraseProvider::new(|| {
    Ok(b"my-passphrase".to_vec())
}));
let vault = IdentityVault::with_default_path(provider);

// Create — refuses to overwrite (O_EXCL, no TOCTOU race)
vault.init(b"my-passphrase").unwrap();

// Backup before risky operations
vault.backup("/tmp/identity.key.bak").unwrap();

// Check existence
assert!(vault.exists());

Derivation hierarchy

root_secret (32 bytes)
  │
  HKDF-Extract(salt="styrene-identity-v1", IKM=root_secret) = PRK
  │
  ├─ Expand(PRK, "styrene-rns-encryption-v1")      → RNS X25519
  ├─ Expand(PRK, "styrene-rns-signing-v1")          → RNS Ed25519
  ├─ Expand(PRK, "styrene-yggdrasil-v1")            → Yggdrasil Ed25519
  ├─ Expand(PRK, "styrene-wireguard-v1")            → WireGuard Curve25519
  ├─ Expand(PRK, "styrene-ssh-host-v1")             → SSH host Ed25519
  ├─ Expand(PRK, "styrene-age-v1")                  → age X25519
  ├─ Expand(PRK, "styrene-git-signing-v1")          → git signing Ed25519
  │
  ├─ SSH user keys (two-level HKDF)
  │   salt="styrene-identity-ssh-user-v1"
  │   ├─ "github"  → per-host SSH Ed25519
  │   └─ "work"    → per-host SSH Ed25519
  │
  └─ Agent signing keys (two-level HKDF)
      salt="styrene-identity-agent-v1"
      ├─ "omegon-primary"   → agent commit signing Ed25519
      └─ "omegon-cleave-0"  → worker commit signing Ed25519

Parameterized key families use two-level HKDF with distinct salts per family. Collisions between flat purposes, SSH user keys, and agent keys are structurally impossible — they derive from different IKM, different salts, and different HKDF trees.

Signer tiers

The IdentitySigner trait abstracts over four storage tiers. All tiers produce the same 32-byte root secret — they are different access paths to the same identity.

Tier Backend Feature Status
A YubiKey FIDO2 hmac-secret yubikey Implemented
B iOS Secure Enclave / Android StrongBox Planned
C Bitwarden / 1Password / macOS Keychain Planned
D Encrypted file (argon2id + ChaCha20Poly1305) file-signer (default) Implemented

SignerChain tries signers in tier order (A→D), using the first available:

use styrene_identity::signer::SignerChain;

let chain = SignerChain::new_sorted(vec![
    Box::new(yubikey_signer),  // tried first
    Box::new(file_signer),     // fallback
]);
let root = chain.root_secret().await?;

Feature flags

Feature Default Enables
file-signer yes FileSigner, IdentityVault (argon2, chacha20poly1305)
signing via file-signer pubkey module (ed25519-dalek, x25519-dalek)
yubikey no YubiKeySigner (FIDO2 hmac-secret)
ssh-agent no StyreneAgent SSH agent protocol

Minimal dependency footprint — disable default-features and pick only what you need:

# Just the derivation hierarchy, no file I/O or crypto
styrene-identity = { version = "0.1", default-features = false }

# Derivation + public key helpers, no file signer
styrene-identity = { version = "0.1", default-features = false, features = ["signing"] }

# Full file-based identity (default)
styrene-identity = "0.1"

File format

The Tier D identity file (~/.config/styrene/identity.key) is 97 bytes:

STID [version:1] [salt:32] [nonce:12] [ciphertext:32+16]
 4B      1B          32B       12B          48B
  • Encryption: argon2id (m=64MiB, t=3, p=1) → ChaCha20Poly1305
  • Permissions: 0o600, set atomically at creation via O_EXCL
  • Backward compat: legacy 92-byte headerless files (pre-v1) are still readable

Identity hash

The canonical identity hash is SHA-256 of the RNS signing Ed25519 public key, truncated to 16 bytes (32 hex chars). This is the mesh identity used by Signum, styrened, and cross-service attribution:

use styrene_identity::derive::{KeyDeriver, KeyPurpose};
use styrene_identity::pubkey::ed25519_verifying_key;
use sha2::{Digest, Sha256};

let deriver = KeyDeriver::new(&[0x42u8; 32]);
let seed = deriver.derive(KeyPurpose::RnsSigning);
let pubkey = ed25519_verifying_key(&seed);
let hash = Sha256::digest(pubkey.as_bytes());
let identity_hash = hex::encode(&hash[..16]); // 32 hex chars

Git commit signing

Derived keys work with git's SSH signing (gpg.format = ssh). Agent keys enable cryptographic distinction between human and agent commits:

Committer Key Comment in git log --show-signature
Human GitSigning styrene-git-signing
Agent Agent("omegon-primary") styrene-agent:omegon-primary

All keys trace back to the same root — one identity, multiple signers.

Security properties

  • Zeroize-on-drop for all secret material (RootSecret, KeyDeriver PRK, DerivedKeys, derived seeds)
  • No private keys on disk — the SSH agent derives keys in memory per request
  • Domain-separated HKDF — fixed salt prevents collision with any other HKDF usage
  • Hardened KDF — argon2id params exceed OWASP minimums
  • Atomic file creationO_EXCL prevents overwrites with no TOCTOU race
  • Credential injection — passphrases and PINs via traits, never environment variables

See SECURITY.md for the full threat model and accepted risks.

Linkability warning

All keys derived from one root are cryptographically linked. This is by design for attribution and recovery, but it means derived keys cannot provide anonymity. For anonymous or pseudonymous identities, use an independent root:

use styrene_identity::signer::RootSecret;

// Ephemeral: CSPRNG-generated, no file, zeroized on drop
let anon = RootSecret::ephemeral();

// Or: separate persistent identity
// nex identity init --path ~/.config/styrene/pseudonym.key

See docs/unlinkability.md for the full model, anti-patterns, and decision matrix.

Test vectors

From a root secret of 0x42 repeated 32 times:

RnsEncryption = aefdbd63fb6746c2edb73bba3bcb34f61909077f65fe033c9372b55f6ace0c0c
GitSigning    = 6eb3d3ef12a2447f6de281d6f896eba20ad0b0add3bc6fce80499f36b7343842
SSH(github)   = 3c261af80e084a637fd20e0f7274a4106702894f0d23c47e855f6c9adce20d75
Agent(omegon) = 4dd66edcda091a5e3d15aa3fb8ec32d81e212d94760b61915b1d6f204b0672e2

These are pinned in the test suite. Any implementation of the derivation hierarchy must reproduce them.

Ecosystem usage

Crate/Binary Dependency Purpose
nex styrene-identity = "0.1" nex identity init/show/link — generate and manage identities
aether path dep Mesh node identity and RBAC
auspex path dep (signing only) Operator identity for monitoring agents
vox path dep LXMF mesh identity
styrened workspace member Daemon identity — RNS, SSH agent, mesh signing

License

MIT