diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index 3473a68b..2038e7fe 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -81,6 +81,8 @@ rsa = ["dep:rsa", "alloc", "encoding/bigint", "rand_core"] sha1 = ["dep:sha1"] tdes = ["cipher/tdes", "encryption"] +[lints] +workspace = true + [package.metadata.docs.rs] all-features = true -rustdoc-args = ["--cfg", "docsrs"] diff --git a/ssh-key/README.md b/ssh-key/README.md index 1544656e..821091f5 100644 --- a/ssh-key/README.md +++ b/ssh-key/README.md @@ -84,13 +84,6 @@ The "Feature" column lists the name of `ssh-key` crate features which can be enabled to provide full support for the "Keygen", "Sign", and "Verify" functionality for a particular SSH key algorithm. -## Minimum Supported Rust Version - -This crate requires **Rust 1.85** at a minimum. - -We may change the MSRV in the future, but it will be accompanied by a minor -version bump. - ## License Licensed under either of: diff --git a/ssh-key/src/algorithm.rs b/ssh-key/src/algorithm.rs index 7a74b15e..ac644928 100644 --- a/ssh-key/src/algorithm.rs +++ b/ssh-key/src/algorithm.rs @@ -140,12 +140,14 @@ impl Algorithm { /// - `sk-ssh-ed25519@openssh.com` (FIDO/U2F key) /// /// Any other algorithms are mapped to the [`Algorithm::Other`] variant. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event the algorithm name is not known. pub fn new(id: &str) -> Result { Ok(id.parse()?) } - /// Decode algorithm from the given string identifier as used by - /// the OpenSSH certificate format. + /// Decode algorithm from the given string identifier as used by the OpenSSH certificate format. /// /// OpenSSH certificate algorithms end in `*-cert-v01@openssh.com`. /// See [PROTOCOL.certkeys] for more information. @@ -163,6 +165,9 @@ impl Algorithm { /// Any other algorithms are mapped to the [`Algorithm::Other`] variant. /// /// [PROTOCOL.certkeys]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD + /// + /// # Errors + /// Returns [`Error::AlgorithmUnknown`] in the event the algorithm is not known. pub fn new_certificate(id: &str) -> Result { match id { CERT_DSA => Ok(Algorithm::Dsa), @@ -193,6 +198,7 @@ impl Algorithm { } /// Get the string identifier which corresponds to this algorithm. + #[must_use] pub fn as_str(&self) -> &str { match self { Algorithm::Dsa => SSH_DSA, @@ -222,6 +228,7 @@ impl Algorithm { /// /// [PROTOCOL.certkeys]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD #[cfg(feature = "alloc")] + #[must_use] pub fn to_certificate_type(&self) -> String { match self { Algorithm::Dsa => CERT_DSA, @@ -246,21 +253,25 @@ impl Algorithm { } /// Is the algorithm DSA? + #[must_use] pub fn is_dsa(self) -> bool { self == Algorithm::Dsa } /// Is the algorithm ECDSA? + #[must_use] pub fn is_ecdsa(self) -> bool { matches!(self, Algorithm::Ecdsa { .. }) } /// Is the algorithm Ed25519? + #[must_use] pub fn is_ed25519(self) -> bool { self == Algorithm::Ed25519 } /// Is the algorithm RSA? + #[must_use] pub fn is_rsa(self) -> bool { matches!(self, Algorithm::Rsa { .. }) } @@ -340,11 +351,15 @@ impl EcdsaCurve { /// - `nistp256` /// - `nistp384` /// - `nistp521` + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event the algorithm name is not known. pub fn new(id: &str) -> Result { Ok(id.parse()?) } /// Get the string identifier which corresponds to this ECDSA elliptic curve. + #[must_use] pub fn as_str(self) -> &'static str { match self { EcdsaCurve::NistP256 => "nistp256", @@ -370,6 +385,12 @@ impl AsRef for EcdsaCurve { } } +impl From for Algorithm { + fn from(curve: EcdsaCurve) -> Algorithm { + Algorithm::Ecdsa { curve } + } +} + impl Label for EcdsaCurve {} impl fmt::Display for EcdsaCurve { @@ -410,11 +431,15 @@ impl HashAlg { /// /// - `sha256` /// - `sha512` + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event the algorithm name is not known. pub fn new(id: &str) -> Result { Ok(id.parse()?) } /// Get the string identifier for this hash algorithm. + #[must_use] pub fn as_str(self) -> &'static str { match self { HashAlg::Sha256 => SHA256, @@ -423,6 +448,7 @@ impl HashAlg { } /// Get the size of a digest produced by this hash function. + #[must_use] pub const fn digest_size(self) -> usize { match self { HashAlg::Sha256 => 32, @@ -432,6 +458,7 @@ impl HashAlg { /// Compute a digest of the given message using this hash function. #[cfg(feature = "alloc")] + #[must_use] pub fn digest(self, msg: &[u8]) -> Vec { match self { HashAlg::Sha256 => Sha256::digest(msg).to_vec(), @@ -497,11 +524,16 @@ impl KdfAlg { /// /// # Supported KDF names /// - `none` + /// - `bcrypt` + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event the algorithm name is not known. pub fn new(kdfname: &str) -> Result { Ok(kdfname.parse()?) } /// Get the string identifier which corresponds to this algorithm. + #[must_use] pub fn as_str(self) -> &'static str { match self { Self::None => NONE, @@ -510,6 +542,7 @@ impl KdfAlg { } /// Is the KDF algorithm "none"? + #[must_use] pub fn is_none(self) -> bool { self == Self::None } diff --git a/ssh-key/src/algorithm/name.rs b/ssh-key/src/algorithm/name.rs index a7ba8aba..f2add806 100644 --- a/ssh-key/src/algorithm/name.rs +++ b/ssh-key/src/algorithm/name.rs @@ -37,6 +37,9 @@ pub struct AlgorithmName { impl AlgorithmName { /// Create a new algorithm identifier. + /// + /// # Errors + /// Returns [`LabelError`] in the event the identifier is invalid. pub fn new(id: impl Into) -> Result { let id = id.into(); validate_algorithm_id(&id, MAX_ALGORITHM_NAME_LEN)?; @@ -45,17 +48,23 @@ impl AlgorithmName { } /// Get the string identifier which corresponds to this algorithm name. + #[must_use] pub fn as_str(&self) -> &str { &self.id } /// Get the string identifier which corresponds to the OpenSSH certificate format. + #[must_use] + #[allow(clippy::missing_panics_doc, reason = "should not panic")] pub fn certificate_type(&self) -> String { let (name, domain) = split_algorithm_id(&self.id).expect("format checked in constructor"); format!("{name}{CERT_STR_SUFFIX}@{domain}") } /// Create a new [`AlgorithmName`] from an OpenSSH certificate format string identifier. + /// + /// # Errors + /// Returns [`LabelError`] in the event the identifier is invalid. pub fn from_certificate_type(id: &str) -> Result { validate_algorithm_id(id, MAX_CERT_STR_LEN)?; @@ -65,9 +74,9 @@ impl AlgorithmName { .strip_suffix(CERT_STR_SUFFIX) .ok_or_else(|| LabelError::new(id))?; - let algorithm_name = format!("{name}@{domain}"); - - Ok(Self { id: algorithm_name }) + Ok(Self { + id: format!("{name}@{domain}"), + }) } } diff --git a/ssh-key/src/authorized_keys.rs b/ssh-key/src/authorized_keys.rs index c8db4195..fe3f5322 100644 --- a/ssh-key/src/authorized_keys.rs +++ b/ssh-key/src/authorized_keys.rs @@ -1,13 +1,13 @@ //! Parser for `AuthorizedKeysFile`-formatted data. use crate::{Error, PublicKey, Result}; -use core::str; +use core::{ + fmt::{self, Debug}, + str, +}; #[cfg(feature = "alloc")] -use { - alloc::string::{String, ToString}, - core::fmt, -}; +use alloc::string::{String, ToString}; #[cfg(feature = "std")] use { @@ -44,14 +44,19 @@ pub struct AuthorizedKeys<'a> { impl<'a> AuthorizedKeys<'a> { /// Create a new parser for the given input buffer. + #[must_use] pub fn new(input: &'a str) -> Self { Self { lines: input.lines(), } } - /// Read an [`AuthorizedKeys`] file from the filesystem, returning an - /// [`Entry`] vector on success. + /// Read an [`AuthorizedKeys`] file from the filesystem, returning an [`Entry`] vector on + /// success. + /// + /// # Errors + /// - Returns [`Error::Io`] in event of I/O errors reading the file. + /// - Propagates [`Entry`] parsing errors as [`Error::FormatEncoding`]. #[cfg(feature = "std")] pub fn read_file(path: impl AsRef) -> Result> { // TODO(tarcieri): permissions checks @@ -59,9 +64,7 @@ impl<'a> AuthorizedKeys<'a> { AuthorizedKeys::new(&input).collect() } - /// Get the next line, trimming any comments and trailing whitespace. - /// - /// Ignores empty lines. + /// Get the next line, trimming any comments and trailing whitespace. Ignores empty lines. fn next_line_trimmed(&mut self) -> Option<&'a str> { loop { let mut line = self.lines.next()?; @@ -81,11 +84,17 @@ impl<'a> AuthorizedKeys<'a> { } } +impl Debug for AuthorizedKeys<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AuthorizedKeys").finish_non_exhaustive() + } +} + impl Iterator for AuthorizedKeys<'_> { type Item = Result; fn next(&mut self) -> Option> { - self.next_line_trimmed().map(|line| line.parse()) + self.next_line_trimmed().map(str::parse) } } @@ -103,11 +112,13 @@ pub struct Entry { impl Entry { /// Get configuration options for this entry. #[cfg(feature = "alloc")] + #[must_use] pub fn config_opts(&self) -> &ConfigOpts { &self.config_opts } /// Get public key for this entry. + #[must_use] pub fn public_key(&self) -> &PublicKey { &self.public_key } @@ -206,6 +217,9 @@ pub struct ConfigOpts(String); #[cfg(feature = "alloc")] impl ConfigOpts { /// Parse an options string. + /// + /// # Errors + /// pub fn new(string: impl Into) -> Result { let ret = Self(string.into()); ret.iter().validate()?; @@ -213,16 +227,19 @@ impl ConfigOpts { } /// Borrow the configuration options as a `str`. + #[must_use] pub fn as_str(&self) -> &str { self.0.as_str() } /// Are there no configuration options? + #[must_use] pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Iterate over the comma-delimited configuration options. + #[must_use] pub fn iter(&self) -> ConfigOptsIter<'_> { ConfigOptsIter(self.as_str()) } @@ -259,6 +276,9 @@ impl<'a> ConfigOptsIter<'a> { /// Create new configuration options iterator. /// /// Validates that the options are well-formed. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of encoding errors. pub fn new(s: &'a str) -> Result { let ret = Self(s); ret.clone().validate()?; diff --git a/ssh-key/src/certificate.rs b/ssh-key/src/certificate.rs index 281eef73..1a37ee57 100644 --- a/ssh-key/src/certificate.rs +++ b/ssh-key/src/certificate.rs @@ -34,7 +34,8 @@ use { /// OpenSSH supports X.509-like certificate authorities, but using a custom /// encoding format. /// -/// # ⚠️ Security Warning +///
+/// Security Warning /// /// Certificates must be validated before they can be trusted! /// @@ -45,6 +46,7 @@ use { /// /// See "Certificate Validation" documentation below for more information on /// how to properly validate certificates. +///
/// /// # Certificate Validation /// @@ -68,7 +70,7 @@ use { /// #[cfg_attr(all(feature = "p256", feature = "std"), doc = " ```")] #[cfg_attr(not(all(feature = "p256", feature = "std")), doc = " ```ignore")] -/// # fn main() -> Result<(), Box> { +/// # fn main() -> Result<(), ssh_key::Error> { /// use ssh_key::{Certificate, Fingerprint}; /// use std::str::FromStr; /// @@ -171,6 +173,10 @@ impl Certificate { /// ```text /// ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlc...8REbCaAw== user@example.com /// ``` + /// + /// # Errors + /// - Returns [`Error::AlgorithmUnknown`] in the event of an algorithm mismatch. + /// - Returns [`Error::Encoding`] in the event of an encoding error. pub fn from_openssh(certificate_str: &str) -> Result { let encapsulation = SshFormat::decode(certificate_str.trim_end().as_bytes())?; let mut reader = Base64Reader::new(encapsulation.base64_data)?; @@ -186,6 +192,9 @@ impl Certificate { } /// Parse a raw binary OpenSSH certificate. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. pub fn from_bytes(mut bytes: &[u8]) -> Result { let reader = &mut bytes; let cert = Certificate::decode(reader)?; @@ -193,6 +202,9 @@ impl Certificate { } /// Encode OpenSSH certificate to a [`String`]. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. pub fn to_openssh(&self) -> Result { SshFormat::encode_string( &self.algorithm().to_certificate_type(), @@ -202,11 +214,18 @@ impl Certificate { } /// Serialize OpenSSH certificate as raw bytes. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. pub fn to_bytes(&self) -> Result> { Ok(self.encode_vec()?) } /// Read OpenSSH certificate from a file. + /// + /// # Errors + /// - Returns [`Error::Io`] in the event of an I/O error. + /// - Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "std")] pub fn read_file(path: &Path) -> Result { let input = fs::read_to_string(path)?; @@ -214,6 +233,10 @@ impl Certificate { } /// Write OpenSSH certificate to a file. + /// + /// # Errors + /// - Returns [`Error::Io`] in the event of an I/O error. + /// - Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "std")] pub fn write_file(&self, path: &Path) -> Result<()> { let encoded = self.to_openssh()?; @@ -222,11 +245,13 @@ impl Certificate { } /// Get the public key algorithm for this certificate. + #[must_use] pub fn algorithm(&self) -> Algorithm { self.public_key.algorithm() } /// Get the comment on this certificate. + #[must_use] pub fn comment(&self) -> &str { self.comment.as_str_lossy() } @@ -241,11 +266,13 @@ impl Certificate { /// /// It's included to make attacks that depend on inducing collisions in the /// signature hash infeasible. + #[must_use] pub fn nonce(&self) -> &[u8] { &self.nonce } /// Get this certificate's public key data. + #[must_use] pub fn public_key(&self) -> &KeyData { &self.public_key } @@ -255,12 +282,14 @@ impl Certificate { /// /// If a CA does not wish to number its certificates, it must set this /// field to zero. + #[must_use] pub fn serial(&self) -> u64 { self.serial } /// Specifies whether this certificate is for identification of a user or /// a host. + #[must_use] pub fn cert_type(&self) -> CertType { self.cert_type } @@ -270,6 +299,7 @@ impl Certificate { /// /// The intention is that the contents of this field are used to identify /// the identity principal in log messages. + #[must_use] pub fn key_id(&self) -> &str { &self.key_id } @@ -281,16 +311,19 @@ impl Certificate { /// /// As a special case, a zero-length "valid principals" field means the /// certificate is valid for any principal of the specified type. + #[must_use] pub fn valid_principals(&self) -> &[String] { &self.valid_principals } /// Valid after (Unix time), i.e. certificate issuance time. + #[must_use] pub fn valid_after(&self) -> u64 { self.valid_after } /// Valid before (Unix time), i.e. certificate expiration time. + #[must_use] pub fn valid_before(&self) -> u64 { self.valid_before } @@ -323,6 +356,7 @@ impl Certificate { /// All options are "critical"; if an implementation does not recognize an /// option, then the validating party should refuse to accept the /// certificate. + #[must_use] pub fn critical_options(&self) -> &OptionsMap { &self.critical_options } @@ -332,17 +366,20 @@ impl Certificate { /// /// If an implementation does not recognise an extension, then it should /// ignore it. + #[must_use] pub fn extensions(&self) -> &OptionsMap { &self.extensions } /// Signature key of signing CA. + #[must_use] pub fn signature_key(&self) -> &KeyData { &self.signature_key } /// Signature computed over all preceding fields from the initial string up /// to, and including the signature key. + #[must_use] pub fn signature(&self) -> &Signature { &self.signature } @@ -350,10 +387,15 @@ impl Certificate { /// Perform certificate validation using the system clock to check that /// the current time is within the certificate's validity window. /// - /// # ⚠️ Security Warning: Some Assembly Required + ///
+ /// Security Warning: Some Assembly Required + /// + /// See [`Certificate::validate_at`] documentation for important notes on how to properly + /// validate certificates! + ///
/// - /// See [`Certificate::validate_at`] documentation for important notes on - /// how to properly validate certificates! + /// # Errors + /// Returns [`Error::CertificateValidation`] if the certificate failed to validate #[cfg(feature = "std")] pub fn validate<'a, I>(&self, ca_fingerprints: I) -> Result<()> where @@ -367,29 +409,28 @@ impl Certificate { /// Checks for the following: /// /// - Specified Unix timestamp is within the certificate's valid range - /// - Certificate's signature validates against the public key included in - /// the certificate - /// - Fingerprint of the public key included in the certificate matches one - /// of the trusted certificate authority (CA) fingerprints provided in - /// the `ca_fingerprints` parameter. + /// - Certificate's signature validates against the public key included in the certificate + /// - Fingerprint of the public key included in the certificate matches one of the trusted + /// certificate authority (CA) fingerprints provided in the `ca_fingerprints` parameter. /// /// NOTE: only SHA-256 fingerprints are supported at this time. /// - /// # ⚠️ Security Warning: Some Assembly Required + ///
+ /// Security Warning: Some Assembly Required /// - /// This method does not perform the full set of validation checks needed - /// to determine if a certificate is to be trusted. + /// This method does not perform the full set of validation checks needed to determine if a + /// certificate is to be trusted. /// - /// If this method succeeds, the following properties still need to be - /// checked to ensure the certificate is valid: + /// If this method succeeds, the following properties still need to be checked to ensure the + /// certificate is valid: /// - /// - `valid_principals` is empty or contains the expected principal - /// - `critical_options` is empty or contains *only* options which are - /// recognized, and that the recognized options are all valid + /// - `valid_principals` is empty or contains the expected principal. + /// - `critical_options` is empty or contains *only* options which are recognized, and that the + /// recognized options are all valid. + ///
/// - /// ## Returns - /// - `Ok` if the certificate validated successfully - /// - `Error::CertificateValidation` if the certificate failed to validate + /// # Errors + /// Returns [`Error::CertificateValidation`] if the certificate failed to validate pub fn validate_at<'a, I>(&self, unix_timestamp: u64, ca_fingerprints: I) -> Result<()> where I: IntoIterator, @@ -420,17 +461,17 @@ impl Certificate { /// Verify the signature on the certificate against the public key in the /// certificate. /// - /// # ⚠️ Security Warning - /// - /// DON'T USE THIS! + ///
+ /// Security Warning: DON'T USE THIS! /// /// This function alone does not provide any security guarantees whatsoever. /// - /// It verifies the signature in the certificate matches the CA public key - /// in the certificate, but does not ensure the CA is trusted. + /// It verifies the signature in the certificate matches the CA public key in the certificate, + /// but does not ensure the CA is trusted. /// - /// It is public only for testing purposes, and deliberately hidden from - /// the documentation for that reason. + /// It is public only for testing purposes, and deliberately hidden from the documentation for + /// that reason. + ///
#[doc(hidden)] pub fn verify_signature(&self) -> Result<()> { let mut tbs_certificate = Vec::new(); @@ -459,6 +500,9 @@ impl Certificate { } /// Decode [`Certificate`] for the specified algorithm. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. pub fn decode_as(reader: &mut impl Reader, algorithm: Algorithm) -> Result { Ok(Self { nonce: Vec::decode(reader)?, @@ -523,7 +567,10 @@ impl FromStr for Certificate { } } -#[allow(clippy::to_string_trait_impl)] +#[allow( + clippy::to_string_trait_impl, + reason = "`SshFormat` lacks `core::fmt` support" +)] impl ToString for Certificate { fn to_string(&self) -> String { self.to_openssh().expect("SSH certificate encoding error") diff --git a/ssh-key/src/certificate/builder.rs b/ssh-key/src/certificate/builder.rs index cf6ac3e9..fcd498c3 100644 --- a/ssh-key/src/certificate/builder.rs +++ b/ssh-key/src/certificate/builder.rs @@ -3,15 +3,15 @@ use super::{CertType, Certificate, Field, OptionsMap}; use crate::{Comment, Result, Signature, SigningKey, public}; use alloc::{string::String, vec::Vec}; +use core::fmt::{self, Debug, Formatter}; #[cfg(feature = "rand_core")] use rand_core::CryptoRng; - #[cfg(feature = "std")] use {super::UnixTime, std::time::SystemTime}; #[cfg(doc)] -use crate::PrivateKey; +use crate::{Error, PrivateKey}; /// OpenSSH certificate builder. /// @@ -34,15 +34,12 @@ use crate::PrivateKey; /// /// ## Example /// +#[cfg_attr(all(feature = "ed25519", feature = "getrandom"), doc = " ```")] #[cfg_attr( - all(feature = "ed25519", feature = "getrandom", feature = "std"), - doc = " ```" -)] -#[cfg_attr( - not(all(feature = "ed25519", feature = "getrandom", feature = "std")), + not(all(feature = "ed25519", feature = "getrandom")), doc = " ```ignore" )] -/// # fn main() -> Result<(), Box> { +/// # fn main() -> Result<(), ssh_key::Error> { /// use ssh_key::{Algorithm, PrivateKey, certificate, rand_core::{TryRngCore, OsRng}}; /// use std::time::{SystemTime, UNIX_EPOCH}; /// @@ -97,8 +94,11 @@ impl Builder { /// Create a new certificate builder for the given subject's public key. /// - /// Also requires a nonce (random value typically 16 or 32 bytes long) and - /// the validity window of the certificate as Unix seconds. + /// Also requires a nonce (random value typically 16 or 32 bytes long) and the validity window + /// of the certificate as Unix seconds. + /// + /// # Errors + /// Returns [`Error::CertificateFieldInvalid`] if `valid_before < valid_after`. pub fn new( nonce: impl Into>, public_key: impl Into, @@ -124,8 +124,10 @@ impl Builder { }) } - /// Create a new certificate builder with the validity window specified - /// using [`SystemTime`] values. + /// Create a new certificate builder with the validity window specified using [`SystemTime`]s. + /// + /// # Errors + /// Returns [`Error::CertificateFieldInvalid`] if `valid_before < valid_after`. #[cfg(feature = "std")] pub fn new_with_validity_times( nonce: impl Into>, @@ -142,8 +144,11 @@ impl Builder { Self::new(nonce, public_key, valid_after.into(), valid_before.into()) } - /// Create a new certificate builder, generating a random nonce using the - /// provided random number generator. + /// Create a new certificate builder, generating a random nonce using the provided random number + /// generator. + /// + /// # Errors + /// Returns [`Error::CertificateFieldInvalid`] if `valid_before < valid_after`. #[cfg(feature = "rand_core")] pub fn new_with_random_nonce( rng: &mut impl CryptoRng, @@ -159,6 +164,9 @@ impl Builder { /// Set certificate serial number. /// /// Default: `0`. + /// + /// # Errors + /// Returns [`Error::CertificateFieldInvalid`] if called multiple times. pub fn serial(&mut self, serial: u64) -> Result<&mut Self> { if self.serial.is_some() { return Err(Field::Serial.invalid_error()); @@ -171,6 +179,9 @@ impl Builder { /// Set certificate type: user or host. /// /// Default: [`CertType::User`]. + /// + /// # Errors + /// Returns [`Error::CertificateFieldInvalid`] if called multiple times. pub fn cert_type(&mut self, cert_type: CertType) -> Result<&mut Self> { if self.cert_type.is_some() { return Err(Field::Type.invalid_error()); @@ -183,6 +194,9 @@ impl Builder { /// Set key ID: label to identify this particular certificate. /// /// Default `""` + /// + /// # Errors + /// Returns [`Error::CertificateFieldInvalid`] if called multiple times. pub fn key_id(&mut self, key_id: impl Into) -> Result<&mut Self> { if self.key_id.is_some() { return Err(Field::KeyId.invalid_error()); @@ -193,6 +207,9 @@ impl Builder { } /// Add a principal (i.e. username or hostname) to `valid_principals`. + /// + /// # Errors + /// Currently does not return errors. pub fn valid_principal(&mut self, principal: impl Into) -> Result<&mut Self> { match &mut self.valid_principals { Some(principals) => principals.push(principal.into()), @@ -204,11 +221,15 @@ impl Builder { /// Mark this certificate as being valid for all principals. /// - /// # ⚠️ Security Warning + ///
+ /// Security Warning + /// + /// Use this method with care! It generates "golden ticket" certificates which can e.g. + /// authenticate as any user on a system, or impersonate any host. + ///
/// - /// Use this method with care! It generates "golden ticket" certificates - /// which can e.g. authenticate as any user on a system, or impersonate - /// any host. + /// # Errors + /// Currently does not return errors. pub fn all_principals_valid(&mut self) -> Result<&mut Self> { self.valid_principals = Some(Vec::new()); Ok(self) @@ -217,6 +238,10 @@ impl Builder { /// Add a critical option to this certificate. /// /// Critical options must be recognized or the certificate must be rejected. + /// + /// # Errors + /// Returns [`Error::CertificateFieldInvalid`] if `name` is already configured as a critical + /// option. pub fn critical_option( &mut self, name: impl Into, @@ -236,6 +261,9 @@ impl Builder { /// Add an extension to this certificate. /// /// Extensions can be unrecognized without impacting the certificate. + /// + /// # Errors + /// Returns [`Error::CertificateFieldInvalid`] if `name` is already configured as an extension. pub fn extension( &mut self, name: impl Into, @@ -255,6 +283,9 @@ impl Builder { /// Add a comment to this certificate. /// /// Default `""` + /// + /// # Errors + /// Returns [`Error::CertificateFieldInvalid`] if called multiple times. pub fn comment(&mut self, comment: impl Into) -> Result<&mut Self> { if self.comment.is_some() { return Err(Field::Comment.invalid_error()); @@ -267,6 +298,10 @@ impl Builder { /// Sign the certificate using the provided signer type. /// /// The [`PrivateKey`] type can be used as a signer. + /// + /// # Errors + /// - Returns [`Error::CertificateFieldInvalid`] if any fields are invalid. + /// - Returns [`Error::Signature`] if signature generation failed. pub fn sign(self, signing_key: &S) -> Result { // Empty valid principals result in a "golden ticket", so this check // ensures that was explicitly configured via `all_principals_valid`. @@ -305,3 +340,9 @@ impl Builder { Ok(cert) } } + +impl Debug for Builder { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Builder").finish_non_exhaustive() + } +} diff --git a/ssh-key/src/certificate/cert_type.rs b/ssh-key/src/certificate/cert_type.rs index 51f94a39..485aeb2c 100644 --- a/ssh-key/src/certificate/cert_type.rs +++ b/ssh-key/src/certificate/cert_type.rs @@ -18,11 +18,13 @@ pub enum CertType { impl CertType { /// Is this a host certificate? + #[must_use] pub fn is_host(self) -> bool { self == CertType::Host } /// Is this a user certificate? + #[must_use] pub fn is_user(self) -> bool { self == CertType::User } @@ -48,6 +50,7 @@ impl Encode for CertType { } impl From for u32 { + #[allow(clippy::as_conversions, reason = "repr(u32) enum")] fn from(cert_type: CertType) -> u32 { cert_type as u32 } diff --git a/ssh-key/src/certificate/field.rs b/ssh-key/src/certificate/field.rs index 727c81b6..822a1c61 100644 --- a/ssh-key/src/certificate/field.rs +++ b/ssh-key/src/certificate/field.rs @@ -51,6 +51,7 @@ pub enum Field { impl Field { /// Get the field name as a string + #[must_use] pub fn as_str(self) -> &'static str { match self { Self::PublicKey => "public key", @@ -70,6 +71,7 @@ impl Field { } /// Get an [`Error`] that this field is invalid. + #[must_use] pub fn invalid_error(self) -> Error { Error::CertificateFieldInvalid(self) } diff --git a/ssh-key/src/certificate/options_map.rs b/ssh-key/src/certificate/options_map.rs index 6acf1082..3fc43bc5 100644 --- a/ssh-key/src/certificate/options_map.rs +++ b/ssh-key/src/certificate/options_map.rs @@ -14,6 +14,7 @@ pub struct OptionsMap(pub BTreeMap); impl OptionsMap { /// Create a new [`OptionsMap`]. + #[must_use] pub fn new() -> Self { Self::default() } @@ -94,7 +95,7 @@ impl Encode for OptionsMap { if data.is_empty() { 0usize.encode(writer)?; } else { - data.encode_prefixed(writer)? + data.encode_prefixed(writer)?; } } diff --git a/ssh-key/src/certificate/unix_time.rs b/ssh-key/src/certificate/unix_time.rs index 165a42d7..636fe87a 100644 --- a/ssh-key/src/certificate/unix_time.rs +++ b/ssh-key/src/certificate/unix_time.rs @@ -12,11 +12,12 @@ use { }; /// Maximum allowed value for a Unix timestamp. -pub const MAX_SECS: u64 = i64::MAX as u64; +#[allow(clippy::as_conversions, reason = "constant")] +pub(super) const MAX_SECS: u64 = i64::MAX as u64; /// Sentinel value meaning "no expiry" per OpenSSH PROTOCOL.certkeys. /// When `valid_before` is set to this value, the certificate never expires. -pub const FOREVER_SECS: u64 = u64::MAX; +pub(super) const FOREVER_SECS: u64 = u64::MAX; /// Unix timestamps as used in OpenSSH certificates. #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] @@ -38,7 +39,7 @@ impl UnixTime { /// than or equal to `i64::MAX`, or `u64::MAX` (the OpenSSH "no expiry" /// sentinel defined in PROTOCOL.certkeys). #[cfg(not(feature = "std"))] - pub fn new(secs: u64) -> Result { + pub(super) fn new(secs: u64) -> Result { if secs == FOREVER_SECS || secs <= MAX_SECS { Ok(Self { secs }) } else { @@ -55,7 +56,7 @@ impl UnixTime { /// `u64::MAX` is the OpenSSH "no expiry" sentinel (PROTOCOL.certkeys) and /// is accepted; its `SystemTime` representation is capped at `MAX_SECS`. #[cfg(feature = "std")] - pub fn new(secs: u64) -> Result { + pub(super) fn new(secs: u64) -> Result { // u64::MAX is OpenSSH's sentinel for "certificate never expires". // Cap the SystemTime representation at MAX_SECS so it remains valid, // but preserve the original secs value for encoding round-trips. @@ -73,7 +74,7 @@ impl UnixTime { /// Get the current time as a Unix timestamp. #[cfg(feature = "std")] - pub fn now() -> Result { + pub(super) fn now() -> Result { SystemTime::now().try_into() } } diff --git a/ssh-key/src/comment.rs b/ssh-key/src/comment.rs index 0e9e7086..b6b71a52 100644 --- a/ssh-key/src/comment.rs +++ b/ssh-key/src/comment.rs @@ -109,11 +109,16 @@ impl fmt::Display for Comment { impl Comment { /// Interpret the comment as raw binary data. + #[must_use] pub fn as_bytes(&self) -> &[u8] { &self.0 } /// Interpret the comment as a UTF-8 string. + /// + /// # Errors + /// Returns [`Error::CharacterEncoding`] in the event the underlying bytes cannot be interpreted + /// as valid UTF-8. pub fn as_str(&self) -> Result<&str, Error> { Ok(str::from_utf8(&self.0)?) } @@ -123,6 +128,7 @@ impl Comment { /// This is the maximal prefix of the comment which can be interpreted as valid UTF-8. // TODO(tarcieri): precompute and store the offset which represents this prefix? #[cfg(feature = "alloc")] + #[must_use] pub fn as_str_lossy(&self) -> &str { for i in (1..=self.len()).rev() { if let Ok(s) = str::from_utf8(&self.0[..i]) { @@ -134,11 +140,13 @@ impl Comment { } /// Is the comment empty? + #[must_use] pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Get the length of this comment in bytes. + #[must_use] pub fn len(&self) -> usize { self.0.len() } diff --git a/ssh-key/src/dot_ssh.rs b/ssh-key/src/dot_ssh.rs index e48de5a0..bd3f9e3c 100644 --- a/ssh-key/src/dot_ssh.rs +++ b/ssh-key/src/dot_ssh.rs @@ -1,12 +1,16 @@ //! `~/.ssh` support. use crate::{Fingerprint, PrivateKey, PublicKey, Result}; +use core::fmt::{self, Debug}; use std::{ env, fs::{self, ReadDir}, path::{Path, PathBuf}, }; +#[cfg(doc)] +use crate::Error; + /// `~/.ssh` directory support (or similarly structured directories). #[derive(Clone, Eq, PartialEq)] pub struct DotSsh { @@ -17,8 +21,9 @@ impl DotSsh { /// Open `~/.ssh` if the home directory can be located. /// /// Returns `None` if the home directory couldn't be located. + #[must_use] pub fn new() -> Option { - #[allow(deprecated)] // NOTE: no longer deprecated as of Rust 1.86 + #[allow(deprecated, reason = "TODO MSRV: Rust 1.86 un-deprecates this")] env::home_dir().map(|path| Self::open(path.join(".ssh"))) } @@ -35,16 +40,21 @@ impl DotSsh { } /// Get the path to the `~/.ssh` directory (or whatever [`DotSsh::open`] was called with). + #[must_use] pub fn path(&self) -> &Path { &self.path } /// Get the path to the `~/.ssh/config` configuration file. Does not check if it exists. + #[must_use] pub fn config_path(&self) -> PathBuf { self.path.join("config") } /// Iterate over the private keys in the `~/.ssh` directory. + /// + /// # Errors + /// Returns [`Error::Io`] in the event of I/O errors. pub fn private_keys(&self) -> Result> { Ok(PrivateKeysIter { read_dir: fs::read_dir(&self.path)?, @@ -52,6 +62,7 @@ impl DotSsh { } /// Find a private key whose public key has the given key fingerprint. + #[must_use] pub fn private_key_with_fingerprint(&self, fingerprint: Fingerprint) -> Option { self.private_keys() .ok()? @@ -59,6 +70,9 @@ impl DotSsh { } /// Iterate over the public keys in the `~/.ssh` directory. + /// + /// # Errors + /// Returns [`Error::Io`] in the event of I/O errors. pub fn public_keys(&self) -> Result> { Ok(PublicKeysIter { read_dir: fs::read_dir(&self.path)?, @@ -66,6 +80,7 @@ impl DotSsh { } /// Find a public key with the given key fingerprint. + #[must_use] pub fn public_key_with_fingerprint(&self, fingerprint: Fingerprint) -> Option { self.public_keys() .ok()? @@ -73,16 +88,28 @@ impl DotSsh { } /// Write a private key into `~/.ssh`. + /// + /// # Errors + /// Returns [`Error::Io`] in the event of I/O errors. pub fn write_private_key(&self, filename: impl AsRef, key: &PrivateKey) -> Result<()> { key.write_openssh_file(self.path.join(filename), Default::default()) } /// Write a public key into `~/.ssh`. + /// + /// # Errors + /// Returns [`Error::Io`] in the event of I/O errors. pub fn write_public_key(&self, filename: impl AsRef, key: &PublicKey) -> Result<()> { key.write_openssh_file(self.path.join(filename)) } } +impl Debug for DotSsh { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DotSsh").finish_non_exhaustive() + } +} + impl Default for DotSsh { /// Calls [`DotSsh::new`] and panics if the home directory could not be located. fn default() -> Self { @@ -91,7 +118,7 @@ impl Default for DotSsh { } /// Iterator over the private keys in the `~/.ssh` directory. -pub struct PrivateKeysIter { +pub(crate) struct PrivateKeysIter { read_dir: ReadDir, } @@ -110,7 +137,7 @@ impl Iterator for PrivateKeysIter { } /// Iterator over the public keys in the `~/.ssh` directory. -pub struct PublicKeysIter { +pub(crate) struct PublicKeysIter { read_dir: ReadDir, } diff --git a/ssh-key/src/error.rs b/ssh-key/src/error.rs index d1729bd7..43c219a4 100644 --- a/ssh-key/src/error.rs +++ b/ssh-key/src/error.rs @@ -83,6 +83,9 @@ pub enum Error { #[cfg(feature = "rand_core")] RngFailure, + /// Signature errors. + Signature, + /// Invalid timestamp (e.g. in a certificate) Time, @@ -127,6 +130,7 @@ impl fmt::Display for Error { Error::PublicKey => write!(f, "public key is incorrect"), #[cfg(feature = "rand_core")] Error::RngFailure => write!(f, "random number generator failure"), + Error::Signature => write!(f, "signature error"), Error::Time => write!(f, "invalid time"), Error::TrailingData { remaining } => write!( f, @@ -193,7 +197,7 @@ impl From for Error { #[cfg(not(feature = "alloc"))] impl From for Error { fn from(_: signature::Error) -> Error { - Error::Crypto + Error::Signature } } @@ -204,7 +208,7 @@ impl From for Error { err.source() .and_then(|source| source.downcast_ref().cloned()) - .unwrap_or(Error::Crypto) + .unwrap_or(Error::Signature) } } diff --git a/ssh-key/src/fingerprint.rs b/ssh-key/src/fingerprint.rs index 1fd157ac..74d3faf0 100644 --- a/ssh-key/src/fingerprint.rs +++ b/ssh-key/src/fingerprint.rs @@ -14,15 +14,14 @@ use encoding::{ }; use sha2::{Digest, Sha256, Sha512}; -/// Fingerprint encoding error message. -const FINGERPRINT_ERR_MSG: &str = "fingerprint encoding error"; - #[cfg(feature = "alloc")] use alloc::string::{String, ToString}; - #[cfg(all(feature = "alloc", feature = "serde"))] use serde::{Deserialize, Serialize, de, ser}; +/// Fingerprint encoding error message. +const FINGERPRINT_ERR_MSG: &str = "fingerprint encoding error"; + /// SSH public key fingerprints. /// /// Fingerprints have an associated key fingerprint algorithm, i.e. a hash @@ -59,6 +58,8 @@ impl Fingerprint { /// Create a fingerprint of the given public key data using the provided /// hash algorithm. + #[must_use] + #[allow(clippy::missing_panics_doc, reason = "should not panic")] pub fn new(algorithm: HashAlg, public_key: &public::KeyData) -> Self { match algorithm { HashAlg::Sha256 => { @@ -79,6 +80,7 @@ impl Fingerprint { } /// Get the hash algorithm used for this fingerprint. + #[must_use] pub fn algorithm(self) -> HashAlg { match self { Self::Sha256(_) => HashAlg::Sha256, @@ -87,6 +89,7 @@ impl Fingerprint { } /// Get the name of the hash algorithm (upper case e.g. "SHA256"). + #[must_use] pub fn prefix(self) -> &'static str { match self.algorithm() { HashAlg::Sha256 => "SHA256", @@ -103,6 +106,7 @@ impl Fingerprint { } /// Get the raw digest output for the fingerprint as bytes. + #[must_use] pub fn as_bytes(&self) -> &[u8] { match self { Self::Sha256(bytes) => bytes.as_slice(), @@ -111,6 +115,7 @@ impl Fingerprint { } /// Get the SHA-256 fingerprint, if this is one. + #[must_use] pub fn sha256(self) -> Option<[u8; HashAlg::Sha256.digest_size()]> { match self { Self::Sha256(fingerprint) => Some(fingerprint), @@ -119,6 +124,7 @@ impl Fingerprint { } /// Get the SHA-512 fingerprint, if this is one. + #[must_use] pub fn sha512(self) -> Option<[u8; HashAlg::Sha512.digest_size()]> { match self { Self::Sha512(fingerprint) => Some(fingerprint), @@ -127,16 +133,21 @@ impl Fingerprint { } /// Is this fingerprint SHA-256? + #[must_use] pub fn is_sha256(self) -> bool { matches!(self, Self::Sha256(_)) } /// Is this fingerprint SHA-512? + #[must_use] pub fn is_sha512(self) -> bool { matches!(self, Self::Sha512(_)) } /// Format "randomart" for this fingerprint using the provided formatter. + /// + /// # Errors + /// Propagates errors from [`fmt::Formatter`]. pub fn fmt_randomart(self, header: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result { Randomart::new(header, self).fmt(f) } @@ -157,6 +168,7 @@ impl Fingerprint { /// +----[SHA256]-----+ /// ``` #[cfg(feature = "alloc")] + #[must_use] pub fn to_randomart(self, header: &str) -> String { Randomart::new(header, self).to_string() } diff --git a/ssh-key/src/fingerprint/randomart.rs b/ssh-key/src/fingerprint/randomart.rs index 88a40821..c64d6206 100644 --- a/ssh-key/src/fingerprint/randomart.rs +++ b/ssh-key/src/fingerprint/randomart.rs @@ -6,13 +6,15 @@ //! //! +#![allow(clippy::as_conversions, clippy::integer_division_remainder_used)] + use super::Fingerprint; use core::fmt; -const WIDTH: usize = 17; +const NVALUES: u8 = 16; +const WIDTH: usize = (NVALUES + 1) as usize; const HEIGHT: usize = 9; -const VALUES: &[u8; 17] = b" .o+=*BOX@%&#/^SE"; -const NVALUES: u8 = VALUES.len() as u8 - 1; +const VALUES: &[u8; WIDTH] = b" .o+=*BOX@%&#/^SE"; type Field = [[u8; WIDTH]; HEIGHT]; diff --git a/ssh-key/src/kdf.rs b/ssh-key/src/kdf.rs index de93337c..5a66a8c9 100644 --- a/ssh-key/src/kdf.rs +++ b/ssh-key/src/kdf.rs @@ -41,6 +41,9 @@ pub enum Kdf { impl Kdf { /// Initialize KDF configuration for the given algorithm. + /// + /// # Errors + /// Returns [`Error::RngFailure`] on RNG errors. #[cfg(feature = "encryption")] pub fn new(algorithm: KdfAlg, rng: &mut R) -> Result { let mut salt = vec![0u8; DEFAULT_SALT_SIZE]; @@ -60,6 +63,7 @@ impl Kdf { } /// Get the KDF algorithm. + #[must_use] pub fn algorithm(&self) -> KdfAlg { match self { Self::None => KdfAlg::None, @@ -69,6 +73,10 @@ impl Kdf { } /// Derive an encryption key from the given password. + /// + /// # Errors + /// Returns [`Error::Crypto`] in the event the underlying password hashing primitive returns an + /// error. #[cfg(feature = "encryption")] pub fn derive(&self, password: impl AsRef<[u8]>, output: &mut [u8]) -> Result<()> { match self { @@ -83,6 +91,10 @@ impl Kdf { /// Derive key and IV for the given [`Cipher`]. /// /// Returns two byte vectors containing the key and IV respectively. + /// + /// # Errors + /// - Returns [`Error::Decrypted`] if `cipher` is [`Cipher::None`]. + /// - Returns [`Error::Encoding`] in the event of a length error. #[cfg(feature = "encryption")] pub fn derive_key_and_iv( &self, @@ -110,17 +122,20 @@ impl Kdf { } /// Is the KDF configured as `none`? + #[must_use] pub fn is_none(&self) -> bool { self == &Self::None } /// Is the KDF configured as anything other than `none`? + #[must_use] pub fn is_some(&self) -> bool { !self.is_none() } /// Is the KDF configured as `bcrypt` (i.e. bcrypt-pbkdf)? #[cfg(feature = "alloc")] + #[must_use] pub fn is_bcrypt(&self) -> bool { matches!(self, Self::Bcrypt { .. }) } @@ -174,7 +189,7 @@ impl Encode for Kdf { Self::Bcrypt { salt, rounds } => { [8, salt.len()].checked_sum()?.encode(writer)?; salt.encode(writer)?; - rounds.encode(writer)? + rounds.encode(writer)?; } } diff --git a/ssh-key/src/known_hosts.rs b/ssh-key/src/known_hosts.rs index a185f313..4c265d59 100644 --- a/ssh-key/src/known_hosts.rs +++ b/ssh-key/src/known_hosts.rs @@ -1,14 +1,15 @@ //! Parser for `KnownHostsFile`-formatted data. use crate::{Error, PublicKey, Result}; -use core::str; -use encoding::base64::{Base64, Encoding}; - -use { - alloc::string::{String, ToString}, - alloc::vec::Vec, - core::fmt, +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; +use core::{ + fmt::{self, Debug}, + str, }; +use encoding::base64::{Base64, Encoding}; #[cfg(feature = "std")] use std::{fs, path::Path}; @@ -51,14 +52,18 @@ pub struct KnownHosts<'a> { impl<'a> KnownHosts<'a> { /// Create a new parser for the given input buffer. + #[must_use] pub fn new(input: &'a str) -> Self { Self { lines: input.lines(), } } - /// Read a [`KnownHosts`] file from the filesystem, returning an - /// [`Entry`] vector on success. + /// Read a [`KnownHosts`] file from the filesystem, returning an [`Entry`] vector on success. + /// + /// # Errors + /// - Returns [`Error::Io`] in event of I/O errors reading the file. + /// - Propagates [`Entry`] parsing errors as [`Error::FormatEncoding`]. #[cfg(feature = "std")] pub fn read_file(path: impl AsRef) -> Result> { // TODO(tarcieri): permissions checks @@ -66,9 +71,7 @@ impl<'a> KnownHosts<'a> { KnownHosts::new(&input).collect() } - /// Get the next line, trimming any comments and trailing whitespace. - /// - /// Ignores empty lines. + /// Get the next line, trimming any comments and trailing whitespace. Ignores empty lines. fn next_line_trimmed(&mut self) -> Option<&'a str> { loop { let mut line = self.lines.next()?; @@ -88,11 +91,17 @@ impl<'a> KnownHosts<'a> { } } +impl Debug for KnownHosts<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KnownHosts").finish_non_exhaustive() + } +} + impl Iterator for KnownHosts<'_> { type Item = Result; fn next(&mut self) -> Option> { - self.next_line_trimmed().map(|line| line.parse()) + self.next_line_trimmed().map(str::parse) } } @@ -111,16 +120,19 @@ pub struct Entry { impl Entry { /// Get the marker for this entry, if present. + #[must_use] pub fn marker(&self) -> Option<&Marker> { self.marker.as_ref() } /// Get the host pattern enumerator for this entry + #[must_use] pub fn host_patterns(&self) -> &HostPatterns { &self.host_patterns } /// Get public key for this entry. + #[must_use] pub fn public_key(&self) -> &PublicKey { &self.public_key } @@ -190,10 +202,11 @@ impl ToString for Entry { /// Markers associated with this host key entry. /// /// There can only be one of these per host key entry. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Marker { - /// This host entry's public key is for a certificate authority's private key + /// This host entry's public key is for a certificate authority's private key. CertAuthority, + /// This host entry's public key has been revoked, and should not be allowed to connect /// regardless of any other entry. Revoked, @@ -201,6 +214,7 @@ pub enum Marker { impl Marker { /// Get the string form of the marker + #[must_use] pub fn as_str(&self) -> &str { match self { Self::CertAuthority => "@cert-authority", diff --git a/ssh-key/src/lib.rs b/ssh-key/src/lib.rs index 5cd0a7ce..00f9c0be 100644 --- a/ssh-key/src/lib.rs +++ b/ssh-key/src/lib.rs @@ -1,27 +1,11 @@ #![no_std] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("../README.md")] #![doc( html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg", html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg" )] -#![forbid(unsafe_code)] -#![warn( - clippy::alloc_instead_of_core, - clippy::arithmetic_side_effects, - clippy::mod_module_files, - clippy::panic, - clippy::panic_in_result_fn, - clippy::std_instead_of_alloc, - clippy::std_instead_of_core, - clippy::unwrap_used, - missing_docs, - rust_2018_idioms, - unused_lifetimes, - unused_qualifications -)] -// TODO(tarcieri): fix `getrandom` feature -#![allow(unexpected_cfgs)] +#![allow(unexpected_cfgs, reason = "TODO fix getrandom feature")] //! ## Usage //! @@ -43,9 +27,8 @@ //! //! #### Example //! -#![cfg_attr(feature = "std", doc = "```")] -#![cfg_attr(not(feature = "std"), doc = "```ignore")] -//! # fn main() -> Result<(), Box> { +//! ``` +//! # fn main() -> Result<(), ssh_key::Error> { //! use ssh_key::PublicKey; //! //! let encoded_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti user@example.com"; @@ -53,7 +36,6 @@ //! //! // Key attributes //! assert_eq!(public_key.algorithm(), ssh_key::Algorithm::Ed25519); -//! assert_eq!(public_key.comment().as_bytes(), b"user@example.com"); //! //! // Key data: in this example an Ed25519 key //! if let Some(ed25519_public_key) = public_key.key_data().ed25519() { @@ -82,9 +64,8 @@ //! //! #### Example //! -#![cfg_attr(feature = "std", doc = " ```")] -#![cfg_attr(not(feature = "std"), doc = " ```ignore")] -//! # fn main() -> Result<(), Box> { +//! ``` +//! # fn main() -> Result<(), ssh_key::Error> { //! use ssh_key::PrivateKey; //! //! // WARNING: don't actually hardcode private keys in source code!!! @@ -102,7 +83,6 @@ //! //! // Key attributes //! assert_eq!(private_key.algorithm(), ssh_key::Algorithm::Ed25519); -//! assert_eq!(private_key.comment().as_bytes(), b"user@example.com"); //! //! // Key data: in this example an Ed25519 key //! if let Some(ed25519_keypair) = private_key.key_data().ed25519() { @@ -130,13 +110,11 @@ //! //! ## `serde` support //! -//! When the `serde` feature of this crate is enabled, the [`Certificate`], -//! [`Fingerprint`], and [`PublicKey`] types receive impls of `serde`'s -//! [`Deserialize`][`serde::Deserialize`] and [`Serialize`][`serde::Serialize`] -//! traits. +//! When the `serde` feature of this crate is enabled, the [`Certificate`], [`Fingerprint`], and +//! [`PublicKey`] types receive impls of `serde`'s [`Deserialize`][`serde::Deserialize`] and +//! [`Serialize`][`serde::Serialize`] traits. //! -//! Serializing/deserializing [`PrivateKey`] using `serde` is presently -//! unsupported. +//! Serializing/deserializing [`PrivateKey`] using `serde` is presently unsupported. #[cfg(feature = "alloc")] #[macro_use] diff --git a/ssh-key/src/ppk.rs b/ssh-key/src/ppk.rs index 682f3e42..a7efd6f1 100644 --- a/ssh-key/src/ppk.rs +++ b/ssh-key/src/ppk.rs @@ -22,17 +22,17 @@ use encoding::base64::{self, Base64, Encoding}; use encoding::{Decode, Encode, LabelError, Reader}; #[derive(Debug)] -pub enum Kdf { +pub(crate) enum Kdf { Argon2 { kdf: Argon2<'static>, salt: Vec }, PpkV2, } impl Kdf { - pub fn new_v2() -> Self { + pub(crate) fn new_v2() -> Self { Self::PpkV2 } - pub fn new_v3(algorithm: &str, ppk: &PpkWrapper) -> Result { + pub(crate) fn new_v3(algorithm: &str, ppk: &PpkWrapper) -> Result { let argon_algorithm = match algorithm { "Argon2i" => Ok(argon2::Algorithm::Argon2i), "Argon2d" => Ok(argon2::Algorithm::Argon2d), @@ -69,7 +69,7 @@ impl Kdf { Ok(Self::Argon2 { kdf: argon, salt }) } - pub fn derive(&self, password: &[u8], output: &mut [u8]) -> Result<(), argon2::Error> { + pub(crate) fn derive(&self, password: &[u8], output: &mut [u8]) -> Result<(), argon2::Error> { match self { Kdf::Argon2 { kdf, salt } => kdf.hash_password_into(password, salt, output), Kdf::PpkV2 => Ok(()), @@ -78,7 +78,7 @@ impl Kdf { } #[derive(Debug)] -pub enum Cipher { +pub(crate) enum Cipher { Aes256Cbc, } @@ -140,11 +140,11 @@ impl Cipher { } } - pub fn derive_mac_key(&self, kdf: &Kdf, password: &str) -> Result { + pub(crate) fn derive_mac_key(&self, kdf: &Kdf, password: &str) -> Result { Ok(Cipher::derive_aes_params(kdf, password)?.2) } - pub fn decrypt(&self, buf: &mut [u8], kdf: &Kdf, password: &str) -> Result<(), Error> { + pub(crate) fn decrypt(&self, buf: &mut [u8], kdf: &Kdf, password: &str) -> Result<(), Error> { let (key, iv, _) = Cipher::derive_aes_params(kdf, password)?; match self { Cipher::Aes256Cbc => cipher::Cipher::Aes256Cbc @@ -155,7 +155,7 @@ impl Cipher { } #[derive(Debug)] -pub struct PpkEncryption { +pub(crate) struct PpkEncryption { pub cipher: Cipher, pub kdf: Kdf, pub passphrase: String, @@ -191,7 +191,7 @@ impl TryFrom<&str> for PpkKey { } } -pub struct PpkWrapper { +pub(crate) struct PpkWrapper { pub version: u8, pub algorithm: Algorithm, pub public_key: Option>, @@ -338,13 +338,13 @@ impl TryFrom<&str> for PpkWrapper { } #[derive(Debug)] -pub struct PpkContainer { +pub(crate) struct PpkContainer { pub public_key: PublicKey, pub keypair_data: KeypairData, } impl PpkContainer { - pub fn new(mut ppk: PpkWrapper, passphrase: Option) -> Result { + pub(crate) fn new(mut ppk: PpkWrapper, passphrase: Option) -> Result { let encryption = match ppk.values.get(&PpkKey::Encryption).map(String::as_str) { None | Some("none") => None, Some("aes256-cbc") => { diff --git a/ssh-key/src/private.rs b/ssh-key/src/private.rs index c3d2bc42..f0ce4eb4 100644 --- a/ssh-key/src/private.rs +++ b/ssh-key/src/private.rs @@ -10,9 +10,9 @@ //! When the `encryption` feature of this crate is enabled, it's possible to //! decrypt keys which have been encrypted under a password: //! -#![cfg_attr(all(feature = "encryption", feature = "std"), doc = " ```")] -#![cfg_attr(not(all(feature = "encryption", feature = "std")), doc = " ```ignore")] -//! # fn main() -> Result<(), Box> { +#![cfg_attr(feature = "encryption", doc = " ```")] +#![cfg_attr(not(feature = "encryption"), doc = " ```ignore")] +//! # fn main() -> Result<(), ssh_key::Error> { //! use ssh_key::PrivateKey; //! //! // WARNING: don't actually hardcode private keys in source code!!! @@ -47,24 +47,14 @@ //! The example below also requires enabling this crate's `getrandom` feature. //! #![cfg_attr( - all( - feature = "ed25519", - feature = "encryption", - feature = "getrandom", - feature = "std" - ), + all(feature = "ed25519", feature = "encryption", feature = "getrandom",), doc = " ```" )] #![cfg_attr( - not(all( - feature = "ed25519", - feature = "encryption", - feature = "getrandom", - feature = "std" - )), + not(all(feature = "ed25519", feature = "encryption", feature = "getrandom",)), doc = " ```ignore" )] -//! # fn main() -> Result<(), Box> { +//! # fn main() -> Result<(), ssh_key::Error> { //! use ssh_key::{Algorithm, PrivateKey, rand_core::{OsRng, TryRngCore}}; //! //! // Generate a random key @@ -88,15 +78,12 @@ //! well as the crate feature identified in backticks in the title of each //! example. //! +#![cfg_attr(all(feature = "ed25519", feature = "getrandom"), doc = " ```")] #![cfg_attr( - all(feature = "ed25519", feature = "getrandom", feature = "std"), - doc = " ```" -)] -#![cfg_attr( - not(all(feature = "ed25519", feature = "getrandom", feature = "std")), + not(all(feature = "ed25519", feature = "getrandom")), doc = " ```ignore" )] -//! # fn main() -> Result<(), Box> { +//! # fn main() -> Result<(), ssh_key::Error> { //! use ssh_key::{Algorithm, PrivateKey, rand_core::{OsRng, TryRngCore}}; //! //! let private_key = PrivateKey::random(&mut OsRng.unwrap_err(), Algorithm::Ed25519)?; @@ -218,7 +205,10 @@ impl PrivateKey { /// Create a new unencrypted private key with the given keypair data and comment. /// - /// On `no_std` platforms, use `PrivateKey::from(key_data)` instead. + /// On `no_alloc` platforms, use `PrivateKey::try_from(key_data)` instead. + /// + /// # Errors + /// Returns [`Error::Encrypted`] if the key is encrypted. #[cfg(feature = "alloc")] pub fn new(key_data: KeypairData, comment: impl Into) -> Result { if key_data.is_encrypted() { @@ -237,6 +227,9 @@ impl PrivateKey { /// ```text /// -----BEGIN OPENSSH PRIVATE KEY----- /// ``` + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. pub fn from_openssh(pem: impl AsRef<[u8]>) -> Result { Self::decode_pem(pem) } @@ -248,6 +241,9 @@ impl PrivateKey { /// ```text /// PuTTY-User-Key-File-: /// ``` + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "ppk")] pub fn from_ppk(ppk: impl AsRef, passphrase: Option) -> Result { use crate::ppk::PpkContainer; @@ -265,6 +261,9 @@ impl PrivateKey { } /// Parse a raw binary SSH private key. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. pub fn from_bytes(mut bytes: &[u8]) -> Result { let reader = &mut bytes; let private_key = Self::decode(reader)?; @@ -272,6 +271,9 @@ impl PrivateKey { } /// Encode OpenSSH-formatted (PEM) private key. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. pub fn encode_openssh<'o>( &self, line_ending: LineEnding, @@ -280,14 +282,20 @@ impl PrivateKey { Ok(self.encode_pem(line_ending, out)?) } - /// Encode an OpenSSH-formatted PEM private key, allocating a - /// self-zeroizing [`String`] for the result. + /// Encode an OpenSSH-formatted PEM private key, allocating a self-zeroizing [`String`] for the + /// result. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "alloc")] pub fn to_openssh(&self, line_ending: LineEnding) -> Result> { Ok(self.encode_pem_string(line_ending).map(Zeroizing::new)?) } /// Serialize SSH private key as raw bytes. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "alloc")] pub fn to_bytes(&self) -> Result>> { let mut private_key_bytes = Vec::with_capacity(self.encoded_len()?); @@ -341,6 +349,9 @@ impl PrivateKey { /// ``` /// /// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD + /// + /// # Errors + /// Propagates errors from [`SshSig::sign`]. #[cfg(feature = "alloc")] pub fn sign(&self, namespace: &str, hash_alg: HashAlg, msg: &[u8]) -> Result { SshSig::sign(self, namespace, hash_alg, msg) @@ -351,6 +362,9 @@ impl PrivateKey { /// These signatures can be produced using `ssh-keygen -Y sign`. /// /// For more information, see [`PrivateKey::sign`]. + /// + /// # Errors + /// Propagates errors from [`SshSig::sign_digest`]. #[cfg(feature = "alloc")] pub fn sign_digest( &self, @@ -365,6 +379,9 @@ impl PrivateKey { /// These signatures can be produced using `ssh-keygen -Y sign`. /// /// For more information, see [`PrivateKey::sign`]. + /// + /// # Errors + /// Propagates errors from [`SshSig::sign_prehash`]. #[cfg(feature = "alloc")] pub fn sign_prehash( &self, @@ -376,6 +393,10 @@ impl PrivateKey { } /// Read private key from an OpenSSH-formatted PEM source. + /// + /// # Errors + /// - Returns [`Error::Io`] on I/O errors. + /// - Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "std")] pub fn read_openssh(reader: &mut impl Read) -> Result { let pem = Zeroizing::new(io::read_to_string(reader)?); @@ -383,6 +404,10 @@ impl PrivateKey { } /// Read private key from an OpenSSH-formatted PEM file. + /// + /// # Errors + /// - Returns [`Error::Io`] on I/O errors. + /// - Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "std")] pub fn read_openssh_file(path: impl AsRef) -> Result { // TODO(tarcieri): verify file permissions match `UNIX_FILE_PERMISSIONS` @@ -391,6 +416,10 @@ impl PrivateKey { } /// Write private key as an OpenSSH-formatted PEM file. + /// + /// # Errors + /// - Returns [`Error::Io`] on I/O errors. + /// - Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "std")] pub fn write_openssh(&self, writer: &mut impl Write, line_ending: LineEnding) -> Result<()> { let pem = self.to_openssh(line_ending)?; @@ -399,6 +428,10 @@ impl PrivateKey { } /// Write private key as an OpenSSH-formatted PEM file. + /// + /// # Errors + /// - Returns [`Error::Io`] on I/O errors. + /// - Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "std")] pub fn write_openssh_file( &self, @@ -418,6 +451,7 @@ impl PrivateKey { /// Attempt to decrypt an encrypted private key using the provided /// password to derive an encryption key. /// + /// # Errors /// Returns [`Error::Decrypted`] if the private key is already decrypted. #[cfg(feature = "encryption")] pub fn decrypt(&self, password: impl AsRef<[u8]>) -> Result { @@ -443,6 +477,7 @@ impl PrivateKey { /// - Cipher: [`Cipher::Aes256Ctr`] /// - KDF: [`Kdf::Bcrypt`] (i.e. `bcrypt-pbkdf`) /// + /// # Errors /// Returns [`Error::Encrypted`] if the private key is already encrypted. #[cfg(feature = "encryption")] pub fn encrypt( @@ -456,6 +491,7 @@ impl PrivateKey { /// Encrypt an unencrypted private key using the provided password to /// derive an encryption key for the provided [`Cipher`]. /// + /// # Errors /// Returns [`Error::Encrypted`] if the private key is already encrypted. #[cfg(feature = "encryption")] pub fn encrypt_with_cipher( @@ -477,6 +513,7 @@ impl PrivateKey { /// Encrypt an unencrypted private key using the provided cipher and KDF /// configuration. /// + /// # Errors /// Returns [`Error::Encrypted`] if the private key is already encrypted. #[cfg(feature = "encryption")] pub fn encrypt_with( @@ -509,17 +546,20 @@ impl PrivateKey { } /// Get the digital signature [`Algorithm`] used by this key. + #[must_use] pub fn algorithm(&self) -> Algorithm { self.public_key.algorithm() } /// Comment on the key (e.g. email address). #[cfg(feature = "alloc")] + #[must_use] pub fn comment(&self) -> &Comment { self.public_key.comment() } /// Cipher algorithm (a.k.a. `ciphername`). + #[must_use] pub fn cipher(&self) -> Cipher { self.cipher } @@ -527,11 +567,13 @@ impl PrivateKey { /// Compute key fingerprint. /// /// Use [`Default::default()`] to use the default hash function (SHA-256). + #[must_use] pub fn fingerprint(&self, hash_alg: HashAlg) -> Fingerprint { self.public_key.fingerprint(hash_alg) } /// Is this key encrypted? + #[must_use] pub fn is_encrypted(&self) -> bool { let ret = self.key_data.is_encrypted(); debug_assert_eq!(ret, self.cipher.is_some()); @@ -541,24 +583,27 @@ impl PrivateKey { /// Key Derivation Function (KDF) used to encrypt this key. /// /// Returns [`Kdf::None`] if this key is not encrypted. + #[must_use] pub fn kdf(&self) -> &Kdf { &self.kdf } /// Keypair data. + #[must_use] pub fn key_data(&self) -> &KeypairData { &self.key_data } /// Get the [`PublicKey`] which corresponds to this private key. + #[must_use] pub fn public_key(&self) -> &PublicKey { &self.public_key } /// Generate a random key which uses the given algorithm. /// - /// # Returns - /// - `Error::AlgorithmUnknown` if the algorithm is unsupported. + /// # Errors + /// Returns `Error::AlgorithmUnknown` if the algorithm is unsupported. #[cfg(feature = "rand_core")] #[allow(unreachable_code, unused_variables)] pub fn random(rng: &mut R, algorithm: Algorithm) -> Result { @@ -741,11 +786,11 @@ impl CtEq for PrivateKey { fn ct_eq(&self, other: &Self) -> Choice { // Constant-time with respect to private key data self.key_data.ct_eq(&other.key_data) - & Choice::from( - (self.cipher == other.cipher + & Choice::from(u8::from( + self.cipher == other.cipher && self.kdf == other.kdf - && self.public_key == other.public_key) as u8, - ) + && self.public_key == other.public_key, + )) } } @@ -861,7 +906,7 @@ impl Encode for PrivateKey { 4, // number of keys (uint32) self.public_key.key_data().encoded_len_prefixed()?, private_key_len, - self.auth_tag.map(|tag| tag.len()).unwrap_or(0), + self.auth_tag.map_or(0, |tag| tag.len()), ] .checked_sum() } diff --git a/ssh-key/src/private/dsa.rs b/ssh-key/src/private/dsa.rs index 496845fe..46b421f3 100644 --- a/ssh-key/src/private/dsa.rs +++ b/ssh-key/src/private/dsa.rs @@ -26,6 +26,9 @@ pub struct DsaPrivateKey { impl DsaPrivateKey { /// Create a new DSA private key given the value `x`. + /// + /// # Errors + /// Returns [`Error::FormatEncoding`] if `x` is negative. pub fn new(x: Mpint) -> Result { if x.is_positive() { Ok(Self { inner: x }) @@ -35,11 +38,13 @@ impl DsaPrivateKey { } /// Get the serialized private key as bytes. + #[must_use] pub fn as_bytes(&self) -> &[u8] { self.inner.as_bytes() } /// Get the inner [`Mpint`]. + #[must_use] pub fn as_mpint(&self) -> &Mpint { &self.inner } @@ -155,23 +160,29 @@ impl DsaKeypair { /// Generate a random DSA private key. #[cfg(all(feature = "dsa", feature = "rand_core"))] + #[expect(clippy::missing_errors_doc, reason = "TODO")] pub fn random(rng: &mut R) -> Result { let components = dsa::Components::generate(rng, Self::KEY_SIZE); Ok(dsa::SigningKey::generate(rng, components).into()) } /// Create a new [`DsaKeypair`] with the given `public` and `private` components. + /// + /// # Errors + /// Returns [`Error::Crypto`] if the `public` key does not match the `private` key (TODO). pub fn new(public: DsaPublicKey, private: DsaPrivateKey) -> Result { - // TODO(tarcieri): validate the `public` and `private` components match + // TODO(tarcieri): actually validate the `public` and `private` components match Ok(Self { public, private }) } /// Get the public component of this key. + #[must_use] pub fn public(&self) -> &DsaPublicKey { &self.public } /// Get the private component of this key. + #[must_use] pub fn private(&self) -> &DsaPrivateKey { &self.private } @@ -179,7 +190,7 @@ impl DsaKeypair { impl CtEq for DsaKeypair { fn ct_eq(&self, other: &Self) -> Choice { - Choice::from((self.public == other.public) as u8) & self.private.ct_eq(&other.private) + Choice::from(u8::from(self.public == other.public)) & self.private.ct_eq(&other.private) } } diff --git a/ssh-key/src/private/ecdsa.rs b/ssh-key/src/private/ecdsa.rs index eebb32ed..7e0ea8d9 100644 --- a/ssh-key/src/private/ecdsa.rs +++ b/ssh-key/src/private/ecdsa.rs @@ -25,11 +25,13 @@ pub struct EcdsaPrivateKey { impl EcdsaPrivateKey { /// Borrow the inner byte array as a slice. + #[must_use] pub fn as_slice(&self) -> &[u8] { self.bytes.as_ref() } /// Convert to the inner byte array. + #[must_use] pub fn into_bytes(self) -> [u8; SIZE] { self.bytes } @@ -211,6 +213,10 @@ pub enum EcdsaKeypair { impl EcdsaKeypair { /// Generate a random ECDSA private key. + /// + /// # Errors + /// Returns [`Error::AlgorithmUnsupported`] if support has not been enabled for the given + /// elliptic curve, e.g. the `p256`, `p384`, or `p521` crate feature has not been enabled. #[cfg(feature = "rand_core")] #[allow(unused_variables)] pub fn random(rng: &mut R, curve: EcdsaCurve) -> Result { @@ -243,11 +249,14 @@ impl EcdsaKeypair { }) } #[cfg(not(all(feature = "p256", feature = "p384", feature = "p521")))] - _ => Err(Error::AlgorithmUnknown), + _ => Err(Error::AlgorithmUnsupported { + algorithm: curve.into(), + }), } } /// Get the [`Algorithm`] for this public key type. + #[must_use] pub fn algorithm(&self) -> Algorithm { Algorithm::Ecdsa { curve: self.curve(), @@ -255,6 +264,7 @@ impl EcdsaKeypair { } /// Get the [`EcdsaCurve`] for this key. + #[must_use] pub fn curve(&self) -> EcdsaCurve { match self { Self::NistP256 { .. } => EcdsaCurve::NistP256, @@ -264,6 +274,7 @@ impl EcdsaKeypair { } /// Get the bytes representing the public key. + #[must_use] pub fn public_key_bytes(&self) -> &[u8] { match self { Self::NistP256 { public, .. } => public.as_ref(), @@ -273,6 +284,7 @@ impl EcdsaKeypair { } /// Get the bytes representing the private key. + #[must_use] pub fn private_key_bytes(&self) -> &[u8] { match self { Self::NistP256 { private, .. } => private.as_ref(), @@ -284,8 +296,9 @@ impl EcdsaKeypair { impl CtEq for EcdsaKeypair { fn ct_eq(&self, other: &Self) -> Choice { - let public_eq = - Choice::from((EcdsaPublicKey::from(self) == EcdsaPublicKey::from(other)) as u8); + let public_eq = Choice::from(u8::from( + EcdsaPublicKey::from(self) == EcdsaPublicKey::from(other), + )); let private_key_a = match self { Self::NistP256 { private, .. } => private.as_slice(), diff --git a/ssh-key/src/private/ed25519.rs b/ssh-key/src/private/ed25519.rs index 08ef4e61..56400ef4 100644 --- a/ssh-key/src/private/ed25519.rs +++ b/ssh-key/src/private/ed25519.rs @@ -29,11 +29,13 @@ impl Ed25519PrivateKey { } /// Parse Ed25519 private key from bytes. + #[must_use] pub fn from_bytes(bytes: &[u8; Self::BYTE_SIZE]) -> Self { Self(*bytes) } /// Convert to the inner byte array. + #[must_use] pub fn to_bytes(&self) -> [u8; Self::BYTE_SIZE] { self.0 } @@ -163,12 +165,15 @@ impl Ed25519Keypair { /// Expand a keypair from a 32-byte seed value. #[cfg(feature = "ed25519")] + #[must_use] pub fn from_seed(seed: &[u8; Ed25519PrivateKey::BYTE_SIZE]) -> Self { Ed25519PrivateKey::from_bytes(seed).into() } - /// Parse Ed25519 keypair from 64-bytes which comprise the serialized - /// private and public keys. + /// Parse Ed25519 keypair from 64-bytes which comprise the serialized private and public keys. + /// + /// # Errors + /// Returns [`Error::Crypto`] if the public key does not match the private key. pub fn from_bytes(bytes: &[u8; Self::BYTE_SIZE]) -> Result { let (priv_bytes, pub_bytes) = bytes.split_at(Ed25519PrivateKey::BYTE_SIZE); let private = Ed25519PrivateKey::try_from(priv_bytes)?; @@ -184,6 +189,8 @@ impl Ed25519Keypair { } /// Serialize an Ed25519 keypair as bytes. + #[must_use] + #[allow(clippy::integer_division_remainder_used, reason = "constant")] pub fn to_bytes(&self) -> [u8; Self::BYTE_SIZE] { let mut result = [0u8; Self::BYTE_SIZE]; result[..(Self::BYTE_SIZE / 2)].copy_from_slice(self.private.as_ref()); @@ -194,7 +201,7 @@ impl Ed25519Keypair { impl CtEq for Ed25519Keypair { fn ct_eq(&self, other: &Self) -> Choice { - Choice::from((self.public == other.public) as u8) & self.private.ct_eq(&other.private) + Choice::from(u8::from(self.public == other.public)) & self.private.ct_eq(&other.private) } } diff --git a/ssh-key/src/private/keypair.rs b/ssh-key/src/private/keypair.rs index f8a9d00d..f187bbaf 100644 --- a/ssh-key/src/private/keypair.rs +++ b/ssh-key/src/private/keypair.rs @@ -63,6 +63,9 @@ pub enum KeypairData { impl KeypairData { /// Get the [`Algorithm`] for this private key. + /// + /// # Errors + /// Returns [`Error::Encrypted`] if the private key is encrypted. pub fn algorithm(&self) -> Result { Ok(match self { #[cfg(feature = "alloc")] @@ -85,6 +88,7 @@ impl KeypairData { /// Get DSA keypair if this key is the correct type. #[cfg(feature = "alloc")] + #[must_use] pub fn dsa(&self) -> Option<&DsaKeypair> { match self { Self::Dsa(key) => Some(key), @@ -94,6 +98,7 @@ impl KeypairData { /// Get ECDSA private key if this key is the correct type. #[cfg(feature = "ecdsa")] + #[must_use] pub fn ecdsa(&self) -> Option<&EcdsaKeypair> { match self { Self::Ecdsa(keypair) => Some(keypair), @@ -102,6 +107,7 @@ impl KeypairData { } /// Get Ed25519 private key if this key is the correct type. + #[must_use] pub fn ed25519(&self) -> Option<&Ed25519Keypair> { match self { Self::Ed25519(key) => Some(key), @@ -112,6 +118,7 @@ impl KeypairData { /// Get the encrypted ciphertext if this key is encrypted. #[cfg(feature = "alloc")] + #[must_use] pub fn encrypted(&self) -> Option<&[u8]> { match self { Self::Encrypted(ciphertext) => Some(ciphertext), @@ -121,6 +128,7 @@ impl KeypairData { /// Get RSA keypair if this key is the correct type. #[cfg(feature = "alloc")] + #[must_use] pub fn rsa(&self) -> Option<&RsaKeypair> { match self { Self::Rsa(key) => Some(key), @@ -130,6 +138,7 @@ impl KeypairData { /// Get FIDO/U2F ECDSA/NIST P-256 private key if this key is the correct type. #[cfg(all(feature = "alloc", feature = "ecdsa"))] + #[must_use] pub fn sk_ecdsa_p256(&self) -> Option<&SkEcdsaSha2NistP256> { match self { Self::SkEcdsaSha2NistP256(sk) => Some(sk), @@ -139,6 +148,7 @@ impl KeypairData { /// Get FIDO/U2F Ed25519 private key if this key is the correct type. #[cfg(feature = "alloc")] + #[must_use] pub fn sk_ed25519(&self) -> Option<&SkEd25519> { match self { Self::SkEd25519(sk) => Some(sk), @@ -148,6 +158,7 @@ impl KeypairData { /// Get the custom, opaque private key if this key is the correct type. #[cfg(feature = "alloc")] + #[must_use] pub fn other(&self) -> Option<&OpaqueKeypair> { match self { Self::Other(key) => Some(key), @@ -157,53 +168,62 @@ impl KeypairData { /// Is this key a DSA key? #[cfg(feature = "alloc")] + #[must_use] pub fn is_dsa(&self) -> bool { matches!(self, Self::Dsa(_)) } /// Is this key an ECDSA key? #[cfg(feature = "ecdsa")] + #[must_use] pub fn is_ecdsa(&self) -> bool { matches!(self, Self::Ecdsa(_)) } /// Is this key an Ed25519 key? + #[must_use] pub fn is_ed25519(&self) -> bool { matches!(self, Self::Ed25519(_)) } /// Is this key encrypted? #[cfg(not(feature = "alloc"))] + #[must_use] pub fn is_encrypted(&self) -> bool { false } /// Is this key encrypted? #[cfg(feature = "alloc")] + #[must_use] pub fn is_encrypted(&self) -> bool { matches!(self, Self::Encrypted(_)) } /// Is this key an RSA key? #[cfg(feature = "alloc")] + #[must_use] pub fn is_rsa(&self) -> bool { matches!(self, Self::Rsa(_)) } /// Is this key a FIDO/U2F ECDSA/NIST P-256 key? #[cfg(all(feature = "alloc", feature = "ecdsa"))] + #[must_use] pub fn is_sk_ecdsa_p256(&self) -> bool { matches!(self, Self::SkEcdsaSha2NistP256(_)) } /// Is this key a FIDO/U2F Ed25519 key? #[cfg(feature = "alloc")] + #[must_use] pub fn is_sk_ed25519(&self) -> bool { matches!(self, Self::SkEd25519(_)) } /// Is this a key with a custom algorithm? #[cfg(feature = "alloc")] + #[must_use] pub fn is_other(&self) -> bool { matches!(self, Self::Other(_)) } @@ -241,6 +261,11 @@ impl KeypairData { } /// Decode [`KeypairData`] for the specified algorithm. + /// + /// # Errors + /// - Returns [`Error::AlgorithmUnknown`] if the provided `algorithm` is unknown or unsupported + /// by this library. + /// - Returns [`Error::Encoding`] in the event of an encoding error. pub fn decode_as(reader: &mut impl Reader, algorithm: Algorithm) -> Result { match algorithm { #[cfg(feature = "alloc")] @@ -286,13 +311,13 @@ impl CtEq for KeypairData { (Self::SkEcdsaSha2NistP256(a), Self::SkEcdsaSha2NistP256(b)) => { // Security Keys store the actual private key in hardware. // The key structs contain all public data. - Choice::from((a == b) as u8) + Choice::from(u8::from(a == b)) } #[cfg(feature = "alloc")] (Self::SkEd25519(a), Self::SkEd25519(b)) => { // Security Keys store the actual private key in hardware. // The key structs contain all public data. - Choice::from((a == b) as u8) + Choice::from(u8::from(a == b)) } #[cfg(feature = "alloc")] (Self::Other(a), Self::Other(b)) => a.ct_eq(b), diff --git a/ssh-key/src/private/opaque.rs b/ssh-key/src/private/opaque.rs index 2e66aed4..2b059e4f 100644 --- a/ssh-key/src/private/opaque.rs +++ b/ssh-key/src/private/opaque.rs @@ -13,17 +13,10 @@ use crate::{ public::{OpaquePublicKey, OpaquePublicKeyBytes}, }; use alloc::vec::Vec; -use core::fmt; +use core::fmt::{self, Debug}; use ctutils::{Choice, CtEq}; use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; -/// An opaque private key. -/// -/// The encoded representation of an `OpaquePrivateKeyBytes` consists of a 4-byte length prefix, -/// followed by its byte representation. -#[derive(Clone)] -pub struct OpaquePrivateKeyBytes(Vec); - /// An opaque keypair. /// /// The encoded representation of an `OpaqueKeypair` consists of the encoded representation of its @@ -36,20 +29,9 @@ pub struct OpaqueKeypair { pub public: OpaquePublicKey, } -/// The underlying representation of an [`OpaqueKeypair`]. -/// -/// The encoded representation of an `OpaqueKeypairBytes` consists of the encoded representation of -/// its [`OpaquePublicKeyBytes`] followed by the encoded representation of its -/// [`OpaquePrivateKeyBytes`]. -pub struct OpaqueKeypairBytes { - /// The opaque private key - pub private: OpaquePrivateKeyBytes, - /// The opaque public key - pub public: OpaquePublicKeyBytes, -} - impl OpaqueKeypair { /// Create a new `OpaqueKeypair`. + #[must_use] pub fn new(private_key: Vec, public: OpaquePublicKey) -> Self { Self { private: OpaquePrivateKeyBytes(private_key), @@ -58,6 +40,7 @@ impl OpaqueKeypair { } /// Get the [`Algorithm`] for this key type. + #[must_use] pub fn algorithm(&self) -> Algorithm { self.public.algorithm() } @@ -77,31 +60,17 @@ impl OpaqueKeypair { } } -impl From> for OpaquePrivateKeyBytes { - fn from(bytes: Vec) -> Self { - Self(bytes) - } -} - -impl Decode for OpaquePrivateKeyBytes { - type Error = Error; - - fn decode(reader: &mut impl Reader) -> Result { - let len = usize::decode(reader)?; - let mut bytes = vec![0; len]; - reader.read(&mut bytes)?; - Ok(Self(bytes)) +impl CtEq for OpaqueKeypair { + fn ct_eq(&self, other: &Self) -> Choice { + Choice::from(u8::from(self.public == other.public)) & self.private.ct_eq(&other.private) } } -impl Decode for OpaqueKeypairBytes { - type Error = Error; - - fn decode(reader: &mut impl Reader) -> Result { - let public = OpaquePublicKeyBytes::decode(reader)?; - let private = OpaquePrivateKeyBytes::decode(reader)?; - - Ok(Self { public, private }) +impl Debug for OpaqueKeypair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OpaqueKeypair") + .field("public", &self.public) + .finish_non_exhaustive() } } @@ -118,9 +87,40 @@ impl Encode for OpaqueKeypair { } } -impl CtEq for OpaqueKeypair { - fn ct_eq(&self, other: &Self) -> Choice { - Choice::from((self.public == other.public) as u8) & self.private.ct_eq(&other.private) +impl From<&OpaqueKeypair> for OpaquePublicKey { + fn from(keypair: &OpaqueKeypair) -> OpaquePublicKey { + keypair.public.clone() + } +} + +/// The underlying representation of an [`OpaqueKeypair`]. +/// +/// The encoded representation of an `OpaqueKeypairBytes` consists of the encoded representation of +/// its [`OpaquePublicKeyBytes`] followed by the encoded representation of its +/// [`OpaquePrivateKeyBytes`]. +pub struct OpaqueKeypairBytes { + /// The opaque private key + pub private: OpaquePrivateKeyBytes, + /// The opaque public key + pub public: OpaquePublicKeyBytes, +} + +impl Debug for OpaqueKeypairBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OpaqueKeypairBytes") + .field("public", &self.public) + .finish_non_exhaustive() + } +} + +impl Decode for OpaqueKeypairBytes { + type Error = Error; + + fn decode(reader: &mut impl Reader) -> Result { + let public = OpaquePublicKeyBytes::decode(reader)?; + let private = OpaquePrivateKeyBytes::decode(reader)?; + + Ok(Self { public, private }) } } @@ -134,28 +134,42 @@ impl Encode for OpaquePrivateKeyBytes { } } -impl From<&OpaqueKeypair> for OpaquePublicKey { - fn from(keypair: &OpaqueKeypair) -> OpaquePublicKey { - keypair.public.clone() +/// An opaque private key. +/// +/// The encoded representation of an `OpaquePrivateKeyBytes` consists of a 4-byte length prefix, +/// followed by its byte representation. +#[derive(Clone)] +pub struct OpaquePrivateKeyBytes(Vec); + +impl AsRef<[u8]> for OpaquePrivateKeyBytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl CtEq for OpaquePrivateKeyBytes { + fn ct_eq(&self, other: &Self) -> Choice { + self.as_ref().ct_eq(other.as_ref()) } } -impl fmt::Debug for OpaqueKeypair { +impl Debug for OpaquePrivateKeyBytes { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("OpaqueKeypair") - .field("public", &self.public) + f.debug_struct("OpaquePrivateKeyBytes") .finish_non_exhaustive() } } -impl CtEq for OpaquePrivateKeyBytes { - fn ct_eq(&self, other: &Self) -> Choice { - self.as_ref().ct_eq(other.as_ref()) +impl Decode for OpaquePrivateKeyBytes { + type Error = Error; + + fn decode(reader: &mut impl Reader) -> Result { + Ok(Self(Vec::decode(reader)?)) } } -impl AsRef<[u8]> for OpaquePrivateKeyBytes { - fn as_ref(&self) -> &[u8] { - &self.0 +impl From> for OpaquePrivateKeyBytes { + fn from(bytes: Vec) -> Self { + Self(bytes) } } diff --git a/ssh-key/src/private/rsa.rs b/ssh-key/src/private/rsa.rs index 1ec72b50..44e66bd3 100644 --- a/ssh-key/src/private/rsa.rs +++ b/ssh-key/src/private/rsa.rs @@ -1,7 +1,7 @@ //! Rivest–Shamir–Adleman (RSA) private keys. use crate::{Error, Mpint, Result, public::RsaPublicKey}; -use core::fmt; +use core::fmt::{self, Debug}; use ctutils::{Choice, CtEq}; use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; use zeroize::Zeroize; @@ -37,6 +37,9 @@ impl RsaPrivateKey { /// - `iqmp`: CRT coefficient: `(inverse of q) mod p`. /// - `p`: First prime factor of `n`. /// - `q`: Second prime factor of `n`. + /// + /// # Errors + /// Returns [`Error::FormatEncoding`] if any of the provided values are negative. pub fn new(d: Mpint, iqmp: Mpint, p: Mpint, q: Mpint) -> Result { if d.is_positive() && iqmp.is_positive() && p.is_positive() && q.is_positive() { Ok(Self { d, iqmp, p, q }) @@ -46,21 +49,25 @@ impl RsaPrivateKey { } /// RSA private exponent. + #[must_use] pub fn d(&self) -> &Mpint { &self.d } /// CRT coefficient: `(inverse of q) mod p`. + #[must_use] pub fn iqmp(&self) -> &Mpint { &self.iqmp } /// First prime factor of `n`. + #[must_use] pub fn p(&self) -> &Mpint { &self.p } /// Second prime factor of `n`. + #[must_use] pub fn q(&self) -> &Mpint { &self.q } @@ -75,11 +82,18 @@ impl CtEq for RsaPrivateKey { } } -impl Eq for RsaPrivateKey {} +impl Debug for RsaPrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RsaPrivateKey").finish_non_exhaustive() + } +} -impl PartialEq for RsaPrivateKey { - fn eq(&self, other: &Self) -> bool { - self.ct_eq(other).into() +impl Drop for RsaPrivateKey { + fn drop(&mut self) { + self.d.zeroize(); + self.iqmp.zeroize(); + self.p.zeroize(); + self.q.zeroize(); } } @@ -115,12 +129,10 @@ impl Encode for RsaPrivateKey { } } -impl Drop for RsaPrivateKey { - fn drop(&mut self) { - self.d.zeroize(); - self.iqmp.zeroize(); - self.p.zeroize(); - self.q.zeroize(); +impl Eq for RsaPrivateKey {} +impl PartialEq for RsaPrivateKey { + fn eq(&self, other: &Self) -> bool { + self.ct_eq(other).into() } } @@ -137,27 +149,34 @@ pub struct RsaKeypair { impl RsaKeypair { /// Generate a random RSA keypair of the given size. #[cfg(feature = "rsa")] + #[expect(clippy::missing_errors_doc, reason = "TODO")] pub fn random(rng: &mut R, bit_size: usize) -> Result { rsa::RsaPrivateKey::new(rng, bit_size)?.try_into() } /// Create a new keypair from the given `public` and `private` key components. + /// + /// # Errors + /// Returns [`Error::Crypto`] if the `public` key does not match the `private` key (TODO). pub fn new(public: RsaPublicKey, private: RsaPrivateKey) -> Result { // TODO(tarcieri): perform validation that the public and private components match? Ok(Self { public, private }) } /// Get the size of the RSA modulus in bits. + #[must_use] pub fn key_size(&self) -> u32 { self.public.key_size() } /// Get the public component of the keypair. + #[must_use] pub fn public(&self) -> &RsaPublicKey { &self.public } /// Get the private component of the keypair. + #[must_use] pub fn private(&self) -> &RsaPrivateKey { &self.private } @@ -165,15 +184,15 @@ impl RsaKeypair { impl CtEq for RsaKeypair { fn ct_eq(&self, other: &Self) -> Choice { - Choice::from((self.public == other.public) as u8) & self.private.ct_eq(&other.private) + Choice::from(u8::from(self.public == other.public)) & self.private.ct_eq(&other.private) } } -impl Eq for RsaKeypair {} - -impl PartialEq for RsaKeypair { - fn eq(&self, other: &Self) -> bool { - self.ct_eq(other).into() +impl Debug for RsaKeypair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RsaKeypair") + .field("public", &self.public) + .finish_non_exhaustive() } } @@ -206,6 +225,13 @@ impl Encode for RsaKeypair { } } +impl Eq for RsaKeypair {} +impl PartialEq for RsaKeypair { + fn eq(&self, other: &Self) -> bool { + self.ct_eq(other).into() + } +} + impl From for RsaPublicKey { fn from(keypair: RsaKeypair) -> RsaPublicKey { keypair.public @@ -218,14 +244,6 @@ impl From<&RsaKeypair> for RsaPublicKey { } } -impl fmt::Debug for RsaKeypair { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RsaKeypair") - .field("public", &self.public) - .finish_non_exhaustive() - } -} - #[cfg(feature = "rsa")] impl TryFrom for rsa::RsaPrivateKey { type Error = Error; diff --git a/ssh-key/src/private/sk.rs b/ssh-key/src/private/sk.rs index 10d96f75..2a580296 100644 --- a/ssh-key/src/private/sk.rs +++ b/ssh-key/src/private/sk.rs @@ -27,6 +27,9 @@ pub struct SkEcdsaSha2NistP256 { #[cfg(feature = "ecdsa")] impl SkEcdsaSha2NistP256 { /// Construct new instance of SkEcdsaSha2NistP256. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "alloc")] pub fn new( public: public::SkEcdsaSha2NistP256, @@ -48,16 +51,19 @@ impl SkEcdsaSha2NistP256 { } /// Get the ECDSA/NIST P-256 public key. + #[must_use] pub fn public(&self) -> &public::SkEcdsaSha2NistP256 { &self.public } /// Get flags. + #[must_use] pub fn flags(&self) -> u8 { self.flags } /// Get FIDO/U2F key handle. + #[must_use] pub fn key_handle(&self) -> &[u8] { &self.key_handle } @@ -117,7 +123,10 @@ pub struct SkEd25519 { } impl SkEd25519 { - /// Construct new instance of SkEd25519. + /// Construct new instance of [`SkEd25519`]. + /// + /// # Errors + /// Returns [`Error::Encoding`] if `key_handle` is longer than 255. #[cfg(feature = "alloc")] pub fn new( public: public::SkEd25519, @@ -139,16 +148,19 @@ impl SkEd25519 { } /// Get the Ed25519 public key. + #[must_use] pub fn public(&self) -> &public::SkEd25519 { &self.public } /// Get flags. + #[must_use] pub fn flags(&self) -> u8 { self.flags } /// Get FIDO/U2F key handle. + #[must_use] pub fn key_handle(&self) -> &[u8] { &self.key_handle } diff --git a/ssh-key/src/public.rs b/ssh-key/src/public.rs index 0991f427..9dd0b106 100644 --- a/ssh-key/src/public.rs +++ b/ssh-key/src/public.rs @@ -125,6 +125,9 @@ impl PublicKey { /// ```text /// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti foo@bar.com /// ``` + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. pub fn from_openssh(public_key: &str) -> Result { let encapsulation = SshFormat::decode(public_key.trim_end().as_bytes())?; let mut reader = Base64Reader::new(encapsulation.base64_data)?; @@ -145,6 +148,9 @@ impl PublicKey { } /// Parse a raw binary SSH public key. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. pub fn from_bytes(mut bytes: &[u8]) -> Result { let reader = &mut bytes; let key_data = KeyData::decode(reader)?; @@ -152,6 +158,9 @@ impl PublicKey { } /// Encode OpenSSH-formatted public key. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. pub fn encode_openssh<'o>(&self, out: &'o mut [u8]) -> Result<&'o str> { #[cfg(not(feature = "alloc"))] let comment = ""; @@ -161,8 +170,10 @@ impl PublicKey { SshFormat::encode(self.algorithm().as_str(), &self.key_data, comment, out) } - /// Encode an OpenSSH-formatted public key, allocating a [`String`] for - /// the result. + /// Encode an OpenSSH-formatted public key, allocating a [`String`] for the result. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "alloc")] pub fn to_openssh(&self) -> Result { SshFormat::encode_string( @@ -173,6 +184,9 @@ impl PublicKey { } /// Serialize SSH public key as raw bytes. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "alloc")] pub fn to_bytes(&self) -> Result> { Ok(self.key_data.encode_vec()?) @@ -232,6 +246,11 @@ impl PublicKey { /// /// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD /// [Digest]: https://docs.rs/digest/latest/digest/trait.Digest.html + /// + /// # Errors + /// - Returns [`Error::PublicKey`] if this key does not match the one in the signature. + /// - Returns [`Error::Namespace`] if the provided `namespace` does not match the signature. + /// - Returns [`Error::Signature`] in the event signature verification failed. #[cfg(feature = "alloc")] pub fn verify(&self, namespace: &str, msg: &[u8], signature: &SshSig) -> Result<()> { self.verify_prehash( @@ -244,6 +263,11 @@ impl PublicKey { /// Verify the [`SshSig`] signature is valid the given message [`Digest`] using this public key. /// /// See [`PublicKey::verify`] for more information. + /// + /// # Errors + /// - Returns [`Error::PublicKey`] if this key does not match the one in the signature. + /// - Returns [`Error::Namespace`] if the provided `namespace` does not match the signature. + /// - Returns [`Error::Signature`] in the event signature verification failed. #[cfg(feature = "alloc")] pub fn verify_digest( &self, @@ -262,6 +286,11 @@ impl PublicKey { /// public key. /// /// See [`PublicKey::verify`] for more information. + /// + /// # Errors + /// - Returns [`Error::PublicKey`] if this key does not match the one in the signature. + /// - Returns [`Error::Namespace`] if the provided `namespace` does not match the signature. + /// - Returns [`Error::Signature`] in the event signature verification failed. #[cfg(feature = "alloc")] pub fn verify_prehash( &self, @@ -281,6 +310,10 @@ impl PublicKey { } /// Read public key from an OpenSSH-formatted source. + /// + /// # Errors + /// - Returns [`Error::Io`] in the event of an I/O error. + /// - Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "std")] pub fn read_openssh(reader: &mut impl Read) -> Result { let input = io::read_to_string(reader)?; @@ -288,6 +321,10 @@ impl PublicKey { } /// Read public key from an OpenSSH-formatted file. + /// + /// # Errors + /// - Returns [`Error::Io`] in the event of an I/O error. + /// - Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "std")] pub fn read_openssh_file(path: impl AsRef) -> Result { let mut file = File::open(path)?; @@ -295,6 +332,10 @@ impl PublicKey { } /// Write public key as an OpenSSH-formatted file. + /// + /// # Errors + /// - Returns [`Error::Io`] in the event of an I/O error. + /// - Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "std")] pub fn write_openssh(&self, writer: &mut impl Write) -> Result<()> { let mut encoded = self.to_openssh()?; @@ -305,6 +346,10 @@ impl PublicKey { } /// Write public key as an OpenSSH-formatted file. + /// + /// # Errors + /// - Returns [`Error::Io`] in the event of an I/O error. + /// - Returns [`Error::Encoding`] in the event of an encoding error. #[cfg(feature = "std")] pub fn write_openssh_file(&self, path: impl AsRef) -> Result<()> { let mut file = File::create(path)?; @@ -312,17 +357,20 @@ impl PublicKey { } /// Get the digital signature [`Algorithm`] used by this key. + #[must_use] pub fn algorithm(&self) -> Algorithm { self.key_data.algorithm() } /// Comment on the key (e.g. email address). #[cfg(feature = "alloc")] + #[must_use] pub fn comment(&self) -> &Comment { &self.comment } /// Public key data. + #[must_use] pub fn key_data(&self) -> &KeyData { &self.key_data } @@ -330,6 +378,7 @@ impl PublicKey { /// Compute key fingerprint. /// /// Use [`Default::default()`] to use the default hash function (SHA-256). + #[must_use] pub fn fingerprint(&self, hash_alg: HashAlg) -> Fingerprint { self.key_data.fingerprint(hash_alg) } diff --git a/ssh-key/src/public/dsa.rs b/ssh-key/src/public/dsa.rs index 5a4cd678..d34bb65a 100644 --- a/ssh-key/src/public/dsa.rs +++ b/ssh-key/src/public/dsa.rs @@ -34,6 +34,9 @@ impl DsaPublicKey { /// - `g`: generator of a subgroup of order `q` in the multiplicative group `GF(p)`, such /// that `1 < g < p`. /// - `y`: the public key, where `y = gˣ mod p`. + /// + /// # Errors + /// Returns [`Error::FormatEncoding`] in the event any of the components are negative. pub fn new(p: Mpint, q: Mpint, g: Mpint, y: Mpint) -> Result { if p.is_positive() && q.is_positive() && g.is_positive() && y.is_positive() { Ok(Self { p, q, g, y }) @@ -43,22 +46,26 @@ impl DsaPublicKey { } /// Prime modulus. + #[must_use] pub fn p(&self) -> &Mpint { &self.p } /// Prime divisor of `p - 1`. + #[must_use] pub fn q(&self) -> &Mpint { &self.q } /// Generator of a subgroup of order `q` in the multiplicative group `GF(p)`, such that /// `1 < g < p`. + #[must_use] pub fn g(&self) -> &Mpint { &self.g } /// The public key, where `y = gˣ mod p`. + #[must_use] pub fn y(&self) -> &Mpint { &self.y } diff --git a/ssh-key/src/public/ecdsa.rs b/ssh-key/src/public/ecdsa.rs index 054403be..f79bf1d6 100644 --- a/ssh-key/src/public/ecdsa.rs +++ b/ssh-key/src/public/ecdsa.rs @@ -6,13 +6,13 @@ use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; use sec1::consts::{U32, U48, U66}; /// ECDSA/NIST P-256 public key. -pub type EcdsaNistP256PublicKey = sec1::EncodedPoint; +pub(super) type EcdsaNistP256PublicKey = sec1::EncodedPoint; /// ECDSA/NIST P-384 public key. -pub type EcdsaNistP384PublicKey = sec1::EncodedPoint; +pub(super) type EcdsaNistP384PublicKey = sec1::EncodedPoint; /// ECDSA/NIST P-521 public key. -pub type EcdsaNistP521PublicKey = sec1::EncodedPoint; +pub(super) type EcdsaNistP521PublicKey = sec1::EncodedPoint; /// Elliptic Curve Digital Signature Algorithm (ECDSA) public key. /// @@ -43,6 +43,9 @@ impl EcdsaPublicKey { /// Parse an ECDSA public key from a SEC1-encoded point. /// /// Determines the key type from the SEC1 tag byte and length. + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. pub fn from_sec1_bytes(bytes: &[u8]) -> Result { match bytes { [tag, rest @ ..] => { @@ -50,8 +53,9 @@ impl EcdsaPublicKey { sec1::point::Tag::CompressedEvenY | sec1::point::Tag::CompressedOddY => { rest.len() } + #[allow(clippy::integer_division_remainder_used, reason = "public input")] sec1::point::Tag::Uncompressed => rest.len() / 2, - _ => return Err(Error::AlgorithmUnknown), + _ => return Err(Error::FormatEncoding), }; match point_size { @@ -66,6 +70,7 @@ impl EcdsaPublicKey { } /// Borrow the SEC1-encoded key data as bytes. + #[must_use] pub fn as_sec1_bytes(&self) -> &[u8] { match self { EcdsaPublicKey::NistP256(point) => point.as_bytes(), @@ -75,6 +80,7 @@ impl EcdsaPublicKey { } /// Get the [`Algorithm`] for this public key type. + #[must_use] pub fn algorithm(&self) -> Algorithm { Algorithm::Ecdsa { curve: self.curve(), @@ -82,6 +88,7 @@ impl EcdsaPublicKey { } /// Get the [`EcdsaCurve`] for this key. + #[must_use] pub fn curve(&self) -> EcdsaCurve { match self { EcdsaPublicKey::NistP256(_) => EcdsaCurve::NistP256, diff --git a/ssh-key/src/public/key_data.rs b/ssh-key/src/public/key_data.rs index f0c3b011..5924ca98 100644 --- a/ssh-key/src/public/key_data.rs +++ b/ssh-key/src/public/key_data.rs @@ -59,6 +59,7 @@ pub enum KeyData { impl KeyData { /// Get the [`Algorithm`] for this public key. + #[must_use] pub fn algorithm(&self) -> Algorithm { match self { #[cfg(feature = "alloc")] @@ -80,6 +81,7 @@ impl KeyData { /// Get DSA public key if this key is the correct type. #[cfg(feature = "alloc")] + #[must_use] pub fn dsa(&self) -> Option<&DsaPublicKey> { match self { Self::Dsa(key) => Some(key), @@ -89,6 +91,7 @@ impl KeyData { /// Get ECDSA public key if this key is the correct type. #[cfg(feature = "ecdsa")] + #[must_use] pub fn ecdsa(&self) -> Option<&EcdsaPublicKey> { match self { Self::Ecdsa(key) => Some(key), @@ -97,6 +100,7 @@ impl KeyData { } /// Get Ed25519 public key if this key is the correct type. + #[must_use] pub fn ed25519(&self) -> Option<&Ed25519PublicKey> { match self { Self::Ed25519(key) => Some(key), @@ -108,12 +112,14 @@ impl KeyData { /// Compute key fingerprint. /// /// Use [`Default::default()`] to use the default hash function (SHA-256). + #[must_use] pub fn fingerprint(&self, hash_alg: HashAlg) -> Fingerprint { Fingerprint::new(hash_alg, self) } /// Get RSA public key if this key is the correct type. #[cfg(feature = "alloc")] + #[must_use] pub fn rsa(&self) -> Option<&RsaPublicKey> { match self { Self::Rsa(key) => Some(key), @@ -123,6 +129,7 @@ impl KeyData { /// Get FIDO/U2F ECDSA/NIST P-256 public key if this key is the correct type. #[cfg(feature = "ecdsa")] + #[must_use] pub fn sk_ecdsa_p256(&self) -> Option<&SkEcdsaSha2NistP256> { match self { Self::SkEcdsaSha2NistP256(sk) => Some(sk), @@ -131,6 +138,7 @@ impl KeyData { } /// Get FIDO/U2F Ed25519 public key if this key is the correct type. + #[must_use] pub fn sk_ed25519(&self) -> Option<&SkEd25519> { match self { Self::SkEd25519(sk) => Some(sk), @@ -140,6 +148,7 @@ impl KeyData { /// Get the custom, opaque public key if this key is the correct type. #[cfg(feature = "alloc")] + #[must_use] pub fn other(&self) -> Option<&OpaquePublicKey> { match self { Self::Other(key) => Some(key), @@ -149,6 +158,7 @@ impl KeyData { /// Get the certificate if this key is the correct type. #[cfg(feature = "alloc")] + #[must_use] pub fn certificate(&self) -> Option<&Certificate> { match self { Self::Certificate(certificate) => Some(certificate.as_ref()), @@ -158,6 +168,7 @@ impl KeyData { /// Get the certificate, consuming the [`KeyData`], if this key is the correct type. #[cfg(feature = "alloc")] + #[must_use] pub fn into_certificate(self) -> Option { match self { Self::Certificate(certificate) => Some(*certificate), @@ -167,51 +178,64 @@ impl KeyData { /// Is this key a DSA key? #[cfg(feature = "alloc")] + #[must_use] pub fn is_dsa(&self) -> bool { matches!(self, Self::Dsa(_)) } /// Is this key an ECDSA key? #[cfg(feature = "ecdsa")] + #[must_use] pub fn is_ecdsa(&self) -> bool { matches!(self, Self::Ecdsa(_)) } /// Is this key an Ed25519 key? + #[must_use] pub fn is_ed25519(&self) -> bool { matches!(self, Self::Ed25519(_)) } /// Is this key an RSA key? #[cfg(feature = "alloc")] + #[must_use] pub fn is_rsa(&self) -> bool { matches!(self, Self::Rsa(_)) } /// Is this key a FIDO/U2F ECDSA/NIST P-256 key? #[cfg(feature = "ecdsa")] + #[must_use] pub fn is_sk_ecdsa_p256(&self) -> bool { matches!(self, Self::SkEcdsaSha2NistP256(_)) } /// Is this key a FIDO/U2F Ed25519 key? + #[must_use] pub fn is_sk_ed25519(&self) -> bool { matches!(self, Self::SkEd25519(_)) } /// Is this a key with a custom algorithm? #[cfg(feature = "alloc")] + #[must_use] pub fn is_other(&self) -> bool { matches!(self, Self::Other(_)) } /// Is this a certificate? #[cfg(feature = "alloc")] + #[must_use] pub fn is_certificate(&self) -> bool { matches!(self, Self::Certificate(_)) } /// Decode [`KeyData`] for the specified algorithm. + /// + /// # Errors + /// - Returns [`Error::AlgorithmUnknown`] if the provided `algorithm` is unknown or unsupported + /// by this library. + /// - Returns [`Error::Encoding`] in the event of an encoding error. pub fn decode_as(reader: &mut impl Reader, algorithm: Algorithm) -> Result { match algorithm { #[cfg(feature = "alloc")] @@ -237,6 +261,9 @@ impl KeyData { } /// Decode [`KeyData`] as a certificate with the specified algorithm. + /// + /// # Errors + /// Propagates errors from [`Certificate::decode_as`]. #[cfg(feature = "alloc")] pub fn decode_as_certificate(reader: &mut impl Reader, algorithm: Algorithm) -> Result { Certificate::decode_as(reader, algorithm).map(|cert| Self::Certificate(Box::new(cert))) diff --git a/ssh-key/src/public/opaque.rs b/ssh-key/src/public/opaque.rs index 0af61b8f..4cd58cf0 100644 --- a/ssh-key/src/public/opaque.rs +++ b/ssh-key/src/public/opaque.rs @@ -24,15 +24,9 @@ pub struct OpaquePublicKey { pub key: OpaquePublicKeyBytes, } -/// The underlying representation of an [`OpaquePublicKey`]. -/// -/// The encoded representation of an `OpaquePublicKeyBytes` consists of a 4-byte length prefix, -/// followed by its byte representation. -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub struct OpaquePublicKeyBytes(Vec); - impl OpaquePublicKey { /// Create a new `OpaquePublicKey`. + #[must_use] pub fn new(key: Vec, algorithm: Algorithm) -> Self { Self { key: OpaquePublicKeyBytes(key), @@ -41,6 +35,7 @@ impl OpaquePublicKey { } /// Get the [`Algorithm`] for this public key type. + #[must_use] pub fn algorithm(&self) -> Algorithm { self.algorithm.clone() } @@ -54,40 +49,44 @@ impl OpaquePublicKey { } } -impl Decode for OpaquePublicKeyBytes { - type Error = Error; - - fn decode(reader: &mut impl Reader) -> Result { - let len = usize::decode(reader)?; - let mut bytes = vec![0; len]; - reader.read(&mut bytes)?; - Ok(Self(bytes)) +impl AsRef<[u8]> for OpaquePublicKey { + fn as_ref(&self) -> &[u8] { + self.key.as_ref() } } -impl Encode for OpaquePublicKeyBytes { +impl Encode for OpaquePublicKey { fn encoded_len(&self) -> encoding::Result { - self.0.encoded_len() + self.key.encoded_len() } fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> { - self.0.encode(writer) + self.key.encode(writer) } } -impl Encode for OpaquePublicKey { - fn encoded_len(&self) -> encoding::Result { - self.key.encoded_len() - } +/// The underlying representation of an [`OpaquePublicKey`]. +/// +/// The encoded representation of an `OpaquePublicKeyBytes` consists of a 4-byte length prefix, +/// followed by its byte representation. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct OpaquePublicKeyBytes(Vec); - fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> { - self.key.encode(writer) +impl Decode for OpaquePublicKeyBytes { + type Error = Error; + + fn decode(reader: &mut impl Reader) -> Result { + Ok(Self(Vec::decode(reader)?)) } } -impl AsRef<[u8]> for OpaquePublicKey { - fn as_ref(&self) -> &[u8] { - self.key.as_ref() +impl Encode for OpaquePublicKeyBytes { + fn encoded_len(&self) -> encoding::Result { + self.0.encoded_len() + } + + fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> { + self.0.encode(writer) } } diff --git a/ssh-key/src/public/rsa.rs b/ssh-key/src/public/rsa.rs index 725c7ce6..8c2d8c1a 100644 --- a/ssh-key/src/public/rsa.rs +++ b/ssh-key/src/public/rsa.rs @@ -31,6 +31,9 @@ impl RsaPublicKey { /// /// - `e`: RSA public exponent. /// - `n`: RSA modulus. + /// + /// # Errors + /// Returns [`Error::FormatEncoding`] in the event one of the components is odd. pub fn new(e: Mpint, n: Mpint) -> Result { if !e.is_positive() { return Err(Error::FormatEncoding); @@ -49,16 +52,19 @@ impl RsaPublicKey { } /// Get the RSA public exponent. + #[must_use] pub fn e(&self) -> &Mpint { &self.e } /// Get the RSA modulus. + #[must_use] pub fn n(&self) -> &Mpint { &self.n } /// Get the size of the RSA modulus in bits. + #[must_use] pub fn key_size(&self) -> u32 { self.bits } diff --git a/ssh-key/src/public/sk.rs b/ssh-key/src/public/sk.rs index 4220bd14..c1ca4fb8 100644 --- a/ssh-key/src/public/sk.rs +++ b/ssh-key/src/public/sk.rs @@ -18,6 +18,7 @@ const DEFAULT_APPLICATION_STRING: &str = "ssh:"; /// Security Key (FIDO/U2F) ECDSA/NIST P-256 public key as specified in /// [PROTOCOL.u2f](https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.u2f?annotate=HEAD). #[cfg(feature = "ecdsa")] +#[cfg_attr(not(feature = "alloc"), expect(missing_copy_implementations))] #[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)] pub struct SkEcdsaSha2NistP256 { /// Elliptic curve point representing a public key. @@ -40,6 +41,7 @@ impl SkEcdsaSha2NistP256 { } /// Get the elliptic curve point for this Security Key. + #[must_use] pub fn ec_point(&self) -> &EcdsaNistP256PublicKey { &self.ec_point } @@ -52,6 +54,7 @@ impl SkEcdsaSha2NistP256 { /// Get the FIDO/U2F application (typically `ssh:`). #[cfg(feature = "alloc")] + #[must_use] pub fn application(&self) -> &str { &self.application } @@ -122,6 +125,7 @@ impl From for EcdsaNistP256PublicKey { /// Security Key (FIDO/U2F) Ed25519 public key as specified in /// [PROTOCOL.u2f](https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.u2f?annotate=HEAD). #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] +#[cfg_attr(not(feature = "alloc"), allow(missing_copy_implementations))] pub struct SkEd25519 { /// Ed25519 public key. public_key: Ed25519PublicKey, @@ -142,18 +146,21 @@ impl SkEd25519 { } /// Get the Ed25519 private key for this security key. + #[must_use] pub fn public_key(&self) -> &Ed25519PublicKey { &self.public_key } /// Get the FIDO/U2F application (typically `ssh:`). #[cfg(not(feature = "alloc"))] + #[must_use] pub fn application(&self) -> &str { DEFAULT_APPLICATION_STRING } /// Get the FIDO/U2F application (typically `ssh:`). #[cfg(feature = "alloc")] + #[must_use] pub fn application(&self) -> &str { &self.application } diff --git a/ssh-key/src/public/ssh_format.rs b/ssh-key/src/public/ssh_format.rs index d72d2f9f..eca96ebc 100644 --- a/ssh-key/src/public/ssh_format.rs +++ b/ssh-key/src/public/ssh_format.rs @@ -111,7 +111,7 @@ impl<'a> SshFormat<'a> { /// rather than this estimate. #[cfg(feature = "alloc")] fn base64_len_approx(input_len: usize) -> usize { - (((input_len.saturating_mul(4)) / 3).saturating_add(3)) & !3 + input_len.div_ceil(3).saturating_mul(4) } /// Parse a segment of the public key. diff --git a/ssh-key/src/signature.rs b/ssh-key/src/signature.rs index eff80106..2564871e 100644 --- a/ssh-key/src/signature.rs +++ b/ssh-key/src/signature.rs @@ -37,7 +37,8 @@ use sha2::Sha256; #[cfg(any(feature = "dsa", feature = "ed25519", feature = "p256"))] use sha2::Digest; -const DSA_SIGNATURE_SIZE: usize = 40; +const DSA_COMPONENT_SIZE: usize = 20; +const DSA_SIGNATURE_SIZE: usize = DSA_COMPONENT_SIZE * 2; const ED25519_SIGNATURE_SIZE: usize = 64; const SK_SIGNATURE_TRAILER_SIZE: usize = 5; // flags(u8), counter(u32) const SK_ED25519_SIGNATURE_SIZE: usize = ED25519_SIGNATURE_SIZE + SK_SIGNATURE_TRAILER_SIZE; @@ -98,8 +99,8 @@ impl Signature { /// See specifications in toplevel [`Signature`] documentation for how to /// format the raw signature data for a given algorithm. /// - /// # Returns - /// - [`Error::Encoding`] if the signature is not the correct length. + /// # Errors + /// Returns [`Error::Encoding`] if the signature is not the correct length. pub fn new(algorithm: Algorithm, data: impl Into>) -> Result { let data = data.into(); @@ -119,11 +120,13 @@ impl Signature { } /// Get the [`Algorithm`] associated with this signature. + #[must_use] pub fn algorithm(&self) -> Algorithm { self.algorithm.clone() } /// Get the raw signature as bytes. + #[must_use] pub fn as_bytes(&self) -> &[u8] { &self.data } @@ -335,7 +338,7 @@ impl Signer for DsaKeypair { for component in [signature.r(), signature.s()] { let bytes = component.to_be_bytes_trimmed_vartime(); - let pad_len = (DSA_SIGNATURE_SIZE / 2).saturating_sub(bytes.len()); + let pad_len = DSA_COMPONENT_SIZE.saturating_sub(bytes.len()); data.extend(core::iter::repeat_n(0, pad_len)); data.extend_from_slice(&bytes); } @@ -389,12 +392,16 @@ impl TryFrom<&Signature> for dsa::Signature { return Err(encoding::Error::Length.into()); } - let component_size = DSA_SIGNATURE_SIZE / 2; - let component_bits = component_size.saturating_mul(8) as u32; - let components = data.split_at(component_size); + let components = data.split_at(DSA_COMPONENT_SIZE); - let r = Uint::from_be_slice(components.0, component_bits)?; - let s = Uint::from_be_slice(components.1, component_bits)?; + #[expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "constant" + )] + const COMPONENT_BITS: u32 = DSA_COMPONENT_SIZE.saturating_mul(8) as u32; + let r = Uint::from_be_slice(components.0, COMPONENT_BITS)?; + let s = Uint::from_be_slice(components.1, COMPONENT_BITS)?; Ok(Self::from_components(r, s).ok_or(encoding::Error::MpintEncoding)?) } } @@ -554,9 +561,11 @@ fn zero_pad_field_bytes + Copy>(m: Mpint) -> Option { use core::mem::size_of; let bytes = m.as_positive_bytes()?; - size_of::() - .checked_sub(bytes.len()) - .map(|i| B::from_iter(core::iter::repeat_n(0u8, i).chain(bytes.iter().cloned()))) + size_of::().checked_sub(bytes.len()).map(|i| { + core::iter::repeat_n(0u8, i) + .chain(bytes.iter().cloned()) + .collect() + }) } #[cfg(feature = "p256")] @@ -890,7 +899,7 @@ mod tests { #[cfg(feature = "dsa")] #[test] fn try_sign_and_verify_dsa() { - use super::{DSA_SIGNATURE_SIZE, DsaKeypair}; + use super::{DSA_COMPONENT_SIZE, DsaKeypair}; use encoding::Decode as _; use signature::{Signer as _, Verifier as _}; @@ -937,12 +946,7 @@ mod tests { let data = hex!( "F0000040713d5f6fffe0000e6421ab0b3a69774d3da02fd72b107d6b32b6dad7c1660bbf507bf3eac3304cc5058f7e6f81b04239b8471459b1f3b387e2626f7eb8f6bcdd3200000006626c616465320000000e7373682d636f6e6e656374696f6e00000009686f73746261736564000000077373682d647373000001b2000000077373682d6473730000008100c161fb30c9e4e3602c8510f93bbd48d813da845dfcc75f3696e440cd019d609809608cd592b8430db901d7b43740740045b547c60fb035d69f9c64d3dfbfb13bb3edd8ccfdd44705739a639eb70f4aed16b0b8355de1b21cd9d442eff250895573a8af7ce2fb71fb062e887482dab5c68139845fb8afafc5f3819dc782920d510000001500f3fb6762430332bd5950edc5cd1ae6f17b88514f0000008061ef1394d864905e8efec3b610b7288a6522893af2a475f910796e0de47c8b065d365e942e80e471d1e6d4abdee1d3d3ede7103c6996432f1a9f9a671a31388672d63555077911fc69e641a997087260d22cdbf4965aa64bb382204f88987890ec225a5a7723a977dc1ecc5e04cf678f994692b20470adbf697489f800817b920000008100a9a6f1b65fc724d65df7441908b34af66489a4a3872cbbba25ea1bcfc83f25c4af1a62e339eefc814907cfaf0cb6d2d16996212a32a27a63013f01c57d0630f0be16c8c69d16fc25438e613b904b98aeb3e7c356fa8e75ee1d474c9f82f1280c5a6c18e9e607fcf7586eefb75ea9399da893b807375ac1396fd586bf2771619800000015746f6d61746f7373682e6c6f63616c646f6d61696e00000009746f6d61746f737368" ); - check_signature_component_lengths( - &keypair, - &data, - DSA_SIGNATURE_SIZE / 2, - DSA_SIGNATURE_SIZE / 2, - ); + check_signature_component_lengths(&keypair, &data, DSA_COMPONENT_SIZE, DSA_COMPONENT_SIZE); let signature = keypair.try_sign(&data[..]).expect("dsa try_sign is ok"); keypair .public() @@ -956,8 +960,8 @@ mod tests { check_signature_component_lengths( &keypair, &data, - DSA_SIGNATURE_SIZE / 2 - 1, - DSA_SIGNATURE_SIZE / 2, + DSA_COMPONENT_SIZE - 1, + DSA_COMPONENT_SIZE, ); let signature = keypair .try_sign(&data[..]) diff --git a/ssh-key/src/sshsig.rs b/ssh-key/src/sshsig.rs index a3437586..4cdf59b8 100644 --- a/ssh-key/src/sshsig.rs +++ b/ssh-key/src/sshsig.rs @@ -51,12 +51,14 @@ impl SshSig { /// The preamble is the six-byte sequence "SSHSIG". /// - /// It is included to ensure that manual signatures can never be confused - /// with any message signed during SSH user or host authentication. + /// It is included to ensure that manual signatures can never be confused with any message + /// signed during SSH user or host authentication. const MAGIC_PREAMBLE: &'static [u8] = b"SSHSIG"; - /// Create a new signature with the given public key, namespace, hash - /// algorithm, and signature. + /// Create a new signature with the given public key, namespace, hash algorithm, and signature. + /// + /// # Errors + /// Returns [`Error::Namespace`] if the namespace is empty. pub fn new( public_key: public::KeyData, namespace: impl Into, @@ -86,6 +88,9 @@ impl SshSig { /// ```text /// -----BEGIN SSH SIGNATURE----- /// ``` + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. pub fn from_pem(pem: impl AsRef<[u8]>) -> Result { Self::decode_pem(pem) } @@ -95,6 +100,9 @@ impl SshSig { /// ```text /// -----BEGIN SSH SIGNATURE----- /// ``` + /// + /// # Errors + /// Returns [`Error::Encoding`] in the event of an encoding error. pub fn to_pem(&self, line_ending: LineEnding) -> Result { Ok(self.encode_pem_string(line_ending)?) } @@ -102,6 +110,13 @@ impl SshSig { /// Sign the given message with the provided signing key. /// /// See also: [`PrivateKey::sign`]. + /// + /// # Errors + /// - Returns [`Error::AlgorithmUnsupported`] if this signing key's algorithm is unsupported + /// by this library with its enabled features. + /// - Returns [`Error::Crypto`] in the event of an error occurring in the cryptographic + /// implementation. + /// - Returns [`Error::Namespace`] if the namespace is empty. pub fn sign( signing_key: &S, namespace: &str, @@ -117,11 +132,18 @@ impl SshSig { } /// Sign the given message digest with the provided signing key. - pub fn sign_digest( - signing_key: &S, - namespace: &str, - digest: D, - ) -> Result { + /// + /// # Errors + /// - Returns [`Error::AlgorithmUnsupported`] if this signing key's algorithm is unsupported + /// by this library with its enabled features. + /// - Returns [`Error::Crypto`] in the event of an error occurring in the cryptographic + /// implementation. + /// - Returns [`Error::Namespace`] if the namespace is empty. + pub fn sign_digest(signing_key: &S, namespace: &str, digest: D) -> Result + where + S: SigningKey, + D: AssociatedHashAlg + Digest, + { Self::sign_prehash( signing_key, namespace, @@ -131,6 +153,13 @@ impl SshSig { } /// Sign the given prehashed message digest with the provided signing key. + /// + /// # Errors + /// - Returns [`Error::AlgorithmUnsupported`] if this signing key's algorithm is unsupported + /// by this library with its enabled features. + /// - Returns [`Error::Crypto`] in the event of an error occurring in the cryptographic + /// implementation. + /// - Returns [`Error::Namespace`] if the namespace is empty. pub fn sign_prehash( signing_key: &S, namespace: &str, @@ -158,18 +187,24 @@ impl SshSig { /// Get the raw "enveloped" message over which the signature for a given input message is /// computed. /// - /// This is a low-level function intended for uses cases which can't be - /// expressed using [`SshSig::sign`], such as if the [`SigningKey`] trait - /// can't be used for some reason. + /// This is a low-level function intended for uses cases which can't be expressed using + /// [`SshSig::sign`], such as if the [`SigningKey`] trait can't be used for some reason. + /// + /// Once a [`Signature`] has been computed over the returned byte vector, [`SshSig::new`] can be + /// used to construct the final signature. /// - /// Once a [`Signature`] has been computed over the returned byte vector, - /// [`SshSig::new`] can be used to construct the final signature. + /// # Errors + /// Returns [`Error::Namespace`] if the namespace is empty. pub fn signed_data(namespace: &str, hash_alg: HashAlg, msg: &[u8]) -> Result> { Self::signed_data_for_prehash(namespace, hash_alg, hash_alg.digest(msg).as_slice()) } /// Get the raw message over which the signature for a given message digest (passed as the /// `prehash` parameter) is computed. + /// + /// # Errors + /// - Returns [`Error::HashSize`] if `prehash` is not the right length for `hash_alg`. + /// - Returns [`Error::Namespace`] if the namespace is empty. pub fn signed_data_for_prehash( namespace: &str, hash_alg: HashAlg, @@ -194,9 +229,9 @@ impl SshSig { /// Verify the given prehashed message digest against this signature. /// - /// Note that this method does not verify the public key or namespace - /// are correct and thus is crate-private so as to ensure these parameters - /// are always authenticated by users of the public API. + /// Note that this method does not verify the public key or namespace are correct and thus is + /// crate-private so as to ensure these parameters are always authenticated by users of the + /// public API. pub(crate) fn verify_prehash(&self, prehash: &[u8]) -> Result<()> { if prehash.len() != self.hash_alg.digest_size() { return Err(Error::HashSize); @@ -214,6 +249,7 @@ impl SshSig { } /// Get the signature algorithm. + #[must_use] pub fn algorithm(&self) -> Algorithm { self.signature.algorithm() } @@ -222,12 +258,14 @@ impl SshSig { /// /// Verifiers MUST reject signatures with versions greater than those /// they support. + #[must_use] pub fn version(&self) -> Version { self.version } /// Get public key which corresponds to the signing key that produced /// this signature. + #[must_use] pub fn public_key(&self) -> &public::KeyData { &self.public_key } @@ -239,6 +277,7 @@ impl SshSig { /// This prevents cross-protocol attacks caused by signatures /// intended for one intended domain being accepted in another. /// The namespace value MUST NOT be the empty string. + #[must_use] pub fn namespace(&self) -> &str { &self.namespace } @@ -248,6 +287,7 @@ impl SshSig { /// The reserved value is present to encode future information /// (e.g. tags) into the signature. Implementations should ignore /// the reserved field if it is not empty. + #[must_use] pub fn reserved(&self) -> &[u8] { &self.reserved } @@ -259,16 +299,19 @@ impl SshSig { /// operation, which may be of concern if the signing key is held in limited /// or slow hardware or on a remote ssh-agent. The supported hash algorithms /// are "sha256" and "sha512". + #[must_use] pub fn hash_alg(&self) -> HashAlg { self.hash_alg } /// Get the structured signature over the given message. + #[must_use] pub fn signature(&self) -> &Signature { &self.signature } /// Get the bytes which comprise the serialized signature. + #[must_use] pub fn signature_bytes(&self) -> &[u8] { self.signature.as_bytes() } diff --git a/ssh-key/tests/algorithm_name.rs b/ssh-key/tests/algorithm_name.rs index b4d05fee..b7c52e40 100644 --- a/ssh-key/tests/algorithm_name.rs +++ b/ssh-key/tests/algorithm_name.rs @@ -2,8 +2,8 @@ #![cfg(feature = "alloc")] +use core::str::FromStr; use ssh_key::AlgorithmName; -use std::str::FromStr; #[test] fn additional_algorithm_name() { diff --git a/ssh-key/tests/certificate.rs b/ssh-key/tests/certificate.rs index 5ba9c861..f83c164f 100644 --- a/ssh-key/tests/certificate.rs +++ b/ssh-key/tests/certificate.rs @@ -1,11 +1,12 @@ //! OpenSSH certificate tests. #![cfg(feature = "alloc")] +#![allow(clippy::unwrap_used, reason = "tests")] +use core::str::FromStr; use encoding::{Base64Reader, Decode, Encode, Reader}; use hex_literal::hex; use ssh_key::{Algorithm, Certificate, public::KeyData}; -use std::str::FromStr; #[cfg(feature = "ecdsa")] use ssh_key::EcdsaCurve; @@ -287,18 +288,18 @@ fn decode_keydata(certificate_str: &str) { #[cfg(feature = "ecdsa")] #[test] fn decode_ecdsa_keydata() { - decode_keydata(ECDSA_P256_CERT_EXAMPLE) + decode_keydata(ECDSA_P256_CERT_EXAMPLE); } #[cfg(feature = "ed25519")] #[test] fn decode_ed25519_keydata() { - decode_keydata(ED25519_CERT_EXAMPLE) + decode_keydata(ED25519_CERT_EXAMPLE); } #[test] fn decode_rsa_4096_keydata() { - decode_keydata(RSA_4096_CERT_EXAMPLE) + decode_keydata(RSA_4096_CERT_EXAMPLE); } fn encode_keydata(certificate_str: &str) { @@ -399,7 +400,7 @@ fn reject_expired_certificate() { fn reject_certificate_with_future_valid_after() { let cert = Certificate::from_str(ED25519_CERT_EXAMPLE).unwrap(); let ca = CA_FINGERPRINT.parse().unwrap(); - assert!(cert.validate_at(PAST_TIMESTAMP, &[ca]).is_err()) + assert!(cert.validate_at(PAST_TIMESTAMP, &[ca]).is_err()); } #[cfg(feature = "p256")] diff --git a/ssh-key/tests/certificate_builder.rs b/ssh-key/tests/certificate_builder.rs index bfae5361..17632d9b 100644 --- a/ssh-key/tests/certificate_builder.rs +++ b/ssh-key/tests/certificate_builder.rs @@ -14,10 +14,10 @@ use ssh_key::{Algorithm, PrivateKey, certificate}; use ssh_key::EcdsaCurve; #[cfg(all(feature = "ed25519", feature = "rsa"))] -use std::str::FromStr; +use core::str::FromStr; #[cfg(all(feature = "ed25519", feature = "std"))] -use std::time::{Duration, SystemTime}; +use {core::time::Duration, std::time::SystemTime}; /// Example Unix timestamp when a certificate was issued (2020-09-13 12:26:40 UTC). const ISSUED_AT: u64 = 1600000000; diff --git a/ssh-key/tests/private_key.rs b/ssh-key/tests/private_key.rs index cc12771d..30d33570 100644 --- a/ssh-key/tests/private_key.rs +++ b/ssh-key/tests/private_key.rs @@ -1,14 +1,14 @@ //! SSH private key tests. +#![allow(clippy::unwrap_used, reason = "tests")] + use hex_literal::hex; use ssh_key::{Algorithm, Cipher, KdfAlg, PrivateKey}; #[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] use ssh_key::EcdsaCurve; - #[cfg(feature = "alloc")] use ssh_key::LineEnding; - #[cfg(feature = "std")] use { ssh_key::PublicKey, @@ -105,6 +105,7 @@ const OPENSSH_NON_UTF8_COMMENT_EXAMPLE: &str = include_str!("examples/non_utf8_c /// Get a path into the `tests/scratch` directory. #[cfg(feature = "std")] +#[must_use] pub fn scratch_path(filename: &str) -> PathBuf { PathBuf::from(&format!("tests/scratch/{}", filename)) } @@ -621,43 +622,43 @@ fn encode_dsa_openssh() { #[cfg(all(feature = "alloc", feature = "p256"))] #[test] fn encode_ecdsa_p256_openssh() { - encoding_test(OPENSSH_ECDSA_P256_EXAMPLE) + encoding_test(OPENSSH_ECDSA_P256_EXAMPLE); } #[cfg(all(feature = "alloc", feature = "p384"))] #[test] fn encode_ecdsa_p384_openssh() { - encoding_test(OPENSSH_ECDSA_P384_EXAMPLE) + encoding_test(OPENSSH_ECDSA_P384_EXAMPLE); } #[cfg(all(feature = "alloc", feature = "p521"))] #[test] fn encode_ecdsa_p521_openssh() { - encoding_test(OPENSSH_ECDSA_P521_EXAMPLE) + encoding_test(OPENSSH_ECDSA_P521_EXAMPLE); } #[cfg(feature = "alloc")] #[test] fn encode_ed25519_openssh() { - encoding_test(OPENSSH_ED25519_EXAMPLE) + encoding_test(OPENSSH_ED25519_EXAMPLE); } #[cfg(feature = "alloc")] #[test] fn encode_rsa_3072_openssh() { - encoding_test(OPENSSH_RSA_3072_EXAMPLE) + encoding_test(OPENSSH_RSA_3072_EXAMPLE); } #[cfg(feature = "alloc")] #[test] fn encode_rsa_4096_openssh() { - encoding_test(OPENSSH_RSA_4096_EXAMPLE) + encoding_test(OPENSSH_RSA_4096_EXAMPLE); } #[cfg(feature = "alloc")] #[test] fn encode_custom_algorithm_openssh() { - encoding_test(OPENSSH_OPAQUE_EXAMPLE) + encoding_test(OPENSSH_OPAQUE_EXAMPLE); } /// Common behavior of all encoding tests @@ -668,7 +669,7 @@ fn encoding_test(private_key: &str) { #[cfg(feature = "std")] if !matches!(key.algorithm(), Algorithm::Other(_)) { - encoding_integration_test(key) + encoding_integration_test(key); } } @@ -705,7 +706,7 @@ fn encoding_integration_test(private_key: PrivateKey) { { Ok(output) => { assert_eq!(output.status.code().unwrap(), 0); - let ssh_keygen_output = std::str::from_utf8(&output.stdout).unwrap(); + let ssh_keygen_output = core::str::from_utf8(&output.stdout).unwrap(); PublicKey::from_openssh(ssh_keygen_output).unwrap() } Err(err) => { diff --git a/ssh-key/tests/public_key.rs b/ssh-key/tests/public_key.rs index 998d3e5e..3e777393 100644 --- a/ssh-key/tests/public_key.rs +++ b/ssh-key/tests/public_key.rs @@ -54,6 +54,7 @@ const OPENSSH_OPAQUE_EXAMPLE: &str = include_str!("examples/id_opaque.pub"); /// Get a path into the `tests/scratch` directory. #[cfg(feature = "std")] +#[must_use] pub fn scratch_path(filename: &str) -> PathBuf { PathBuf::from(&format!("tests/scratch/{}.pub", filename)) } diff --git a/ssh-key/tests/sshsig.rs b/ssh-key/tests/sshsig.rs index 09b64ed0..2b0ae637 100644 --- a/ssh-key/tests/sshsig.rs +++ b/ssh-key/tests/sshsig.rs @@ -311,7 +311,7 @@ fn verify_ed25519() { // invalid message assert_eq!( verifying_key.verify(NAMESPACE_EXAMPLE, b"bogus!", &signature), - Err(Error::Crypto) + Err(Error::Signature) ); } @@ -336,6 +336,6 @@ fn verify_sk_ed25519() { // invalid message assert_eq!( verifying_key.verify(NAMESPACE_EXAMPLE, b"bogus!", &signature), - Err(Error::Crypto) + Err(Error::Signature) ); }