Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ssh-key/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 24 additions & 6 deletions ssh-key/src/algorithm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -389,8 +395,8 @@ impl HashAlg {
///
/// # Supported hash algorithms
///
/// - `SHA256`
/// - `SHA512`
/// - `sha256`
/// - `sha512`
pub fn new(id: &str) -> Result<Self> {
match id {
SHA256 => Ok(HashAlg::Sha256),
Expand All @@ -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<u8> {
match self {
HashAlg::Sha256 => Sha256::digest(msg).to_vec(),
HashAlg::Sha512 => Sha512::digest(msg).to_vec(),
}
}
}

impl AlgString for HashAlg {}

impl AsRef<str> for HashAlg {
fn as_ref(&self) -> &str {
self.as_str()
Expand Down Expand Up @@ -480,14 +498,14 @@ impl KdfAlg {
}
}

impl AlgString for KdfAlg {}

impl AsRef<str> for KdfAlg {
fn as_ref(&self) -> &str {
self.as_str()
}
}

impl AlgString for KdfAlg {}

impl Default for KdfAlg {
fn default() -> KdfAlg {
KdfAlg::Bcrypt
Expand Down
6 changes: 1 addition & 5 deletions ssh-key/src/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
4 changes: 2 additions & 2 deletions ssh-key/src/certificate/builder.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down
20 changes: 0 additions & 20 deletions ssh-key/src/certificate/signing_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Signature> {
/// Get the [`public::KeyData`] for this signing key.
fn public_key(&self) -> public::KeyData;
}

impl<T> SigningKey for T
where
T: Signer<Signature>,
public::KeyData: for<'a> From<&'a T>,
{
fn public_key(&self) -> public::KeyData {
self.into()
}
}
13 changes: 12 additions & 1 deletion ssh-key/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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"),
Expand All @@ -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),
}
}
}
Expand Down
19 changes: 16 additions & 3 deletions ssh-key/src/fingerprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,24 +119,37 @@ 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)
}
}

impl FromStr for Fingerprint {
type Err = Error;

fn from_str(id: &str) -> Result<Self> {
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()?)),
}
Expand Down
11 changes: 10 additions & 1 deletion ssh-key/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -180,11 +182,18 @@ 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")]
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;
34 changes: 26 additions & 8 deletions ssh-key/src/private.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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> {
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")))]
Expand Down
29 changes: 28 additions & 1 deletion ssh-key/src/public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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")))]
Expand Down
Loading