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

Signed Certificate Timestamp verification #326

Merged
merged 2 commits into from
Jun 20, 2024
Merged
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
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")]
tnytown marked this conversation as resolved.
Show resolved Hide resolved
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")]
tnytown marked this conversation as resolved.
Show resolved Hide resolved
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>;
tnytown marked this conversation as resolved.
Show resolved Hide resolved

/// 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, ... ?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I only saw p256 SCTs while testing this changeset against the public-good prod and staging, do we need to support other key types?

(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!");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This expect should be safe: the only error that encode should return in this context is IO-related (and hasher shouldn't ever refuse a write.)

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
Loading