Skip to main content

shuttle/
auth.rs

1use ed25519_dalek::SigningKey;
2use russh_keys::key::KeyPair;
3use std::sync::Arc;
4use styrene_identity::derive::KeyDeriver;
5use styrene_identity::signer::{RootSecret, SignerError};
6use zeroize::Zeroize;
7
8/// Derive an SSH key pair for a given identity label.
9///
10/// Uses two-level HKDF: root_secret → ssh-user-master → per-label Ed25519 seed.
11/// The seed exists in memory only during key construction and is zeroized after.
12pub fn derive_key_pair(root: &RootSecret, label: &str) -> Result<Arc<KeyPair>, AuthError> {
13    let deriver = KeyDeriver::new(root.as_bytes());
14    let mut seed = deriver
15        .derive_ssh_user_key(label)
16        .map_err(|e| AuthError::Derivation(e.to_string()))?;
17    let signing_key = SigningKey::from_bytes(&seed);
18    seed.zeroize();
19    Ok(Arc::new(KeyPair::Ed25519(signing_key)))
20}
21
22/// Derive the public key bytes for a label (for fingerprint display, authorized_keys export).
23pub fn derive_public_key_bytes(root: &RootSecret, label: &str) -> Result<[u8; 32], AuthError> {
24    let deriver = KeyDeriver::new(root.as_bytes());
25    let mut seed = deriver
26        .derive_ssh_user_key(label)
27        .map_err(|e| AuthError::Derivation(e.to_string()))?;
28    let signing_key = SigningKey::from_bytes(&seed);
29    seed.zeroize();
30    let verifying_key = signing_key.verifying_key();
31    Ok(verifying_key.to_bytes())
32}
33
34/// Compute a hex fingerprint of the public key for display.
35pub fn public_key_fingerprint(root: &RootSecret, label: &str) -> Result<String, AuthError> {
36    let pubkey_bytes = derive_public_key_bytes(root, label)?;
37    Ok(hex::encode(pubkey_bytes))
38}
39
40#[derive(Debug, thiserror::Error)]
41pub enum AuthError {
42    #[error("key derivation failed: {0}")]
43    Derivation(String),
44    #[error("signer error: {0}")]
45    Signer(#[from] SignerError),
46    #[error("authentication rejected by server")]
47    Rejected,
48}