Skip to content

Commit

Permalink
Merge pull request #326 from trail-of-forks/ap/wip-sct
Browse files Browse the repository at this point in the history
Signed Certificate Timestamp verification
  • Loading branch information
flavio committed Jun 20, 2024
2 parents 7e87c34 + 6249cc5 commit 1c2245e
Show file tree
Hide file tree
Showing 8 changed files with 638 additions and 7 deletions.
41 changes: 38 additions & 3 deletions src/bundle/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,19 @@ 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::{verify_sct, CertificateEmbeddedSCT};
use crate::errors::{Result as SigstoreResult, SigstoreError};
use crate::fulcio::oauth::OauthTokenProvider;
use crate::fulcio::{self, FulcioClient, FULCIO_ROOT};
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;

/// An asynchronous Sigstore signing session.
///
Expand Down Expand Up @@ -128,7 +134,12 @@ impl<'ctx> SigningSession<'ctx> {
return Err(SigstoreError::ExpiredSigningSession());
}

// TODO(tnytown): verify SCT here, sigstore-rs#326
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)?;
}

// Sign artifact.
let input_hash: &[u8] = &hasher.clone().finalize();
Expand Down Expand Up @@ -247,29 +258,53 @@ pub mod blocking {
pub struct SigningContext {
fulcio: FulcioClient,
rekor_config: RekorConfiguration,
ctfe_keyring: Keyring,
}

impl SigningContext {
/// Manually constructs a [`SigningContext`] from its constituent data.
pub fn new(fulcio: FulcioClient, rekor_config: RekorConfiguration) -> Self {
pub fn new(
fulcio: FulcioClient,
rekor_config: RekorConfiguration,
ctfe_keyring: Keyring,
) -> Self {
Self {
fulcio,
rekor_config,
ctfe_keyring,
}
}

/// Returns a [`SigningContext`] configured against the public-good production Sigstore
/// infrastructure.
pub fn production() -> SigstoreResult<Self> {
#[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()?)?,
))
}

/// Returns a [`SigningContext`] configured against the public-good production Sigstore
/// infrastructure.
///
/// Async callers should use [`SigningContext::async_production`].
#[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())
}

/// Configures and returns a [`SigningSession`] with the held context.
pub async fn signer(&self, identity_token: IdentityToken) -> SigstoreResult<SigningSession> {
SigningSession::new(self, identity_token).await
Expand Down
3 changes: 3 additions & 0 deletions src/bundle/verify/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ pub enum CertificateErrorKind {
#[error("certificate expired before time of signing")]
Expired,

#[error("certificate SCT verification failed")]
Sct(#[source] crate::crypto::transparency::SCTError),

#[error("certificate verification failed")]
VerificationFailed(#[source] webpki::Error),
}
Expand Down
17 changes: 14 additions & 3 deletions src/bundle/verify/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ use x509_cert::der::Encode;

use crate::{
bundle::Bundle,
crypto::{CertificatePool, CosignVerificationKey, Signature},
crypto::{
keyring::Keyring,
transparency::{verify_sct, CertificateEmbeddedSCT},
CertificatePool, CosignVerificationKey, Signature,
},
errors::Result as SigstoreResult,
rekor::apis::configuration::Configuration as RekorConfiguration,
trust::TrustRoot,
Expand All @@ -46,6 +50,7 @@ pub struct Verifier {
#[allow(dead_code)]
rekor_config: RekorConfiguration,
cert_pool: CertificatePool,
ctfe_keyring: Keyring,
}

impl Verifier {
Expand All @@ -57,10 +62,12 @@ impl Verifier {
trust_repo: R,
) -> SigstoreResult<Self> {
let cert_pool = CertificatePool::from_certificates(trust_repo.fulcio_certs()?, [])?;
let ctfe_keyring = Keyring::new(trust_repo.ctfe_keys()?)?;

Ok(Self {
rekor_config,
cert_pool,
ctfe_keyring,
})
}

Expand Down Expand Up @@ -110,14 +117,18 @@ impl Verifier {
.try_into()
.map_err(CertificateErrorKind::Malformed)?;

let _trusted_chain = self
let trusted_chain = self
.cert_pool
.verify_cert_with_time(&ee_cert, UnixTime::since_unix_epoch(issued_at))
.map_err(CertificateErrorKind::VerificationFailed)?;

debug!("signing certificate chains back to trusted root");

// TODO(tnytown): verify SCT here, sigstore-rs#326
let sct_context =
CertificateEmbeddedSCT::new_with_verified_path(&materials.certificate, &trusted_chain)
.map_err(CertificateErrorKind::Sct)?;
verify_sct(&sct_context, &self.ctfe_keyring).map_err(CertificateErrorKind::Sct)?;
debug!("signing certificate's SCT is valid");

// 2) Verify that the signing certificate belongs to the signer.
policy.verify(&materials.certificate)?;
Expand Down
165 changes: 165 additions & 0 deletions src/crypto/keyring.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright 2023 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::collections::HashMap;

use const_oid::db::rfc5912::{ID_EC_PUBLIC_KEY, RSA_ENCRYPTION, SECP_256_R_1};
use digest::Digest;
use ring::{signature as ring_signature, signature::UnparsedPublicKey};
use thiserror::Error;
use x509_cert::{
der,
der::{Decode, Encode},
spki::SubjectPublicKeyInfoOwned,
};

#[derive(Error, Debug)]
pub enum KeyringError {
#[error("malformed key")]
KeyMalformed(#[from] x509_cert::der::Error),
#[error("unsupported algorithm")]
AlgoUnsupported,

#[error("requested key not in keyring")]
KeyNotFound,
#[error("verification failed")]
VerificationFailed,
}
type Result<T> = std::result::Result<T, KeyringError>;

/// A CT signing key.
struct Key {
inner: UnparsedPublicKey<Vec<u8>>,
/// The key's RFC 6962-style "key ID".
/// <https://datatracker.ietf.org/doc/html/rfc6962#section-3.2>
fingerprint: [u8; 32],
}

impl Key {
/// Creates a `Key` from a DER blob containing a SubjectPublicKeyInfo object.
pub fn new(spki_bytes: &[u8]) -> Result<Self> {
let spki = SubjectPublicKeyInfoOwned::from_der(spki_bytes)?;
let (algo, params) = if let Some(params) = &spki.algorithm.parameters {
// Special-case RSA keys, which don't have SPKI parameters.
if spki.algorithm.oid == RSA_ENCRYPTION && params == &der::Any::null() {
// TODO(tnytown): Do we need to support RSA keys?
return Err(KeyringError::AlgoUnsupported);
};

(spki.algorithm.oid, params.decode_as()?)
} else {
return Err(KeyringError::AlgoUnsupported);
};

match (algo, params) {
// TODO(tnytown): should we also accept ed25519, p384, ... ?
(ID_EC_PUBLIC_KEY, SECP_256_R_1) => Ok(Key {
inner: UnparsedPublicKey::new(
&ring_signature::ECDSA_P256_SHA256_ASN1,
spki.subject_public_key.raw_bytes().to_owned(),
),
fingerprint: {
let mut hasher = sha2::Sha256::new();
spki.encode(&mut hasher).expect("failed to hash key!");
hasher.finalize().into()
},
}),
_ => Err(KeyringError::AlgoUnsupported),
}
}
}

/// Represents a set of CT signing keys, each of which is potentially a valid signer for
/// Signed Certificate Timestamps (SCTs) or Signed Tree Heads (STHs).
pub struct Keyring(HashMap<[u8; 32], Key>);

impl Keyring {
/// Creates a `Keyring` from DER encoded SPKI-format public keys.
pub fn new<'a>(keys: impl IntoIterator<Item = &'a [u8]>) -> Result<Self> {
Ok(Self(
keys.into_iter()
.flat_map(Key::new)
.map(|k| Ok((k.fingerprint, k)))
.collect::<Result<_>>()?,
))
}

/// Verifies `data` against a `signature` with a public key identified by `key_id`.
pub fn verify(&self, key_id: &[u8; 32], signature: &[u8], data: &[u8]) -> Result<()> {
let key = self.0.get(key_id).ok_or(KeyringError::KeyNotFound)?;

key.inner
.verify(data, signature)
.or(Err(KeyringError::VerificationFailed))?;

Ok(())
}
}

#[cfg(test)]
mod tests {
use super::Keyring;
use crate::crypto::signing_key::ecdsa::{ECDSAKeys, EllipticCurve};
use digest::Digest;
use std::io::Write;

#[test]
fn verify_keyring() {
let message = b"some message";

// Create a key pair and a keyring containing the public key.
let key_pair = ECDSAKeys::new(EllipticCurve::P256).unwrap();
let signer = key_pair.to_sigstore_signer().unwrap();
let pub_key = key_pair.as_inner().public_key_to_der().unwrap();
let keyring = Keyring::new([pub_key.as_slice()]).unwrap();

// Generate the signature.
let signature = signer.sign(message).unwrap();

// Generate the key id.
let mut hasher = sha2::Sha256::new();
hasher.write(pub_key.as_slice()).unwrap();
let key_id: [u8; 32] = hasher.finalize().into();

// Check for success.
assert!(keyring
.verify(&key_id, signature.as_slice(), message)
.is_ok());

// Check for failure with incorrect key id.
assert!(keyring
.verify(&[0; 32], signature.as_slice(), message)
.is_err());

// Check for failure with incorrect payload.
let incorrect_message = b"another message";

assert!(keyring
.verify(&key_id, signature.as_slice(), incorrect_message)
.is_err());

// Check for failure with incorrect keyring.
let incorrect_key_pair = ECDSAKeys::new(EllipticCurve::P256).unwrap();
let incorrect_keyring = Keyring::new([incorrect_key_pair
.as_inner()
.public_key_to_der()
.unwrap()
.as_slice()])
.unwrap();

assert!(incorrect_keyring
.verify(&key_id, signature.as_slice(), message)
.is_err());
}
}
5 changes: 5 additions & 0 deletions src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ pub(crate) mod certificate;
pub(crate) mod certificate_pool;
#[cfg(feature = "cert")]
pub(crate) use certificate_pool::CertificatePool;
#[cfg(feature = "cert")]
pub(crate) mod keyring;

pub mod verification_key;

Expand All @@ -188,6 +190,9 @@ use self::signing_key::{

pub mod signing_key;

#[cfg(any(feature = "sign", feature = "verify"))]
pub(crate) mod transparency;

#[cfg(test)]
pub(crate) mod tests {
use chrono::{DateTime, TimeDelta, Utc};
Expand Down
Loading

0 comments on commit 1c2245e

Please sign in to comment.