Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ssh-key: externally generated signatures for certificates #333

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
110 changes: 110 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ssh-key/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
39 changes: 31 additions & 8 deletions ssh-key/src/certificate/builder.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down Expand Up @@ -264,18 +268,15 @@ impl Builder {
Ok(self)
}

/// Sign the certificate using the provided signer type.
///
/// The [`PrivateKey`] type can be used as a signer.
pub fn sign<S: SigningKey>(self, signing_key: &S) -> Result<Certificate> {
fn placeholder_cert(self, signature_key: KeyData) -> Result<Certificate> {
// 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 {
Some(principals) => principals,
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(),
Expand All @@ -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<S: SigningKey>(self, signing_key: &S) -> Result<Certificate> {
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)?;
Expand All @@ -304,4 +311,20 @@ impl Builder {

Ok(cert)
}

/// Sign the certificate asynchronously using the provided signer type.
pub async fn sign_async<S: AsyncSigningKey>(self, signing_key: &S) -> Result<Certificate> {
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)
}
}
2 changes: 1 addition & 1 deletion ssh-key/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ pub use crate::{
certificate::Certificate,
known_hosts::KnownHosts,
mpint::Mpint,
signature::{Signature, SigningKey},
signature::{AsyncSigner, AsyncSigningKey, Signature, SigningKey},
sshsig::SshSig,
};

Expand Down
21 changes: 21 additions & 0 deletions ssh-key/src/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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<S> {
// 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<Output = signature::Result<S>>;
/// 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;
}
Comment on lines +67 to +79
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the upstream trait from async-signature (which we plan on merging into the signature crate soon)


/// Async pendant to the sync [`Signer`] trait
pub trait AsyncSigningKey: AsyncSigner<Signature> {
/// 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
Expand Down
19 changes: 18 additions & 1 deletion ssh-key/src/sshsig.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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<S: AsyncSigningKey>(
signing_key: &S,
namespace: &str,
hash_alg: HashAlg,
msg: &[u8],
) -> Result<Self> {
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.
///
Expand Down
Loading
Loading