348 lines
10 KiB
Rust
348 lines
10 KiB
Rust
|
|
//! `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(())
|
||
|
|
}
|
||
|
|
}
|