| // 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 |