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
[]
= "0.1"
Generate an identity
use ;
use IdentitySigner;
let provider = Boxnew;
let signer = new;
signer.generate.expect;
Derive keys from a root secret
use ;
let root_secret = ; // in practice, from a signer
let deriver = new;
// Flat-purpose keys
let git_seed = deriver.derive; // Ed25519 seed
let age_key = deriver.derive; // X25519 private key
let ssh_seed = deriver.derive; // Ed25519 seed
// Parameterized keys (two-level HKDF — structurally collision-free)
let github_ssh = deriver.derive_ssh_user_key.unwrap;
let agent_key = deriver.derive_agent_key.unwrap;
// All keys are deterministic: same root → same keys, always.
Public key derivation
use ;
use ;
let deriver = new;
let git_vk = ed25519_verifying_key;
let age_pk = x25519_public_key;
Lifecycle management with IdentityVault
use IdentityVault;
use ClosurePassphraseProvider;
let provider = Boxnew;
let vault = with_default_path;
// Create — refuses to overwrite (O_EXCL, no TOCTOU race)
vault.init.unwrap;
// Backup before risky operations
vault.backup.unwrap;
// Check existence
assert!;
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 SignerChain;
let chain = new_sorted;
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
= { = "0.1", = false }
# Derivation + public key helpers, no file signer
= { = "0.1", = false, = ["signing"] }
# Full file-based identity (default)
= "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 ;
use ed25519_verifying_key;
use ;
let deriver = new;
let seed = deriver.derive;
let pubkey = ed25519_verifying_key;
let hash = digest;
let identity_hash = encode; // 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,KeyDeriverPRK,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 creation —
O_EXCLprevents 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 RootSecret;
// Ephemeral: CSPRNG-generated, no file, zeroized on drop
let anon = 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