diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index 1c9e6750..faf0e2e8 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -41,14 +41,14 @@ signature = { version = "1.6.4", optional = true, default-features = false } subtle = { version = "2", optional = true, default-features = false } [dev-dependencies] -hex-literal = "0.3" +hex-literal = "0.3.4" rand_chacha = "0.3" tempfile = "3" zeroize_derive = "1.3" # hack to make minimal-versions lint happy (pulled in by `ed25519-dalek`) [features] default = ["ecdsa", "rand_core", "std"] -alloc = ["base64ct/alloc", "signature", "zeroize/alloc"] +alloc = ["base64ct/alloc", "signature/hazmat-preview", "zeroize/alloc"] std = [ "alloc", "base64ct/std", diff --git a/ssh-key/src/algorithm.rs b/ssh-key/src/algorithm.rs index fc14429d..2465cd91 100644 --- a/ssh-key/src/algorithm.rs +++ b/ssh-key/src/algorithm.rs @@ -3,6 +3,12 @@ use crate::{decode::Decode, encode::Encode, reader::Reader, writer::Writer, Error, Result}; use core::{fmt, str}; +#[cfg(feature = "alloc")] +use { + alloc::vec::Vec, + sha2::{Digest, Sha256, Sha512}, +}; + /// bcrypt-pbkdf const BCRYPT: &str = "bcrypt"; @@ -49,10 +55,10 @@ const RSA_SHA2_256: &str = "rsa-sha2-256"; const RSA_SHA2_512: &str = "rsa-sha2-512"; /// SHA-256 hash function -const SHA256: &str = "SHA256"; +const SHA256: &str = "sha256"; /// SHA-512 hash function -const SHA512: &str = "SHA512"; +const SHA512: &str = "sha512"; /// Digital Signature Algorithm const SSH_DSA: &str = "ssh-dss"; @@ -389,8 +395,8 @@ impl HashAlg { /// /// # Supported hash algorithms /// - /// - `SHA256` - /// - `SHA512` + /// - `sha256` + /// - `sha512` pub fn new(id: &str) -> Result { match id { SHA256 => Ok(HashAlg::Sha256), @@ -414,8 +420,20 @@ impl HashAlg { HashAlg::Sha512 => 64, } } + + /// Compute a digest of the given message using this hash function. + #[cfg(feature = "alloc")] + #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] + pub fn digest(self, msg: &[u8]) -> Vec { + match self { + HashAlg::Sha256 => Sha256::digest(msg).to_vec(), + HashAlg::Sha512 => Sha512::digest(msg).to_vec(), + } + } } +impl AlgString for HashAlg {} + impl AsRef for HashAlg { fn as_ref(&self) -> &str { self.as_str() @@ -480,14 +498,14 @@ impl KdfAlg { } } +impl AlgString for KdfAlg {} + impl AsRef for KdfAlg { fn as_ref(&self) -> &str { self.as_str() } } -impl AlgString for KdfAlg {} - impl Default for KdfAlg { fn default() -> KdfAlg { KdfAlg::Bcrypt diff --git a/ssh-key/src/certificate.rs b/ssh-key/src/certificate.rs index ccf742f4..26e99e05 100644 --- a/ssh-key/src/certificate.rs +++ b/ssh-key/src/certificate.rs @@ -4,13 +4,9 @@ mod builder; mod cert_type; mod field; mod options_map; -mod signing_key; mod unix_time; -pub use self::{ - builder::Builder, cert_type::CertType, field::Field, options_map::OptionsMap, - signing_key::SigningKey, -}; +pub use self::{builder::Builder, cert_type::CertType, field::Field, options_map::OptionsMap}; use self::unix_time::UnixTime; use crate::{ diff --git a/ssh-key/src/certificate/builder.rs b/ssh-key/src/certificate/builder.rs index 07a1c3fa..88fb10e7 100644 --- a/ssh-key/src/certificate/builder.rs +++ b/ssh-key/src/certificate/builder.rs @@ -1,7 +1,7 @@ //! OpenSSH certificate builder. -use super::{unix_time::UnixTime, CertType, Certificate, Field, OptionsMap, SigningKey}; -use crate::{public, Result, Signature}; +use super::{unix_time::UnixTime, CertType, Certificate, Field, OptionsMap}; +use crate::{public, Result, Signature, SigningKey}; use alloc::{string::String, vec::Vec}; #[cfg(feature = "rand_core")] diff --git a/ssh-key/src/certificate/signing_key.rs b/ssh-key/src/certificate/signing_key.rs index a359acaa..48583738 100644 --- a/ssh-key/src/certificate/signing_key.rs +++ b/ssh-key/src/certificate/signing_key.rs @@ -5,23 +5,3 @@ use signature::Signer; #[cfg(doc)] use super::Builder; - -/// Certificate signing key trait for the certificate [`Builder`]. -/// -/// This trait is automatically impl'd for any types which impl the -/// [`Signer`] trait for the OpenSSH certificate [`Signature`] type and also -/// support a [`From`] conversion for [`public::KeyData`]. -pub trait SigningKey: Signer { - /// Get the [`public::KeyData`] for this signing key. - fn public_key(&self) -> public::KeyData; -} - -impl SigningKey for T -where - T: Signer, - public::KeyData: for<'a> From<&'a T>, -{ - fn public_key(&self) -> public::KeyData { - self.into() - } -} diff --git a/ssh-key/src/error.rs b/ssh-key/src/error.rs index 5bab7ca3..4436e791 100644 --- a/ssh-key/src/error.rs +++ b/ssh-key/src/error.rs @@ -55,13 +55,16 @@ pub enum Error { /// Invalid length. Length, + /// Namespace invalid. + Namespace, + /// Overflow errors. Overflow, /// PEM encoding errors. Pem(pem::Error), - /// Public key does not match private key. + /// Public key is incorrect. PublicKey, /// Invalid timestamp (e.g. in a certificate) @@ -72,6 +75,12 @@ pub enum Error { /// Number of bytes of remaining data at end of message. remaining: usize, }, + + /// Unsupported version. + Version { + /// Version number. + number: u32, + }, } impl fmt::Display for Error { @@ -94,6 +103,7 @@ impl fmt::Display for Error { #[cfg(feature = "std")] Error::Io(err) => write!(f, "I/O error: {}", std::io::Error::from(*err)), Error::Length => write!(f, "length invalid"), + Error::Namespace => write!(f, "namespace invalid"), Error::Overflow => write!(f, "internal overflow error"), Error::Pem(err) => write!(f, "{}", err), Error::PublicKey => write!(f, "public key is incorrect"), @@ -103,6 +113,7 @@ impl fmt::Display for Error { "unexpected trailing data at end of message ({} bytes)", remaining ), + Error::Version { number: version } => write!(f, "version unsupported: {}", version), } } } diff --git a/ssh-key/src/fingerprint.rs b/ssh-key/src/fingerprint.rs index 9aa20454..7231e439 100644 --- a/ssh-key/src/fingerprint.rs +++ b/ssh-key/src/fingerprint.rs @@ -119,10 +119,16 @@ impl AsRef<[u8]> for Fingerprint { impl Display for Fingerprint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Fingerprints use a special upper-case hash algorithm encoding. + let algorithm = match self.algorithm() { + HashAlg::Sha256 => "SHA256", + HashAlg::Sha512 => "SHA512", + }; + // Buffer size is the largest digest size of of any supported hash function let mut buf = [0u8; Self::SHA512_BASE64_SIZE]; let base64 = Base64Unpadded::encode(self.as_bytes(), &mut buf).map_err(|_| fmt::Error)?; - write!(f, "{}:{}", self.algorithm(), base64) + write!(f, "{}:{}", algorithm, base64) } } @@ -130,13 +136,20 @@ impl FromStr for Fingerprint { type Err = Error; fn from_str(id: &str) -> Result { - let (algorithm, base64) = id.split_once(':').ok_or(Error::Algorithm)?; + let (alg_str, base64) = id.split_once(':').ok_or(Error::Algorithm)?; + + // Fingerprints use a special upper-case hash algorithm encoding. + let algorithm = match alg_str { + "SHA256" => HashAlg::Sha256, + "SHA512" => HashAlg::Sha512, + _ => return Err(Error::Algorithm), + }; // Buffer size is the largest digest size of of any supported hash function let mut buf = [0u8; HashAlg::Sha512.digest_size()]; let decoded_bytes = Base64Unpadded::decode(base64, &mut buf)?; - match algorithm.parse()? { + match algorithm { HashAlg::Sha256 => Ok(Self::Sha256(decoded_bytes.try_into()?)), HashAlg::Sha512 => Ok(Self::Sha512(decoded_bytes.try_into()?)), } diff --git a/ssh-key/src/lib.rs b/ssh-key/src/lib.rs index dae2440b..6b8621ad 100644 --- a/ssh-key/src/lib.rs +++ b/ssh-key/src/lib.rs @@ -163,6 +163,8 @@ mod writer; mod mpint; #[cfg(feature = "alloc")] mod signature; +#[cfg(feature = "alloc")] +mod sshsig; pub use crate::{ algorithm::{Algorithm, EcdsaCurve, HashAlg, KdfAlg}, @@ -180,7 +182,11 @@ pub use sha2; #[cfg(feature = "alloc")] pub use crate::{ - certificate::Certificate, known_hosts::KnownHosts, mpint::MPInt, signature::Signature, + certificate::Certificate, + known_hosts::KnownHosts, + mpint::MPInt, + signature::{Signature, SigningKey}, + sshsig::SshSig, }; #[cfg(feature = "ecdsa")] @@ -188,3 +194,6 @@ pub use sec1; #[cfg(feature = "rand_core")] pub use rand_core; + +/// Line width used by the PEM encoding of OpenSSH documents. +const PEM_LINE_WIDTH: usize = 70; diff --git a/ssh-key/src/private.rs b/ssh-key/src/private.rs index 45a1d89f..85bc843a 100644 --- a/ssh-key/src/private.rs +++ b/ssh-key/src/private.rs @@ -119,10 +119,13 @@ pub use self::ed25519::{Ed25519Keypair, Ed25519PrivateKey}; pub use self::keypair::KeypairData; #[cfg(feature = "alloc")] -pub use self::{ - dsa::{DsaKeypair, DsaPrivateKey}, - rsa::{RsaKeypair, RsaPrivateKey}, - sk::SkEd25519, +pub use crate::{ + private::{ + dsa::{DsaKeypair, DsaPrivateKey}, + rsa::{RsaKeypair, RsaPrivateKey}, + sk::SkEd25519, + }, + SshSig, }; #[cfg(feature = "ecdsa")] @@ -139,7 +142,7 @@ use crate::{ public, reader::Reader, writer::Writer, - Algorithm, Cipher, Error, Fingerprint, HashAlg, Kdf, PublicKey, Result, + Algorithm, Cipher, Error, Fingerprint, HashAlg, Kdf, PublicKey, Result, PEM_LINE_WIDTH, }; use core::str; @@ -176,9 +179,6 @@ const MAX_BLOCK_SIZE: usize = 16; /// Padding bytes to use. const PADDING_BYTES: [u8; MAX_BLOCK_SIZE - 1] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; -/// Line width used by the PEM encoding of OpenSSH private keys. -const PEM_LINE_WIDTH: usize = 70; - /// Unix file permissions for SSH private keys. #[cfg(all(unix, feature = "std"))] const UNIX_FILE_PERMISSIONS: u32 = 0o600; @@ -283,6 +283,24 @@ impl PrivateKey { Ok(Zeroizing::new(private_key_bytes)) } + /// Sign the given message using this private key, returning an [`SshSig`]. + /// + /// These signatures can be produced using `ssh-keygen -Y sign`. They're + /// encoded as PEM and begin with the following: + /// + /// ```text + /// -----BEGIN SSH SIGNATURE----- + /// ``` + /// + /// See [PROTOCOL.sshsig] for more information. + /// + /// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD + #[cfg(feature = "alloc")] + #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] + pub fn sign(&self, namespace: &str, hash_alg: HashAlg, msg: &[u8]) -> Result { + SshSig::sign(self, namespace, hash_alg, msg) + } + /// Read private key from an OpenSSH-formatted PEM file. #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] diff --git a/ssh-key/src/public.rs b/ssh-key/src/public.rs index fcc5b0c4..60446269 100644 --- a/ssh-key/src/public.rs +++ b/ssh-key/src/public.rs @@ -33,7 +33,7 @@ use core::str::FromStr; #[cfg(feature = "alloc")] use { - crate::{checked::CheckedSum, writer::base64_len}, + crate::{checked::CheckedSum, writer::base64_len, SshSig}, alloc::{ borrow::ToOwned, string::{String, ToString}, @@ -166,6 +166,33 @@ impl PublicKey { Ok(public_key_bytes) } + /// Verify the [`SshSig`] signature over the given message using this + /// public key. + /// + /// These signatures can be produced using `ssh-keygen -Y sign`. They're + /// encoded as PEM and begin with the following: + /// + /// ```text + /// -----BEGIN SSH SIGNATURE----- + /// ``` + /// + /// See [PROTOCOL.sshsig] for more information. + /// + /// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD + #[cfg(feature = "alloc")] + #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] + pub fn verify(&self, namespace: &str, msg: &[u8], signature: &SshSig) -> Result<()> { + if self.key_data() != signature.public_key() { + return Err(Error::PublicKey); + } + + if namespace != signature.namespace() { + return Err(Error::Namespace); + } + + signature.verify(msg) + } + /// Read public key from an OpenSSH-formatted file. #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] diff --git a/ssh-key/src/signature.rs b/ssh-key/src/signature.rs index 9daf4ced..adec815f 100644 --- a/ssh-key/src/signature.rs +++ b/ssh-key/src/signature.rs @@ -34,10 +34,32 @@ use { const DSA_SIGNATURE_SIZE: usize = 40; const ED25519_SIGNATURE_SIZE: usize = 64; -/// Digital signature (e.g. DSA, ECDSA, Ed25519). +/// Trait for signing keys which produce a [`Signature`]. /// -/// These are used as part of the OpenSSH certificate format to represent -/// signatures by certificate authorities (CAs). +/// This trait is automatically impl'd for any types which impl the +/// [`Signer`] trait for the SSH [`Signature`] type and also support a [`From`] +/// conversion for [`public::KeyData`]. +pub trait SigningKey: Signer { + /// Get the [`public::KeyData`] for this signing key. + fn public_key(&self) -> public::KeyData; +} + +impl SigningKey for T +where + T: Signer, + public::KeyData: for<'a> From<&'a T>, +{ + fn public_key(&self) -> public::KeyData { + self.into() + } +} + +/// Low-level digital signature (e.g. DSA, ECDSA, Ed25519). +/// +/// These are low-level signatures used as part of the OpenSSH certificate +/// format to represent signatures by certificate authorities (CAs), as well +/// as the higher-level [`SshSig`][`crate::SshSig`] format, which provides +/// general-purpose signing functionality using SSH keys. /// /// From OpenSSH's [PROTOCOL.certkeys] specification: /// @@ -102,6 +124,16 @@ impl Signature { Ok(Self { algorithm, data }) } + /// Get the [`Algorithm`] associated with this signature. + pub fn algorithm(&self) -> Algorithm { + self.algorithm + } + + /// Get the raw signature as bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + /// Placeholder signature used by the certificate builder. /// /// This is guaranteed generate an error if anything attempts to encode it. @@ -118,18 +150,6 @@ impl Signature { } } -impl Signature { - /// Get the [`Algorithm`] associated with this signature. - pub fn algorithm(&self) -> Algorithm { - self.algorithm - } - - /// Get the raw signature as bytes. - pub fn as_bytes(&self) -> &[u8] { - &self.data - } -} - impl AsRef<[u8]> for Signature { fn as_ref(&self) -> &[u8] { self.as_bytes() @@ -217,6 +237,8 @@ impl Signer for private::KeypairData { #[allow(unused_variables)] fn try_sign(&self, message: &[u8]) -> signature::Result { match self { + #[cfg(feature = "dsa")] + Self::Dsa(keypair) => keypair.try_sign(message), #[cfg(any(feature = "p256", feature = "p384"))] Self::Ecdsa(keypair) => keypair.try_sign(message), #[cfg(feature = "ed25519")] @@ -238,6 +260,8 @@ impl Verifier for public::KeyData { #[allow(unused_variables)] fn verify(&self, message: &[u8], signature: &Signature) -> signature::Result<()> { match self { + #[cfg(feature = "dsa")] + Self::Dsa(pk) => pk.verify(message, signature), #[cfg(any(feature = "p256", feature = "p384"))] Self::Ecdsa(pk) => pk.verify(message, signature), #[cfg(feature = "ed25519")] @@ -477,7 +501,7 @@ impl TryFrom<&Signature> for p384::ecdsa::Signature { } #[cfg(any(feature = "p256", feature = "p384"))] -#[cfg_attr(docsrs, doc(cfg(feature = "p256", feature = "p384")))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "p256", feature = "p384"))))] impl Signer for EcdsaKeypair { fn try_sign(&self, message: &[u8]) -> signature::Result { match self { diff --git a/ssh-key/src/sshsig.rs b/ssh-key/src/sshsig.rs new file mode 100644 index 00000000..9024379e --- /dev/null +++ b/ssh-key/src/sshsig.rs @@ -0,0 +1,328 @@ +//! `sshsig` implementation. + +use crate::{ + checked::CheckedSum, + decode::Decode, + encode::Encode, + pem::{self, PemLabel}, + public, + reader::Reader, + writer::Writer, + Algorithm, Error, HashAlg, Result, Signature, SigningKey, PEM_LINE_WIDTH, +}; +use alloc::{borrow::ToOwned, string::String, string::ToString, vec::Vec}; +use base64ct::LineEnding; +use core::str::FromStr; +use signature::{Signer, Verifier}; + +type Version = u32; + +/// `sshsig` provides a general-purpose signature format based on SSH keys and +/// wire formats. +/// +/// These signatures can be produced using `ssh-keygen -Y sign`. They're +/// encoded as PEM and begin with the following: +/// +/// ```text +/// -----BEGIN SSH SIGNATURE----- +/// ``` +/// +/// See [PROTOCOL.sshsig] for more information. +/// +/// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SshSig { + version: Version, + public_key: public::KeyData, + namespace: String, + reserved: Vec, + hash_alg: HashAlg, + signature: Signature, +} + +impl SshSig { + /// Supported version. + pub const VERSION: Version = 1; + + /// 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. + const MAGIC_PREAMBLE: &'static [u8] = b"SSHSIG"; + + /// Decode signature from PEM which begins with the following: + /// + /// ```text + /// -----BEGIN SSH SIGNATURE----- + /// ``` + pub fn from_pem(pem: impl AsRef<[u8]>) -> Result { + let mut reader = pem::Decoder::new_wrapped(pem.as_ref(), PEM_LINE_WIDTH)?; + Self::validate_pem_label(reader.type_label())?; + let signature = Self::decode(&mut reader)?; + reader.finish(signature) + } + + /// Encode signature as PEM which begins with the following: + /// + /// ```text + /// -----BEGIN SSH SIGNATURE----- + /// ``` + pub fn to_pem(&self, line_ending: LineEnding) -> Result { + let encoded_len = pem::encapsulated_len_wrapped( + Self::PEM_LABEL, + PEM_LINE_WIDTH, + line_ending, + self.encoded_len()?, + )?; + + let mut buf = vec![0u8; encoded_len]; + let mut writer = + pem::Encoder::new_wrapped(Self::PEM_LABEL, PEM_LINE_WIDTH, line_ending, &mut buf)?; + + self.encode(&mut writer)?; + let actual_len = writer.finish()?; + buf.truncate(actual_len); + Ok(String::from_utf8(buf)?) + } + + /// Sign the given message with the provided signing key. + pub fn sign( + signing_key: &S, + namespace: &str, + hash_alg: HashAlg, + msg: &[u8], + ) -> Result { + if namespace.is_empty() { + return Err(Error::Namespace); + } + + let public_key = signing_key.public_key(); + let namespace = namespace.to_owned(); + let reserved = Vec::new(); + let hash = hash_alg.digest(msg); + + let signature = SignedData { + namespace: namespace.as_str(), + reserved: reserved.as_slice(), + hash_alg, + hash: hash.as_slice(), + } + .sign(signing_key)?; + + Ok(Self { + version: Self::VERSION, + public_key, + namespace, + reserved, + hash_alg, + signature, + }) + } + + /// Verify the given message 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. + pub(crate) fn verify(&self, msg: &[u8]) -> Result<()> { + let signed_data = SignedData { + namespace: self.namespace.as_str(), + reserved: self.reserved.as_slice(), + hash_alg: self.hash_alg, + hash: self.hash_alg.digest(msg).as_slice(), + } + .to_bytes()?; + + Ok(self.public_key.verify(&signed_data, &self.signature)?) + } + + /// Get the signature algorithm. + pub fn algorithm(&self) -> Algorithm { + self.signature.algorithm() + } + + /// Get version number for this signature. + /// + /// Verifiers MUST reject signatures with versions greater than those + /// they support. + pub fn version(&self) -> Version { + self.version + } + + /// Get public key which corresponds to the signing key that produced + /// this signature. + pub fn public_key(&self) -> &public::KeyData { + &self.public_key + } + + /// Get the namespace (i.e. domain identifier) for this signature. + /// + /// The purpose of the namespace value is to specify a unambiguous + /// interpretation domain for the signature, e.g. file signing. + /// 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. + pub fn namespace(&self) -> &str { + &self.namespace + } + + /// Get reserved data associated with this signature. Typically empty. + /// + /// 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. + pub fn reserved(&self) -> &[u8] { + &self.reserved + } + + /// Get the hash algorithm used to produce this signature. + /// + /// Data to be signed is first hashed with the specified `hash_alg`. + /// This is done to limit the amount of data presented to the signature + /// 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". + pub fn hash_alg(&self) -> HashAlg { + self.hash_alg + } + + /// Get the structured signature over the given message. + pub fn signature(&self) -> &Signature { + &self.signature + } + + /// Get the bytes which comprise the serialized signature. + pub fn signature_bytes(&self) -> &[u8] { + self.signature.as_bytes() + } +} + +impl Decode for SshSig { + fn decode(reader: &mut impl Reader) -> Result { + let mut magic_preamble = [0u8; Self::MAGIC_PREAMBLE.len()]; + reader.read(&mut magic_preamble)?; + + if magic_preamble != Self::MAGIC_PREAMBLE { + return Err(Error::FormatEncoding); + } + + let version = Version::decode(reader)?; + + if version > Self::VERSION { + return Err(Error::Version { number: version }); + } + + let public_key = reader.read_nested(public::KeyData::decode)?; + let namespace = String::decode(reader)?; + + if namespace.is_empty() { + return Err(Error::Namespace); + } + + let reserved = Vec::decode(reader)?; + let hash_alg = HashAlg::decode(reader)?; + let signature = reader.read_nested(Signature::decode)?; + + Ok(Self { + version, + public_key, + namespace, + reserved, + hash_alg, + signature, + }) + } +} + +impl Encode for SshSig { + fn encoded_len(&self) -> Result { + [ + Self::MAGIC_PREAMBLE.len(), + self.version.encoded_len()?, + 4, // public key length prefix (uint32) + self.public_key.encoded_len()?, + self.namespace.encoded_len()?, + self.reserved.encoded_len()?, + self.hash_alg.encoded_len()?, + 4, // signature length prefix (uint32) + self.signature.encoded_len()?, + ] + .checked_sum() + } + + fn encode(&self, writer: &mut impl Writer) -> Result<()> { + writer.write(Self::MAGIC_PREAMBLE)?; + self.version.encode(writer)?; + self.public_key.encode_nested(writer)?; + self.namespace.encode(writer)?; + self.reserved.encode(writer)?; + self.hash_alg.encode(writer)?; + self.signature.encode_nested(writer)?; + Ok(()) + } +} + +impl FromStr for SshSig { + type Err = Error; + + fn from_str(s: &str) -> Result { + Self::from_pem(s) + } +} + +impl PemLabel for SshSig { + const PEM_LABEL: &'static str = "SSH SIGNATURE"; +} + +impl ToString for SshSig { + fn to_string(&self) -> String { + self.to_pem(LineEnding::default()) + .expect("SSH signature encoding error") + } +} + +/// Data to be signed. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct SignedData<'a> { + namespace: &'a str, + reserved: &'a [u8], + hash_alg: HashAlg, + hash: &'a [u8], +} + +impl<'a> SignedData<'a> { + fn sign(&self, signer: &S) -> Result + where + S: Signer, + { + Ok(signer.try_sign(&self.to_bytes()?)?) + } + + fn to_bytes(self) -> Result> { + let mut signed_bytes = Vec::with_capacity(self.encoded_len()?); + self.encode(&mut signed_bytes)?; + Ok(signed_bytes) + } +} + +impl<'a> Encode for SignedData<'a> { + fn encoded_len(&self) -> Result { + [ + SshSig::MAGIC_PREAMBLE.len(), + self.namespace.encoded_len()?, + self.reserved.encoded_len()?, + self.hash_alg.encoded_len()?, + self.hash.encoded_len()?, + ] + .checked_sum() + } + + fn encode(&self, writer: &mut impl Writer) -> Result<()> { + writer.write(SshSig::MAGIC_PREAMBLE)?; + self.namespace.encode(writer)?; + self.reserved.encode(writer)?; + self.hash_alg.encode(writer)?; + self.hash.encode(writer)?; + Ok(()) + } +} diff --git a/ssh-key/tests/examples/sshsig_dsa_1024 b/ssh-key/tests/examples/sshsig_dsa_1024 new file mode 100644 index 00000000..c543d344 --- /dev/null +++ b/ssh-key/tests/examples/sshsig_dsa_1024 @@ -0,0 +1,13 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAbEAAAAHc3NoLWRzcwAAAIEA3D2JJQ7ZRiEUyyyNSBbjpRGq/xsGsO +Ad4XwcsE5YG8q5cXZHHYn9fKGBfjxI4sy6/SFw9p6OXItqtpucX0XZXh2Sk+llIn7uW4eb +ESM3HCGx22DxS15cBaR4LOtDoy9ElkdwMGNiHnooa+yVsWcmwYteUjg9ALKXprA0ibBgaK +UAAAAVAIkYFTeFl/5C0/0mH+dt82WEW7uHAAAAgEc5s5CKhBVGbcexVvuY7LcVUqFwugs7 +eqgb2BOR3gp656G0UALf6tySJfvFIKcT/kEEp0vtU/1ZFdpzY2Wv0/CXd7vM+633rCsIe3 +9Nlfq+R9cqRulQiPnNKp+/I2tYppgmR/PABDCtc1LUeiXrvpR38MMSfahq10SGRLdt5Ydc +AAAAgGBCprP9hhNEyyHMzNhxniWqC+CYDnnLq/SHf17wcfYDl3A1Lqw9TDaPKdr6V7R1x4 +1EmJ8WV3Un5ZgzS+aq5KvXUMNq+ASJ05JpfB8y8888mouZvN21PXo34aKP1T1JNBMc9BxD +fGc00eBABK3NkluEs5VsMMOjkE7ssxQAsN9IAAAAB2V4YW1wbGUAAAAAAAAABnNoYTUxMg +AAADcAAAAHc3NoLWRzcwAAACgpmje+ZfkBQr6VEub3uY2yZNOwXSdFsEtmylVrgY2cEr39 +gGnpa50C +-----END SSH SIGNATURE----- diff --git a/ssh-key/tests/examples/sshsig_ecdsa_p256 b/ssh-key/tests/examples/sshsig_ecdsa_p256 new file mode 100644 index 00000000..99c3a82e --- /dev/null +++ b/ssh-key/tests/examples/sshsig_ecdsa_p256 @@ -0,0 +1,7 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAE +EEfB/YcwzlNFe+jZJAmOw2SIMPkqqKI2OsZW/dRSH6YxPlEfGJG06eWq+OFC0GrRWmakJX +8/BR2E6KDi+RuoBwRwAAAAdleGFtcGxlAAAAAAAAAAZzaGE1MTIAAABjAAAAE2VjZHNhLX +NoYTItbmlzdHAyNTYAAABIAAAAID884rM9MmiHWlfwoX+JY6rrivMILmCEganDbXs6ShAB +AAAAIEYXgcOegKAJkfP1P7kw2AnlEIY0hRwtUm7moeouFySh +-----END SSH SIGNATURE----- diff --git a/ssh-key/tests/examples/sshsig_ecdsa_p384 b/ssh-key/tests/examples/sshsig_ecdsa_p384 new file mode 100644 index 00000000..b30edc25 --- /dev/null +++ b/ssh-key/tests/examples/sshsig_ecdsa_p384 @@ -0,0 +1,8 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAIgAAAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAAhuaXN0cDM4NAAAAG +EELm6C3FQH8QShERfHwFsZk8POs9sl+uaLoWlQKk/5OV2a02tUPoAU/xXXCOIfCfWFqm36 +1XW3m5Q0GLhhmNm82bB//5OZsV1D00766y5Wt7M8/4gLJCs+C1ivlsdYQexBAAAAB2V4YW +1wbGUAAAAAAAAABnNoYTUxMgAAAIQAAAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAGkAAAAw +WRSBydn20NFbMvdQGNRgO+e/AFdDm9eaT/AqQk3rFKKyuyC71gaiGXke3BWByW57AAAAMQ +DshCxIODastNeRULgAtklWgLC/6i1JQeelDPMFVD/T4tvQKBy2tnpUf1+9FKpoB34= +-----END SSH SIGNATURE----- diff --git a/ssh-key/tests/examples/sshsig_ed25519 b/ssh-key/tests/examples/sshsig_ed25519 new file mode 100644 index 00000000..5ac3759b --- /dev/null +++ b/ssh-key/tests/examples/sshsig_ed25519 @@ -0,0 +1,6 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgsz6u836i33yqAQ3v3qNOJB9l8b +UppPQ+0UMn9cVKq2IAAAAHZXhhbXBsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQy +NTUxOQAAAEBPEav+tMGNnox4MuzM7rlHyVBajCn8B0kAyiOWwPKprNsG3i6X+voz/WCSik +/FowYwqhgCABUJSvRX3AERVBUP +-----END SSH SIGNATURE----- diff --git a/ssh-key/tests/examples/sshsig_rsa_3072 b/ssh-key/tests/examples/sshsig_rsa_3072 new file mode 100644 index 00000000..7ce19aa4 --- /dev/null +++ b/ssh-key/tests/examples/sshsig_rsa_3072 @@ -0,0 +1,19 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAKaOR4ybyTcmQ2t/Xp5vmk +bhtzvsHoy3dU3ixqW2xFXy8BKnJZr8+UGB1p6V05o0nk0rSCpTcrKJQ3Mdt1xzznvZ7shQ +EMlL+uVpYBGJIvhqizZVs1fSTnpnnNin2b9urmb3+aVv49CQ0GMiGKaClg2KrZPAGJh4Dq +0tvv1w+0cDRxfkEuT9rmhSkuyJHiQj9/5D3y9UMpqwpddWHlguQuhuuu4MHp6vYD185whQ +XQ7gkJEuH8NzXrWATd9CthMxB6dumlnN/Gtl9DxjAs+8qOeqb5dFf6ltO1om6PQSBNLNQr +4RnGhLDwI3CJmnGuPB5xMxVDzD+ytCaHgAEa5OqTTA/wdwjuGD5+kG/uSJ6OHlf856HG34 ++67zm70ZVdvVrRq/++Em9QIFy4hK8ID/PXBUnTF0uFvX9mJMN1PPI1tlDQ5CKPMr57VKWQ +2Gn7d4ZVm7ek1m+dOmnAhe/fCDqRXUeh2RYaCHVrJjsG5znZnyiQNiq8lq3kLM6Pk5pA2v ++QAAAAdleGFtcGxlAAAAAAAAAAZzaGE1MTIAAAGUAAAADHJzYS1zaGEyLTUxMgAAAYBgAr +3zDww5X0fRn4fSzHi0okXxU8N4R0qJp2GJ/z9MlwIbxJ+NuWV9MbCRc27lBxklvos69kJ0 +zD0yXfpBHB+ABPO+7GPOm/8cj1ldy7AvjfGYb+UOwppAwGueaRGoeYXdiM5d9obpDLpX0u +UOoPkvLoXT/RHC8H+imHvDDfWTAvTe2a4Dukf78VA0ZGOI0BDO7ZIkcNL9t380cggEEzgj +Dj5215DG1Bk9cSw2P2WozuOeaTBuJttY+8/yhO7B9LVVJIPkxDewmbhQs5ycWWuUSUzGZC +YyHPTVbEu0zJSStQBUELR7qneFfHPpFO43L05yKmHOf+wtpo5Cp2JXV3+lpOtjdsP1Odu6 +/Gu7MX2qXgNEeTa9tFlybWxzTGKGMb0jrAHO4ivVlfYl+gjpd5URxf8pllnOaHQutPBTZX +UR1izJqucaJyQdyuI4Q2B7jl20zQlpwEqM6GiCbct7BJ3i38DgBXdcrz735+Tmuf+6LPMV +S9OHR3RCWZBFnFyt7dA= +-----END SSH SIGNATURE----- diff --git a/ssh-key/tests/private_key.rs b/ssh-key/tests/private_key.rs index 9bb02e58..004fe684 100644 --- a/ssh-key/tests/private_key.rs +++ b/ssh-key/tests/private_key.rs @@ -19,9 +19,6 @@ use { #[cfg(feature = "alloc")] const OPENSSH_DSA_EXAMPLE: &str = include_str!("examples/id_dsa_1024"); -/// Ed25519 OpenSSH-formatted private key -const OPENSSH_ED25519_EXAMPLE: &str = include_str!("examples/id_ed25519"); - /// ECDSA/P-256 OpenSSH-formatted public key #[cfg(feature = "ecdsa")] const OPENSSH_ECDSA_P256_EXAMPLE: &str = include_str!("examples/id_ecdsa_p256"); @@ -34,6 +31,9 @@ const OPENSSH_ECDSA_P384_EXAMPLE: &str = include_str!("examples/id_ecdsa_p384"); #[cfg(feature = "ecdsa")] const OPENSSH_ECDSA_P521_EXAMPLE: &str = include_str!("examples/id_ecdsa_p521"); +/// Ed25519 OpenSSH-formatted private key +const OPENSSH_ED25519_EXAMPLE: &str = include_str!("examples/id_ed25519"); + /// RSA (3072-bit) OpenSSH-formatted public key #[cfg(feature = "alloc")] const OPENSSH_RSA_3072_EXAMPLE: &str = include_str!("examples/id_rsa_3072"); diff --git a/ssh-key/tests/sshsig.rs b/ssh-key/tests/sshsig.rs new file mode 100644 index 00000000..8c0f717b --- /dev/null +++ b/ssh-key/tests/sshsig.rs @@ -0,0 +1,161 @@ +//! `sshsig` signature tests. + +#![cfg(feature = "alloc")] + +use hex_literal::hex; +use ssh_key::{Algorithm, HashAlg, LineEnding, PublicKey, SshSig}; + +#[cfg(feature = "ed25519")] +use ssh_key::{Error, PrivateKey}; + +/// DSA OpenSSH-formatted private key. +#[cfg(feature = "dsa")] +const DSA_PRIVATE_KEY: &str = include_str!("examples/id_dsa_1024"); + +/// DSA OpenSSH-formatted public key. +#[cfg(feature = "dsa")] +const DSA_PUBLIC_KEY: &str = include_str!("examples/id_dsa_1024.pub"); + +/// ECDSA/P-256 OpenSSH-formatted private key. +#[cfg(feature = "p256")] +const ECDSA_P256_PRIVATE_KEY: &str = include_str!("examples/id_ecdsa_p256"); + +/// ECDSA/P-256 OpenSSH-formatted public key. +#[cfg(feature = "p256")] +const ECDSA_P256_PUBLIC_KEY: &str = include_str!("examples/id_ecdsa_p256.pub"); + +/// Ed25519 OpenSSH-formatted private key. +#[cfg(feature = "ed25519")] +const ED25519_PRIVATE_KEY: &str = include_str!("examples/id_ed25519"); + +/// Ed25519 OpenSSH-formatted public key. +const ED25519_PUBLIC_KEY: &str = include_str!("examples/id_ed25519.pub"); + +/// `sshsig`-encoded signature. +const ED25519_SIGNATURE: &str = include_str!("examples/sshsig_ed25519"); + +/// Bytes of the raw Ed25519 signature. +const ED25519_SIGNATURE_BYTES: [u8; 64] = hex!( + "4f11abfeb4c18d9e8c7832eccceeb947c9505a8c29fc074900ca2396c0f2a9ac" + "db06de2e97fafa33fd60928a4fc5a30630aa18020015094af457dc011154150f" +); + +/// RSA OpenSSH-formatted private key. +#[cfg(feature = "rsa")] +const RSA_PRIVATE_KEY: &str = include_str!("examples/id_rsa_3072"); + +/// RSA OpenSSH-formatted public key. +#[cfg(feature = "rsa")] +const RSA_PUBLIC_KEY: &str = include_str!("examples/id_rsa_3072.pub"); + +/// Example message to be signed/verified. +#[cfg(feature = "ed25519")] +const MSG_EXAMPLE: &[u8] = b"testing"; + +/// Example domain/namespace used for the message. +const NAMESPACE_EXAMPLE: &str = "example"; + +#[test] +fn decode_ed25519() { + let sshsig = ED25519_SIGNATURE.parse::().unwrap(); + let public_key = ED25519_PUBLIC_KEY.parse::().unwrap(); + + assert_eq!(sshsig.algorithm(), Algorithm::Ed25519); + assert_eq!(sshsig.version(), 1); + assert_eq!(sshsig.public_key(), public_key.key_data()); + assert_eq!(sshsig.namespace(), NAMESPACE_EXAMPLE); + assert_eq!(sshsig.reserved(), &[]); + assert_eq!(sshsig.hash_alg(), HashAlg::Sha512); + assert_eq!(sshsig.signature_bytes(), ED25519_SIGNATURE_BYTES); +} + +#[test] +fn encode_ed25519() { + let sshsig = ED25519_SIGNATURE.parse::().unwrap(); + let sshsig_pem = sshsig.to_pem(LineEnding::LF).unwrap(); + assert_eq!(&sshsig_pem, ED25519_SIGNATURE); +} + +#[test] +#[cfg(feature = "dsa")] +fn sign_dsa() { + let signing_key = PrivateKey::from_openssh(DSA_PRIVATE_KEY).unwrap(); + let verifying_key = DSA_PUBLIC_KEY.parse::().unwrap(); + + let signature = signing_key + .sign(NAMESPACE_EXAMPLE, HashAlg::Sha512, MSG_EXAMPLE) + .unwrap(); + + assert_eq!( + verifying_key.verify(NAMESPACE_EXAMPLE, MSG_EXAMPLE, &signature), + Ok(()) + ); +} + +#[test] +#[cfg(feature = "p256")] +fn sign_ecdsa_p256() { + let signing_key = PrivateKey::from_openssh(ECDSA_P256_PRIVATE_KEY).unwrap(); + let verifying_key = ECDSA_P256_PUBLIC_KEY.parse::().unwrap(); + + let signature = signing_key + .sign(NAMESPACE_EXAMPLE, HashAlg::Sha512, MSG_EXAMPLE) + .unwrap(); + + assert_eq!( + verifying_key.verify(NAMESPACE_EXAMPLE, MSG_EXAMPLE, &signature), + Ok(()) + ); +} + +#[test] +#[cfg(feature = "ed25519")] +fn sign_ed25519() { + let signing_key = PrivateKey::from_openssh(ED25519_PRIVATE_KEY).unwrap(); + let signature = signing_key + .sign(NAMESPACE_EXAMPLE, HashAlg::Sha512, MSG_EXAMPLE) + .unwrap(); + + assert_eq!(signature, ED25519_SIGNATURE.parse::().unwrap()); +} + +#[test] +#[cfg(feature = "rsa")] +fn sign_rsa() { + let signing_key = PrivateKey::from_openssh(RSA_PRIVATE_KEY).unwrap(); + let verifying_key = RSA_PUBLIC_KEY.parse::().unwrap(); + + let signature = signing_key + .sign(NAMESPACE_EXAMPLE, HashAlg::Sha512, MSG_EXAMPLE) + .unwrap(); + + assert_eq!( + verifying_key.verify(NAMESPACE_EXAMPLE, MSG_EXAMPLE, &signature), + Ok(()) + ); +} + +#[test] +#[cfg(feature = "ed25519")] +fn verify_ed25519() { + let verifying_key = ED25519_PUBLIC_KEY.parse::().unwrap(); + let signature = ED25519_SIGNATURE.parse::().unwrap(); + + // valid + assert_eq!( + verifying_key.verify(NAMESPACE_EXAMPLE, MSG_EXAMPLE, &signature), + Ok(()) + ); + + // bad namespace + assert_eq!( + verifying_key.verify("bogus namespace", MSG_EXAMPLE, &signature), + Err(Error::Namespace) + ); + + // invalid message + assert_eq!( + verifying_key.verify(NAMESPACE_EXAMPLE, b"bogus!", &signature), + Err(Error::Crypto) + ); +}