blob: b054880ddb5e9e6e38b7b58c8da12ebc3ee08067 [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 "crypto/default/timestamp_verifier.h"
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/log/absl_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(
auto verified_timestamp_or,
VerifiedTimestamp::Create(time, std::move(chain),
std::move(message_imprint_hash), hash_algorithm,
nonce));
VerifiedTimestamp verified_timestamp = verified_timestamp_or;
if (tsa_roots_ == nullptr) {
ABSL_LOG_EVERY_N_SEC(INFO, 60)
<< "Bypassing C2PA TSA certificate trust check.";
} else {
absl::Status trust_status =
CheckTsaCertificateTrust(*tsa_roots_, verified_timestamp, ess_cert_ids);
ABSL_RETURN_IF_ERROR(trust_status);
}
ABSL_VLOG(1) << "Verified timestamp: " << verified_timestamp.asserted_time();
return verified_timestamp;
}
} // namespace credentio