Skip to content

Commit 5e8e0c1

Browse files
committed
feat(ssh-key): added AsyncSigner
1 parent 0b0d51a commit 5e8e0c1

File tree

7 files changed

+225
-12
lines changed

7 files changed

+225
-12
lines changed

Cargo.lock

Lines changed: 110 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ssh-key/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ sha1 = { version = "=0.11.0-pre.4", optional = true, default-features = false }
4343
[dev-dependencies]
4444
hex-literal = "0.4.1"
4545
rand_chacha = "0.3"
46+
tokio = { version = "1.43.0", features = ["macros", "test-util"] }
4647

4748
[features]
4849
default = ["ecdsa", "rand_core", "std"]

ssh-key/src/certificate/builder.rs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
//! OpenSSH certificate builder.
22
33
use super::{CertType, Certificate, Field, OptionsMap};
4-
use crate::{public, Result, Signature, SigningKey};
4+
use crate::{
5+
public::{self, KeyData},
6+
signature::AsyncSigningKey,
7+
Result, Signature, SigningKey,
8+
};
59
use alloc::{string::String, vec::Vec};
610

711
#[cfg(feature = "rand_core")]
@@ -264,18 +268,15 @@ impl Builder {
264268
Ok(self)
265269
}
266270

267-
/// Sign the certificate using the provided signer type.
268-
///
269-
/// The [`PrivateKey`] type can be used as a signer.
270-
pub fn sign<S: SigningKey>(self, signing_key: &S) -> Result<Certificate> {
271+
fn placeholder_cert(self, signature_key: KeyData) -> Result<Certificate> {
271272
// Empty valid principals result in a "golden ticket", so this check
272273
// ensures that was explicitly configured via `all_principals_valid`.
273274
let valid_principals = match self.valid_principals {
274275
Some(principals) => principals,
275276
None => return Err(Field::ValidPrincipals.invalid_error()),
276277
};
277278

278-
let mut cert = Certificate {
279+
Ok(Certificate {
279280
nonce: self.nonce,
280281
public_key: self.public_key,
281282
serial: self.serial.unwrap_or_default(),
@@ -288,10 +289,16 @@ impl Builder {
288289
extensions: self.extensions,
289290
reserved: Vec::new(),
290291
comment: self.comment.unwrap_or_default(),
291-
signature_key: signing_key.public_key(),
292+
signature_key,
292293
signature: Signature::placeholder(),
293-
};
294+
})
295+
}
294296

297+
/// Sign the certificate using the provided signer type.
298+
///
299+
/// The [`PrivateKey`] type can be used as a signer.
300+
pub fn sign<S: SigningKey>(self, signing_key: &S) -> Result<Certificate> {
301+
let mut cert = self.placeholder_cert(signing_key.public_key())?;
295302
let mut tbs_cert = Vec::new();
296303
cert.encode_tbs(&mut tbs_cert)?;
297304
cert.signature = signing_key.try_sign(&tbs_cert)?;
@@ -304,4 +311,20 @@ impl Builder {
304311

305312
Ok(cert)
306313
}
314+
315+
/// Sign the certificate asynchronously using the provided signer type.
316+
pub async fn sign_async<S: AsyncSigningKey>(self, signing_key: &S) -> Result<Certificate> {
317+
let mut cert = self.placeholder_cert(signing_key.public_key())?;
318+
let mut tbs_cert = Vec::new();
319+
cert.encode_tbs(&mut tbs_cert)?;
320+
cert.signature = signing_key.try_sign(&tbs_cert).await?;
321+
322+
#[cfg(debug_assertions)]
323+
cert.validate_at(
324+
cert.valid_after,
325+
&[cert.signature_key.fingerprint(Default::default())],
326+
)?;
327+
328+
Ok(cert)
329+
}
307330
}

ssh-key/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ pub use crate::{
184184
certificate::Certificate,
185185
known_hosts::KnownHosts,
186186
mpint::Mpint,
187-
signature::{Signature, SigningKey},
187+
signature::{AsyncSigner, AsyncSigningKey, Signature, SigningKey},
188188
sshsig::SshSig,
189189
};
190190

ssh-key/src/signature.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use crate::{private, public, Algorithm, EcdsaCurve, Error, Mpint, PrivateKey, PublicKey, Result};
44
use alloc::vec::Vec;
55
use core::fmt;
6+
use core::future::Future;
67
use encoding::{CheckedSum, Decode, Encode, Reader, Writer};
78
use signature::{SignatureEncoding, Signer, Verifier};
89

@@ -63,6 +64,26 @@ where
6364
}
6465
}
6566

67+
/// Sign the provided message bytestring using `Self` (e.g. a cryptographic key
68+
/// or connection to an HSM), returning a digital signature.
69+
pub trait AsyncSigner<S> {
70+
// Using an associated type here to force the implementor to be explicit with Send/Sync
71+
/// Future type which will be returned by `try_sign`
72+
type SignFuture: Future<Output = signature::Result<S>>;
73+
/// Attempt to sign the given message, returning a digital signature on
74+
/// success, or an error if something went wrong.
75+
///
76+
/// The main intended use case for signing errors is when communicating
77+
/// with external signers, e.g. cloud KMS, HSMs, or other hardware tokens.
78+
fn try_sign(&self, msg: &[u8]) -> Self::SignFuture;
79+
}
80+
81+
/// Async pendant to the sync [`Signer`] trait
82+
pub trait AsyncSigningKey: AsyncSigner<Signature> {
83+
/// Get the [`public::KeyData`] for this signing key.
84+
fn public_key(&self) -> public::KeyData;
85+
}
86+
6687
/// Low-level digital signature (e.g. DSA, ECDSA, Ed25519).
6788
///
6889
/// These are low-level signatures used as part of the OpenSSH certificate

ssh-key/src/sshsig.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! `sshsig` implementation.
22
3-
use crate::{public, Algorithm, Error, HashAlg, Result, Signature, SigningKey};
3+
use crate::{
4+
public, signature::AsyncSigningKey, Algorithm, Error, HashAlg, Result, Signature, SigningKey,
5+
};
46
use alloc::{string::String, string::ToString, vec::Vec};
57
use core::str::FromStr;
68
use encoding::{
@@ -125,6 +127,21 @@ impl SshSig {
125127
Self::new(signing_key.public_key(), namespace, hash_alg, signature)
126128
}
127129

130+
/// Sign the given message with the provided signing key.
131+
pub async fn sign_async<S: AsyncSigningKey>(
132+
signing_key: &S,
133+
namespace: &str,
134+
hash_alg: HashAlg,
135+
msg: &[u8],
136+
) -> Result<Self> {
137+
if namespace.is_empty() {
138+
return Err(Error::Namespace);
139+
}
140+
let signed_data = Self::signed_data(namespace, hash_alg, msg)?;
141+
let signature = signing_key.try_sign(&signed_data).await?;
142+
Self::new(signing_key.public_key(), namespace, hash_alg, signature)
143+
}
144+
128145
/// Get the raw message over which the signature for a given message
129146
/// needs to be computed.
130147
///

0 commit comments

Comments
 (0)