| // 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 "tsp/timestamp_verifier.h" |
| |
| #include <cstdint> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/log/absl_log.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 "crypto/algorithms.h" |
| #include "crypto/default/hasher.h" |
| #include "crypto/hash.h" |
| #include "openssl/asn1.h" |
| #include "openssl/base.h" |
| #include "openssl/bytestring.h" |
| #include "openssl/mem.h" |
| #include "openssl/nid.h" |
| #include "openssl/obj.h" |
| #include "openssl/pki/verify.h" |
| #include "openssl/pki/verify_error.h" |
| #include "openssl/stack.h" |
| #include "openssl/x509.h" |
| #include "tsp/parsed_timestamp_token.h" |
| #include "tsp/timestamp_parsing.h" |
| #include "tsp/verified_timestamp.h" |
| namespace credentio { |
| |
| namespace { |
| |
| // Represents the `EssCertIdV2` SEQUENCE, part of the `SigningCertificateV2` |
| // attribute as defined in RFC 5035. |
| struct EssCertIdV2 { |
| HashAlgorithm hash_algorithm; |
| std::string cert_hash; |
| }; |
| |
| absl::Status CertMatchesEssCertId(absl::string_view cert, EssCertIdV2 id) { |
| auto hasher = CreateHasher(id.hash_algorithm); |
| if (!hasher.ok()) { |
| return absl::Status( |
| absl::StatusCode::kFailedPrecondition, |
| absl::StrCat("Invalid HashAlgorithm value: ", id.hash_algorithm, "; ", |
| hasher.status().message())); |
| } |
| (*hasher)->Update(cert); |
| if ((*hasher)->Digest() == id.cert_hash) { |
| return absl::OkStatus(); |
| } else { |
| return absl::UnauthenticatedError( |
| R"(certificate does not match EssCertIDV2 value)"); |
| } |
| } |
| |
| absl::Status CheckTsaCertificateTrust( |
| const bssl::VerifyTrustStore& tsa_roots, |
| const VerifiedTimestamp& verified_timestamp, |
| absl::Span<const EssCertIdV2> ess_cert_ids) { |
| if (ess_cert_ids.empty()) { |
| return absl::InvalidArgumentError( |
| R"(SigningCertificateV2 signed attribute is empty; at minimum, TSAs must record their own leaf certificate in this attribute)"); |
| } |
| absl::Status status = CertMatchesEssCertId( |
| verified_timestamp.tsa_certificate(), ess_cert_ids[0]); |
| if (!status.ok()) { |
| return absl::Status( |
| status.code(), |
| absl::StrCat("TSA cert does not match SigningCertificateV2 attribute: ", |
| status.message())); |
| } |
| |
| auto intermediate_certs_der = |
| verified_timestamp.certificate_chain().subspan(1); |
| if (intermediate_certs_der.size() > 2) { |
| return absl::UnauthenticatedError( |
| absl::StrCat("excessive timestamp certificate chain length: ", |
| intermediate_certs_der.size() + 1)); |
| } |
| |
| bssl::CertificateVerifyOptions opts; |
| |
| opts.key_purpose = |
| bssl::CertificateVerifyOptions::KeyPurpose::C2PA_TIMESTAMPING; |
| opts.leaf_cert = verified_timestamp.tsa_certificate(); |
| opts.intermediates.insert(opts.intermediates.end(), |
| intermediate_certs_der.begin(), |
| intermediate_certs_der.end()); |
| // Timestamp cert chain validity periods are evaluated with respect to the |
| // attested time, per |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_validate_the_time_stamp: |
| // "Validate that the attested time, as found in the genTime field (in the |
| // timeStampToken), falls within the validity period of the TSA’s signing |
| // certificate." |
| opts.time = absl::ToTimeT(verified_timestamp.asserted_time()); |
| opts.trust_store = &tsa_roots; |
| bssl::VerifyError error; |
| if (!bssl::CertificateVerify(opts, &error)) { |
| if (error.Code() == bssl::VerifyError::StatusCode::CERTIFICATE_EXPIRED || |
| error.Code() == |
| bssl::VerifyError::StatusCode::CERTIFICATE_NOT_YET_VALID) { |
| return absl::OutOfRangeError(error.DiagnosticString()); |
| } |
| return absl::UnauthenticatedError( |
| absl::StrCat("timestamp certificate chain could not be validated: ", |
| error.DiagnosticString())); |
| } |
| return absl::OkStatus(); |
| } |
| |
| // Extracts the `certs` field of the `SigningCertificateV2` attribute (attribute |
| // defined in RFC 5035). |
| absl::StatusOr<std::vector<EssCertIdV2>> GetEssCertIds( |
| absl::string_view signing_certificate_v2_bytes) { |
| std::vector<EssCertIdV2> out; |
| CBS signing_certificate_cbs; |
| CBS_init( |
| &signing_certificate_cbs, |
| reinterpret_cast<const uint8_t*>(signing_certificate_v2_bytes.data()), |
| signing_certificate_v2_bytes.size()); |
| |
| CBS signing_certificate_sequence; |
| if (!CBS_get_asn1(&signing_certificate_cbs, &signing_certificate_sequence, |
| CBS_ASN1_SEQUENCE)) { |
| return absl::InvalidArgumentError( |
| R"(could not parse DER SEQUENCE for SigningCertificateV2 attribute value)"); |
| } |
| |
| CBS certs_sequence; |
| if (!CBS_get_asn1(&signing_certificate_sequence, &certs_sequence, |
| CBS_ASN1_SEQUENCE)) { |
| return absl::InvalidArgumentError( |
| R"(could not parse `certs` field (DER SEQUENCE) of SigningCertificateV2 attribute value)"); |
| } |
| |
| CBS ess_cert_id_cbs; |
| while (CBS_get_asn1(&certs_sequence, &ess_cert_id_cbs, CBS_ASN1_SEQUENCE)) { |
| // The hash algorithm is an optional field, if it is not present it defaults |
| // to SHA-256. |
| int algorithm_nid = NID_sha256; |
| CBS original = ess_cert_id_cbs; |
| CBS oid_cbs; |
| if (CBS_get_asn1(&ess_cert_id_cbs, &oid_cbs, CBS_ASN1_OBJECT)) { |
| algorithm_nid = OBJ_cbs2nid(&oid_cbs); |
| if (algorithm_nid == NID_undef) { |
| bssl::UniquePtr<char> oid_txt_uniq(CBS_asn1_oid_to_text(&oid_cbs)); |
| absl::string_view oid_txt; |
| if (oid_txt_uniq != nullptr) { |
| oid_txt = oid_txt_uniq.get(); |
| } else { |
| oid_txt = "<unprintable OID>"; |
| } |
| return absl::InvalidArgumentError(absl::StrCat( |
| R"(unrecognized OID in `certs.hashAlgorithm` field of SigningCertificateV2 attribute value; OID value: )", |
| oid_txt)); |
| } |
| } else { |
| ess_cert_id_cbs = original; |
| } |
| auto hash_algorithm = NidToHashAlgorithm(algorithm_nid); |
| if (!hash_algorithm.ok()) { |
| return absl::Status( |
| hash_algorithm.status().code(), |
| absl::StrCat( |
| "unsupported hash algorithm in `certs.hashAlgorithm` field of " |
| "SigningCertificateV2 attribute value: ", |
| hash_algorithm.status().message())); |
| } |
| |
| CBS hash_cbs; |
| if (!CBS_get_asn1(&ess_cert_id_cbs, &hash_cbs, CBS_ASN1_OCTETSTRING)) { |
| return absl::InvalidArgumentError( |
| R"(could not parse `certs.hash` field (DER OCTETSTRING) of SigningCertificateV2 attribute value)"); |
| } |
| out.push_back(EssCertIdV2{ |
| .hash_algorithm = *hash_algorithm, |
| .cert_hash = |
| std::string(reinterpret_cast<const char*>(CBS_data(&hash_cbs)), |
| CBS_len(&hash_cbs))}); |
| } |
| |
| return out; |
| } |
| |
| } // namespace |
| |
| absl::StatusOr<VerifiedTimestamp> TimestampVerifier::VerifyTimestampToken( |
| const ParsedTimestampToken& parsed_token) const { |
| ABSL_ASSIGN_OR_RETURN(auto chain, parsed_token.GetCertificateChain()); |
| |
| // CMS supports more digest algorithms than C2PA, so we need to check that the |
| // digest algorithm is supported. |
| ABSL_RETURN_IF_ERROR(parsed_token.GetMessageImprintHashAlgorithm().status()); |
| |
| if (chain.empty()) { |
| // This would indicate a bug in the CMS library. |
| return absl::InternalError("TSA leaf cert missing"); |
| } |
| |
| absl::Status verify_status = parsed_token.VerifySignature(); |
| if (!verify_status.ok()) { |
| return absl::UnauthenticatedError( |
| absl::StrCat("the timestamp signature could not be verified: ", |
| verify_status.message())); |
| } |
| |
| ABSL_ASSIGN_OR_RETURN(auto signing_certificate_v2_bytes, |
| parsed_token.GetSigningCertificateV2Bytes()); |
| |
| ABSL_ASSIGN_OR_RETURN(auto ess_cert_ids, |
| GetEssCertIds(signing_certificate_v2_bytes)); |
| |
| ABSL_ASSIGN_OR_RETURN(auto tst_info_bytes, parsed_token.GetTstInfoBytes()); |
| CBS cbs; |
| CBS_init(&cbs, reinterpret_cast<const uint8_t*>(tst_info_bytes.data()), |
| tst_info_bytes.size()); |
| |
| absl::Time time; |
| std::string message_imprint_hash; |
| HashAlgorithm hash_algorithm; |
| std::string nonce; |
| ABSL_RETURN_IF_ERROR( |
| ParseTstInfo(cbs, &time, &message_imprint_hash, &hash_algorithm, &nonce)); |
| |
| ABSL_ASSIGN_OR_RETURN( |
| VerifiedTimestamp verified_timestamp, |
| VerifiedTimestamp::Create(time, std::move(chain), |
| std::move(message_imprint_hash), hash_algorithm, |
| nonce)); |
| |
| if (tsa_roots_ == nullptr) { |
| ABSL_LOG_EVERY_N_SEC(INFO, 60) |
| << "Bypassing C2PA TSA certificate trust check."; |
| } else { |
| ABSL_RETURN_IF_ERROR(CheckTsaCertificateTrust( |
| *tsa_roots_, verified_timestamp, ess_cert_ids)); |
| } |
| ABSL_VLOG(1) << "Verified timestamp: " << verified_timestamp.asserted_time(); |
| return verified_timestamp; |
| } |
| |
| } // namespace credentio |