| // 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, | |
| const VersionedVerifiedTimestamp& oldest_trusted_timestamp) { | |
| switch (oldest_trusted_timestamp.version) { | |
| case TimestampVersion::kV1: | |
| return CheckMessageImprintHash( | |
| hash_checker_factory, status_set, oldest_trusted_timestamp.timestamp, | |
| ConstructSigStructure(protected_headers_bstr, external_payload, | |
| "CounterSignature")); | |
| 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> VerifyV1Timestamp( | |
| absl::string_view timestamp_resp, | |
| const CryptoReadHandler& crypto_read_handler, | |
| ValidationStatusSet* status_set) { | |
| auto timestamp_token = ParseTimestampResp(timestamp_resp); | |
| if (!timestamp_token.ok()) { | |
| return RecordInformationalAndReturnStatus(timestamp_token.status(), | |
| status_set); | |
| } | |
| auto verified_timestamp = | |
| crypto_read_handler.VerifyTimestamp(*timestamp_token); | |
| if (!verified_timestamp.ok()) { | |
| return RecordInformationalAndReturnStatus(verified_timestamp.status(), | |
| status_set); | |
| } | |
| return VersionedVerifiedTimestamp{.timestamp = *std::move(verified_timestamp), | |
| .version = TimestampVersion::kV1}; | |
| } | |
| absl::StatusOr<VersionedVerifiedTimestamp> VerifyV2Timestamp( | |
| absl::string_view timestamp_token, | |
| const CryptoReadHandler& crypto_read_handler, | |
| ValidationStatusSet* status_set) { | |
| absl::StatusOr<VerifiedTimestamp> attempt = | |
| crypto_read_handler.VerifyTimestamp(timestamp_token); | |
| if (attempt.ok()) { | |
| return VersionedVerifiedTimestamp{.timestamp = *std::move(attempt), | |
| .version = TimestampVersion::kV2}; | |
| } | |
| return RecordInformationalAndReturnStatus(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()) { | |
| if (absl::IsUnimplemented(protected_headers.status())) { | |
| RecordStatus(status_set, FailureStatusCode::kAlgorithmUnsupported, | |
| {.url = kClaimSignatureLabel, | |
| .explanation = protected_headers.status().message()}); | |
| return std::nullopt; | |
| } | |
| 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(), | |
| *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)->VerifyCoseSignature( | |
| 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> serial_number = | |
| parsed_certs.GetSerialNumberHex(0); | |
| if (serial_number.ok()) { | |
| signature_info.set_cert_serial_number(*serial_number); | |
| } | |
| 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 |