blob: 5af913ceb86172c3003268bab57cfd3dfbece14b [file] [edit]
// 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