rustica_keys/ssh/pubkey.rs
1use ring::digest;
2
3use std::fmt;
4use std::fs::File;
5use std::io::{self, Read};
6use std::path::Path;
7
8use super::keytype::{Curve, CurveKind};
9
10use super::error::{Error, ErrorKind, Result};
11use super::keytype::{KeyType, KeyTypeKind};
12use super::reader::Reader;
13use super::writer::Writer;
14
15
16/// A type which represents the different kinds a public key can be.
17#[derive(Debug, PartialEq, Clone)]
18pub enum PublicKeyKind {
19 /// Represents an RSA public key.
20 Rsa(RsaPublicKey),
21
22 /// Represents an ECDSA public key.
23 Ecdsa(EcdsaPublicKey),
24
25 /// Represents an ED25519 public key.
26 Ed25519(Ed25519PublicKey),
27}
28
29/// RSA public key.
30/// The format of RSA public keys is described in RFC 4253, section 6.6
31#[derive(Debug, PartialEq, Clone)]
32pub struct RsaPublicKey {
33 /// Exponent of key.
34 pub e: Vec<u8>,
35
36 /// Modulus of key.
37 pub n: Vec<u8>,
38}
39
40/// ECDSA public key.
41/// The format of ECDSA public keys is described in RFC 5656, section 3.1.
42#[derive(Debug, PartialEq, Clone)]
43pub struct EcdsaPublicKey {
44 /// The curve being used.
45 pub curve: Curve,
46
47 /// The public key.
48 pub key: Vec<u8>,
49}
50
51/// ED25519 public key.
52/// The format of ED25519 public keys is described in https://tools.ietf.org/html/draft-bjh21-ssh-ed25519-02
53#[derive(Debug, PartialEq, Clone)]
54pub struct Ed25519PublicKey {
55 /// The public key.
56 pub key: Vec<u8>,
57}
58
59
60/// A type which represents an OpenSSH public key.
61#[derive(Debug, PartialEq, Clone)]
62pub struct PublicKey {
63 /// Key type.
64 pub key_type: KeyType,
65
66 /// The kind of public key.
67 pub kind: PublicKeyKind,
68
69 /// Associated comment, if any.
70 pub comment: Option<String>,
71}
72
73impl fmt::Display for PublicKey {
74 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
75 let comment = match &self.comment {
76 Some(c) => c,
77 None => "",
78 };
79
80 write!(
81 f,
82 "{} {} {}",
83 self.key_type,
84 base64::encode(&self.encode()),
85 comment
86 )
87 }
88}
89
90/// The `FingerprintKind` enum represents the different fingerprint representation.
91#[derive(Debug, PartialEq)]
92pub enum FingerprintKind {
93 /// A kind used to represent the fingerprint using SHA256.
94 Sha256,
95
96 /// A kind used to represent the fingerprint using SHA384.
97 Sha384,
98
99 /// A kind used to represent the fingerprint using SHA512.
100 Sha512,
101}
102
103impl fmt::Display for FingerprintKind {
104 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
105 let kind = match *self {
106 FingerprintKind::Sha256 => "SHA256",
107 FingerprintKind::Sha384 => "SHA384",
108 FingerprintKind::Sha512 => "SHA512",
109 };
110
111 write!(f, "{}", kind)
112 }
113}
114
115/// A type that represents an OpenSSH public key fingerprint.
116#[derive(Debug)]
117pub struct Fingerprint {
118 /// The kind used to represent the fingerprint.
119 pub kind: FingerprintKind,
120
121 /// The computed fingerprint.
122 pub hash: String,
123}
124
125impl fmt::Display for Fingerprint {
126 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
127 write!(f, "{}:{}", self.kind, self.hash)
128 }
129}
130
131impl Fingerprint {
132 /// Computes the fingerprint of a byte sequence using a given fingerprint representation.
133 ///
134 /// This method computes a fingerprint the way OpenSSH does it and is generally being
135 /// used to compute the fingerprint of an already encoded OpenSSH public key.
136 ///
137 /// # Example
138 /// ```rust
139 /// # use rustica_keys::ssh::{Fingerprint, FingerprintKind};
140 /// let fp = Fingerprint::compute(FingerprintKind::Sha256, "some data".as_bytes());
141 /// assert_eq!(fp.kind, FingerprintKind::Sha256);
142 /// assert_eq!(fp.hash, "EweZDmulyhRes16ZGCqb7EZTG8VN32VqYCx4D6AkDe4");
143 /// ```
144 pub fn compute<T: ?Sized + AsRef<[u8]>>(kind: FingerprintKind, data: &T) -> Fingerprint {
145 let digest = match kind {
146 FingerprintKind::Sha256 => digest::digest(&digest::SHA256, &data.as_ref()).as_ref().to_vec(),
147 FingerprintKind::Sha384 => digest::digest(&digest::SHA384, &data.as_ref()).as_ref().to_vec(),
148 FingerprintKind::Sha512 => digest::digest(&digest::SHA512, &data.as_ref()).as_ref().to_vec(),
149 };
150
151 let mut encoded = base64::encode(&digest);
152
153 // Trim padding characters from end
154 let hash = match encoded.find('=') {
155 Some(offset) => encoded.drain(..offset).collect(),
156 None => encoded,
157 };
158
159 Fingerprint {
160 kind,
161 hash,
162 }
163 }
164}
165
166impl PublicKey {
167 /// Reads an OpenSSH public key from a given path.
168 ///
169 /// # Examples
170 ///
171 /// ```rust
172 /// # use rustica_keys::ssh::PublicKey;
173 /// # fn example() {
174 /// let key = PublicKey::from_path("/path/to/id_ed25519.pub");
175 /// # }
176 /// ```
177 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<PublicKey> {
178 let mut contents = String::new();
179 File::open(path)?.read_to_string(&mut contents)?;
180
181 PublicKey::from_string(&contents)
182 }
183
184 /// Reads an OpenSSH public key from a given string.
185 ///
186 /// # Examples
187 ///
188 /// ```rust
189 /// # use rustica_keys::ssh::PublicKey;
190 /// let key = PublicKey::from_string("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHkbe7gwx7s0dlApEEzpUyOAPrzPLy4czEZw/sh8m8rd me@home").unwrap();
191 /// let fp = key.fingerprint();
192 /// assert_eq!(fp.hash, "ciQkdxjFUhk2E2vRkWJD9kB8pi+EneOkaCJJHNWzPC4");
193 /// ```
194 pub fn from_string(contents: &str) -> Result<PublicKey> {
195 let mut iter = contents.split_whitespace();
196
197 let kt_name = iter
198 .next()
199 .ok_or_else(|| Error::with_kind(ErrorKind::InvalidFormat))?;
200
201 let data = iter
202 .next()
203 .ok_or_else(|| Error::with_kind(ErrorKind::InvalidFormat))?;
204
205 let comment = iter.next().map(String::from);
206
207 let key_type = KeyType::from_name(&kt_name)?;
208
209 let decoded = base64::decode(&data)?;
210 let mut reader = Reader::new(&decoded);
211
212 // Validate key type before reading rest of the data
213 let kt_from_reader = reader.read_string()?;
214 if kt_name != kt_from_reader {
215 return Err(Error::with_kind(ErrorKind::KeyTypeMismatch));
216 }
217
218 // Construct a new `PublicKey` value and preserve the `comment` value.
219 let k = PublicKey::from_reader(&kt_name, &mut reader)?;
220 let key = PublicKey {
221 key_type,
222 kind: k.kind,
223 comment,
224 };
225
226 Ok(key)
227 }
228
229 /// Reads a public key from a given byte sequence.
230 ///
231 /// The byte sequence is expected to be the base64 decoded body of the public key.
232 ///
233 /// # Example
234 ///
235 /// ```rust
236 /// # use rustica_keys::ssh::PublicKey;
237 /// let data = vec![0, 0, 0, 11, 115, 115, 104, 45,
238 /// 101, 100, 50, 53, 53, 49, 57,
239 /// 0, 0, 0, 32, 121, 27, 123, 184,
240 /// 48, 199, 187, 52, 118, 80, 41, 16,
241 /// 76, 233, 83, 35, 128, 62, 188,
242 /// 207, 47, 46, 28, 204, 70, 112,
243 /// 254, 200, 124, 155, 202, 221];
244 ///
245 /// let key = PublicKey::from_bytes(&data).unwrap();
246 /// let fp = key.fingerprint();
247 /// assert_eq!(fp.hash, "ciQkdxjFUhk2E2vRkWJD9kB8pi+EneOkaCJJHNWzPC4");
248 /// ```
249 pub fn from_bytes<T: ?Sized + AsRef<[u8]>>(data: &T) -> Result<PublicKey> {
250 let mut reader = Reader::new(&data);
251 let kt_name = reader.read_string()?;
252 PublicKey::from_reader(&kt_name, &mut reader)
253 }
254
255 // This function is used for extracting a public key from an existing reader, e.g.
256 // we already have a reader for reading an OpenSSH certificate key and
257 // we want to extract the public key information from it.
258 pub(crate) fn from_reader(kt_name: &str, reader: &mut Reader) -> Result<PublicKey> {
259 let kt = KeyType::from_name(&kt_name)?;
260
261 let kind = match kt.kind {
262 KeyTypeKind::Rsa | KeyTypeKind::RsaCert => {
263 let k = RsaPublicKey {
264 e: reader.read_mpint()?,
265 n: reader.read_mpint()?,
266 };
267
268 PublicKeyKind::Rsa(k)
269 }
270 KeyTypeKind::Ecdsa | KeyTypeKind::EcdsaCert => {
271 let identifier = reader.read_string()?;
272 let curve = Curve::from_identifier(&identifier)?;
273 let key = reader.read_bytes()?;
274 let k = EcdsaPublicKey {
275 curve,
276 key,
277 };
278
279 PublicKeyKind::Ecdsa(k)
280 }
281 KeyTypeKind::Ed25519 | KeyTypeKind::Ed25519Cert => {
282 let k = Ed25519PublicKey {
283 key: reader.read_bytes()?,
284 };
285
286 PublicKeyKind::Ed25519(k)
287 }
288 };
289
290 let key = PublicKey {
291 key_type: kt,
292 kind,
293 comment: None,
294 };
295
296 Ok(key)
297 }
298
299 /// Returns the number of bits of the public key.
300 ///
301 /// # Example
302 ///
303 /// ```rust
304 /// # use rustica_keys::ssh::PublicKey;
305 /// let key = PublicKey::from_string("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHkbe7gwx7s0dlApEEzpUyOAPrzPLy4czEZw/sh8m8rd me@home").unwrap();
306 /// assert_eq!(key.bits(), 256);
307 /// ```
308 pub fn bits(&self) -> usize {
309 match self.kind {
310 // For RSA public key the size of the key is the number of bits of the modulus
311 PublicKeyKind::Rsa(ref k) => k.n.len() * 8,
312 // ECDSA key size depends on the curve
313 PublicKeyKind::Ecdsa(ref k) => match k.curve.kind {
314 CurveKind::Nistp256 => 256,
315 CurveKind::Nistp384 => 384,
316 CurveKind::Nistp521 => 521,
317 },
318 // ED25519 key size is 256 bits
319 // https://tools.ietf.org/html/draft-josefsson-eddsa-ed25519-03#section-5.5
320 PublicKeyKind::Ed25519(_) => 256,
321 }
322 }
323
324 /// Encodes the public key in an OpenSSH compatible format.
325 ///
326 /// # Example
327 /// ```rust
328 /// # use rustica_keys::ssh::PublicKey;
329 /// let key = PublicKey::from_string("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHkbe7gwx7s0dlApEEzpUyOAPrzPLy4czEZw/sh8m8rd me@home").unwrap();
330 /// assert_eq!(key.encode(), vec![0, 0, 0, 11, 115, 115, 104, 45, 101, 100, 50, 53, 53, 49, 57, 0, 0, 0, 32, 121, 27, 123, 184, 48, 199, 187, 52, 118, 80, 41, 16, 76, 233, 83, 35, 128, 62, 188, 207, 47, 46, 28, 204, 70, 112, 254, 200, 124, 155, 202, 221]);
331 /// ```
332 pub fn encode(&self) -> Vec<u8> {
333 let mut w = Writer::new();
334
335 w.write_string(self.key_type.plain);
336 match self.kind {
337 PublicKeyKind::Rsa(ref k) => {
338 w.write_mpint(&k.e);
339 w.write_mpint(&k.n);
340 }
341 PublicKeyKind::Ecdsa(ref k) => {
342 w.write_string(&k.curve.identifier);
343 w.write_bytes(&k.key);
344 }
345 PublicKeyKind::Ed25519(ref k) => {
346 w.write_bytes(&k.key);
347 }
348 }
349
350 w.into_bytes()
351 }
352
353 /// Computes the fingerprint of the public key using the
354 /// default OpenSSH fingerprint representation with SHA256.
355 ///
356 /// # Example
357 ///
358 /// ```rust
359 /// # use rustica_keys::ssh::{FingerprintKind, PublicKey};
360 /// # fn example() {
361 /// let key = PublicKey::from_path("/path/to/id_ed25519.pub").unwrap();
362 /// let fp = key.fingerprint();
363 /// println!("{}", fp.hash);
364 /// # }
365 /// ```
366 pub fn fingerprint(&self) -> Fingerprint {
367 self.fingerprint_with(FingerprintKind::Sha256)
368 }
369
370 /// Computes the fingerprint of the public key using a given
371 /// fingerprint representation.
372 ///
373 /// # Example
374 ///
375 /// ```rust
376 /// # use rustica_keys::ssh::{FingerprintKind, PublicKey};
377 /// # fn example() {
378 /// let key = PublicKey::from_path("/path/to/id_ed25519.pub").unwrap();
379 /// let sha512fp = key.fingerprint_with(FingerprintKind::Sha512);
380 /// println!("{}", sha512fp.hash);
381 /// # }
382 /// ```
383 pub fn fingerprint_with(&self, kind: FingerprintKind) -> Fingerprint {
384 Fingerprint::compute(kind, &self.encode())
385 }
386
387 /// Writes the public key to a given writer.
388 ///
389 /// # Example
390 /// ```rust
391 /// # use rustica_keys::ssh::PublicKey;
392 /// use std::fs::File;
393 /// # fn example() {
394 /// let key = PublicKey::from_string("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...").unwrap();
395 /// let mut file = File::create("/path/to/id_ed25519.pub").unwrap();
396 /// key.write(&mut file).unwrap();
397 /// # }
398 /// ```
399 pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
400 let encoded = self.encode();
401 let data = base64::encode(&encoded);
402 match self.comment {
403 Some(ref c) => w.write_fmt(format_args!("{} {} {}\n", self.key_type.name, data, c)),
404 None => w.write_fmt(format_args!("{} {}\n", self.key_type.name, data)),
405 }
406 }
407}