rsa_openssl_format/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use length_encoded::{LengthEncodedReader, LengthEncodedWriter};
4use num_bigint_dig::BigUint;
5use rsa::{RsaPublicKey, traits::PublicKeyParts};
6
7mod length_encoded;
8
9const SSH_RSA: &str = "ssh-rsa";
10
11#[derive(Debug, thiserror::Error, PartialEq)]
12pub enum RsaPubKeyError {
13    #[error("Invalid base64.")]
14    InvalidBase64(#[from] data_encoding::DecodeError),
15
16    #[error("Unsupported key type {0} (only RSA keys are supported).")]
17    UnsupportedKeyType(String),
18
19    #[error("Length is invalid (not enough bytes).")]
20    InvalidLength,
21
22    #[error("Malformed (expected `ssh-rsa <base64-encoded data> <comment>`)")]
23    Malformed,
24
25    #[error("RSA error: {0}")]
26    Rsa(#[from] rsa::errors::Error),
27}
28
29pub trait AuthorizedKeysFormat {
30    fn to_openssl(&self, comment: &str) -> String;
31    fn from_openssl(openssl_pubkey: &str) -> Result<(Self, String), RsaPubKeyError>
32    where
33        Self: Sized;
34}
35
36pub fn to_base64(key: &RsaPublicKey) -> String {
37    let mut writer = LengthEncodedWriter::new();
38
39    writer.write_length_encoded(SSH_RSA.as_bytes()).unwrap();
40    writer.write_length_encoded(&key.e().to_bytes_be()).unwrap();
41    let mut modulus_bytes = Vec::with_capacity(key.n().bits() as usize + 1);
42    modulus_bytes.push(0);
43    modulus_bytes.extend(key.n().to_bytes_be());
44    writer.write_length_encoded(&modulus_bytes).unwrap();
45
46    let buf = writer.take();
47    data_encoding::BASE64.encode(&buf)
48}
49
50pub fn from_base64(base64: &str) -> Result<RsaPublicKey, RsaPubKeyError> {
51    let buf = data_encoding::BASE64.decode(base64.as_bytes())?;
52    let mut reader = LengthEncodedReader::new(buf);
53
54    let key_type = reader
55        .read_length_encoded()
56        .map_err(|_| RsaPubKeyError::InvalidLength)?;
57    let key_type = std::str::from_utf8(&key_type)
58        .map_err(|_| RsaPubKeyError::UnsupportedKeyType(format!("{:?}", key_type)))?;
59    if key_type != SSH_RSA {
60        return Err(RsaPubKeyError::UnsupportedKeyType(key_type.to_string()));
61    }
62
63    let e = reader
64        .read_length_encoded()
65        .map_err(|_| RsaPubKeyError::InvalidLength)?;
66    let e = BigUint::from_bytes_be(&e);
67
68    let n = reader
69        .read_length_encoded()
70        .map_err(|_| RsaPubKeyError::InvalidLength)?;
71    let n = BigUint::from_bytes_be(&n);
72
73    Ok(RsaPublicKey::new(n, e)?)
74}
75
76impl AuthorizedKeysFormat for RsaPublicKey {
77    fn to_openssl(&self, comment: &str) -> String {
78        format!("{} {} {}", SSH_RSA, to_base64(self), comment)
79    }
80
81    fn from_openssl(openssl_pubkey: &str) -> Result<(Self, String), RsaPubKeyError> {
82        let Some((key_type, rest)) = openssl_pubkey.split_once(' ') else {
83            return Err(RsaPubKeyError::Malformed);
84        };
85
86        if key_type != SSH_RSA {
87            return Err(RsaPubKeyError::UnsupportedKeyType(key_type.to_string()));
88        }
89
90        let (key, comment) = if let Some((key, comment)) = rest.split_once(' ') {
91            (key, Some(comment.to_string()))
92        } else {
93            (rest, None)
94        };
95
96        let pubkey = from_base64(key)?;
97
98        Ok((pubkey, comment.unwrap_or_default()))
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_round_trips() {
108        let test_keys = include_str!("../test_keys.txt");
109        for line in test_keys.lines() {
110            let line = line.trim();
111
112            let (key, comment) = RsaPublicKey::from_openssl(line).unwrap();
113            let line_with_roundtrip = key.to_openssl(&comment);
114
115            let (key_after_roundtrip, comment_after_roundtrip) =
116                RsaPublicKey::from_openssl(&line_with_roundtrip).unwrap();
117            assert_eq!(key, key_after_roundtrip);
118            assert_eq!(comment, comment_after_roundtrip);
119
120            assert_eq!(line, line_with_roundtrip);
121        }
122    }
123}