ssh-key-fork-arti 0.6.7

Pure Rust implementation of SSH key file format decoders/encoders as described in RFC4251/RFC4253 and OpenSSH key formats, as well as "sshsig" signatures and certificates (including certificate validation and certificate authority support), with further support for the `authorized_keys` and `known_hosts` file formats.
//! `sshsig` implementation.

use crate::{public, Algorithm, Error, HashAlg, Result, Signature, SigningKey};
use alloc::{string::String, string::ToString, vec::Vec};
use core::str::FromStr;
use encoding::{
    pem::{LineEnding, PemLabel},
    CheckedSum, Decode, DecodePem, Encode, EncodePem, Reader, Writer,
};
use signature::Verifier;

#[cfg(doc)]
use crate::{PrivateKey, PublicKey};

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.
///
/// # Usage
///
/// See [`PrivateKey::sign`] and [`PublicKey::verify`] for usage 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<u8>,
    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";

    /// Create a new signature with the given public key, namespace, hash
    /// algorithm, and signature.
    pub fn new(
        public_key: public::KeyData,
        namespace: impl Into<String>,
        hash_alg: HashAlg,
        signature: Signature,
    ) -> Result<Self> {
        let version = Self::VERSION;
        let namespace = namespace.into();
        let reserved = Vec::new();

        if namespace.is_empty() {
            return Err(Error::Namespace);
        }

        Ok(Self {
            version,
            public_key,
            namespace,
            reserved,
            hash_alg,
            signature,
        })
    }

    /// Decode signature from PEM which begins with the following:
    ///
    /// ```text
    /// -----BEGIN SSH SIGNATURE-----
    /// ```
    pub fn from_pem(pem: impl AsRef<[u8]>) -> Result<Self> {
        Self::decode_pem(pem)
    }

    /// Encode signature as PEM which begins with the following:
    ///
    /// ```text
    /// -----BEGIN SSH SIGNATURE-----
    /// ```
    pub fn to_pem(&self, line_ending: LineEnding) -> Result<String> {
        Ok(self.encode_pem_string(line_ending)?)
    }

    /// Sign the given message with the provided signing key.
    ///
    /// See also: [`PrivateKey::sign`].
    pub fn sign<S: SigningKey>(
        signing_key: &S,
        namespace: &str,
        hash_alg: HashAlg,
        msg: &[u8],
    ) -> Result<Self> {
        if namespace.is_empty() {
            return Err(Error::Namespace);
        }

        if signing_key.public_key().is_sk_ed25519() {
            return Err(Algorithm::SkEd25519.unsupported_error());
        }

        #[cfg(feature = "ecdsa")]
        if signing_key.public_key().is_sk_ecdsa_p256() {
            return Err(Algorithm::SkEcdsaSha2NistP256.unsupported_error());
        }

        let signed_data = Self::signed_data(namespace, hash_alg, msg)?;
        let signature = signing_key.try_sign(&signed_data)?;
        Self::new(signing_key.public_key(), namespace, hash_alg, signature)
    }

    /// Get the raw message over which the signature for a given message
    /// needs to be 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.
    ///
    /// Once a [`Signature`] has been computed over the returned byte vector,
    /// [`SshSig::new`] can be used to construct the final signature.
    pub fn signed_data(namespace: &str, hash_alg: HashAlg, msg: &[u8]) -> Result<Vec<u8>> {
        if namespace.is_empty() {
            return Err(Error::Namespace);
        }

        SignedData {
            namespace,
            reserved: &[],
            hash_alg,
            hash: hash_alg.digest(msg).as_slice(),
        }
        .to_bytes()
    }

    /// 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 {
    type Error = Error;

    fn decode(reader: &mut impl Reader) -> Result<Self> {
        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_prefixed(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_prefixed(Signature::decode)?;

        Ok(Self {
            version,
            public_key,
            namespace,
            reserved,
            hash_alg,
            signature,
        })
    }
}

impl Encode for SshSig {
    fn encoded_len(&self) -> encoding::Result<usize> {
        [
            Self::MAGIC_PREAMBLE.len(),
            self.version.encoded_len()?,
            self.public_key.encoded_len_prefixed()?,
            self.namespace.encoded_len()?,
            self.reserved.encoded_len()?,
            self.hash_alg.encoded_len()?,
            self.signature.encoded_len_prefixed()?,
        ]
        .checked_sum()
    }

    fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> {
        writer.write(Self::MAGIC_PREAMBLE)?;
        self.version.encode(writer)?;
        self.public_key.encode_prefixed(writer)?;
        self.namespace.encode(writer)?;
        self.reserved.encode(writer)?;
        self.hash_alg.encode(writer)?;
        self.signature.encode_prefixed(writer)?;
        Ok(())
    }
}

impl FromStr for SshSig {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        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 to_bytes(self) -> Result<Vec<u8>> {
        let mut signed_bytes = Vec::with_capacity(self.encoded_len()?);
        self.encode(&mut signed_bytes)?;
        Ok(signed_bytes)
    }
}

impl Encode for SignedData<'_> {
    fn encoded_len(&self) -> encoding::Result<usize> {
        [
            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) -> encoding::Result<()> {
        writer.write(SshSig::MAGIC_PREAMBLE)?;
        self.namespace.encode(writer)?;
        self.reserved.encode(writer)?;
        self.hash_alg.encode(writer)?;
        self.hash.encode(writer)?;
        Ok(())
    }
}