| // Copyright 2026 Google LLC |
| // |
| // 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 |
| // |
| // https://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. |
| // |
| |
| #include "crypto/default/default_crypto_read_handler.h" |
| |
| #include <cstddef> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/base/nullability.h" |
| #include "absl/log/absl_check.h" |
| #include "absl/log/absl_log.h" |
| #include "absl/log/check.h" |
| #include "absl/log/log.h" |
| #include "absl/status/status.h" |
| #include "absl/status/status_macros.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/time/time.h" |
| #include "absl/types/span.h" |
| #include "cose/simple_cms_parser.h" |
| #include "crypto/algorithms.h" |
| #include "crypto/crypto_read_handler.h" |
| #include "crypto/default/eku_verifier.h" |
| #include "crypto/default/trust_store.h" |
| #include "crypto/default/x509_certificate.h" |
| #include "openssl/pki/ocsp.h" |
| #include "openssl/pki/verify.h" |
| #include "openssl/pki/verify_error.h" |
| #include "tsp/timestamp_verifier.h" |
| #include "tsp/verified_timestamp.h" |
| |
| namespace credentio { |
| namespace { |
| |
| class DefaultParsedCertificates : public ParsedCertificates { |
| public: |
| DefaultParsedCertificates() = default; |
| ~DefaultParsedCertificates() override = default; |
| // If `all_claim_signer_roots_` is nullptr, VerifyClaimSignerTrust skips trust |
| // checks. If `legacy_claim_signer_roots_` is nullptr, VerifyClaimSignerTrust |
| // accepts only the C2PA EKU. |
| explicit DefaultParsedCertificates( |
| std::vector<std::unique_ptr<X509Certificate>> certificate_chain_x509, |
| std::vector<std::string> certificate_chain_der, |
| bssl::VerifyTrustStore* absl_nullable all_claim_signer_roots, |
| bssl::VerifyTrustStore* absl_nullable legacy_claim_signer_roots) |
| : certificate_chain_x509_(std::move(certificate_chain_x509)), |
| certificate_chain_der_(std::move(certificate_chain_der)), |
| all_claim_signer_roots_(all_claim_signer_roots), |
| legacy_claim_signer_roots_(legacy_claim_signer_roots) { |
| ABSL_CHECK(!certificate_chain_x509_.empty()); |
| ABSL_CHECK_EQ(certificate_chain_der_.size(), |
| certificate_chain_x509_.size()); |
| } |
| |
| absl::StatusOr<std::vector<std::string>> VerifyClaimSignerTrust( |
| absl::Time content_time) const override { |
| if (all_claim_signer_roots_ == nullptr) { |
| ABSL_LOG_EVERY_N_SEC(INFO, 60) |
| << "Bypassing C2PA claim signer certificate trust check."; |
| return std::vector<std::string>(); |
| } |
| if (certificate_chain_der_.size() > 3) { |
| return absl::UnauthenticatedError( |
| absl::StrCat("Excessive certificate chain length: ", |
| certificate_chain_der_.size())); |
| } |
| |
| bssl::CertificateVerifyOptions opts; |
| opts.leaf_cert = certificate_chain_der_.at(0); |
| if (EkuVerifier::CheckLeafCertEku(opts.leaf_cert)) { |
| // `EkuVerifier::CheckLeafCertEku` returns true iff the version 2.2 |
| // requirements are met: |
| // 1. The leaf certificate asserts the `digitalSignature` KU bit. |
| // 2. The leaf certificate has the C2PA claim signing EKU. |
| // 3. The leaf certificate does not have the `anyExtendedKeyUsage` EKU. |
| // So, we can skip the EKU checks in BoringSSL. |
| opts.key_purpose = bssl::CertificateVerifyOptions::KeyPurpose::ANY_EKU; |
| opts.trust_store = all_claim_signer_roots_; |
| } else { |
| // Version 2.2 requirements are not met. Fall back to version 2.1 checks |
| // in BoringSSL. |
| opts.key_purpose = |
| bssl::CertificateVerifyOptions::KeyPurpose::C2PA_MANIFEST; |
| if (legacy_claim_signer_roots_ == nullptr) { |
| return absl::InvalidArgumentError( |
| "Extended Key Usage c2pa-kp-claimSigning not present"); |
| } |
| opts.trust_store = legacy_claim_signer_roots_; |
| } |
| for (int i = 1; i < certificate_chain_der_.size(); ++i) { |
| opts.intermediates.push_back(certificate_chain_der_.at(i)); |
| } |
| opts.time = absl::ToTimeT(content_time); |
| bssl::VerifyError error; |
| // This check only verifies that there is a valid chain from the leaf |
| // certificate to one of the trusted root certificates. Note that this is |
| // a relaxed version of the requirement in |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#x509_certificates |
| // which requires the certificates in the chain to be ordered starting with |
| // the certificate containing the end-entity key (i.e., the leaf |
| // certificate) followed by the certificate that signed it, and so on. |
| auto chain = bssl::CertificateVerify(opts, &error); |
| if (!chain) { |
| switch (error.Code()) { |
| case bssl::VerifyError::StatusCode::CERTIFICATE_NOT_YET_VALID: |
| case bssl::VerifyError::StatusCode::CERTIFICATE_EXPIRED: |
| return absl::OutOfRangeError( |
| absl::StrCat("certificate outside validity period: ", |
| error.DiagnosticString())); |
| case bssl::VerifyError::StatusCode::PATH_NOT_FOUND: |
| return absl::UnauthenticatedError(absl::StrCat( |
| "untrusted claim signer: ", error.DiagnosticString())); |
| default: |
| return absl::InvalidArgumentError(absl::StrCat( |
| "Failed to verify the trust chain: ", error.DiagnosticString())); |
| } |
| } |
| return *chain; |
| } |
| |
| absl::Status VerifySignature(absl::string_view signature, |
| absl::string_view data, |
| SigningAlgorithm algorithm) const override { |
| return leaf().VerifySignature(signature, data, algorithm); |
| } |
| |
| size_t GetCertificateCount() const override { |
| return certificate_chain_x509_.size(); |
| } |
| |
| absl::StatusOr<std::string> GetSubject(size_t index) const override { |
| if (index >= certificate_chain_x509_.size()) { |
| return absl::OutOfRangeError("Certificate index out of bounds"); |
| } |
| return certificate_chain_x509_[index]->GetSubject(); |
| } |
| |
| absl::StatusOr<std::string> GetIssuer(size_t index) const override { |
| if (index >= certificate_chain_x509_.size()) { |
| return absl::OutOfRangeError("Certificate index out of bounds"); |
| } |
| return certificate_chain_x509_[index]->GetIssuer(); |
| } |
| |
| absl::StatusOr<absl::Time> GetStartTime(size_t index) const override { |
| if (index >= certificate_chain_x509_.size()) { |
| return absl::OutOfRangeError("Certificate index out of bounds"); |
| } |
| return certificate_chain_x509_[index]->StartTime(); |
| } |
| |
| absl::StatusOr<absl::Time> GetEndTime(size_t index) const override { |
| if (index >= certificate_chain_x509_.size()) { |
| return absl::OutOfRangeError("Certificate index out of bounds"); |
| } |
| return certificate_chain_x509_[index]->EndTime(); |
| } |
| |
| absl::StatusOr<std::string> GetSerialNumberHex(size_t index) const override { |
| if (index >= certificate_chain_x509_.size()) { |
| return absl::OutOfRangeError("Certificate index out of bounds"); |
| } |
| return certificate_chain_x509_[index]->GetSerialNumberHex(); |
| } |
| |
| absl::StatusOr<std::string> GetAssuranceLevel(size_t index) const override { |
| if (index >= certificate_chain_x509_.size()) { |
| return absl::OutOfRangeError("Certificate index out of bounds"); |
| } |
| return certificate_chain_x509_[index]->GetAssuranceLevel(); |
| } |
| |
| absl::StatusOr<std::string> GetConformingProductId( |
| size_t index) const override { |
| if (index >= certificate_chain_x509_.size()) { |
| return absl::OutOfRangeError("Certificate index out of bounds"); |
| } |
| return certificate_chain_x509_[index]->GetConformingProductId(); |
| } |
| |
| const X509Certificate& leaf() const { |
| return *certificate_chain_x509_.front(); |
| } |
| |
| #ifndef NDEBUG |
| [[maybe_unused]] std::string CertChainDebugString() const { |
| std::string s; |
| for (const auto& cert : certificate_chain_x509_) { |
| absl::StrAppend(&s, cert->DebugString()); |
| } |
| return s; |
| } |
| #endif |
| |
| private: |
| std::vector<std::unique_ptr<X509Certificate>> certificate_chain_x509_; |
| std::vector<std::string> certificate_chain_der_; |
| bssl::VerifyTrustStore* absl_nullable all_claim_signer_roots_; |
| bssl::VerifyTrustStore* absl_nullable legacy_claim_signer_roots_; |
| }; |
| |
| // An implementation of CryptoReadHandler intended for use in production. |
| // It uses Tink, BoringSSL, and SimpleCMS. |
| class DefaultCryptoReadHandler : public CryptoReadHandler { |
| public: |
| DefaultCryptoReadHandler( |
| absl_nullable std::unique_ptr<bssl::VerifyTrustStore> |
| all_claim_signer_roots, |
| absl_nullable std::unique_ptr<bssl::VerifyTrustStore> |
| legacy_claim_signer_roots, |
| absl_nullable std::unique_ptr<bssl::VerifyTrustStore> tsa_roots, |
| TrustEnvironment trust_environment) |
| : all_claim_signer_roots_(std::move(all_claim_signer_roots)), |
| legacy_claim_signer_roots_(std::move(legacy_claim_signer_roots)), |
| tsa_roots_(std::move(tsa_roots)), |
| timestamp_verifier_( |
| std::make_unique<TimestampVerifier>(tsa_roots_.get())), |
| cms_parser_(std::make_unique<SimpleCmsParser>()), |
| trust_environment_(trust_environment) {} |
| ~DefaultCryptoReadHandler() override = default; |
| |
| absl::StatusOr<std::unique_ptr<ParsedCertificates>> ParseCertificatesDer( |
| absl::Span<const absl::string_view> certificates) const override; |
| |
| absl::StatusOr<OCSPRevocationStatus> VerifyOcspResponse( |
| absl::string_view ocsp_response_der, absl::string_view certificate_der, |
| absl::string_view issuer_certificate_der, |
| absl::Time verify_time) const override { |
| bssl::OCSPVerifyResult::ResponseStatus response_status; |
| bssl::OCSPRevocationStatus revocation_status = bssl::CheckOCSP( |
| ocsp_response_der, certificate_der, issuer_certificate_der, |
| absl::ToUnixSeconds(verify_time), std::nullopt, &response_status); |
| |
| switch (response_status) { |
| case bssl::OCSPVerifyResult::PROVIDED: |
| switch (revocation_status) { |
| case bssl::OCSPRevocationStatus::GOOD: |
| return OCSPRevocationStatus::kGood; |
| case bssl::OCSPRevocationStatus::REVOKED: |
| return OCSPRevocationStatus::kRevoked; |
| case bssl::OCSPRevocationStatus::UNKNOWN: |
| return OCSPRevocationStatus::kUnknown; |
| default: |
| return absl::InternalError("Unknown revocation status"); |
| } |
| case bssl::OCSPVerifyResult::NO_MATCHING_RESPONSE: |
| return absl::UnauthenticatedError( |
| "OCSP response signature failed verification or no matching " |
| "response found."); |
| case bssl::OCSPVerifyResult::INVALID_DATE: |
| case bssl::OCSPVerifyResult::BAD_PRODUCED_AT: |
| return absl::OutOfRangeError( |
| "verify_time is outside the valid time window of the OCSP " |
| "response."); |
| default: |
| return absl::InvalidArgumentError( |
| "OCSP response is malformed or invalid."); |
| } |
| } |
| |
| absl::StatusOr<VerifiedTimestamp> VerifyTimestamp( |
| absl::string_view cms) const override { |
| ABSL_ASSIGN_OR_RETURN(auto parsed_token, |
| cms_parser_->ParseTimestampToken(cms)); |
| return timestamp_verifier_->VerifyTimestampToken(*parsed_token); |
| } |
| |
| TrustEnvironment trust_environment() const override { |
| return trust_environment_; |
| } |
| |
| private: |
| absl_nullable std::unique_ptr<bssl::VerifyTrustStore> all_claim_signer_roots_; |
| absl_nullable std::unique_ptr<bssl::VerifyTrustStore> |
| legacy_claim_signer_roots_; |
| absl_nullable std::unique_ptr<bssl::VerifyTrustStore> tsa_roots_; |
| std::unique_ptr<TimestampVerifier> timestamp_verifier_; |
| std::unique_ptr<SimpleCmsParser> cms_parser_; |
| TrustEnvironment trust_environment_; |
| }; |
| |
| } // namespace |
| |
| absl::StatusOr<std::unique_ptr<ParsedCertificates>> |
| DefaultCryptoReadHandler::ParseCertificatesDer( |
| absl::Span<const absl::string_view> certificates) const { |
| if (certificates.empty()) { |
| return absl::InvalidArgumentError("No leaf certificate provided"); |
| } |
| // Construct the X.509 public key certificate from DER bytes. |
| // The first element of the certificate chain is the signer's public key DER |
| // bytes. The certificate chain starts with the signer's certificate and the |
| // following elements constitute the rest of the chain. |
| std::vector<std::unique_ptr<X509Certificate>> certificates_x509; |
| std::vector<std::string> certificates_der; |
| certificates_der.reserve(certificates.size()); |
| for (int i = 0; i < certificates.size(); ++i) { |
| const auto& cert_der = certificates[i]; |
| certificates_der.push_back(std::string(cert_der)); |
| ABSL_ASSIGN_OR_RETURN(auto x509, X509Certificate::Create(/*der=*/cert_der)); |
| ABSL_DVLOG(1) << "Cert validity start time = " << x509->StartTime(); |
| ABSL_DVLOG(1) << "Cert validity end time = " << x509->EndTime(); |
| if (x509 == nullptr) { |
| return absl::InternalError("X509Certificate::Create returned nullptr"); |
| } |
| ABSL_RETURN_IF_ERROR(x509->IsValidC2paCertificate(/*is_leaf=*/(i == 0))); |
| certificates_x509.push_back(std::move(x509)); |
| } |
| return std::make_unique<DefaultParsedCertificates>( |
| std::move(certificates_x509), std::move(certificates_der), |
| all_claim_signer_roots_.get(), legacy_claim_signer_roots_.get()); |
| } |
| |
| absl::StatusOr<std::unique_ptr<CryptoReadHandler>> |
| CreateDefaultCryptoReadHandler(const DefaultCryptoReadHandlerOptions& options) { |
| std::unique_ptr<bssl::VerifyTrustStore> all_claim_signer_roots; |
| std::unique_ptr<bssl::VerifyTrustStore> legacy_claim_signer_roots; |
| if (!options.skip_claim_signer_trust_checks_for_test) { |
| if (!options.legacy_claim_signer_trust_anchors_pem.empty()) { |
| // If the string is nonempty, it must contain at least one cert. |
| ABSL_ASSIGN_OR_RETURN( |
| legacy_claim_signer_roots, |
| LoadTrustStore(options.legacy_claim_signer_trust_anchors_pem)); |
| } |
| ABSL_ASSIGN_OR_RETURN(all_claim_signer_roots, |
| LoadTrustStore(absl::StrCat( |
| options.claim_signer_trust_anchors_pem, |
| options.legacy_claim_signer_trust_anchors_pem))); |
| } |
| std::unique_ptr<bssl::VerifyTrustStore> tsa_roots; |
| if (!options.skip_tsa_trust_checks_for_test) { |
| ABSL_ASSIGN_OR_RETURN(tsa_roots, |
| LoadTrustStore(options.tsa_trust_anchors_pem)); |
| } |
| return std::make_unique<DefaultCryptoReadHandler>( |
| /*all_claim_signer_roots=*/std::move(all_claim_signer_roots), |
| /*legacy_claim_signer_roots=*/std::move(legacy_claim_signer_roots), |
| std::move(tsa_roots), options.trust_environment); |
| } |
| |
| } // namespace credentio |