Skip to main content

auths_core/crypto/ssh/
signatures.rs

1//! SSHSIG signature creation and PEM encoding.
2
3use sha2::{Digest, Sha512};
4use ssh_key::private::{Ed25519Keypair as SshEd25519Keypair, KeypairData};
5use ssh_key::{HashAlg, LineEnding, PrivateKey as SshPrivateKey, SshSig};
6
7use super::CryptoError;
8use super::SecureSeed;
9use super::encoding::{encode_ssh_pubkey, encode_ssh_signature};
10
11/// Create an SSHSIG signature and return it as a PEM-armored string.
12///
13/// Uses the `ssh-key` crate to produce a standard SSHSIG signature from
14/// an Ed25519 seed and arbitrary data, suitable for Git commit signing.
15///
16/// Args:
17/// * `seed`: The Ed25519 private key seed.
18/// * `data`: The raw bytes to sign.
19/// * `namespace`: The SSHSIG namespace (e.g., "git").
20///
21/// Usage:
22/// ```ignore
23/// let pem = create_sshsig(&seed, b"commit data", "git")?;
24/// assert!(pem.starts_with("-----BEGIN SSH SIGNATURE-----"));
25/// ```
26pub fn create_sshsig(
27    seed: &SecureSeed,
28    data: &[u8],
29    namespace: &str,
30    curve: auths_crypto::CurveType,
31) -> Result<String, CryptoError> {
32    match curve {
33        auths_crypto::CurveType::Ed25519 => create_sshsig_ed25519(seed, data, namespace),
34        auths_crypto::CurveType::P256 => create_sshsig_p256(seed, data, namespace),
35    }
36}
37
38fn create_sshsig_ed25519(
39    seed: &SecureSeed,
40    data: &[u8],
41    namespace: &str,
42) -> Result<String, CryptoError> {
43    let ssh_keypair = SshEd25519Keypair::from_seed(seed.as_bytes());
44    let keypair_data = KeypairData::Ed25519(ssh_keypair);
45    let private_key = SshPrivateKey::new(keypair_data, "auths-sign")
46        .map_err(|e| CryptoError::SshKeyConstruction(e.to_string()))?;
47
48    let sshsig = SshSig::sign(&private_key, namespace, HashAlg::Sha512, data)
49        .map_err(|e| CryptoError::SigningFailed(e.to_string()))?;
50
51    sshsig
52        .to_pem(LineEnding::LF)
53        .map_err(|e| CryptoError::PemEncoding(e.to_string()))
54}
55
56fn create_sshsig_p256(
57    seed: &SecureSeed,
58    data: &[u8],
59    namespace: &str,
60) -> Result<String, CryptoError> {
61    use ssh_key::private::EcdsaKeypair;
62
63    use p256::elliptic_curve::sec1::ToEncodedPoint;
64
65    let secret_key = p256::SecretKey::from_slice(seed.as_bytes())
66        .map_err(|e| CryptoError::SigningFailed(format!("P-256 seed: {e}")))?;
67    let public_key = secret_key.public_key();
68
69    let ecdsa_keypair = EcdsaKeypair::NistP256 {
70        public: public_key.to_encoded_point(false), // uncompressed
71        private: ssh_key::private::EcdsaPrivateKey::from(secret_key),
72    };
73
74    let keypair_data = KeypairData::Ecdsa(ecdsa_keypair);
75    let private_key = SshPrivateKey::new(keypair_data, "auths-sign")
76        .map_err(|e| CryptoError::SshKeyConstruction(e.to_string()))?;
77
78    let sshsig = SshSig::sign(&private_key, namespace, HashAlg::Sha256, data)
79        .map_err(|e| CryptoError::SigningFailed(e.to_string()))?;
80
81    sshsig
82        .to_pem(LineEnding::LF)
83        .map_err(|e| CryptoError::PemEncoding(e.to_string()))
84}
85
86/// Construct the data blob that SSHSIG signs (the "message to sign").
87///
88/// Format per OpenSSH sshsig.c:
89///   literal "SSHSIG"   -- 6 raw bytes, NO length prefix
90///   string  namespace  -- 4-byte length + data
91///   string  reserved   -- 4-byte length + data (empty)
92///   string  hash_alg   -- 4-byte length + data ("sha512")
93///   string  H(message) -- 4-byte length + sha512(message)
94///
95/// Args:
96/// * `data`: The raw message bytes to hash.
97/// * `namespace`: The SSHSIG namespace (e.g., "git").
98///
99/// Usage:
100/// ```ignore
101/// let blob = construct_sshsig_signed_data(b"commit data", "git")?;
102/// let sig = agent_sign(&socket, &pubkey, &blob)?;
103/// ```
104pub fn construct_sshsig_signed_data(data: &[u8], namespace: &str) -> Result<Vec<u8>, CryptoError> {
105    let mut blob = Vec::new();
106
107    // Magic preamble: 6 raw bytes, NOT a length-prefixed SSH string.
108    blob.extend_from_slice(b"SSHSIG");
109
110    blob.extend_from_slice(&(namespace.len() as u32).to_be_bytes());
111    blob.extend_from_slice(namespace.as_bytes());
112
113    // Reserved (empty)
114    blob.extend_from_slice(&0u32.to_be_bytes());
115
116    let hash_algo = b"sha512";
117    blob.extend_from_slice(&(hash_algo.len() as u32).to_be_bytes());
118    blob.extend_from_slice(hash_algo);
119
120    let mut hasher = Sha512::new();
121    hasher.update(data);
122    let hash = hasher.finalize();
123    blob.extend_from_slice(&(hash.len() as u32).to_be_bytes());
124    blob.extend_from_slice(&hash);
125
126    Ok(blob)
127}
128
129/// Construct the final SSHSIG PEM from a public key, curve, and raw signature.
130///
131/// Builds the full SSHSIG binary structure (magic, version, pubkey,
132/// namespace, signature) and encodes it as base64-wrapped PEM.
133///
134/// Args:
135/// * `pubkey`: Raw public key bytes.
136/// * `signature`: Raw signature bytes.
137/// * `namespace`: The SSHSIG namespace (e.g., "git").
138/// * `curve`: The curve type of the key/signature.
139///
140/// Usage:
141/// ```ignore
142/// let sig_data = construct_sshsig_signed_data(data, "git")?;
143/// let raw_sig = agent_sign(&socket, &pubkey, &sig_data)?;
144/// let pem = construct_sshsig_pem(&pubkey, &raw_sig, "git", CurveType::Ed25519)?;
145/// ```
146pub fn construct_sshsig_pem(
147    pubkey: &[u8],
148    signature: &[u8],
149    namespace: &str,
150    curve: auths_crypto::CurveType,
151) -> Result<String, CryptoError> {
152    let mut blob = Vec::new();
153
154    blob.extend_from_slice(b"SSHSIG");
155
156    // Version
157    blob.extend_from_slice(&1u32.to_be_bytes());
158
159    // Public key blob (SSH wire format)
160    let pubkey_blob = encode_ssh_pubkey(pubkey, curve);
161    blob.extend_from_slice(&(pubkey_blob.len() as u32).to_be_bytes());
162    blob.extend_from_slice(&pubkey_blob);
163
164    // Namespace
165    blob.extend_from_slice(&(namespace.len() as u32).to_be_bytes());
166    blob.extend_from_slice(namespace.as_bytes());
167
168    // Reserved (empty)
169    blob.extend_from_slice(&0u32.to_be_bytes());
170
171    // Hash algorithm
172    let hash_algo = b"sha512";
173    blob.extend_from_slice(&(hash_algo.len() as u32).to_be_bytes());
174    blob.extend_from_slice(hash_algo);
175
176    // Signature blob (SSH signature format)
177    let sig_blob = encode_ssh_signature(signature, curve);
178    blob.extend_from_slice(&(sig_blob.len() as u32).to_be_bytes());
179    blob.extend_from_slice(&sig_blob);
180
181    let b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &blob);
182
183    let wrapped: String = b64
184        .chars()
185        .collect::<Vec<_>>()
186        .chunks(70)
187        .map(|c| c.iter().collect::<String>())
188        .collect::<Vec<_>>()
189        .join("\n");
190
191    Ok(format!(
192        "-----BEGIN SSH SIGNATURE-----\n{}\n-----END SSH SIGNATURE-----\n",
193        wrapped
194    ))
195}