Skip to content

Commit 05d3f1e

Browse files
committed
[WIP] ssh-key: initial "sshsig" support
Initial support for the "sshsig" format as described in: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD
1 parent d2280a0 commit 05d3f1e

15 files changed

+582
-67
lines changed

ssh-key/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ zeroize_derive = "1.3" # hack to make minimal-versions lint happy (pulled in by
4848

4949
[features]
5050
default = ["ecdsa", "rand_core", "std"]
51-
alloc = ["base64ct/alloc", "signature", "zeroize/alloc"]
51+
alloc = ["base64ct/alloc", "signature/hazmat-preview", "zeroize/alloc"]
5252
std = [
5353
"alloc",
5454
"base64ct/std",

ssh-key/src/algorithm.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
use crate::{decode::Decode, encode::Encode, reader::Reader, writer::Writer, Error, Result};
44
use core::{fmt, str};
55

6+
#[cfg(feature = "alloc")]
7+
use {
8+
alloc::vec::Vec,
9+
sha2::{Digest, Sha256, Sha512},
10+
};
11+
612
/// bcrypt-pbkdf
713
const BCRYPT: &str = "bcrypt";
814

@@ -49,10 +55,10 @@ const RSA_SHA2_256: &str = "rsa-sha2-256";
4955
const RSA_SHA2_512: &str = "rsa-sha2-512";
5056

5157
/// SHA-256 hash function
52-
const SHA256: &str = "SHA256";
58+
const SHA256: &str = "sha256";
5359

5460
/// SHA-512 hash function
55-
const SHA512: &str = "SHA512";
61+
const SHA512: &str = "sha512";
5662

5763
/// Digital Signature Algorithm
5864
const SSH_DSA: &str = "ssh-dss";
@@ -389,8 +395,8 @@ impl HashAlg {
389395
///
390396
/// # Supported hash algorithms
391397
///
392-
/// - `SHA256`
393-
/// - `SHA512`
398+
/// - `sha256`
399+
/// - `sha512`
394400
pub fn new(id: &str) -> Result<Self> {
395401
match id {
396402
SHA256 => Ok(HashAlg::Sha256),
@@ -414,8 +420,20 @@ impl HashAlg {
414420
HashAlg::Sha512 => 64,
415421
}
416422
}
423+
424+
/// Compute a digest of the given message using this hash function.
425+
#[cfg(feature = "alloc")]
426+
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
427+
pub fn digest(self, msg: &[u8]) -> Vec<u8> {
428+
match self {
429+
HashAlg::Sha256 => Sha256::digest(msg).to_vec(),
430+
HashAlg::Sha512 => Sha512::digest(msg).to_vec(),
431+
}
432+
}
417433
}
418434

435+
impl AlgString for HashAlg {}
436+
419437
impl AsRef<str> for HashAlg {
420438
fn as_ref(&self) -> &str {
421439
self.as_str()
@@ -480,14 +498,14 @@ impl KdfAlg {
480498
}
481499
}
482500

501+
impl AlgString for KdfAlg {}
502+
483503
impl AsRef<str> for KdfAlg {
484504
fn as_ref(&self) -> &str {
485505
self.as_str()
486506
}
487507
}
488508

489-
impl AlgString for KdfAlg {}
490-
491509
impl Default for KdfAlg {
492510
fn default() -> KdfAlg {
493511
KdfAlg::Bcrypt

ssh-key/src/certificate.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,9 @@ mod builder;
44
mod cert_type;
55
mod field;
66
mod options_map;
7-
mod signing_key;
87
mod unix_time;
98

10-
pub use self::{
11-
builder::Builder, cert_type::CertType, field::Field, options_map::OptionsMap,
12-
signing_key::SigningKey,
13-
};
9+
pub use self::{builder::Builder, cert_type::CertType, field::Field, options_map::OptionsMap};
1410

1511
use self::unix_time::UnixTime;
1612
use crate::{

ssh-key/src/certificate/builder.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! OpenSSH certificate builder.
22
3-
use super::{unix_time::UnixTime, CertType, Certificate, Field, OptionsMap, SigningKey};
4-
use crate::{public, Result, Signature};
3+
use super::{unix_time::UnixTime, CertType, Certificate, Field, OptionsMap};
4+
use crate::{public, Result, Signature, SigningKey};
55
use alloc::{string::String, vec::Vec};
66

77
#[cfg(feature = "rand_core")]

ssh-key/src/certificate/signing_key.rs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,3 @@ use signature::Signer;
55

66
#[cfg(doc)]
77
use super::Builder;
8-
9-
/// Certificate signing key trait for the certificate [`Builder`].
10-
///
11-
/// This trait is automatically impl'd for any types which impl the
12-
/// [`Signer`] trait for the OpenSSH certificate [`Signature`] type and also
13-
/// support a [`From`] conversion for [`public::KeyData`].
14-
pub trait SigningKey: Signer<Signature> {
15-
/// Get the [`public::KeyData`] for this signing key.
16-
fn public_key(&self) -> public::KeyData;
17-
}
18-
19-
impl<T> SigningKey for T
20-
where
21-
T: Signer<Signature>,
22-
public::KeyData: for<'a> From<&'a T>,
23-
{
24-
fn public_key(&self) -> public::KeyData {
25-
self.into()
26-
}
27-
}

ssh-key/src/error.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,16 @@ pub enum Error {
5555
/// Invalid length.
5656
Length,
5757

58+
/// Namespace invalid.
59+
Namespace,
60+
5861
/// Overflow errors.
5962
Overflow,
6063

6164
/// PEM encoding errors.
6265
Pem(pem::Error),
6366

64-
/// Public key does not match private key.
67+
/// Public key is incorrect.
6568
PublicKey,
6669

6770
/// Invalid timestamp (e.g. in a certificate)
@@ -72,6 +75,12 @@ pub enum Error {
7275
/// Number of bytes of remaining data at end of message.
7376
remaining: usize,
7477
},
78+
79+
/// Unsupported version.
80+
Version {
81+
/// Version number.
82+
number: u32,
83+
},
7584
}
7685

7786
impl fmt::Display for Error {
@@ -94,6 +103,7 @@ impl fmt::Display for Error {
94103
#[cfg(feature = "std")]
95104
Error::Io(err) => write!(f, "I/O error: {}", std::io::Error::from(*err)),
96105
Error::Length => write!(f, "length invalid"),
106+
Error::Namespace => write!(f, "namespace invalid"),
97107
Error::Overflow => write!(f, "internal overflow error"),
98108
Error::Pem(err) => write!(f, "{}", err),
99109
Error::PublicKey => write!(f, "public key is incorrect"),
@@ -103,6 +113,7 @@ impl fmt::Display for Error {
103113
"unexpected trailing data at end of message ({} bytes)",
104114
remaining
105115
),
116+
Error::Version { number: version } => write!(f, "version unsupported: {}", version),
106117
}
107118
}
108119
}

ssh-key/src/fingerprint.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,24 +119,37 @@ impl AsRef<[u8]> for Fingerprint {
119119

120120
impl Display for Fingerprint {
121121
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122+
// Fingerprints use a special upper-case hash algorithm encoding.
123+
let algorithm = match self.algorithm() {
124+
HashAlg::Sha256 => "SHA256",
125+
HashAlg::Sha512 => "SHA512",
126+
};
127+
122128
// Buffer size is the largest digest size of of any supported hash function
123129
let mut buf = [0u8; Self::SHA512_BASE64_SIZE];
124130
let base64 = Base64Unpadded::encode(self.as_bytes(), &mut buf).map_err(|_| fmt::Error)?;
125-
write!(f, "{}:{}", self.algorithm(), base64)
131+
write!(f, "{}:{}", algorithm, base64)
126132
}
127133
}
128134

129135
impl FromStr for Fingerprint {
130136
type Err = Error;
131137

132138
fn from_str(id: &str) -> Result<Self> {
133-
let (algorithm, base64) = id.split_once(':').ok_or(Error::Algorithm)?;
139+
let (alg_str, base64) = id.split_once(':').ok_or(Error::Algorithm)?;
140+
141+
// Fingerprints use a special upper-case hash algorithm encoding.
142+
let algorithm = match alg_str {
143+
"SHA256" => HashAlg::Sha256,
144+
"SHA512" => HashAlg::Sha512,
145+
_ => return Err(Error::Algorithm),
146+
};
134147

135148
// Buffer size is the largest digest size of of any supported hash function
136149
let mut buf = [0u8; HashAlg::Sha512.digest_size()];
137150
let decoded_bytes = Base64Unpadded::decode(base64, &mut buf)?;
138151

139-
match algorithm.parse()? {
152+
match algorithm {
140153
HashAlg::Sha256 => Ok(Self::Sha256(decoded_bytes.try_into()?)),
141154
HashAlg::Sha512 => Ok(Self::Sha512(decoded_bytes.try_into()?)),
142155
}

ssh-key/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ mod writer;
163163
mod mpint;
164164
#[cfg(feature = "alloc")]
165165
mod signature;
166+
#[cfg(feature = "alloc")]
167+
mod sshsig;
166168

167169
pub use crate::{
168170
algorithm::{Algorithm, EcdsaCurve, HashAlg, KdfAlg},
@@ -180,11 +182,18 @@ pub use sha2;
180182

181183
#[cfg(feature = "alloc")]
182184
pub use crate::{
183-
certificate::Certificate, known_hosts::KnownHosts, mpint::MPInt, signature::Signature,
185+
certificate::Certificate,
186+
known_hosts::KnownHosts,
187+
mpint::MPInt,
188+
signature::{Signature, SigningKey},
189+
sshsig::SshSig,
184190
};
185191

186192
#[cfg(feature = "ecdsa")]
187193
pub use sec1;
188194

189195
#[cfg(feature = "rand_core")]
190196
pub use rand_core;
197+
198+
/// Line width used by the PEM encoding of OpenSSH documents.
199+
const PEM_LINE_WIDTH: usize = 70;

ssh-key/src/private.rs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,13 @@ pub use self::ed25519::{Ed25519Keypair, Ed25519PrivateKey};
119119
pub use self::keypair::KeypairData;
120120

121121
#[cfg(feature = "alloc")]
122-
pub use self::{
123-
dsa::{DsaKeypair, DsaPrivateKey},
124-
rsa::{RsaKeypair, RsaPrivateKey},
125-
sk::SkEd25519,
122+
pub use crate::{
123+
private::{
124+
dsa::{DsaKeypair, DsaPrivateKey},
125+
rsa::{RsaKeypair, RsaPrivateKey},
126+
sk::SkEd25519,
127+
},
128+
SshSig,
126129
};
127130

128131
#[cfg(feature = "ecdsa")]
@@ -139,7 +142,7 @@ use crate::{
139142
public,
140143
reader::Reader,
141144
writer::Writer,
142-
Algorithm, Cipher, Error, Fingerprint, HashAlg, Kdf, PublicKey, Result,
145+
Algorithm, Cipher, Error, Fingerprint, HashAlg, Kdf, PublicKey, Result, PEM_LINE_WIDTH,
143146
};
144147
use core::str;
145148

@@ -176,9 +179,6 @@ const MAX_BLOCK_SIZE: usize = 16;
176179
/// Padding bytes to use.
177180
const PADDING_BYTES: [u8; MAX_BLOCK_SIZE - 1] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
178181

179-
/// Line width used by the PEM encoding of OpenSSH private keys.
180-
const PEM_LINE_WIDTH: usize = 70;
181-
182182
/// Unix file permissions for SSH private keys.
183183
#[cfg(all(unix, feature = "std"))]
184184
const UNIX_FILE_PERMISSIONS: u32 = 0o600;
@@ -283,6 +283,24 @@ impl PrivateKey {
283283
Ok(Zeroizing::new(private_key_bytes))
284284
}
285285

286+
/// Sign the given message using this private key, returning an [`SshSig`].
287+
///
288+
/// These signatures can be produced using `ssh-keygen -Y sign`. They're
289+
/// encoded as PEM and begin with the following:
290+
///
291+
/// ```text
292+
/// -----BEGIN SSH SIGNATURE-----
293+
/// ```
294+
///
295+
/// See [PROTOCOL.sshsig] for more information.
296+
///
297+
/// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD
298+
#[cfg(feature = "alloc")]
299+
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
300+
pub fn sign(&self, namespace: &str, hash_alg: HashAlg, msg: &[u8]) -> Result<SshSig> {
301+
SshSig::sign(self, namespace, hash_alg, msg)
302+
}
303+
286304
/// Read private key from an OpenSSH-formatted PEM file.
287305
#[cfg(feature = "std")]
288306
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]

ssh-key/src/public.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use core::str::FromStr;
3333

3434
#[cfg(feature = "alloc")]
3535
use {
36-
crate::{checked::CheckedSum, writer::base64_len},
36+
crate::{checked::CheckedSum, writer::base64_len, SshSig},
3737
alloc::{
3838
borrow::ToOwned,
3939
string::{String, ToString},
@@ -166,6 +166,33 @@ impl PublicKey {
166166
Ok(public_key_bytes)
167167
}
168168

169+
/// Verify the [`SshSig`] signature over the given message using this
170+
/// public key.
171+
///
172+
/// These signatures can be produced using `ssh-keygen -Y sign`. They're
173+
/// encoded as PEM and begin with the following:
174+
///
175+
/// ```text
176+
/// -----BEGIN SSH SIGNATURE-----
177+
/// ```
178+
///
179+
/// See [PROTOCOL.sshsig] for more information.
180+
///
181+
/// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD
182+
#[cfg(feature = "alloc")]
183+
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
184+
pub fn verify(&self, namespace: &str, msg: &[u8], signature: &SshSig) -> Result<()> {
185+
if self.key_data() != signature.public_key() {
186+
return Err(Error::PublicKey);
187+
}
188+
189+
if namespace != signature.namespace() {
190+
return Err(Error::Namespace);
191+
}
192+
193+
signature.verify(msg)
194+
}
195+
169196
/// Read public key from an OpenSSH-formatted file.
170197
#[cfg(feature = "std")]
171198
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]

0 commit comments

Comments
 (0)