From 029248897ed8d51012e845dda877232ee6d91937 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Tue, 28 Nov 2023 18:54:12 -0600 Subject: [PATCH] sign: construct `AsyncSigningSession` Signed-off-by: Andrew Pan --- Cargo.toml | 1 + src/fulcio/mod.rs | 14 +++--- src/sign.rs | 122 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 103 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9c1104a4d1..aa6f9226ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,6 +115,7 @@ signature = { version = "2.0" } sigstore_protobuf_specs = "0.1.0-rc.2" thiserror = "1.0.30" tokio = { version = "1.17.0", features = ["rt"] } +tokio-util = { version = "0.7.10", features = ["io-util"] } tough = { version = "0.14", features = ["http"], optional = true } tracing = "0.1.31" url = "2.2.2" diff --git a/src/fulcio/mod.rs b/src/fulcio/mod.rs index 512488b737..81477cab6e 100644 --- a/src/fulcio/mod.rs +++ b/src/fulcio/mod.rs @@ -204,18 +204,18 @@ impl FulcioClient { /// Request a certificate from Fulcio with the V2 endpoint. /// - /// TODO(tnytown): This (and other API clients) probably be autogenerated. See sigstore-rs#209. + /// TODO(tnytown): This (and other API clients) should be autogenerated. See sigstore-rs#209. /// /// https://github.com/sigstore/fulcio/blob/main/fulcio.proto /// /// Additionally, it might not be reasonable to expect callers to correctly construct and pass /// in an X509 CSR. - pub fn request_cert_v2( + pub async fn request_cert_v2( &self, request: x509_cert::request::CertReq, identity: &IdentityToken, ) -> Result { - let client = reqwest::blocking::Client::new(); + let client = reqwest::Client::new(); macro_rules! headers { ($($key:expr => $val:expr),+) => { @@ -238,8 +238,10 @@ impl FulcioClient { .json(&CreateSigningCertificateRequest { certificate_signing_request: request, }) - .send()? - .json()?; + .send() + .await? + .json() + .await?; let sct_embedded = matches!( response, @@ -252,7 +254,7 @@ impl FulcioClient { if certs.len() < 2 { return Err(SigstoreError::FulcioClientError( - "Certificate chain too short: certs.len() < 2", + "Certificate chain too short: certs.len() < 2".into(), )); } diff --git a/src/sign.rs b/src/sign.rs index 58f413e9a8..1d4b36838e 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Types for signing artifacts and producing Sigstore Bundles. + use std::io::{self, Read}; use std::time::SystemTime; @@ -30,6 +32,8 @@ use sigstore_protobuf_specs::{ DevSigstoreRekorV1InclusionProof, DevSigstoreRekorV1KindVersion, DevSigstoreRekorV1TransparencyLogEntry, }; +use tokio::io::AsyncRead; +use tokio_util::io::SyncIoBridge; use url::Url; use x509_cert::attr::{AttributeTypeAndValue, AttributeValue}; use x509_cert::builder::{Builder, RequestBuilder as CertRequestBuilder}; @@ -45,20 +49,26 @@ use crate::rekor::apis::entries_api::create_log_entry; use crate::rekor::models::LogEntry; use crate::rekor::models::{hashedrekord, proposed_entry::ProposedEntry as ProposedLogEntry}; -/// A Sigstore signing session. +/// An asynchronous Sigstore signing session. /// /// Sessions hold a provided user identity and key materials tied to that identity. A single -/// session may be used to sign multiple items. For more information, see [`Self::sign()`]. -pub struct SigningSession<'ctx> { +/// session may be used to sign multiple items. For more information, see [`AsyncSigningSession::sign`](Self::sign). +/// +/// This signing session operates asynchronously. To construct a synchronous [SigningSession], +/// use [`SigningContext::signer()`]. +pub struct AsyncSigningSession<'ctx> { context: &'ctx SigningContext, identity_token: IdentityToken, private_key: ecdsa::SigningKey, certs: fulcio::CertificateResponse, } -impl<'ctx> SigningSession<'ctx> { - fn new(context: &'ctx SigningContext, identity_token: IdentityToken) -> SigstoreResult { - let (private_key, certs) = Self::materials(&context.fulcio, &identity_token)?; +impl<'ctx> AsyncSigningSession<'ctx> { + async fn new( + context: &'ctx SigningContext, + identity_token: IdentityToken, + ) -> SigstoreResult> { + let (private_key, certs) = Self::materials(&context.fulcio, &identity_token).await?; Ok(Self { context, identity_token, @@ -67,7 +77,7 @@ impl<'ctx> SigningSession<'ctx> { }) } - fn materials( + async fn materials( fulcio: &FulcioClient, token: &IdentityToken, ) -> SigstoreResult<(ecdsa::SigningKey, fulcio::CertificateResponse)> { @@ -76,7 +86,7 @@ impl<'ctx> SigningSession<'ctx> { vec![ // SET OF AttributeTypeAndValue vec![ - // AttributeTypeAndValue, `emailAddress=...`` + // AttributeTypeAndValue, `emailAddress=...` AttributeTypeAndValue { oid: const_oid::db::rfc3280::EMAIL_ADDRESS, value: AttributeValue::new( @@ -96,7 +106,7 @@ impl<'ctx> SigningSession<'ctx> { })?; let cert_req = builder.build::()?; - Ok((private_key, fulcio.request_cert_v2(cert_req, token)?)) + Ok((private_key, fulcio.request_cert_v2(cert_req, token).await?)) } /// Check if the session's identity token or key material is expired. @@ -115,20 +125,11 @@ impl<'ctx> SigningSession<'ctx> { !self.identity_token.in_validity_period() || SystemTime::now() > not_after } - /// Signs for the input with the session's identity. If the identity is expired, - /// [SigstoreError::ExpiredSigningSession] is returned. - /// - /// TODO(tnytown): Make this async safe. We may need to make the underlying trait functions - /// implementations async and wrap them with executors for the sync variants. Our async - /// variants would also need to use async variants of common traits (AsyncRead? AsyncHasher?) - pub fn sign(&self, input: &mut R) -> SigstoreResult { + async fn sign_digest(&self, hasher: Sha256) -> SigstoreResult { if self.is_expired() { return Err(SigstoreError::ExpiredSigningSession()); } - let mut hasher = Sha256::new(); - io::copy(input, &mut hasher)?; - // TODO(tnytown): Verify SCT here. // Sign artifact. @@ -158,12 +159,8 @@ impl<'ctx> SigningSession<'ctx> { }, }; - // HACK(tnytown): We aren't async yet. - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - let entry = rt - .block_on(create_log_entry(&self.context.rekor_config, proposed_entry)) + let entry = create_log_entry(&self.context.rekor_config, proposed_entry) + .await .map_err(|err| SigstoreError::RekorClientError(err.to_string()))?; // TODO(tnytown): Maybe run through the verification flow here? See sigstore-rs#296. @@ -175,13 +172,72 @@ impl<'ctx> SigningSession<'ctx> { log_entry: entry, }) } + + /// Signs for the input with the session's identity. If the identity is expired, + /// [SigstoreError::ExpiredSigningSession] is returned. + pub async fn sign( + &self, + input: R, + ) -> SigstoreResult { + if self.is_expired() { + return Err(SigstoreError::ExpiredSigningSession()); + } + + let mut sync_input = SyncIoBridge::new(input); + let hasher = tokio::task::spawn_blocking(move || -> SigstoreResult<_> { + let mut hasher = Sha256::new(); + io::copy(&mut sync_input, &mut hasher)?; + Ok(hasher) + }) + .await??; + + self.sign_digest(hasher).await + } +} + +/// A synchronous Sigstore signing session. +/// +/// Sessions hold a provided user identity and key materials tied to that identity. A single +/// session may be used to sign multiple items. For more information, see [`SigningSession::sign`](Self::sign). +/// +/// This signing session operates synchronously, thus it cannot be used in an asynchronous context. +/// To construct an asynchronous [SigningSession], use [`SigningContext::async_signer()`]. +pub struct SigningSession<'ctx> { + inner: AsyncSigningSession<'ctx>, + rt: tokio::runtime::Runtime, +} + +impl<'ctx> SigningSession<'ctx> { + fn new(ctx: &'ctx SigningContext, token: IdentityToken) -> SigstoreResult { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let inner = rt.block_on(AsyncSigningSession::new(ctx, token))?; + Ok(Self { inner, rt }) + } + + /// Check if the session's identity token or key material is expired. + /// + /// If the session is expired, it cannot be used for signing operations, and a new session + /// must be created with a fresh identity token. + pub fn is_expired(&self) -> bool { + self.inner.is_expired() + } + + /// Signs for the input with the session's identity. If the identity is expired, + /// [SigstoreError::ExpiredSigningSession] is returned. + pub fn sign(&self, mut input: R) -> SigstoreResult { + let mut hasher = Sha256::new(); + io::copy(&mut input, &mut hasher)?; + self.rt.block_on(self.inner.sign_digest(hasher)) + } } /// A Sigstore signing context. /// /// Contexts hold Fulcio (CA) and Rekor (CT) configurations which signing sessions can be -/// constructed against. Use [`Self::production()`] to create a context against the public-good -/// Sigstore infrastructure. +/// constructed against. Use [`SigningContext::production`](Self::production) to create a context against +/// the public-good Sigstore infrastructure. pub struct SigningContext { fulcio: FulcioClient, rekor_config: RekorConfiguration, @@ -208,7 +264,17 @@ impl SigningContext { ) } - /// Configures and returns a [SigningSession] with the held context. + /// Configures and returns an [AsyncSigningSession] with the held context. + pub async fn async_signer( + &self, + identity_token: IdentityToken, + ) -> SigstoreResult { + AsyncSigningSession::new(self, identity_token).await + } + + /// Configures and returns a [SigningContext] with the held context. + /// + /// Async contexts must use [`SigningContext::async_signer`](Self::async_signer). pub fn signer(&self, identity_token: IdentityToken) -> SigstoreResult { SigningSession::new(self, identity_token) }