use std::{
io::{self, Read},
time::SystemTime,
};
use base64::{Engine as _, engine::general_purpose::STANDARD as base64};
use ecdsa::signature::DigestSigner;
use hex;
use p256::NistP256;
use pkcs8::der::{Encode, EncodePem};
use sha2::{Digest, Sha256};
use sigstore_protobuf_specs::dev::sigstore::bundle::v1::bundle;
use sigstore_protobuf_specs::dev::sigstore::bundle::v1::{
Bundle, VerificationMaterial, verification_material,
};
use sigstore_protobuf_specs::dev::sigstore::common::v1::{
HashAlgorithm, HashOutput, MessageSignature, X509Certificate, X509CertificateChain,
};
use sigstore_protobuf_specs::dev::sigstore::rekor::v1::TransparencyLogEntry;
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};
use x509_cert::ext::pkix as x509_ext;
use crate::bundle::models::Version;
use crate::crypto::keyring::Keyring;
use crate::crypto::transparency::{CertificateEmbeddedSCT, verify_sct};
use crate::errors::{Result as SigstoreResult, SigstoreError};
use crate::fulcio::oauth::OauthTokenProvider;
use crate::fulcio::{self, FULCIO_ROOT, FulcioClient};
use crate::oauth::IdentityToken;
use crate::rekor::apis::configuration::Configuration as RekorConfiguration;
use crate::rekor::apis::entries_api::create_log_entry;
use crate::rekor::models::{hashedrekord, proposed_entry::ProposedEntry as ProposedLogEntry};
use crate::trust::TrustRoot;
#[cfg(feature = "sigstore-trust-root")]
use crate::trust::sigstore::SigstoreTrustRoot;
pub struct SigningSession<'ctx> {
context: &'ctx SigningContext,
identity_token: IdentityToken,
private_key: ecdsa::SigningKey<NistP256>,
certs: fulcio::CertificateResponse,
}
impl<'ctx> SigningSession<'ctx> {
async fn new(
context: &'ctx SigningContext,
identity_token: IdentityToken,
) -> SigstoreResult<SigningSession<'ctx>> {
let (private_key, certs) = Self::materials(&context.fulcio, &identity_token).await?;
Ok(Self {
context,
identity_token,
private_key,
certs,
})
}
async fn materials(
fulcio: &FulcioClient,
token: &IdentityToken,
) -> SigstoreResult<(ecdsa::SigningKey<NistP256>, fulcio::CertificateResponse)> {
let subject =
vec![
vec![
AttributeTypeAndValue {
oid: const_oid::db::rfc3280::EMAIL_ADDRESS,
value: AttributeValue::new(
pkcs8::der::Tag::Utf8String,
token.unverified_claims().email.as_ref(),
)?,
}
].try_into()?
].into();
let private_key =
ecdsa::SigningKey::from(p256::SecretKey::random(&mut pkcs8::rand_core::OsRng));
let mut builder = CertRequestBuilder::new(subject, &private_key)?;
builder.add_extension(&x509_ext::BasicConstraints {
ca: false,
path_len_constraint: None,
})?;
let cert_req = builder.build::<p256::ecdsa::DerSignature>()?;
Ok((private_key, fulcio.request_cert_v2(cert_req, token).await?))
}
pub fn is_expired(&self) -> bool {
let not_after = self
.certs
.cert
.tbs_certificate
.validity
.not_after
.to_system_time();
!self.identity_token.in_validity_period() || SystemTime::now() > not_after
}
async fn sign_digest(&self, hasher: Sha256) -> SigstoreResult<SigningArtifact> {
if self.is_expired() {
return Err(SigstoreError::ExpiredSigningSession());
}
if let Some(detached_sct) = &self.certs.detached_sct {
verify_sct(detached_sct, &self.context.ctfe_keyring)?;
} else {
let sct = CertificateEmbeddedSCT::new(&self.certs.cert, &self.certs.chain)?;
verify_sct(&sct, &self.context.ctfe_keyring)?;
}
let input_hash: &[u8] = &hasher.clone().finalize();
let artifact_signature: p256::ecdsa::Signature = self.private_key.sign_digest(hasher);
let signature_bytes = artifact_signature.to_der().as_bytes().to_owned();
let cert = &self.certs.cert;
let proposed_entry = ProposedLogEntry::Hashedrekord {
api_version: "0.0.1".to_owned(),
spec: hashedrekord::Spec {
signature: hashedrekord::Signature {
content: base64.encode(&signature_bytes),
public_key: hashedrekord::PublicKey::new(
base64.encode(cert.to_pem(pkcs8::LineEnding::LF)?),
),
},
data: hashedrekord::Data {
hash: hashedrekord::Hash {
algorithm: hashedrekord::AlgorithmKind::sha256,
value: hex::encode(input_hash),
},
},
},
};
let log_entry = create_log_entry(&self.context.rekor_config, proposed_entry)
.await
.map_err(|err| SigstoreError::RekorClientError(err.to_string()))?;
let log_entry = log_entry
.try_into()
.or(Err(SigstoreError::RekorClientError(
"Rekor returned malformed LogEntry".into(),
)))?;
Ok(SigningArtifact {
input_digest: input_hash.to_owned(),
cert: cert.to_der()?,
signature: signature_bytes,
log_entry,
})
}
pub async fn sign<R: AsyncRead + Unpin + Send + 'static>(
&self,
input: R,
) -> SigstoreResult<SigningArtifact> {
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
}
}
pub mod blocking {
use super::{SigningSession as AsyncSigningSession, *};
pub struct SigningSession<'ctx> {
inner: AsyncSigningSession<'ctx>,
rt: tokio::runtime::Runtime,
}
impl<'ctx> SigningSession<'ctx> {
pub(crate) fn new(ctx: &'ctx SigningContext, token: IdentityToken) -> SigstoreResult<Self> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let inner = rt.block_on(AsyncSigningSession::new(ctx, token))?;
Ok(Self { inner, rt })
}
pub fn is_expired(&self) -> bool {
self.inner.is_expired()
}
pub fn sign<R: Read>(&self, mut input: R) -> SigstoreResult<SigningArtifact> {
let mut hasher = Sha256::new();
io::copy(&mut input, &mut hasher)?;
self.rt.block_on(self.inner.sign_digest(hasher))
}
}
}
pub struct SigningContext {
fulcio: FulcioClient,
rekor_config: RekorConfiguration,
ctfe_keyring: Keyring,
}
impl SigningContext {
pub fn new(
fulcio: FulcioClient,
rekor_config: RekorConfiguration,
ctfe_keyring: Keyring,
) -> Self {
Self {
fulcio,
rekor_config,
ctfe_keyring,
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "sigstore-trust-root")))]
#[cfg(feature = "sigstore-trust-root")]
pub async fn async_production() -> SigstoreResult<Self> {
let trust_root = SigstoreTrustRoot::new(None).await?;
Ok(Self::new(
FulcioClient::new(
Url::parse(FULCIO_ROOT).expect("constant FULCIO root fails to parse!"),
crate::fulcio::TokenProvider::Oauth(OauthTokenProvider::default()),
),
Default::default(),
Keyring::new(trust_root.ctfe_keys()?.values().copied())?,
))
}
#[cfg_attr(docsrs, doc(cfg(feature = "sigstore-trust-root")))]
#[cfg(feature = "sigstore-trust-root")]
pub fn production() -> SigstoreResult<Self> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
rt.block_on(Self::async_production())
}
pub async fn signer(
&self,
identity_token: IdentityToken,
) -> SigstoreResult<SigningSession<'_>> {
SigningSession::new(self, identity_token).await
}
pub fn blocking_signer(
&self,
identity_token: IdentityToken,
) -> SigstoreResult<blocking::SigningSession<'_>> {
blocking::SigningSession::new(self, identity_token)
}
}
pub struct SigningArtifact {
input_digest: Vec<u8>,
cert: Vec<u8>,
signature: Vec<u8>,
log_entry: TransparencyLogEntry,
}
impl SigningArtifact {
pub fn to_bundle(self) -> Bundle {
let x509_certificate_chain = X509CertificateChain {
certificates: vec![X509Certificate {
raw_bytes: self.cert,
}],
};
let verification_material = Some(VerificationMaterial {
timestamp_verification_data: None,
tlog_entries: vec![self.log_entry],
content: Some(verification_material::Content::X509CertificateChain(
x509_certificate_chain,
)),
});
let message_signature = MessageSignature {
message_digest: Some(HashOutput {
algorithm: HashAlgorithm::Sha2256.into(),
digest: self.input_digest,
}),
signature: self.signature,
};
Bundle {
media_type: Version::Bundle0_2.to_string(),
verification_material,
content: Some(bundle::Content::MessageSignature(message_signature)),
}
}
}