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..73200c5aea --- /dev/null +++ b/src/crypto/transparency.rs @@ -0,0 +1,406 @@ +// 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 thiserror::Error; +use tls_codec::{SerializeBytes, TlsByteVecU16, TlsByteVecU24, TlsSerializeBytes, TlsSize}; +use tracing::debug; +use x509_cert::{ + der, + der::{Decode, Encode}, + ext::pkix::{ + sct::Version, ExtendedKeyUsage, SignedCertificateTimestamp, SignedCertificateTimestampList, + }, + Certificate, +}; + +use super::{ + certificate, + 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] + }; + + // TODO(tnytown): do we need to sanity-check the algo of the certificate here? + + certificate::is_ca(cert).ok()?; + Some(cert) +} + +#[derive(Debug, Error)] +pub enum CertificateErrorKind { + #[error("SCT list extension missing from leaf certificate")] + LeafSCTMissing, + + #[error("cannot find leaf certificate's issuer")] + IssuerMissing, + + #[error("cannot decode leaf certificate's issuer")] + IssuerMalformed, + + #[error("cannot decode SCT")] + LeafSCTMalformed, + + #[error(transparent)] + Der(#[from] der::Error), + + #[error(transparent)] + Tls(#[from] tls_codec::Error), +} + +impl From for CertificateErrorKind { + fn from(value: x509_cert::ext::pkix::Error) -> Self { + match value { + x509_cert::ext::pkix::Error::Der(e) => CertificateErrorKind::Der(e), + x509_cert::ext::pkix::Error::Tls(e) => CertificateErrorKind::Tls(e), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum SCTError { + #[error("failed to extract SCT from certificate")] + Parsing(#[from] CertificateErrorKind), + + #[error("failed to reconstruct signed payload")] + Serialization(#[source] tls_codec::Error), + + #[error("failed to verify SCT")] + Verification(#[from] KeyringError), +} + +#[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<'a> { + cert: &'a Certificate, + sct: SignedCertificateTimestamp, + issuer_id: [u8; 32], +} + +impl<'a> CertificateEmbeddedSCT<'a> { + fn new_with_spki(cert: &'a Certificate, spki: &[u8]) -> Result { + let scts: SignedCertificateTimestampList = match cert.tbs_certificate.get() { + Ok(Some((_, ext))) => ext, + _ => return Err(SCTError::Parsing(CertificateErrorKind::LeafSCTMissing))?, + }; + + // Parse SCT structures. + let sct = match scts + .parse_timestamps() + .map_err(CertificateErrorKind::from)? + .as_slice() + { + [e] => e, + // We expect exactly one element here. Fail if there are more or less. + _ => return Err(CertificateErrorKind::LeafSCTMissing)?, + } + .parse_timestamp() + .map_err(CertificateErrorKind::from)?; + + let issuer_id = { + let mut hasher = sha2::Sha256::new(); + hasher.update(spki); + hasher.finalize().into() + }; + + Ok(Self { + cert, + sct, + issuer_id, + }) + } + + pub fn new(leaf: &'a Certificate, chain: &[Certificate]) -> Result { + // Traverse chain to find the issuer we're verifying against. + let issuer = find_issuer_cert(chain); + let spki = issuer + .ok_or(CertificateErrorKind::IssuerMissing)? + .tbs_certificate + .subject_public_key_info + .to_der() + .map_err(CertificateErrorKind::from)?; + + Self::new_with_spki(leaf, &spki) + } + + pub fn new_with_verified_path( + leaf: &'a Certificate, + chain: &webpki::VerifiedPath, + ) -> Result { + let issuer_spki = if let Some(issuer) = chain.intermediate_certificates().next() { + debug!("intermediate is the leaf's issuer"); + + let issuer = Certificate::from_der(&issuer.der()) + .map_err(CertificateErrorKind::from)? + .tbs_certificate; + issuer + .subject_public_key_info + .to_der() + .map_err(CertificateErrorKind::from)? + } else { + debug!("anchor is the leaf's issuer"); + + // Prefix the SPKI with the DER SEQUENCE tag and a short definite-form length. + let body = &chain.anchor().subject_public_key_info[..]; + let body_len = body + .len() + .try_into() + .or(Err(CertificateErrorKind::IssuerMalformed))?; + let prefix = &[0x30u8, body_len]; + + [prefix, body].concat() + }; + + Self::new_with_spki(leaf, &issuer_spki) + } +} + +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().map_err(SCTError::Serialization)?; + + 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..f2245fce3e 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, &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(), diff --git a/src/verify/models.rs b/src/verify/models.rs index 5a6777fca4..ca590c3ef2 100644 --- a/src/verify/models.rs +++ b/src/verify/models.rs @@ -103,6 +103,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), } diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index 580bb9e24e..4dc447291f 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -22,7 +22,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, tuf::{Repository, SigstoreRepository}, @@ -41,6 +45,7 @@ pub struct AsyncVerifier { #[allow(dead_code)] rekor_config: RekorConfiguration, cert_pool: CertificatePool, + ctfe_keyring: Keyring, } impl AsyncVerifier { @@ -52,10 +57,12 @@ impl AsyncVerifier { trust_repo: R, ) -> SigstoreResult { 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, }) } @@ -100,14 +107,18 @@ impl AsyncVerifier { .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)?;