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) -> Result<String, CryptoError> {
31    let ssh_keypair = SshEd25519Keypair::from_seed(seed.as_bytes());
32    let keypair_data = KeypairData::Ed25519(ssh_keypair);
33    let private_key = SshPrivateKey::new(keypair_data, "auths-sign")
34        .map_err(|e| CryptoError::SshKeyConstruction(e.to_string()))?;
35
36    let sshsig = SshSig::sign(&private_key, namespace, HashAlg::Sha512, data)
37        .map_err(|e| CryptoError::SigningFailed(e.to_string()))?;
38
39    let pem = sshsig
40        .to_pem(LineEnding::LF)
41        .map_err(|e| CryptoError::PemEncoding(e.to_string()))?;
42
43    Ok(pem)
44}
45
46/// Construct the data blob that SSHSIG signs (the "message to sign").
47///
48/// Format per OpenSSH sshsig.c:
49///   literal "SSHSIG"   -- 6 raw bytes, NO length prefix
50///   string  namespace  -- 4-byte length + data
51///   string  reserved   -- 4-byte length + data (empty)
52///   string  hash_alg   -- 4-byte length + data ("sha512")
53///   string  H(message) -- 4-byte length + sha512(message)
54///
55/// Args:
56/// * `data`: The raw message bytes to hash.
57/// * `namespace`: The SSHSIG namespace (e.g., "git").
58///
59/// Usage:
60/// ```ignore
61/// let blob = construct_sshsig_signed_data(b"commit data", "git")?;
62/// let sig = agent_sign(&socket, &pubkey, &blob)?;
63/// ```
64pub fn construct_sshsig_signed_data(data: &[u8], namespace: &str) -> Result<Vec<u8>, CryptoError> {
65    let mut blob = Vec::new();
66
67    // Magic preamble: 6 raw bytes, NOT a length-prefixed SSH string.
68    blob.extend_from_slice(b"SSHSIG");
69
70    blob.extend_from_slice(&(namespace.len() as u32).to_be_bytes());
71    blob.extend_from_slice(namespace.as_bytes());
72
73    // Reserved (empty)
74    blob.extend_from_slice(&0u32.to_be_bytes());
75
76    let hash_algo = b"sha512";
77    blob.extend_from_slice(&(hash_algo.len() as u32).to_be_bytes());
78    blob.extend_from_slice(hash_algo);
79
80    let mut hasher = Sha512::new();
81    hasher.update(data);
82    let hash = hasher.finalize();
83    blob.extend_from_slice(&(hash.len() as u32).to_be_bytes());
84    blob.extend_from_slice(&hash);
85
86    Ok(blob)
87}
88
89/// Construct the final SSHSIG PEM from a public key and raw signature.
90///
91/// Builds the full SSHSIG binary structure (magic, version, pubkey,
92/// namespace, signature) and encodes it as base64-wrapped PEM.
93///
94/// Args:
95/// * `pubkey`: Raw 32-byte Ed25519 public key.
96/// * `signature`: Raw Ed25519 signature bytes.
97/// * `namespace`: The SSHSIG namespace (e.g., "git").
98///
99/// Usage:
100/// ```ignore
101/// let sig_data = construct_sshsig_signed_data(data, "git")?;
102/// let raw_sig = agent_sign(&socket, &pubkey, &sig_data)?;
103/// let pem = construct_sshsig_pem(&pubkey, &raw_sig, "git")?;
104/// ```
105pub fn construct_sshsig_pem(
106    pubkey: &[u8],
107    signature: &[u8],
108    namespace: &str,
109) -> Result<String, CryptoError> {
110    let mut blob = Vec::new();
111
112    blob.extend_from_slice(b"SSHSIG");
113
114    // Version
115    blob.extend_from_slice(&1u32.to_be_bytes());
116
117    // Public key blob (SSH wire format)
118    let pubkey_blob = encode_ssh_pubkey(pubkey);
119    blob.extend_from_slice(&(pubkey_blob.len() as u32).to_be_bytes());
120    blob.extend_from_slice(&pubkey_blob);
121
122    // Namespace
123    blob.extend_from_slice(&(namespace.len() as u32).to_be_bytes());
124    blob.extend_from_slice(namespace.as_bytes());
125
126    // Reserved (empty)
127    blob.extend_from_slice(&0u32.to_be_bytes());
128
129    // Hash algorithm
130    let hash_algo = b"sha512";
131    blob.extend_from_slice(&(hash_algo.len() as u32).to_be_bytes());
132    blob.extend_from_slice(hash_algo);
133
134    // Signature blob (SSH signature format)
135    let sig_blob = encode_ssh_signature(signature);
136    blob.extend_from_slice(&(sig_blob.len() as u32).to_be_bytes());
137    blob.extend_from_slice(&sig_blob);
138
139    let b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &blob);
140
141    let wrapped: String = b64
142        .chars()
143        .collect::<Vec<_>>()
144        .chunks(70)
145        .map(|c| c.iter().collect::<String>())
146        .collect::<Vec<_>>()
147        .join("\n");
148
149    Ok(format!(
150        "-----BEGIN SSH SIGNATURE-----\n{}\n-----END SSH SIGNATURE-----\n",
151        wrapped
152    ))
153}