Skip to main content

ssh_key/
certificate.rs

1//! OpenSSH certificate support.
2
3mod builder;
4mod cert_type;
5mod field;
6mod options_map;
7mod unix_time;
8
9pub use self::{builder::Builder, cert_type::CertType, field::Field, options_map::OptionsMap};
10
11use crate::{
12    Algorithm, Comment, Error, Fingerprint, HashAlg, Result, Signature,
13    public::{KeyData, SshFormat},
14};
15use alloc::{
16    string::{String, ToString},
17    vec::Vec,
18};
19use core::str::FromStr;
20use encoding::{Base64Reader, CheckedSum, Decode, Encode, Reader, Writer};
21use signature::Verifier;
22
23#[cfg(feature = "serde")]
24use serde::{Deserialize, Serialize, de, ser};
25
26#[cfg(feature = "std")]
27use {
28    self::unix_time::UnixTime,
29    std::{fs, path::Path, time::SystemTime},
30};
31
32/// OpenSSH certificate as specified in [PROTOCOL.certkeys].
33///
34/// OpenSSH supports X.509-like certificate authorities, but using a custom
35/// encoding format.
36///
37/// <div class="warning">
38/// <b>Security Warning</b>
39///
40/// Certificates must be validated before they can be trusted!
41///
42/// The [`Certificate`] type does not automatically perform validation checks
43/// and supports parsing certificates which may potentially be invalid.
44/// Just because a [`Certificate`] parses successfully does not mean that it
45/// can be trusted.
46///
47/// See "Certificate Validation" documentation below for more information on
48/// how to properly validate certificates.
49/// </div>
50///
51/// # Certificate Validation
52///
53/// For a certificate to be trusted, the following properties MUST be
54/// validated:
55///
56/// - Certificate is signed by a trusted certificate authority (CA)
57/// - Signature over the certificate verifies successfully
58/// - Current time is within the certificate's validity window
59/// - Certificate authorizes the expected principal
60/// - All critical extensions to the certificate are recognized and validate
61///   successfully.
62///
63/// The [`Certificate::validate`] and [`Certificate::validate_at`] methods can
64/// be used to validate a certificate.
65///
66/// ## Example
67///
68/// The following example walks through how to implement the steps outlined
69/// above for validating a certificate:
70///
71#[cfg_attr(all(feature = "p256", feature = "std"), doc = " ```")]
72#[cfg_attr(not(all(feature = "p256", feature = "std")), doc = " ```ignore")]
73/// # fn main() -> Result<(), ssh_key::Error> {
74/// use ssh_key::{Certificate, Fingerprint};
75/// use std::str::FromStr;
76///
77/// // List of trusted certificate authority (CA) fingerprints
78/// let ca_fingerprints = [
79///     Fingerprint::from_str("SHA256:JQ6FV0rf7qqJHZqIj4zNH8eV0oB8KLKh9Pph3FTD98g")?
80/// ];
81///
82/// // Certificate to be validated
83/// let certificate = Certificate::from_str(
84///     "ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIE7x9ln6uZLLkfXM8iatrnAAuytVHeCznU8VlEgx7TvLAAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqtiAAAAAAAAAAAAAAABAAAAFGVkMjU1MTktd2l0aC1wMjU2LWNhAAAAAAAAAABiUZm7AAAAAPTaMrsAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQR8H9hzDOU0V76NkkCY7DZIgw+SqoojY6xlb91FIfpjE+UR8YkbTp5ar44ULQatFaZqQlfz8FHYTooOL5G6gHBHAAAAZAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAASQAAACEA/0Cwxhkac5AeNYE958j8GgvmkIESDH1TE7QYIqxsFsIAAAAgTEw8WVjlz8AnvyaKGOUELMpyFFJagtD2JFAIAJvilrc= user@example.com"
85/// )?;
86///
87/// // Perform basic certificate validation, ensuring that the certificate is
88/// // signed by a trusted certificate authority (CA) and checking that the
89/// // current system clock time is within the certificate's validity window
90/// certificate.validate(&ca_fingerprints)?;
91///
92/// // Check that the certificate includes the expected principal name
93/// // (i.e. username or hostname)
94/// // if !certificate.principals().contains(expected_principal) { return Err(...) }
95///
96/// // Check that all of the critical extensions are recognized
97/// // if !certificate.critical_options.iter().all(|critical| ...) { return Err(...) }
98///
99/// // If we've made it this far, the certificate can be trusted
100/// Ok(())
101/// # }
102/// ```
103///
104/// # Certificate Builder (SSH CA support)
105///
106/// This crate implements all of the functionality needed for a pure Rust
107/// SSH certificate authority which can build and sign OpenSSH certificates.
108///
109/// See the [`Builder`] type's documentation for more information.
110///
111/// # `serde` support
112///
113/// When the `serde` feature of this crate is enabled, this type receives impls
114/// of [`Deserialize`][`serde::Deserialize`] and [`Serialize`][`serde::Serialize`].
115///
116/// The serialization uses a binary encoding with binary formats like bincode
117/// and CBOR, and the OpenSSH string serialization when used with
118/// human-readable formats like JSON and TOML.
119///
120/// [PROTOCOL.certkeys]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
121#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
122pub struct Certificate {
123    /// CA-provided random bitstring of arbitrary length
124    /// (but typically 16 or 32 bytes).
125    nonce: Vec<u8>,
126
127    /// Public key data.
128    public_key: KeyData,
129
130    /// Serial number.
131    serial: u64,
132
133    /// Certificate type.
134    cert_type: CertType,
135
136    /// Key ID.
137    key_id: String,
138
139    /// Valid principals.
140    valid_principals: Vec<String>,
141
142    /// Valid after.
143    valid_after: u64,
144
145    /// Valid before.
146    valid_before: u64,
147
148    /// Critical options.
149    critical_options: OptionsMap,
150
151    /// Extensions.
152    extensions: OptionsMap,
153
154    /// Reserved field.
155    reserved: Vec<u8>,
156
157    /// Signature key of signing CA.
158    signature_key: KeyData,
159
160    /// Signature over the certificate.
161    signature: Signature,
162
163    /// Comment on the certificate.
164    comment: Comment,
165}
166
167impl Certificate {
168    /// Parse an OpenSSH-formatted certificate.
169    ///
170    /// OpenSSH-formatted certificates look like the following
171    /// (i.e. similar to OpenSSH public keys with `-cert-v01@openssh.com`):
172    ///
173    /// ```text
174    /// ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlc...8REbCaAw== user@example.com
175    /// ```
176    ///
177    /// # Errors
178    /// - Returns [`Error::AlgorithmUnknown`] in the event of an algorithm mismatch.
179    /// - Returns [`Error::Encoding`] in the event of an encoding error.
180    pub fn from_openssh(certificate_str: &str) -> Result<Self> {
181        let encapsulation = SshFormat::decode(certificate_str.trim_end().as_bytes())?;
182        let mut reader = Base64Reader::new(encapsulation.base64_data)?;
183        let mut cert = Certificate::decode(&mut reader)?;
184
185        // Verify that the algorithm in the Base64-encoded data matches the text
186        if encapsulation.algorithm_id != cert.algorithm().to_certificate_type() {
187            return Err(Error::AlgorithmUnknown);
188        }
189
190        cert.comment = Comment::from(encapsulation.comment);
191        Ok(reader.finish(cert)?)
192    }
193
194    /// Parse a raw binary OpenSSH certificate.
195    ///
196    /// # Errors
197    /// Returns [`Error::Encoding`] in the event of an encoding error.
198    pub fn from_bytes(mut bytes: &[u8]) -> Result<Self> {
199        let reader = &mut bytes;
200        let cert = Certificate::decode(reader)?;
201        Ok(reader.finish(cert)?)
202    }
203
204    /// Encode OpenSSH certificate to a [`String`].
205    ///
206    /// # Errors
207    /// Returns [`Error::Encoding`] in the event of an encoding error.
208    pub fn to_openssh(&self) -> Result<String> {
209        SshFormat::encode_string(
210            &self.algorithm().to_certificate_type(),
211            self,
212            self.comment(),
213        )
214    }
215
216    /// Serialize OpenSSH certificate as raw bytes.
217    ///
218    /// # Errors
219    /// Returns [`Error::Encoding`] in the event of an encoding error.
220    pub fn to_bytes(&self) -> Result<Vec<u8>> {
221        Ok(self.encode_vec()?)
222    }
223
224    /// Read OpenSSH certificate from a file.
225    ///
226    /// # Errors
227    /// - Returns [`Error::Io`] in the event of an I/O error.
228    /// - Returns [`Error::Encoding`] in the event of an encoding error.
229    #[cfg(feature = "std")]
230    pub fn read_file(path: &Path) -> Result<Self> {
231        let input = fs::read_to_string(path)?;
232        Self::from_openssh(&input)
233    }
234
235    /// Write OpenSSH certificate to a file.
236    ///
237    /// # Errors
238    /// - Returns [`Error::Io`] in the event of an I/O error.
239    /// - Returns [`Error::Encoding`] in the event of an encoding error.
240    #[cfg(feature = "std")]
241    pub fn write_file(&self, path: &Path) -> Result<()> {
242        let encoded = self.to_openssh()?;
243        fs::write(path, encoded.as_bytes())?;
244        Ok(())
245    }
246
247    /// Get the public key algorithm for this certificate.
248    #[must_use]
249    pub fn algorithm(&self) -> Algorithm {
250        self.public_key.algorithm()
251    }
252
253    /// Get the comment on this certificate.
254    #[must_use]
255    pub fn comment(&self) -> &str {
256        self.comment.as_str_lossy()
257    }
258
259    /// Set the comment on this certificate.
260    pub fn set_comment(&mut self, comment: impl Into<Comment>) {
261        self.comment = comment.into();
262    }
263
264    /// Nonces are a CA-provided random bitstring of arbitrary length
265    /// (but typically 16 or 32 bytes).
266    ///
267    /// It's included to make attacks that depend on inducing collisions in the
268    /// signature hash infeasible.
269    #[must_use]
270    pub fn nonce(&self) -> &[u8] {
271        &self.nonce
272    }
273
274    /// Get this certificate's public key data.
275    #[must_use]
276    pub fn public_key(&self) -> &KeyData {
277        &self.public_key
278    }
279
280    /// Optional certificate serial number set by the CA to provide an
281    /// abbreviated way to refer to certificates from that CA.
282    ///
283    /// If a CA does not wish to number its certificates, it must set this
284    /// field to zero.
285    #[must_use]
286    pub fn serial(&self) -> u64 {
287        self.serial
288    }
289
290    /// Specifies whether this certificate is for identification of a user or
291    /// a host.
292    #[must_use]
293    pub fn cert_type(&self) -> CertType {
294        self.cert_type
295    }
296
297    /// Key IDs are a free-form text field that is filled in by the CA at the
298    /// time of signing.
299    ///
300    /// The intention is that the contents of this field are used to identify
301    /// the identity principal in log messages.
302    #[must_use]
303    pub fn key_id(&self) -> &str {
304        &self.key_id
305    }
306
307    /// List of zero or more principals which this certificate is valid for.
308    ///
309    /// Principals are hostnames for host certificates and usernames for user
310    /// certificates.
311    ///
312    /// As a special case, a zero-length "valid principals" field means the
313    /// certificate is valid for any principal of the specified type.
314    #[must_use]
315    pub fn valid_principals(&self) -> &[String] {
316        &self.valid_principals
317    }
318
319    /// Valid after (Unix time), i.e. certificate issuance time.
320    #[must_use]
321    pub fn valid_after(&self) -> u64 {
322        self.valid_after
323    }
324
325    /// Valid before (Unix time), i.e. certificate expiration time.
326    #[must_use]
327    pub fn valid_before(&self) -> u64 {
328        self.valid_before
329    }
330
331    /// Valid after (system time), i.e. certificate issuance time.
332    ///
333    /// # Returns
334    /// - `Some` if the `u64` value is a valid `SystemTime`
335    /// - `None` if it is not (i.e. overflows `i64`)
336    #[cfg(feature = "std")]
337    pub fn valid_after_time(&self) -> Option<SystemTime> {
338        UnixTime::try_from(self.valid_after).ok().map(Into::into)
339    }
340
341    /// Valid before (system time), i.e. certificate expiration time.
342    ///
343    /// # Returns
344    /// - `Some` if the `u64` value is a valid `SystemTime`
345    /// - `None` if it is not (i.e. overflows `i64`, effectively never-expiring)
346    #[cfg(feature = "std")]
347    pub fn valid_before_time(&self) -> Option<SystemTime> {
348        UnixTime::try_from(self.valid_before).ok().map(Into::into)
349    }
350
351    /// The critical options section of the certificate specifies zero or more
352    /// options on the certificate's validity.
353    ///
354    /// Each named option may only appear once in a certificate.
355    ///
356    /// All options are "critical"; if an implementation does not recognize an
357    /// option, then the validating party should refuse to accept the
358    /// certificate.
359    #[must_use]
360    pub fn critical_options(&self) -> &OptionsMap {
361        &self.critical_options
362    }
363
364    /// The extensions section of the certificate specifies zero or more
365    /// non-critical certificate extensions.
366    ///
367    /// If an implementation does not recognise an extension, then it should
368    /// ignore it.
369    #[must_use]
370    pub fn extensions(&self) -> &OptionsMap {
371        &self.extensions
372    }
373
374    /// Signature key of signing CA.
375    #[must_use]
376    pub fn signature_key(&self) -> &KeyData {
377        &self.signature_key
378    }
379
380    /// Signature computed over all preceding fields from the initial string up
381    /// to, and including the signature key.
382    #[must_use]
383    pub fn signature(&self) -> &Signature {
384        &self.signature
385    }
386
387    /// Perform certificate validation using the system clock to check that
388    /// the current time is within the certificate's validity window.
389    ///
390    /// <div class="warning">
391    /// <b>Security Warning: Some Assembly Required</b>
392    ///
393    /// See [`Certificate::validate_at`] documentation for important notes on how to properly
394    /// validate certificates!
395    /// </div>
396    ///
397    /// # Errors
398    /// Returns [`Error::CertificateValidation`] if the certificate failed to validate
399    #[cfg(feature = "std")]
400    pub fn validate<'a, I>(&self, ca_fingerprints: I) -> Result<()>
401    where
402        I: IntoIterator<Item = &'a Fingerprint>,
403    {
404        self.validate_at(UnixTime::now()?.into(), ca_fingerprints)
405    }
406
407    /// Perform certificate validation.
408    ///
409    /// Checks for the following:
410    ///
411    /// - Specified Unix timestamp is within the certificate's valid range
412    /// - Certificate's signature validates against the public key included in the certificate
413    /// - Fingerprint of the public key included in the certificate matches one of the trusted
414    ///   certificate authority (CA) fingerprints provided in the `ca_fingerprints` parameter.
415    ///
416    /// NOTE: only SHA-256 fingerprints are supported at this time.
417    ///
418    /// <div class="warning">
419    /// <b>Security Warning: Some Assembly Required</b>
420    ///
421    /// This method does not perform the full set of validation checks needed to determine if a
422    /// certificate is to be trusted.
423    ///
424    /// If this method succeeds, the following properties still need to be checked to ensure the
425    /// certificate is valid:
426    ///
427    /// - `valid_principals` is empty or contains the expected principal.
428    /// - `critical_options` is empty or contains *only* options which are recognized, and that the
429    ///   recognized options are all valid.
430    /// </div>
431    ///
432    /// # Errors
433    /// Returns [`Error::CertificateValidation`] if the certificate failed to validate
434    pub fn validate_at<'a, I>(&self, unix_timestamp: u64, ca_fingerprints: I) -> Result<()>
435    where
436        I: IntoIterator<Item = &'a Fingerprint>,
437    {
438        self.verify_signature()?;
439
440        // TODO(tarcieri): support non SHA-256 public key fingerprints?
441        let cert_fingerprint = self.signature_key.fingerprint(HashAlg::Sha256);
442
443        if !ca_fingerprints.into_iter().any(|f| f == &cert_fingerprint) {
444            return Err(Error::CertificateValidation);
445        }
446
447        // From PROTOCOL.certkeys:
448        //
449        //  "valid after" and "valid before" specify a validity period for the
450        //  certificate. Each represents a time in seconds since 1970-01-01
451        //  A certificate is considered valid if:
452        //
453        //     valid after <= current time < valid before
454        if self.valid_after <= unix_timestamp && unix_timestamp < self.valid_before {
455            Ok(())
456        } else {
457            Err(Error::CertificateValidation)
458        }
459    }
460
461    /// Verify the signature on the certificate against the public key in the
462    /// certificate.
463    ///
464    /// <div class="warning">
465    /// <b>Security Warning: DON'T USE THIS!</b>
466    ///
467    /// This function alone does not provide any security guarantees whatsoever.
468    ///
469    /// It verifies the signature in the certificate matches the CA public key in the certificate,
470    /// but does not ensure the CA is trusted.
471    ///
472    /// It is public only for testing purposes, and deliberately hidden from the documentation for
473    /// that reason.
474    /// </div>
475    #[doc(hidden)]
476    pub fn verify_signature(&self) -> Result<()> {
477        let mut tbs_certificate = Vec::new();
478        self.encode_tbs(&mut tbs_certificate)?;
479        self.signature_key
480            .verify(&tbs_certificate, &self.signature)
481            .map_err(|_| Error::CertificateValidation)
482    }
483
484    /// Encode the portion of the certificate "to be signed" by the CA
485    /// (or to be verified against an existing CA signature)
486    fn encode_tbs(&self, writer: &mut impl Writer) -> encoding::Result<()> {
487        self.algorithm().to_certificate_type().encode(writer)?;
488        self.nonce.encode(writer)?;
489        self.public_key.encode_key_data(writer)?;
490        self.serial.encode(writer)?;
491        self.cert_type.encode(writer)?;
492        self.key_id.encode(writer)?;
493        self.valid_principals.encode(writer)?;
494        self.valid_after.encode(writer)?;
495        self.valid_before.encode(writer)?;
496        self.critical_options.encode(writer)?;
497        self.extensions.encode(writer)?;
498        self.reserved.encode(writer)?;
499        self.signature_key.encode_prefixed(writer)
500    }
501
502    /// Decode [`Certificate`] for the specified algorithm.
503    ///
504    /// # Errors
505    /// Returns [`Error::Encoding`] in the event of an encoding error.
506    pub fn decode_as(reader: &mut impl Reader, algorithm: Algorithm) -> Result<Self> {
507        Ok(Self {
508            nonce: Vec::decode(reader)?,
509            public_key: KeyData::decode_as(reader, algorithm)?,
510            serial: u64::decode(reader)?,
511            cert_type: CertType::decode(reader)?,
512            key_id: String::decode(reader)?,
513            valid_principals: Vec::decode(reader)?,
514            valid_after: u64::decode(reader)?,
515            valid_before: u64::decode(reader)?,
516            critical_options: OptionsMap::decode(reader)?,
517            extensions: OptionsMap::decode(reader)?,
518            reserved: Vec::decode(reader)?,
519            signature_key: reader.read_prefixed(KeyData::decode)?,
520            signature: reader.read_prefixed(Signature::decode)?,
521            comment: Comment::default(),
522        })
523    }
524}
525
526impl Decode for Certificate {
527    type Error = Error;
528
529    fn decode(reader: &mut impl Reader) -> Result<Self> {
530        let algorithm = Algorithm::new_certificate(&String::decode(reader)?)?;
531        Self::decode_as(reader, algorithm)
532    }
533}
534
535impl Encode for Certificate {
536    fn encoded_len(&self) -> encoding::Result<usize> {
537        [
538            self.algorithm().to_certificate_type().encoded_len()?,
539            self.nonce.encoded_len()?,
540            self.public_key.encoded_key_data_len()?,
541            self.serial.encoded_len()?,
542            self.cert_type.encoded_len()?,
543            self.key_id.encoded_len()?,
544            self.valid_principals.encoded_len()?,
545            self.valid_after.encoded_len()?,
546            self.valid_before.encoded_len()?,
547            self.critical_options.encoded_len()?,
548            self.extensions.encoded_len()?,
549            self.reserved.encoded_len()?,
550            self.signature_key.encoded_len_prefixed()?,
551            self.signature.encoded_len_prefixed()?,
552        ]
553        .checked_sum()
554    }
555
556    fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> {
557        self.encode_tbs(writer)?;
558        self.signature.encode_prefixed(writer)
559    }
560}
561
562impl FromStr for Certificate {
563    type Err = Error;
564
565    fn from_str(s: &str) -> Result<Self> {
566        Self::from_openssh(s)
567    }
568}
569
570#[allow(
571    clippy::to_string_trait_impl,
572    reason = "`SshFormat` lacks `core::fmt` support"
573)]
574impl ToString for Certificate {
575    fn to_string(&self) -> String {
576        self.to_openssh().expect("SSH certificate encoding error")
577    }
578}
579
580#[cfg(feature = "serde")]
581impl<'de> Deserialize<'de> for Certificate {
582    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
583    where
584        D: de::Deserializer<'de>,
585    {
586        if deserializer.is_human_readable() {
587            let string = String::deserialize(deserializer)?;
588            Self::from_openssh(&string).map_err(de::Error::custom)
589        } else {
590            let bytes = Vec::<u8>::deserialize(deserializer)?;
591            Self::from_bytes(&bytes).map_err(de::Error::custom)
592        }
593    }
594}
595
596#[cfg(feature = "serde")]
597impl Serialize for Certificate {
598    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
599    where
600        S: ser::Serializer,
601    {
602        if serializer.is_human_readable() {
603            self.to_openssh()
604                .map_err(ser::Error::custom)?
605                .serialize(serializer)
606        } else {
607            self.to_bytes()
608                .map_err(ser::Error::custom)?
609                .serialize(serializer)
610        }
611    }
612}