| // 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 <algorithm> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <ctime> | |
| #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/clock_interface.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/bytestring.h" | |
| #include "openssl/pki/ocsp.h" | |
| #include "openssl/pki/verify.h" | |
| #include "openssl/pki/verify_error.h" | |
| #include "openssl/posix_time.h" | |
| #include "tsp/timestamp_verifier.h" | |
| #include "tsp/verified_timestamp.h" | |
| namespace credentio { | |
| namespace { | |
| // Maximum clock skew tolerated when checking `current_time >= thisUpdate`. | |
| constexpr absl::Duration kOcspClockSkewTolerance = absl::Minutes(10); | |
| // Extracts the `thisUpdate` time from the first `SingleResponse` in a | |
| // DER-encoded `OCSPResponse` (RFC 6960 Section 4.2.1), or returns | |
| // `std::nullopt` if the response cannot be parsed. | |
| std::optional<absl::Time> ParseOcspThisUpdate( | |
| absl::string_view ocsp_response_der) { | |
| CBS cbs; | |
| CBS_init(&cbs, reinterpret_cast<const uint8_t*>(ocsp_response_der.data()), | |
| ocsp_response_der.size()); | |
| CBS response_seq, response_bytes_wrapper, response_bytes, | |
| basic_response_octets, basic_response, tbs_response_data; | |
| if (!CBS_get_asn1(&cbs, &response_seq, CBS_ASN1_SEQUENCE) || | |
| !CBS_get_asn1(&response_seq, /*out=*/nullptr, CBS_ASN1_ENUMERATED) || | |
| !CBS_get_asn1(&response_seq, &response_bytes_wrapper, | |
| CBS_ASN1_CONTEXT_SPECIFIC | CBS_ASN1_CONSTRUCTED | 0) || | |
| !CBS_get_asn1(&response_bytes_wrapper, &response_bytes, | |
| CBS_ASN1_SEQUENCE) || | |
| !CBS_get_asn1(&response_bytes, /*out=*/nullptr, CBS_ASN1_OBJECT) || | |
| !CBS_get_asn1(&response_bytes, &basic_response_octets, | |
| CBS_ASN1_OCTETSTRING) || | |
| !CBS_get_asn1(&basic_response_octets, &basic_response, | |
| CBS_ASN1_SEQUENCE) || | |
| !CBS_get_asn1(&basic_response, &tbs_response_data, CBS_ASN1_SEQUENCE)) { | |
| return std::nullopt; | |
| } | |
| CBS responses, single_response, this_update_cbs; | |
| if (!CBS_get_optional_asn1( | |
| &tbs_response_data, /*out=*/nullptr, /*out_present=*/nullptr, | |
| CBS_ASN1_CONTEXT_SPECIFIC | CBS_ASN1_CONSTRUCTED | 0) || | |
| !CBS_get_any_asn1(&tbs_response_data, /*out=*/nullptr, | |
| /*out_tag=*/nullptr) || | |
| !CBS_get_asn1(&tbs_response_data, /*out=*/nullptr, | |
| CBS_ASN1_GENERALIZEDTIME) || | |
| !CBS_get_asn1(&tbs_response_data, &responses, CBS_ASN1_SEQUENCE) || | |
| !CBS_get_asn1(&responses, &single_response, CBS_ASN1_SEQUENCE) || | |
| !CBS_get_asn1(&single_response, /*out=*/nullptr, CBS_ASN1_SEQUENCE) || | |
| !CBS_get_any_asn1(&single_response, /*out=*/nullptr, | |
| /*out_tag=*/nullptr) || | |
| !CBS_get_asn1(&single_response, &this_update_cbs, | |
| CBS_ASN1_GENERALIZEDTIME)) { | |
| return std::nullopt; | |
| } | |
| struct tm tm; | |
| int64_t this_update_posix; | |
| if (!CBS_parse_generalized_time(&this_update_cbs, &tm, | |
| /*allow_timezone_offset=*/0) || | |
| !OPENSSL_tm_to_posix(&tm, &this_update_posix)) { | |
| return std::nullopt; | |
| } | |
| return absl::FromUnixSeconds(this_update_posix); | |
| } | |
| 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, absl::Clock* absl_nonnull clock) | |
| : 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), | |
| clock_(*clock) {} | |
| ~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 asserted_time) const override { | |
| absl::Time verify_time = asserted_time; | |
| if (std::optional<absl::Time> this_update = | |
| ParseOcspThisUpdate(ocsp_response_der); | |
| this_update.has_value()) { | |
| // Per C2PA 2.4 Section 15.9.1, an OCSP response is valid if: | |
| // 1. `current_time >= thisUpdate` (with `kOcspClockSkewTolerance` | |
| // tolerance for clock skew), and | |
| // 2. `asserted_time < thisUpdate` OR `asserted_time` falls within | |
| // `[thisUpdate, nextUpdate)`. | |
| if (clock_.TimeNow() + kOcspClockSkewTolerance < *this_update) { | |
| return absl::OutOfRangeError( | |
| "current_time is earlier than the OCSP response's thisUpdate."); | |
| } | |
| // `bssl::CheckOCSP` checks `thisUpdate <= verify_time < nextUpdate`, so | |
| // clamping `verify_time` to at least `thisUpdate` allows | |
| // `asserted_time < thisUpdate` while still checking | |
| // `asserted_time < nextUpdate`. | |
| verify_time = std::max(asserted_time, *this_update); | |
| } | |
| 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_; | |
| absl::Clock& clock_; | |
| }; | |
| } // 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) { | |
| if (options.clock == nullptr) { | |
| return absl::InvalidArgumentError("clock must not be null"); | |
| } | |
| 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 && | |
| options.trust_environment != CryptoReadHandler::TrustEnvironment::kAny) { | |
| 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 && | |
| options.trust_environment != CryptoReadHandler::TrustEnvironment::kAny) { | |
| 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, options.clock); | |
| } | |
| } // namespace credentio |