rsa_openssl_format/
lib.rs1#![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}