Skip to main content

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}