Skip to main content

auths_core/crypto/
signer.rs

1//! Signing key types.
2
3use crate::config::current_algorithm;
4use crate::crypto::encryption::{decrypt_bytes, encrypt_bytes};
5use crate::error::AgentError;
6use auths_crypto::{CurveType, SecureSeed, TypedSeed};
7use auths_verifier::DevicePublicKey;
8use ssh_agent_lib::ssh_key::{Algorithm as SshAlgorithm, EcdsaCurve};
9use zeroize::Zeroizing;
10
11/// A trait implemented by key types that can sign messages and return their public key.
12pub trait SignerKey: Send + Sync + 'static {
13    /// Returns the public key bytes of this key.
14    fn public_key_bytes(&self) -> Vec<u8>;
15
16    /// Returns the key kind (i.e., SSH algorithm) of this key.
17    fn kind(&self) -> SshAlgorithm;
18
19    /// Signs a message and returns the signature bytes.
20    fn sign(&self, message: &[u8]) -> Result<Vec<u8>, AgentError>;
21}
22
23/// SignerKey implementation backed by a curve-tagged seed.
24///
25/// carries `DevicePublicKey` instead of bare `[u8; 32]` so callers
26/// can't accidentally drop curve information.
27pub struct SeedSignerKey {
28    seed: SecureSeed,
29    public_key: DevicePublicKey,
30    curve: CurveType,
31}
32
33impl SeedSignerKey {
34    /// Create a `SignerKey` from a seed and pre-computed 32-byte Ed25519 pubkey.
35    /// Back-compat constructor — prefer [`SeedSignerKey::new_typed`] when curve
36    /// is known.
37    pub fn new(seed: SecureSeed, public_key: [u8; 32]) -> Self {
38        #[allow(clippy::expect_used)] // INVARIANT: Ed25519 pubkey is always 32 bytes
39        let dpk = DevicePublicKey::try_new(CurveType::Ed25519, &public_key)
40            .expect("Ed25519 public key is always 32 bytes");
41        Self {
42            seed,
43            public_key: dpk,
44            curve: CurveType::Ed25519,
45        }
46    }
47
48    /// Create a curve-tagged `SignerKey` from a seed + typed public key.
49    pub fn new_typed(seed: SecureSeed, public_key: DevicePublicKey) -> Self {
50        let curve = public_key.curve();
51        Self {
52            seed,
53            public_key,
54            curve,
55        }
56    }
57
58    /// Create a `SignerKey` by deriving the public key from an Ed25519 seed.
59    /// Preserved for back-compat; use [`SeedSignerKey::from_typed_seed`] for
60    /// curve-aware construction.
61    pub fn from_seed(seed: SecureSeed) -> Result<Self, AgentError> {
62        let pk_vec = auths_crypto::typed_public_key(&TypedSeed::Ed25519(*seed.as_bytes()))
63            .map_err(|e| {
64                AgentError::CryptoError(format!("Failed to derive public key from seed: {}", e))
65            })?;
66        #[allow(clippy::expect_used)] // INVARIANT: Ed25519 public key is always 32 bytes
67        let public_key: [u8; 32] = pk_vec.try_into().expect("Ed25519 public key is 32 bytes");
68        Ok(Self::new(seed, public_key))
69    }
70
71    /// Create a `SignerKey` from a curve-tagged `TypedSeed`.
72    pub fn from_typed_seed(seed: TypedSeed) -> Result<Self, AgentError> {
73        let curve = seed.curve();
74        let pk_bytes = auths_crypto::typed_public_key(&seed)
75            .map_err(|e| AgentError::CryptoError(format!("Failed to derive pk: {e}")))?;
76        let dpk = DevicePublicKey::try_new(curve, &pk_bytes)
77            .map_err(|e| AgentError::CryptoError(format!("Invalid derived pk: {e}")))?;
78        Ok(Self {
79            seed: seed.to_secure_seed(),
80            public_key: dpk,
81            curve,
82        })
83    }
84
85    /// Returns the curve of the underlying signing key.
86    pub fn curve(&self) -> CurveType {
87        self.curve
88    }
89
90    /// Returns a typed view of the public key.
91    pub fn typed_public_key(&self) -> &DevicePublicKey {
92        &self.public_key
93    }
94}
95
96impl SignerKey for SeedSignerKey {
97    fn public_key_bytes(&self) -> Vec<u8> {
98        self.public_key.as_bytes().to_vec()
99    }
100
101    fn kind(&self) -> SshAlgorithm {
102        match self.curve {
103            CurveType::Ed25519 => SshAlgorithm::Ed25519,
104            CurveType::P256 => SshAlgorithm::Ecdsa {
105                curve: EcdsaCurve::NistP256,
106            },
107        }
108    }
109
110    fn sign(&self, message: &[u8]) -> Result<Vec<u8>, AgentError> {
111        let typed = match self.curve {
112            CurveType::Ed25519 => TypedSeed::Ed25519(*self.seed.as_bytes()),
113            CurveType::P256 => TypedSeed::P256(*self.seed.as_bytes()),
114        };
115        auths_crypto::typed_sign(&typed, message)
116            .map_err(|e| AgentError::CryptoError(format!("{} signing failed: {e}", self.curve)))
117    }
118}
119
120/// Extract a SecureSeed from key bytes in various formats.
121///
122/// Delegates to [`auths_crypto::parse_key_material`] which detects the curve.
123pub fn extract_seed_from_key_bytes(bytes: &[u8]) -> Result<SecureSeed, AgentError> {
124    let parsed = auths_crypto::parse_key_material(bytes)
125        .map_err(|e| AgentError::KeyDeserializationError(e.to_string()))?;
126    Ok(parsed.seed.to_secure_seed())
127}
128
129/// Extract a SecureSeed, public key bytes, and curve type from key bytes.
130///
131/// Delegates to [`auths_crypto::parse_key_material`] which detects the curve
132/// and extracts the public key in one pass. The curve is preserved so callers
133/// never need to infer it from key length.
134pub fn load_seed_and_pubkey(
135    bytes: &[u8],
136) -> Result<(SecureSeed, Vec<u8>, auths_crypto::CurveType), AgentError> {
137    let parsed = auths_crypto::parse_key_material(bytes)
138        .map_err(|e| AgentError::KeyDeserializationError(e.to_string()))?;
139    let curve = parsed.seed.curve();
140    Ok((parsed.seed.to_secure_seed(), parsed.public_key, curve))
141}
142
143/// Encrypts a raw serialized keypair using the configured encryption algorithm and passphrase.
144pub fn encrypt_keypair(raw: &[u8], passphrase: &str) -> Result<Vec<u8>, AgentError> {
145    encrypt_bytes(raw, passphrase, current_algorithm())
146}
147
148/// Decrypts a previously encrypted keypair using the configured encryption algorithm and passphrase.
149pub fn decrypt_keypair(
150    encrypted: &[u8],
151    passphrase: &str,
152) -> Result<Zeroizing<Vec<u8>>, AgentError> {
153    Ok(Zeroizing::new(decrypt_bytes(encrypted, passphrase)?))
154}