From 773490df32721b6c63d5a0cf7005047179c1c586 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Mon, 4 Dec 2023 09:57:08 -0600 Subject: [PATCH] sign: Signed Certificate Timestamp validation Signed-off-by: Andrew Pan Co-authored-by: Alex Cameron --- src/crypto/keyring.rs | 165 +++++++++++++++++ src/crypto/mod.rs | 5 + src/crypto/transparency.rs | 359 +++++++++++++++++++++++++++++++++++++ src/errors.rs | 8 + src/fulcio/mod.rs | 2 +- src/fulcio/models.rs | 2 + src/sign.rs | 20 ++- src/tuf/mod.rs | 1 - 8 files changed, 558 insertions(+), 4 deletions(-) create mode 100644 src/crypto/keyring.rs create mode 100644 src/crypto/transparency.rs diff --git a/src/crypto/keyring.rs b/src/crypto/keyring.rs new file mode 100644 index 0000000000..d24e36496e --- /dev/null +++ b/src/crypto/keyring.rs @@ -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 = std::result::Result; + +/// A CT signing key. +struct Key { + inner: UnparsedPublicKey>, + /// The key's RFC 6962-style "key ID". + /// + fingerprint: [u8; 32], +} + +impl Key { + /// Creates a `Key` from a DER blob containing a SubjectPublicKeyInfo object. + pub fn new(spki_bytes: &[u8]) -> Result { + 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) -> Result { + Ok(Self( + keys.into_iter() + .flat_map(Key::new) + .map(|k| Ok((k.fingerprint, k))) + .collect::>()?, + )) + } + + /// 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()); + } +} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index adfa82ba5e..3d728e0674 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -179,6 +179,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; @@ -190,6 +192,9 @@ use self::signing_key::{ pub mod signing_key; +#[cfg(feature = "sign")] +pub(crate) mod transparency; + #[cfg(test)] pub(crate) mod tests { use chrono::{DateTime, Duration, Utc}; diff --git a/src/crypto/transparency.rs b/src/crypto/transparency.rs new file mode 100644 index 0000000000..8edfc45e33 --- /dev/null +++ b/src/crypto/transparency.rs @@ -0,0 +1,359 @@ +// 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. + +//! Types for Certificate Transparency validation. + +use const_oid::ObjectIdentifier; +use digest::Digest; +use tls_codec::{SerializeBytes, TlsByteVecU16, TlsByteVecU24, TlsSerializeBytes, TlsSize}; +use x509_cert::{ + der, + der::Encode, + ext::pkix::{ + sct::Version, BasicConstraints, ExtendedKeyUsage, SignedCertificateTimestamp, + SignedCertificateTimestampList, + }, + Certificate, +}; + +use super::keyring::{Keyring, KeyringError}; +use crate::fulcio::SigningCertificateDetachedSCT; + +// TODO(tnytown): Migrate to const-oid's CT_PRECERT_SCTS when a new release is cut. +const CT_PRECERT_SCTS: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.11129.2.4.2"); +const PRECERTIFICATE_SIGNING_CERTIFICATE: ObjectIdentifier = + ObjectIdentifier::new_unwrap("1.3.6.1.4.1.11129.2.4.4"); + +fn cert_is_preissuer(cert: &Certificate) -> bool { + let eku: ExtendedKeyUsage = match cert.tbs_certificate.get() { + Ok(Some((_, ext))) => ext, + _ => return false, + }; + + eku.0.contains(&PRECERTIFICATE_SIGNING_CERTIFICATE) +} + +// +fn find_issuer_cert(chain: &[Certificate]) -> Option<&Certificate> { + let cert = if cert_is_preissuer(&chain[0]) { + &chain[1] + } else { + &chain[0] + }; + + let basic_constraints: BasicConstraints = match cert.tbs_certificate.get() { + Ok(Some((_, ext))) => ext, + _ => return None, + }; + + // TODO(tnytown): do we need to sanity-check the algo of the certificate here? + + if basic_constraints.ca { + Some(cert) + } else { + None + } +} + +#[derive(Debug, thiserror::Error)] +pub enum SCTError { + #[error("invalid or missing SignedCertificateTimestampList extension")] + SCTListMalformed, + + #[error("cannot decode SCT")] + SCTMalformed, + + #[error("failed to verify SCT: {0}")] + VerificationFailed(&'static str), + + #[error(transparent)] + Other(#[from] der::Error), +} + +impl From for SCTError { + fn from(_value: KeyringError) -> Self { + Self::VerificationFailed("invalid signature") + } +} + +impl From for SCTError { + fn from(_value: x509_cert::ext::pkix::Error) -> Self { + SCTError::SCTMalformed + } +} + +#[derive(PartialEq, Debug, TlsSerializeBytes, TlsSize)] +#[repr(u8)] +enum SignatureType { + CertificateTimestamp = 0, + TreeHash = 1, +} + +#[derive(PartialEq, Debug)] +#[repr(u16)] +enum LogEntryType { + X509Entry = 0, + PrecertEntry = 1, +} + +#[derive(PartialEq, Debug, TlsSerializeBytes, TlsSize)] +struct PreCert { + // opaque issuer_key_hash[32]; + issuer_key_hash: [u8; 32], + // opaque TBSCertificate<1..2^24-1>; + tbs_certificate: TlsByteVecU24, +} + +#[derive(PartialEq, Debug, TlsSerializeBytes, TlsSize)] +#[repr(u16)] +enum SignedEntry { + // opaque ASN.1Cert<1..2^24-1>; + #[tls_codec(discriminant = "LogEntryType::X509Entry")] + X509Entry(TlsByteVecU24), + #[tls_codec(discriminant = "LogEntryType::PrecertEntry")] + PrecertEntry(PreCert), +} + +#[derive(PartialEq, Debug, TlsSerializeBytes, TlsSize)] +pub struct DigitallySigned { + version: Version, + signature_type: SignatureType, + timestamp: u64, + signed_entry: SignedEntry, + // opaque CtExtensions<0..2^16-1>; + extensions: TlsByteVecU16, + + // XX(tnytown): pass in some useful context. These fields will not be encoded into the + // TLS DigitallySigned blob, but we need them to properly verify the reconstructed + // message. + #[tls_codec(skip)] + log_id: [u8; 32], + #[tls_codec(skip)] + signature: Vec, +} + +#[derive(Debug)] +pub struct CertificateEmbeddedSCT { + cert: Certificate, + sct: SignedCertificateTimestamp, + issuer_id: [u8; 32], +} + +impl CertificateEmbeddedSCT { + pub fn new(cert: Certificate, chain: &[Certificate]) -> Result { + let scts: SignedCertificateTimestampList = match cert.tbs_certificate.get() { + Ok(Some((_, ext))) => ext, + _ => return Err(SCTError::SCTListMalformed), + }; + + // Parse SCT structures. + let sct = match scts + .parse_timestamps() + .or(Err(SCTError::SCTListMalformed))? + .as_slice() + { + [e] => e, + // We expect exactly one element here. Fail if there are more or less. + _ => return Err(SCTError::SCTListMalformed), + } + .parse_timestamp()?; + + // Traverse chain to find the issuer we're verifying against. + let issuer = find_issuer_cert(chain); + let issuer_id = { + let mut hasher = sha2::Sha256::new(); + issuer + .ok_or(SCTError::SCTMalformed)? + .tbs_certificate + .subject_public_key_info + .encode(&mut hasher) + .expect("failed to hash key!"); + hasher.finalize().into() + }; + + Ok(Self { + cert, + sct, + issuer_id, + }) + } +} + +impl From<&CertificateEmbeddedSCT> for DigitallySigned { + fn from(value: &CertificateEmbeddedSCT) -> Self { + // Construct the precert by filtering out the SCT extension. + let mut tbs_precert = value.cert.tbs_certificate.clone(); + tbs_precert.extensions = tbs_precert.extensions.map(|exts| { + exts.iter() + .filter(|v| v.extn_id != CT_PRECERT_SCTS) + .cloned() + .collect() + }); + + // TODO(tnytown): Instead of `expect` on `encode_to_vec`, we may want to implement + // `TryFrom` and pass this error through. When will we fail to encode a certificate + // with a modified extensions list? + let mut tbs_precert_der = Vec::new(); + tbs_precert + .encode_to_vec(&mut tbs_precert_der) + .expect("failed to re-encode Precertificate!"); + + DigitallySigned { + // XX(tnytown): This match is needed because `sct::Version` does not implement Copy. + version: match value.sct.version { + Version::V1 => Version::V1, + }, + signature_type: SignatureType::CertificateTimestamp, + timestamp: value.sct.timestamp, + signed_entry: SignedEntry::PrecertEntry(PreCert { + issuer_key_hash: value.issuer_id, + tbs_certificate: tbs_precert_der.as_slice().into(), + }), + extensions: value.sct.extensions.clone(), + + log_id: value.sct.log_id.key_id, + signature: value.sct.signature.signature.clone().into(), + } + } +} + +impl From<&SigningCertificateDetachedSCT> for DigitallySigned { + fn from(value: &SigningCertificateDetachedSCT) -> Self { + let sct = &value.signed_certificate_timestamp; + + DigitallySigned { + version: Version::V1, + signature_type: SignatureType::CertificateTimestamp, + timestamp: sct.timestamp, + signed_entry: SignedEntry::X509Entry(value.chain.certificates[0].contents().into()), + extensions: sct.extensions.clone().into(), + + log_id: sct.id, + signature: sct.signature.clone(), + } + } +} + +/// Verifies a given signing certificate's Signed Certificate Timestamp. +/// +/// SCT verification as defined by [RFC 6962] guarantees that a given certificate has been submitted +/// to a Certificate Transparency log. Verification should be performed on the signing certificate +/// in Sigstore verify and sign flows. Certificates that fail SCT verification are misissued and +/// MUST NOT be trusted. +/// +/// For more information on Certificate Transparency and the guarantees it provides, see . +/// +/// [RFC 6962]: https://datatracker.ietf.org/doc/html/rfc6962 +pub fn verify_sct(sct: impl Into, keyring: &Keyring) -> Result<(), SCTError> { + let sct: DigitallySigned = sct.into(); + let serialized = sct.tls_serialize().or(Err(SCTError::VerificationFailed( + "unable to reconstruct SCT for signing", + )))?; + + keyring.verify(&sct.log_id, &sct.signature, &serialized)?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{verify_sct, CertificateEmbeddedSCT}; + use crate::crypto::keyring::Keyring; + use crate::fulcio::SigningCertificateDetachedSCT; + use p256::ecdsa::VerifyingKey; + use std::str::FromStr; + use x509_cert::der::DecodePem; + use x509_cert::spki::EncodePublicKey; + use x509_cert::Certificate; + + #[test] + fn verify_embedded_sct() { + let cert_pem = r#"-----BEGIN CERTIFICATE----- +MIICzDCCAlGgAwIBAgIUF96OLbM9/tDVHKCJliXLTFvnfjAwCgYIKoZIzj0EAwMw +NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl +cm1lZGlhdGUwHhcNMjMxMjEzMDU1MDU1WhcNMjMxMjEzMDYwMDU1WjAAMFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAEmir+Lah2291zCsLkmREQNLzf99z571BNB+fa +rerSLGzcwLFK7GRLTGYcO0oStxCYavxRQPMo3JvB8vGtZbn/76OCAXAwggFsMA4G +A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU8U9M +t9GMrRm8+gifPtc63nlP3OIwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y +ZD8wGwYDVR0RAQH/BBEwD4ENYXNjQHRldHN1by5zaDAsBgorBgEEAYO/MAEBBB5o +dHRwczovL2dpdGh1Yi5jb20vbG9naW4vb2F1dGgwLgYKKwYBBAGDvzABCAQgDB5o +dHRwczovL2dpdGh1Yi5jb20vbG9naW4vb2F1dGgwgYkGCisGAQQB1nkCBAIEewR5 +AHcAdQDdPTBqxscRMmMZHhyZZzcCokpeuN48rf+HinKALynujgAAAYxhumYsAAAE +AwBGMEQCIHRRe20lRrNM4xd07mpjTtgaE6FGS3jjF++zW8ZMnth3AiAd6LVAAeVW +hSW4T0XJRw9lGU6/EK9+ELZpEjrY03dJ1zAKBggqhkjOPQQDAwNpADBmAjEAiHqK +W9PQ/5h7VROVIWPaxUo3LhrL2sZanw4bzTDBDY0dRR19ZFzjtAph1RzpQqppAjEA +plAvxwkAIR2jurboJZ4Zm9rNAx8KvA+A5yQFzNkGgKDLjTJrKmSKoIcWV3j7WfdL +-----END CERTIFICATE-----"#; + + let chain_pem = [ + r#"-----BEGIN CERTIFICATE----- +MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw +KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y +MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl +LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7 +7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS +0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB +BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp +KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI +zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR +nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP +mygUY7Ii2zbdCdliiow= +-----END CERTIFICATE-----"#, + r#"-----BEGIN CERTIFICATE----- +MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw +KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y +MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl +LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7 +XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex +X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j +YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY +wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ +KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM +WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9 +TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ +-----END CERTIFICATE-----"#, + ]; + + let ctfe_pem = r#"-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNK +AaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw== +-----END PUBLIC KEY-----"#; + + let cert = Certificate::from_pem(&cert_pem).unwrap(); + let chain = chain_pem.map(|c| Certificate::from_pem(&c).unwrap()); + let sct = CertificateEmbeddedSCT::new(cert, &chain).unwrap(); + let ctfe_key: VerifyingKey = VerifyingKey::from_str(&ctfe_pem).unwrap(); + let keyring = Keyring::new([ctfe_key.to_public_key_der().unwrap().as_bytes()]).unwrap(); + + assert!(verify_sct(&sct, &keyring).is_ok()); + } + + #[test] + fn verify_detached_sct() { + let sct_json = r#"{"chain": {"certificates": ["-----BEGIN CERTIFICATE-----\nMIICUTCCAfigAwIBAgIUAafXe40Q5jthWJMo+JsJJCq09IAwCgYIKoZIzj0EAwIw\naDEMMAoGA1UEBhMDVVNBMQswCQYDVQQIEwJXQTERMA8GA1UEBxMIS2lya2xhbmQx\nFTATBgNVBAkTDDc2NyA2dGggU3QgUzEOMAwGA1UEERMFOTgwMzMxETAPBgNVBAoT\nCHNpZ3N0b3JlMB4XDTIzMTIxNDA3MDkzMFoXDTIzMTIxNDA3MTkzMFowADBZMBMG\nByqGSM49AgEGCCqGSM49AwEHA0IABDQT+qfW/VnHts0GSqI3kOc2z1lygSUWia3y\nIOx5qyWpXS1PwVcTbJnkcQEy1mnAES76NyfN5LsHHW2m53hF4WGjgecwgeQwDgYD\nVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBRpKUIe\nAqDxiw/GzKGRLFAvbaCnujAfBgNVHSMEGDAWgBTjGF7/fiITblnp3yIv3G1DETbS\ncTAbBgNVHREBAf8EETAPgQ1hc2NAdGV0c3VvLnNoMC4GCisGAQQBg78wAQEEIGh0\ndHBzOi8vb2F1dGgyLnNpZ3N0b3JlLmRldi9hdXRoMDAGCisGAQQBg78wAQgEIgwg\naHR0cHM6Ly9vYXV0aDIuc2lnc3RvcmUuZGV2L2F1dGgwCgYIKoZIzj0EAwIDRwAw\nRAIgOW+tCrt44rjWDCMSWhwC0zJRWpqH/qWRgSw2ndK7w3ICIGz0DDAXhvl6JFAz\nQp+40dnoUGKr+y0MF1zVaDOb1y+q\n-----END CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\nMIICFzCCAb2gAwIBAgIUbPNC2sKGpw8cOQfpv8yJii7c7TEwCgYIKoZIzj0EAwIw\naDEMMAoGA1UEBhMDVVNBMQswCQYDVQQIEwJXQTERMA8GA1UEBxMIS2lya2xhbmQx\nFTATBgNVBAkTDDc2NyA2dGggU3QgUzEOMAwGA1UEERMFOTgwMzMxETAPBgNVBAoT\nCHNpZ3N0b3JlMB4XDTIzMTIxNDA2NDIzNloXDTMzMTIxNDA2NDIzNlowaDEMMAoG\nA1UEBhMDVVNBMQswCQYDVQQIEwJXQTERMA8GA1UEBxMIS2lya2xhbmQxFTATBgNV\nBAkTDDc2NyA2dGggU3QgUzEOMAwGA1UEERMFOTgwMzMxETAPBgNVBAoTCHNpZ3N0\nb3JlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfe1ZllZHky68F3jRhY4Hxx7o\nPBoBaD1i9UJtyE8xfIYGVpD1+jSHctZRmiv2ZsDEE6WN3k5lc2O2GyemHJwULqNF\nMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE\nFOMYXv9+IhNuWenfIi/cbUMRNtJxMAoGCCqGSM49BAMCA0gAMEUCIDj5wbYN3ym8\nwY+Uy+FkKASpBQodXdgF+JR9tWhNDlc/AiEAwqMTyLa6Yr+5t1DvnUsR4lQNoXD7\nz8XmxcUnJTenEh4=\n-----END CERTIFICATE-----"]}, "signedCertificateTimestamp": "eyJzY3RfdmVyc2lvbiI6MCwiaWQiOiJla0ppei9acEcrVUVuNXcvR2FJcjYrYXdJK1JLZmtwdC9WOVRldTd2YTFrPSIsInRpbWVzdGFtcCI6MTcwMjUzNzc3MDQyNiwiZXh0ZW5zaW9ucyI6IiIsInNpZ25hdHVyZSI6IkJBTUFSakJFQWlBT28vdDZ4RDY0RkV2TWpGcGFsMUhVVkZxQU5nOXJ3ZEttd3NQU2wxNm5FZ0lnZmFNTlJHMTBxQVY1Z280MzU1WkxVNVVvdHRvWTAwK0l0YXhZYjRkZmV0Zz0ifQ=="}"#; + + let ctfe_pem = r#"-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbbQiLx6GKy6ivhc11wJGbQjc2VX/ +mnuk5d670MTXR3p+LIAcxd5MhqIHpLmyYJ5mDKLEoZ/pC0nPuje3JueBcA== +-----END PUBLIC KEY-----"#; + + let sct: SigningCertificateDetachedSCT = serde_json::from_str(sct_json).unwrap(); + let ctfe_key: VerifyingKey = VerifyingKey::from_str(&ctfe_pem).unwrap(); + let keyring = Keyring::new([ctfe_key.to_public_key_der().unwrap().as_bytes()]).unwrap(); + + assert!(verify_sct(&sct, &keyring).is_ok()); + } +} diff --git a/src/errors.rs b/src/errors.rs index e6b6b723ff..bf8970fe71 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -136,6 +136,14 @@ pub enum SigstoreError { #[error(transparent)] JoinError(#[from] tokio::task::JoinError), + #[cfg(feature = "cert")] + #[error(transparent)] + KeyringError(#[from] crate::crypto::keyring::KeyringError), + + #[cfg(feature = "sign")] + #[error(transparent)] + SCTError(#[from] crate::crypto::transparency::SCTError), + #[cfg(feature = "sign")] #[error(transparent)] ReqwestError(#[from] reqwest::Error), diff --git a/src/fulcio/mod.rs b/src/fulcio/mod.rs index a96f0a18ce..0159b77e7c 100644 --- a/src/fulcio/mod.rs +++ b/src/fulcio/mod.rs @@ -1,4 +1,4 @@ -mod models; +pub(crate) mod models; pub mod oauth; diff --git a/src/fulcio/models.rs b/src/fulcio/models.rs index 3441f4ca44..0a784a76d0 100644 --- a/src/fulcio/models.rs +++ b/src/fulcio/models.rs @@ -119,6 +119,8 @@ pub enum SCTVersion { V1 = 0, } +// TODO(tnytown): Make this type prettier. SigningCertificateDetachedSCT duplicates most of the data +// in cert and chain. pub struct CertificateResponse { pub cert: Certificate, pub chain: Vec, diff --git a/src/sign.rs b/src/sign.rs index 30f75737c7..f2a188e924 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -39,6 +39,8 @@ use x509_cert::builder::{Builder, RequestBuilder as CertRequestBuilder}; use x509_cert::ext::pkix as x509_ext; use crate::bundle::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}; @@ -46,6 +48,7 @@ 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::tuf::{Repository, SigstoreRepository}; /// An asynchronous Sigstore signing session. /// @@ -128,7 +131,12 @@ impl<'ctx> AsyncSigningSession<'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.clone(), &self.certs.chain)?; + verify_sct(&sct, &self.context.ctfe_keyring)?; + } // Sign artifact. let input_hash: &[u8] = &hasher.clone().finalize(); @@ -243,26 +251,34 @@ impl<'ctx> SigningSession<'ctx> { 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 { + let trust_root = SigstoreRepository::new(None)?; 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()?)?, )) } diff --git a/src/tuf/mod.rs b/src/tuf/mod.rs index 31818df732..652b6de1bf 100644 --- a/src/tuf/mod.rs +++ b/src/tuf/mod.rs @@ -270,7 +270,6 @@ impl Repository for SigstoreRepository { /// an async function because it performs blocking operations. fn ctfe_keys(&self) -> Result> { let keys: Vec<_> = Self::tlog_keys(&self.trusted_root.ctlogs).collect(); - if keys.is_empty() { Err(SigstoreError::TufMetadataError( "CTFE keys not found".into(),