| // 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 "cose/cose_sign1.h" |
| |
| #include <cstdint> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/base/nullability.h" |
| #include "absl/log/check.h" |
| #include "absl/log/log.h" |
| #include "absl/memory/memory.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/strings/substitute.h" |
| #include "absl/time/clock_interface.h" |
| #include "absl/time/time.h" |
| #include "absl/types/span.h" |
| #include "constants/labels.h" |
| #include "constants/status_codes.h" |
| #include "cose/ocsp_verifier.h" |
| #include "cose/sig_structure.h" |
| #include "cose/validation_status_util.h" |
| #include "crypto/crypto_read_handler.h" |
| #include "crypto/hash.h" |
| #include "google/protobuf/timestamp.pb.h" |
| #include "proto/assurance_level.pb.h" |
| #include "proto/signature_info.pb.h" |
| #include "proto/validation_status.pb.h" |
| #include "tsp/timestamp_parsing.h" |
| #include "tsp/verified_timestamp.h" |
| |
| namespace credentio { |
| namespace { |
| |
| std::vector<absl::string_view> StringViewVector( |
| absl::Span<const std::string> v) { |
| std::vector<absl::string_view> result; |
| result.reserve(v.size()); |
| for (const auto& e : v) { |
| result.push_back(e); |
| } |
| return result; |
| } |
| |
| // Validators shall accept the header from either the protected or unprotected |
| // bucket, to maintain compatibility with previous versions of this |
| // specification. In compliance with Section 14.2, “Identity of Signers”, if |
| // this header appears in both the protected and unprotected buckets with the |
| // same label, a validator shall reject the claim signature as malformed due to |
| // the presence of multiple credentials. |
| // See |
| // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#x509_certificates. |
| absl::StatusOr<std::vector<std::string>> GetCertificateChain( |
| const ProtectedHeader& protected_header, |
| const UnprotectedHeader& unprotected_header) { |
| if (!protected_header.certificate_chain.empty() && |
| !unprotected_header.certificate_chain.empty()) { |
| return absl::InvalidArgumentError( |
| "Certificate chain is present in both protected and unprotected " |
| "headers"); |
| } |
| if (!protected_header.certificate_chain.empty()) { |
| return protected_header.certificate_chain; |
| } |
| return unprotected_header.certificate_chain; |
| } |
| |
| absl::StatusOr<absl::string_view> ExtractTimestampHeaderValue( |
| const TstContainer& tst_container) { |
| // The `tstTokens` array is expected to contain a single token. If this is |
| // not the case, we need to return a `timeStamp.malformed` status and ignore |
| // the timestamp. See |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_validate_the_time_stamp. |
| if (tst_container.tst_tokens.size() != 1) { |
| return absl::InvalidArgumentError( |
| "there are zero or multiple timestamp tokens in tstTokens"); |
| } |
| return tst_container.tst_tokens[0].val; |
| } |
| |
| std::string ConstructSigStructure(absl::string_view body_protected, |
| absl::string_view external_payload, |
| absl::string_view context) { |
| return EncodeSig1Structure(Sig1Structure{ |
| .context = std::string(context), |
| .body_protected = std::string(body_protected), |
| .external_aad = "", |
| .payload = std::string(external_payload), |
| }); |
| } |
| |
| bool CheckMessageImprintHash(const HashCheckerFactory& hash_checker_factory, |
| ValidationStatusSet* status_set, |
| const VerifiedTimestamp& oldest_trusted_timestamp, |
| absl::string_view timestamp_sig_structure) { |
| absl::StatusOr<std::unique_ptr<HashChecker>> messageimprint_hash_checker = |
| hash_checker_factory.Create( |
| oldest_trusted_timestamp.message_imprint_hash_algorithm()); |
| if (!messageimprint_hash_checker.ok()) { |
| RecordStatus(status_set, InformationalStatusCode::kTimestampMalformed, |
| {.url = kClaimSignatureLabel, |
| .explanation = absl::StrCat( |
| "cannot verify MessageImprint hash; failed to create " |
| "hash checker: ", |
| messageimprint_hash_checker.status())}); |
| return false; |
| } |
| (*messageimprint_hash_checker)->Update(timestamp_sig_structure); |
| if (!(*messageimprint_hash_checker) |
| ->Check(oldest_trusted_timestamp.message_imprint_hash())) { |
| RecordStatus(status_set, InformationalStatusCode::kTimestampMismatch, |
| {.url = kClaimSignatureLabel}); |
| return false; |
| } |
| return true; |
| } |
| |
| // The format of a timestamp. |
| // See v1/v2 descriptions in |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_storing_the_time_stamp |
| enum class TimestampVersion { |
| // A timestamp in the "v1" format, normally found in `sigTst`. |
| kV1, |
| // A timestamp in the "v2" format, normally found in `sigTst2`. |
| kV2, |
| }; |
| |
| struct VersionedVerifiedTimestamp { |
| VerifiedTimestamp timestamp; |
| TimestampVersion version; |
| }; |
| |
| bool MessageImprintMatch( |
| absl::string_view& external_payload, |
| const HashCheckerFactory& hash_checker_factory, |
| ValidationStatusSet* status_set, absl::string_view protected_headers_bstr, |
| absl::string_view signature_bstr, absl::string_view signature, |
| const VersionedVerifiedTimestamp& oldest_trusted_timestamp) { |
| switch (oldest_trusted_timestamp.version) { |
| case TimestampVersion::kV1: { |
| ValidationStatusSet v1_status; |
| if (CheckMessageImprintHash( |
| hash_checker_factory, &v1_status, |
| oldest_trusted_timestamp.timestamp, |
| ConstructSigStructure(protected_headers_bstr, external_payload, |
| "CounterSignature"))) { |
| status_set->MergeFrom(v1_status); |
| return true; |
| } |
| |
| ValidationStatusSet v2_status; |
| if (CheckMessageImprintHash( |
| hash_checker_factory, &v2_status, |
| oldest_trusted_timestamp.timestamp, |
| ConstructSigStructure(protected_headers_bstr, signature, |
| "CounterSignature"))) { |
| status_set->MergeFrom(v2_status); |
| RecordStatus( |
| status_set, |
| InformationalStatusCode::kTimestampV2MessageImprintInV1Format, |
| {.url = kClaimSignatureLabel}); |
| return true; |
| } |
| |
| status_set->MergeFrom(v1_status); |
| return false; |
| } |
| case TimestampVersion::kV2: |
| return CheckMessageImprintHash( |
| hash_checker_factory, status_set, oldest_trusted_timestamp.timestamp, |
| ConstructSigStructure(protected_headers_bstr, signature_bstr, |
| "CounterSignature")); |
| } |
| LOG(DFATAL) << "Fell through end of exhaustive switch statement."; |
| return false; |
| } |
| |
| [[nodiscard]] std::optional<SignatureInfo> RecordInvalidSigningCredential( |
| absl::string_view explanation, ValidationStatusSet* status_set) { |
| RecordStatus(status_set, FailureStatusCode::kSigningCredentialInvalid, |
| {.url = kClaimSignatureLabel, .explanation = explanation}); |
| return std::nullopt; |
| } |
| |
| InformationalStatusCode GetTimestampStatusCode(absl::StatusCode status_code) { |
| switch (status_code) { |
| case absl::StatusCode::kUnauthenticated: |
| return InformationalStatusCode::kTimestampUntrusted; |
| case absl::StatusCode::kOutOfRange: |
| return InformationalStatusCode::kTimestampOutsideValidity; |
| case absl::StatusCode::kInvalidArgument: |
| default: |
| return InformationalStatusCode::kTimestampMalformed; |
| } |
| } |
| |
| absl::Status RecordInformationalAndReturnStatus( |
| absl::Status status, ValidationStatusSet* status_set) { |
| RecordStatus(status_set, GetTimestampStatusCode(status.code()), |
| {.url = kClaimSignatureLabel, .explanation = status.message()}); |
| return status; |
| } |
| |
| absl::StatusOr<VersionedVerifiedTimestamp> VerifyV1TimestampInternal( |
| absl::string_view timestamp_resp, |
| const CryptoReadHandler& crypto_read_handler) { |
| ABSL_ASSIGN_OR_RETURN(auto timestamp_token, |
| ParseTimestampResp(timestamp_resp)); |
| ABSL_ASSIGN_OR_RETURN(auto verified_timestamp, |
| crypto_read_handler.VerifyTimestamp(timestamp_token)); |
| return VersionedVerifiedTimestamp{.timestamp = std::move(verified_timestamp), |
| .version = TimestampVersion::kV1}; |
| } |
| |
| absl::StatusOr<VersionedVerifiedTimestamp> VerifyV1Timestamp( |
| absl::string_view timestamp_resp, |
| const CryptoReadHandler& crypto_read_handler, |
| ValidationStatusSet* status_set) { |
| auto result = VerifyV1TimestampInternal(timestamp_resp, crypto_read_handler); |
| if (!result.ok()) { |
| return RecordInformationalAndReturnStatus(result.status(), status_set); |
| } |
| return result; |
| } |
| |
| absl::StatusOr<VersionedVerifiedTimestamp> VerifyV2Timestamp( |
| absl::string_view timestamp_token, |
| const CryptoReadHandler& crypto_read_handler, |
| ValidationStatusSet* status_set) { |
| absl::StatusOr<VerifiedTimestamp> default_attempt = |
| crypto_read_handler.VerifyTimestamp(timestamp_token); |
| |
| if (default_attempt.ok()) { |
| return VersionedVerifiedTimestamp{.timestamp = *std::move(default_attempt), |
| .version = TimestampVersion::kV2}; |
| } |
| |
| absl::StatusOr<VersionedVerifiedTimestamp> fallback_attempt = |
| VerifyV1TimestampInternal(timestamp_token, crypto_read_handler); |
| |
| if (fallback_attempt.ok()) { |
| RecordStatus(status_set, |
| InformationalStatusCode::kTimestampV1FormatInV2Header, |
| {.url = kClaimSignatureLabel}); |
| return fallback_attempt; |
| } |
| |
| return RecordInformationalAndReturnStatus(default_attempt.status(), |
| status_set); |
| } |
| |
| // Returns the oldest valid timestamp found in the `sigTst2` or `sigTst` |
| // header after building a trust chain from the certificate to an entry in the |
| // validator's trust anchors. |
| // |
| // Returns `std::nullopt` if the timestamp header is not present. |
| // Records a `timeStamp.untrusted` C2PA status to the tracker and returns |
| // an `std::nullopt` if a trust chain cannot be built from the TSA's |
| // certificate to one of the validator's trust anchors. |
| std::optional<VersionedVerifiedTimestamp> VerifyAndGetOldestTrustedTimestamp( |
| const CryptoReadHandler& crypto_read_handler, |
| const UnprotectedHeader& unprotected_header, |
| ValidationStatusSet* status_set) { |
| bool is_v2 = true; |
| auto timestamp_container = unprotected_header.sig_tst2; |
| if (!timestamp_container.has_value()) { |
| is_v2 = false; |
| timestamp_container = unprotected_header.sig_tst; |
| } |
| if (!timestamp_container.has_value()) { |
| return std::nullopt; |
| } |
| |
| auto timestamp_header_value = |
| ExtractTimestampHeaderValue(*timestamp_container); |
| if (!timestamp_header_value.ok()) { |
| DVLOG(1) << timestamp_header_value.status(); |
| RecordStatus(status_set, InformationalStatusCode::kTimestampMalformed, |
| {.url = kClaimSignatureLabel, |
| .explanation = timestamp_header_value.status().ToString()}); |
| return std::nullopt; |
| } |
| |
| absl::StatusOr<VersionedVerifiedTimestamp> verified_timestamp = |
| is_v2 ? VerifyV2Timestamp(*timestamp_header_value, crypto_read_handler, |
| status_set) |
| : VerifyV1Timestamp(*timestamp_header_value, crypto_read_handler, |
| status_set); |
| |
| if (!verified_timestamp.ok()) { |
| DVLOG(1) << verified_timestamp.status(); |
| // VerifyV[12]Timestamp would have already reported any issues to `tracker`. |
| return std::nullopt; |
| } |
| |
| return *verified_timestamp; |
| } |
| |
| absl::StatusOr<google::protobuf::Timestamp> EncodeGoogleApiProto(absl::Time t) { |
| const int64_t s = absl::ToUnixSeconds(t); |
| if (s < -62135596800 || s > 253402300799) { |
| return absl::OutOfRangeError("Timestamp is out of range"); |
| } |
| google::protobuf::Timestamp proto; |
| proto.set_seconds(s); |
| proto.set_nanos((t - absl::FromUnixSeconds(s)) / absl::Nanoseconds(1)); |
| return proto; |
| } |
| |
| } // namespace |
| |
| std::unique_ptr<CoseSign1Verifier> CoseSign1Verifier::Create( |
| const CryptoReadHandler* absl_nonnull crypto_read_handler, |
| const HashCheckerFactory* absl_nonnull hash_checker_factory, |
| absl::Clock* absl_nonnull clock, Options options) { |
| return absl::WrapUnique(new CoseSign1Verifier( |
| crypto_read_handler, hash_checker_factory, clock, options)); |
| } |
| |
| std::optional<SignatureInfo> CoseSign1Verifier::Verify( |
| const CoseSign1TaggedStructure& cose_sign1, |
| absl::string_view external_payload, ValidationStatusSet* status_set) const { |
| // STEP 0: Get the certificate chain and parse the certificates. |
| auto protected_headers = DecodeProtectedHeader(cose_sign1.protected_header); |
| if (!protected_headers.ok()) { |
| return RecordInvalidSigningCredential( |
| absl::StrCat("cannot decode protected header: ", |
| protected_headers.status().message()), |
| status_set); |
| } |
| auto certificate_chain = |
| GetCertificateChain(*protected_headers, cose_sign1.unprotected_header); |
| if (!certificate_chain.ok()) { |
| return RecordInvalidSigningCredential(certificate_chain.status().message(), |
| status_set); |
| } |
| if (certificate_chain->empty()) { |
| return RecordInvalidSigningCredential("no claim signer certificates", |
| status_set); |
| } |
| auto certs = crypto_read_handler_.ParseCertificatesDer( |
| StringViewVector(*certificate_chain)); |
| if (!certs.ok()) { |
| return RecordInvalidSigningCredential( |
| absl::StrCat("malformed claim signer certificates: ", |
| certs.status().message()), |
| status_set); |
| } |
| |
| // STEP 1: Verify the timestamp. |
| // Obtain and verify the oldest verified timestamp from sig_tst2 or sig_tst |
| // in the unprotected header. |
| std::optional<VersionedVerifiedTimestamp> oldest_trusted_timestamp = |
| VerifyAndGetOldestTrustedTimestamp( |
| crypto_read_handler_, cose_sign1.unprotected_header, status_set); |
| // Check that the timestamp matches the signature. |
| bool check_message_imprint = true; |
| if (oldest_trusted_timestamp.has_value()) { |
| if (check_message_imprint && |
| !MessageImprintMatch(external_payload, hash_checker_factory_, |
| status_set, cose_sign1.protected_header, |
| cose_sign1.signature_bstr(), cose_sign1.signature, |
| *oldest_trusted_timestamp)) { |
| oldest_trusted_timestamp = std::nullopt; |
| } else { |
| RecordStatus(status_set, SuccessStatusCode::kTimestampValidated, |
| {.url = kClaimSignatureLabel}); |
| RecordStatus(status_set, SuccessStatusCode::kTimestampTrusted, |
| {.url = kClaimSignatureLabel}); |
| } |
| } |
| |
| // STEP 2: Verify the signer's certificate chain. |
| absl::Time content_time = |
| oldest_trusted_timestamp.has_value() |
| ? oldest_trusted_timestamp->timestamp.asserted_time() |
| : clock_.TimeNow(); |
| absl::StatusOr<std::vector<std::string>> trust_chain = |
| (*certs)->VerifyClaimSignerTrust(content_time); |
| if (!trust_chain.ok()) { |
| switch (trust_chain.status().code()) { |
| case absl::StatusCode::kUnauthenticated: |
| RecordStatus(status_set, FailureStatusCode::kSigningCredentialUntrusted, |
| {.url = kClaimSignatureLabel, |
| .explanation = trust_chain.status().message()}); |
| return std::nullopt; |
| case absl::StatusCode::kOutOfRange: |
| RecordStatus(status_set, |
| FailureStatusCode::kClaimSignatureOutsideValidity, |
| {.url = kClaimSignatureLabel, |
| .explanation = trust_chain.status().message()}); |
| return std::nullopt; |
| case absl::StatusCode::kUnavailable: |
| // Trust list unavailable? |
| RecordStatus(status_set, FailureStatusCode::kGoogleInternalError, |
| {.url = kClaimSignatureLabel, |
| .explanation = trust_chain.status().message()}); |
| return std::nullopt; |
| default: |
| return RecordInvalidSigningCredential(trust_chain.status().message(), |
| status_set); |
| } |
| } |
| |
| // STEP 3: Verify the revocation information of the signer's cert chain. |
| // We first check the certificate revocation list, and then the stapled OCSP |
| // response as described in |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_validate_the_credential_revocation_information. |
| // Online OCSP checks are not supported yet. |
| if (!options_.check_ocsp_responses) { |
| RecordSkippedOcspCheck(status_set); |
| } else if (!oldest_trusted_timestamp.has_value()) { |
| // Trusted timestamp is required for verifying stapled OCSP responses |
| // (https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#ocsp_stapled). |
| RecordSkippedOcspCheck(status_set); |
| } else { |
| OcspVerifier ocsp_verifier(&crypto_read_handler_); |
| if (!ocsp_verifier.VerifyOcspResponses( |
| cose_sign1.unprotected_header.ocsp_responses, *trust_chain, |
| oldest_trusted_timestamp->timestamp.asserted_time(), status_set)) { |
| // OCSP response indicates that a certificate in the chain is revoked. |
| return std::nullopt; |
| } |
| } |
| |
| RecordStatus(status_set, SuccessStatusCode::kSigningCredentialTrusted, |
| {.url = kClaimSignatureLabel}); |
| RecordStatus(status_set, SuccessStatusCode::kClaimSignatureInsideValidity, |
| {.url = kClaimSignatureLabel}); |
| |
| // STEP 4: Verify the signature. |
| if (options_.verify_signature) { |
| auto signing_algorithm = protected_headers->alg; |
| if (absl::Status status = (*certs)->VerifySignature( |
| cose_sign1.signature, |
| ConstructSigStructure(cose_sign1.protected_header, external_payload, |
| "Signature1"), |
| signing_algorithm); |
| !status.ok()) { |
| RecordStatus(status_set, FailureStatusCode::kClaimSignatureMismatch, |
| {.url = kClaimSignatureLabel, |
| .explanation = absl::Substitute( |
| "claim signature validation failed; alg=$0; status=$1", |
| signing_algorithm, status.ToString())}); |
| return std::nullopt; |
| } |
| RecordStatus(status_set, SuccessStatusCode::kClaimSignatureValidated, |
| {.url = kClaimSignatureLabel}); |
| } |
| |
| // Finally, construct and return the SignatureInfo. |
| SignatureInfo signature_info; |
| const ParsedCertificates& parsed_certs = **certs; |
| if (oldest_trusted_timestamp.has_value()) { |
| absl::StatusOr<google::protobuf::Timestamp> time_proto = |
| EncodeGoogleApiProto( |
| oldest_trusted_timestamp->timestamp.asserted_time()); |
| if (time_proto.ok()) { |
| *signature_info.mutable_timestamping_time() = *std::move(time_proto); |
| } else { |
| RecordStatus(status_set, InformationalStatusCode::kTimestampOutOfRange, |
| {.url = kClaimSignatureLabel}); |
| } |
| } |
| absl::StatusOr<std::string> subject = parsed_certs.GetSubject(0); |
| if (subject.ok()) { |
| signature_info.set_issuer(*subject); |
| } |
| absl::StatusOr<std::string> issuer = parsed_certs.GetIssuer(0); |
| if (issuer.ok()) { |
| signature_info.set_certificate_issuer(*issuer); |
| } |
| absl::StatusOr<std::string> assurance_level = |
| parsed_certs.GetAssuranceLevel(0); |
| if (assurance_level.ok()) { |
| if (*assurance_level == "1.3.6.1.4.1.62558.3.10") { |
| signature_info.set_assurance_level(ASSURANCE_LEVEL1); |
| } else if (*assurance_level == "1.3.6.1.4.1.62558.3.20") { |
| signature_info.set_assurance_level(ASSURANCE_LEVEL2); |
| } |
| } |
| absl::StatusOr<std::string> conforming_product_id = |
| parsed_certs.GetConformingProductId(0); |
| if (conforming_product_id.ok()) { |
| signature_info.set_conforming_product_id(*conforming_product_id); |
| } |
| return signature_info; |
| }; |
| |
| } // namespace credentio |