diff --git a/Cargo.lock b/Cargo.lock index 375f7079..a34092d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aead" version = "0.6.0-rc.0" @@ -44,6 +59,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -116,6 +146,15 @@ dependencies = [ "cipher", ] +[[package]] +name = "cc" +version = "1.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -354,6 +393,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + [[package]] name = "group" version = "0.13.0" @@ -430,6 +475,21 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -477,6 +537,15 @@ dependencies = [ "libm", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "opaque-debug" version = "0.3.1" @@ -539,6 +608,12 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "pkcs1" version = "0.8.0-rc.1" @@ -683,6 +758,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc_version" version = "0.4.0" @@ -754,6 +835,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signature" version = "2.3.0-pre.4" @@ -849,6 +936,7 @@ dependencies = [ "ssh-cipher", "ssh-encoding", "subtle", + "tokio", "zeroize", ] @@ -878,6 +966,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typenum" version = "1.17.0" diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index 389e2662..38ab1f8b 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -43,6 +43,7 @@ sha1 = { version = "=0.11.0-pre.4", optional = true, default-features = false } [dev-dependencies] hex-literal = "0.4.1" rand_chacha = "0.3" +tokio = { version = "1.43.0", features = ["macros", "test-util"] } [features] default = ["ecdsa", "rand_core", "std"] diff --git a/ssh-key/src/certificate/builder.rs b/ssh-key/src/certificate/builder.rs index e67fba98..f7ac520d 100644 --- a/ssh-key/src/certificate/builder.rs +++ b/ssh-key/src/certificate/builder.rs @@ -1,7 +1,11 @@ //! OpenSSH certificate builder. use super::{CertType, Certificate, Field, OptionsMap}; -use crate::{public, Result, Signature, SigningKey}; +use crate::{ + public::{self, KeyData}, + signature::AsyncSigningKey, + Result, Signature, SigningKey, +}; use alloc::{string::String, vec::Vec}; #[cfg(feature = "rand_core")] @@ -264,10 +268,7 @@ impl Builder { Ok(self) } - /// Sign the certificate using the provided signer type. - /// - /// The [`PrivateKey`] type can be used as a signer. - pub fn sign(self, signing_key: &S) -> Result { + fn placeholder_cert(self, signature_key: KeyData) -> Result { // Empty valid principals result in a "golden ticket", so this check // ensures that was explicitly configured via `all_principals_valid`. let valid_principals = match self.valid_principals { @@ -275,7 +276,7 @@ impl Builder { None => return Err(Field::ValidPrincipals.invalid_error()), }; - let mut cert = Certificate { + Ok(Certificate { nonce: self.nonce, public_key: self.public_key, serial: self.serial.unwrap_or_default(), @@ -288,10 +289,16 @@ impl Builder { extensions: self.extensions, reserved: Vec::new(), comment: self.comment.unwrap_or_default(), - signature_key: signing_key.public_key(), + signature_key, signature: Signature::placeholder(), - }; + }) + } + /// Sign the certificate using the provided signer type. + /// + /// The [`PrivateKey`] type can be used as a signer. + pub fn sign(self, signing_key: &S) -> Result { + let mut cert = self.placeholder_cert(signing_key.public_key())?; let mut tbs_cert = Vec::new(); cert.encode_tbs(&mut tbs_cert)?; cert.signature = signing_key.try_sign(&tbs_cert)?; @@ -304,4 +311,20 @@ impl Builder { Ok(cert) } + + /// Sign the certificate asynchronously using the provided signer type. + pub async fn sign_async(self, signing_key: &S) -> Result { + let mut cert = self.placeholder_cert(signing_key.public_key())?; + let mut tbs_cert = Vec::new(); + cert.encode_tbs(&mut tbs_cert)?; + cert.signature = signing_key.try_sign(&tbs_cert).await?; + + #[cfg(debug_assertions)] + cert.validate_at( + cert.valid_after, + &[cert.signature_key.fingerprint(Default::default())], + )?; + + Ok(cert) + } } diff --git a/ssh-key/src/lib.rs b/ssh-key/src/lib.rs index 56acda8e..d19354df 100644 --- a/ssh-key/src/lib.rs +++ b/ssh-key/src/lib.rs @@ -184,7 +184,7 @@ pub use crate::{ certificate::Certificate, known_hosts::KnownHosts, mpint::Mpint, - signature::{Signature, SigningKey}, + signature::{AsyncSigner, AsyncSigningKey, Signature, SigningKey}, sshsig::SshSig, }; diff --git a/ssh-key/src/signature.rs b/ssh-key/src/signature.rs index e682ab8e..5ffea7dd 100644 --- a/ssh-key/src/signature.rs +++ b/ssh-key/src/signature.rs @@ -3,6 +3,7 @@ use crate::{private, public, Algorithm, EcdsaCurve, Error, Mpint, PrivateKey, PublicKey, Result}; use alloc::vec::Vec; use core::fmt; +use core::future::Future; use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; use signature::{SignatureEncoding, Signer, Verifier}; @@ -63,6 +64,26 @@ where } } +/// Sign the provided message bytestring using `Self` (e.g. a cryptographic key +/// or connection to an HSM), returning a digital signature. +pub trait AsyncSigner { + // Using an associated type here to force the implementor to be explicit with Send/Sync + /// Future type which will be returned by `try_sign` + type SignFuture: Future>; + /// Attempt to sign the given message, returning a digital signature on + /// success, or an error if something went wrong. + /// + /// The main intended use case for signing errors is when communicating + /// with external signers, e.g. cloud KMS, HSMs, or other hardware tokens. + fn try_sign(&self, msg: &[u8]) -> Self::SignFuture; +} + +/// Async pendant to the sync [`Signer`] trait +pub trait AsyncSigningKey: AsyncSigner { + /// Get the [`public::KeyData`] for this signing key. + fn public_key(&self) -> public::KeyData; +} + /// Low-level digital signature (e.g. DSA, ECDSA, Ed25519). /// /// These are low-level signatures used as part of the OpenSSH certificate diff --git a/ssh-key/src/sshsig.rs b/ssh-key/src/sshsig.rs index 1d55c7fe..7175ea9f 100644 --- a/ssh-key/src/sshsig.rs +++ b/ssh-key/src/sshsig.rs @@ -1,6 +1,8 @@ //! `sshsig` implementation. -use crate::{public, Algorithm, Error, HashAlg, Result, Signature, SigningKey}; +use crate::{ + public, signature::AsyncSigningKey, Algorithm, Error, HashAlg, Result, Signature, SigningKey, +}; use alloc::{string::String, string::ToString, vec::Vec}; use core::str::FromStr; use encoding::{ @@ -125,6 +127,21 @@ impl SshSig { Self::new(signing_key.public_key(), namespace, hash_alg, signature) } + /// Sign the given message with the provided signing key. + pub async fn sign_async( + signing_key: &S, + namespace: &str, + hash_alg: HashAlg, + msg: &[u8], + ) -> Result { + if namespace.is_empty() { + return Err(Error::Namespace); + } + let signed_data = Self::signed_data(namespace, hash_alg, msg)?; + let signature = signing_key.try_sign(&signed_data).await?; + 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. /// diff --git a/ssh-key/tests/sshsig.rs b/ssh-key/tests/sshsig.rs index 09b64ed0..b23ce8dd 100644 --- a/ssh-key/tests/sshsig.rs +++ b/ssh-key/tests/sshsig.rs @@ -3,7 +3,11 @@ #![cfg(feature = "alloc")] use hex_literal::hex; -use ssh_key::{Algorithm, HashAlg, LineEnding, PublicKey, SshSig}; +use signature::Signer; +use ssh_key::{ + Algorithm, AsyncSigner, AsyncSigningKey, HashAlg, LineEnding, PublicKey, Signature, SigningKey, + SshSig, +}; #[cfg(any( feature = "dsa", @@ -11,7 +15,7 @@ use ssh_key::{Algorithm, HashAlg, LineEnding, PublicKey, SshSig}; feature = "p256", feature = "rsa" ))] -use {encoding::Decode, signature::Verifier, ssh_key::PrivateKey, ssh_key::Signature}; +use {encoding::Decode, signature::Verifier, ssh_key::PrivateKey}; #[cfg(feature = "ed25519")] use ssh_key::Error; @@ -339,3 +343,40 @@ fn verify_sk_ed25519() { Err(Error::Crypto) ); } + +#[tokio::test] +#[cfg(feature = "p256")] +async fn sign_async() { + struct AsyncSignerEnvelope>(S); + impl + SigningKey> AsyncSigningKey for AsyncSignerEnvelope { + fn public_key(&self) -> ssh_key::public::KeyData { + self.0.public_key() + } + } + impl> AsyncSigner for AsyncSignerEnvelope { + type SignFuture = std::future::Ready>; + + fn try_sign(&self, msg: &[u8]) -> Self::SignFuture { + std::future::ready(self.0.try_sign(msg)) + } + } + + let signing_key = PrivateKey::from_openssh(ECDSA_P256_PRIVATE_KEY).unwrap(); + let verifying_key = ECDSA_P256_PUBLIC_KEY.parse::().unwrap(); + + let async_signer = AsyncSignerEnvelope(signing_key); + + let signature = SshSig::sign_async( + &async_signer, + NAMESPACE_EXAMPLE, + HashAlg::Sha512, + MSG_EXAMPLE, + ) + .await + .unwrap(); + + assert_eq!( + verifying_key.verify(NAMESPACE_EXAMPLE, MSG_EXAMPLE, &signature), + Ok(()) + ); +}