Skip to main content

auths_core/crypto/ssh/
encoding.rs

1//! SSH wire-format encoding for public keys and signatures.
2//!
3//! Supports both Ed25519 and ECDSA P-256 key types. Key type is detected
4//! from the public key length; signature encoding requires an explicit
5//! curve parameter since both curves produce 64-byte raw signatures.
6
7use auths_crypto::CurveType;
8
9/// Encode a public key in SSH wire format.
10///
11/// Args:
12/// * `pubkey`: Raw public key bytes.
13/// * `curve`: Which curve this key belongs to.
14///
15/// Usage:
16/// ```
17/// use auths_core::crypto::ssh::encode_ssh_pubkey;
18/// use auths_crypto::CurveType;
19/// let blob = encode_ssh_pubkey(&[0x42u8; 32], CurveType::Ed25519);
20/// assert_eq!(&blob[4..15], b"ssh-ed25519");
21/// ```
22pub fn encode_ssh_pubkey(pubkey: &[u8], curve: CurveType) -> Vec<u8> {
23    match curve {
24        CurveType::Ed25519 => encode_ssh_pubkey_ed25519(pubkey),
25        CurveType::P256 => encode_ssh_pubkey_ecdsa_p256(pubkey),
26    }
27}
28
29/// Encode a raw signature in SSH wire format.
30///
31/// Args:
32/// * `signature`: Raw signature bytes (64 for both Ed25519 and P-256 r||s).
33/// * `curve`: Which curve produced the signature.
34///
35/// Usage:
36/// ```
37/// use auths_core::crypto::ssh::encode_ssh_signature;
38/// use auths_crypto::CurveType;
39/// let blob = encode_ssh_signature(&[0xAB; 64], CurveType::Ed25519);
40/// assert_eq!(&blob[4..15], b"ssh-ed25519");
41/// ```
42pub fn encode_ssh_signature(signature: &[u8], curve: CurveType) -> Vec<u8> {
43    match curve {
44        CurveType::Ed25519 => encode_ssh_sig_ed25519(signature),
45        CurveType::P256 => encode_ssh_sig_ecdsa_p256(signature),
46    }
47}
48
49fn encode_ssh_pubkey_ed25519(pubkey: &[u8]) -> Vec<u8> {
50    let mut blob = Vec::new();
51    let key_type = b"ssh-ed25519";
52    blob.extend_from_slice(&(key_type.len() as u32).to_be_bytes());
53    blob.extend_from_slice(key_type);
54    blob.extend_from_slice(&(pubkey.len() as u32).to_be_bytes());
55    blob.extend_from_slice(pubkey);
56    blob
57}
58
59fn encode_ssh_pubkey_ecdsa_p256(pubkey: &[u8]) -> Vec<u8> {
60    let uncompressed = if pubkey.len() == 33 {
61        decompress_p256(pubkey)
62    } else {
63        pubkey.to_vec()
64    };
65
66    let mut blob = Vec::new();
67    let key_type = b"ecdsa-sha2-nistp256";
68    let curve_name = b"nistp256";
69    blob.extend_from_slice(&(key_type.len() as u32).to_be_bytes());
70    blob.extend_from_slice(key_type);
71    blob.extend_from_slice(&(curve_name.len() as u32).to_be_bytes());
72    blob.extend_from_slice(curve_name);
73    blob.extend_from_slice(&(uncompressed.len() as u32).to_be_bytes());
74    blob.extend_from_slice(&uncompressed);
75    blob
76}
77
78fn decompress_p256(compressed: &[u8]) -> Vec<u8> {
79    use p256::PublicKey;
80    use p256::elliptic_curve::sec1::ToEncodedPoint;
81
82    match PublicKey::from_sec1_bytes(compressed) {
83        Ok(pk) => {
84            let point = pk.to_encoded_point(false);
85            point.as_bytes().to_vec()
86        }
87        Err(_) => compressed.to_vec(),
88    }
89}
90
91fn encode_ssh_sig_ed25519(signature: &[u8]) -> Vec<u8> {
92    let mut blob = Vec::new();
93    let sig_type = b"ssh-ed25519";
94    blob.extend_from_slice(&(sig_type.len() as u32).to_be_bytes());
95    blob.extend_from_slice(sig_type);
96    blob.extend_from_slice(&(signature.len() as u32).to_be_bytes());
97    blob.extend_from_slice(signature);
98    blob
99}
100
101fn encode_ssh_sig_ecdsa_p256(signature: &[u8]) -> Vec<u8> {
102    let r = &signature[..32];
103    let s = &signature[32..];
104
105    let mut inner = Vec::new();
106    inner.extend_from_slice(&encode_mpint(r));
107    inner.extend_from_slice(&encode_mpint(s));
108
109    let mut blob = Vec::new();
110    let sig_type = b"ecdsa-sha2-nistp256";
111    blob.extend_from_slice(&(sig_type.len() as u32).to_be_bytes());
112    blob.extend_from_slice(sig_type);
113    blob.extend_from_slice(&(inner.len() as u32).to_be_bytes());
114    blob.extend_from_slice(&inner);
115    blob
116}
117
118fn encode_mpint(bytes: &[u8]) -> Vec<u8> {
119    let trimmed = match bytes.iter().position(|&b| b != 0) {
120        Some(pos) => &bytes[pos..],
121        None => &bytes[bytes.len() - 1..],
122    };
123
124    let mut buf = Vec::new();
125    if trimmed[0] & 0x80 != 0 {
126        buf.extend_from_slice(&((trimmed.len() + 1) as u32).to_be_bytes());
127        buf.push(0x00);
128        buf.extend_from_slice(trimmed);
129    } else {
130        buf.extend_from_slice(&(trimmed.len() as u32).to_be_bytes());
131        buf.extend_from_slice(trimmed);
132    }
133    buf
134}
135
136/// Encode a big integer for the SSH agent `add_identity` wire format.
137///
138/// Args:
139/// * `bytes`: Big-endian integer bytes (leading zeros are stripped).
140///
141/// Usage:
142/// ```
143/// use auths_core::crypto::ssh::encode_mpint_for_agent;
144/// let encoded = encode_mpint_for_agent(&[0x00, 0x01, 0x02]);
145/// assert_eq!(&encoded[..4], &2u32.to_be_bytes());
146/// ```
147pub fn encode_mpint_for_agent(bytes: &[u8]) -> Vec<u8> {
148    encode_mpint(bytes)
149}