blob: 055da02b7b16f32ea9f201d285162353d678efdc [file]
// 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/sig_structure.h"
#include <cstdint>
#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/escaping.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "cbor/cbor.h"
#include "cbor/parse.h"
#include "cppbor/cppbor.h"
#include "crypto/algorithms.h"
namespace credentio {
namespace {
// 'alg' in https://datatracker.ietf.org/doc/html/rfc8152#section-3.1
constexpr int kSigningAlgorithmKey = 1;
// See
// https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_obtaining_the_time_stamp
// for more details.
constexpr absl::string_view kTimestampV2HeaderKey = "sigTst2";
// DEPRECATED. DO NOT GENERATE. Contents of 'sigTst' CBOR map.
constexpr absl::string_view kTimestampV1HeaderKey = "sigTst";
// See
// https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_create_a_temporary_claim_and_signature
// for more details about padding.
constexpr absl::string_view kPadHeaderKey = "pad";
// See
// https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_going_back_and_filling_in
// about using `pad2`.
constexpr absl::string_view kPad2HeaderKey = "pad2";
// https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#x509_certificates
constexpr int kCertificateChainKey = 33;
constexpr absl::string_view kCertificateChainDeprecatedKey = "x5chain";
constexpr absl::string_view kOcspValsKey = "ocspVals";
constexpr absl::string_view kRvalsKey = "rVals";
// https://www.iana.org/assignments/cose/cose.xhtml#algorithms
absl::StatusOr<int64_t> GetAlgorithm(SigningAlgorithm e) {
switch (e) {
case SigningAlgorithm::kEs256:
return -7;
case SigningAlgorithm::kEdDsa:
return -8;
case SigningAlgorithm::kEs384:
return -35;
case SigningAlgorithm::kEs512:
return -36;
case SigningAlgorithm::kPs256:
return -37;
case SigningAlgorithm::kPs384:
return -38;
case SigningAlgorithm::kPs512:
return -39;
}
return absl::InvalidArgumentError("Unsupported algorithm");
}
absl::StatusOr<SigningAlgorithm> GetSigningAlgorithm(cbor::MapView headers) {
auto alg = headers.GetInt64(kSigningAlgorithmKey);
if (!alg.ok()) {
return absl::Status(
alg.status().code(),
absl::StrCat("no COSE algorithm specified in protected headers: ",
alg.status().message()));
}
// https://www.iana.org/assignments/cose/cose.xhtml#algorithms
ABSL_DVLOG(1) << ".cose_alg = " << *alg << ",";
switch (*alg) {
case -7:
return SigningAlgorithm::kEs256;
case -35:
return SigningAlgorithm::kEs384;
case -36:
return SigningAlgorithm::kEs512;
case -37:
return SigningAlgorithm::kPs256;
case -38:
return SigningAlgorithm::kPs384;
case -39:
return SigningAlgorithm::kPs512;
case -8:
return SigningAlgorithm::kEdDsa;
}
return absl::InvalidArgumentError(
absl::StrCat("unsupported COSE algorithm: ", *alg));
}
absl::StatusOr<std::vector<std::string>> GetCertificateChain(
cbor::MapView headers) {
// Check if there is a single certificate in the chain. In this case, label 33
// of the header map (or "x5chain" key) is a bstr.
// Claim generators should only use 33 (integer) key but the validators should
// both accept 33 and "x5chain" keys. If both 33 (integer) and "x5chain"
// (string) keys are present, the label 33 should be used.
auto certificate_chain = headers.GetByteString(kCertificateChainKey);
if (!certificate_chain.ok()) {
certificate_chain = headers.GetByteString(kCertificateChainDeprecatedKey);
}
if (certificate_chain.ok()) {
return std::vector<std::string>({std::string(*certificate_chain)});
}
// Check if there is a list of certificates in the chain. In this case, label
// 33 of the header map (or "x5chain" key) is an array of bstrs.
auto certificate_chain_array = headers.GetArray(kCertificateChainKey);
if (!certificate_chain_array.ok()) {
certificate_chain_array = headers.GetArray(kCertificateChainDeprecatedKey);
}
if (certificate_chain_array.ok()) {
std::vector<std::string> certificate_chain;
for (int i = 0; i < certificate_chain_array->size(); ++i) {
auto certificate_chain_element =
certificate_chain_array->GetByteString(i);
if (!certificate_chain_element.ok()) {
return absl::InvalidArgumentError(
absl::StrCat("Failed to parse the certificate chain element: ",
certificate_chain_element.status()));
}
ABSL_DVLOG(1) << ".certs_b64[" << i << "] = R\"("
<< absl::Base64Escape(*certificate_chain_element) << ")\",";
certificate_chain.push_back(std::string(*certificate_chain_element));
}
return certificate_chain;
}
return std::vector<std::string>();
}
std::vector<std::string> DecodeOcspResponses(cbor::MapView map) {
auto rvals = map.GetOptionalMap(kRvalsKey);
if (!rvals.has_value()) {
return {};
}
auto ocsp_responses = rvals->GetArray(kOcspValsKey);
if (!ocsp_responses.ok()) {
return {};
}
std::vector<std::string> v;
v.reserve(ocsp_responses->size());
for (int i = 0; i < ocsp_responses->size(); ++i) {
auto ocsp_response = ocsp_responses->GetByteString(i);
if (ocsp_response.ok()) {
v.push_back(std::string(*ocsp_response));
}
}
return v;
}
} // namespace
absl::StatusOr<std::string> EncodeProtectedHeader(
const ProtectedHeader& protected_header) {
cppbor::Map m;
ABSL_ASSIGN_OR_RETURN(auto alg, GetAlgorithm(protected_header.alg));
m.add(cppbor::Uint(kSigningAlgorithmKey), cppbor::Nint(alg));
if (protected_header.certificate_chain.size() == 1) {
m.add(cppbor::Uint(kCertificateChainKey),
cppbor::Bstr(protected_header.certificate_chain[0]));
} else {
cppbor::Array chain;
for (const auto& cert : protected_header.certificate_chain) {
chain.add(cppbor::Bstr(cert));
}
m.add(cppbor::Uint(kCertificateChainKey), std::move(chain));
}
return m.toString();
}
absl::StatusOr<ProtectedHeader> DecodeProtectedHeader(absl::string_view cbor) {
ProtectedHeader protected_header;
ABSL_ASSIGN_OR_RETURN(auto res, cbor::Parse(cbor));
ABSL_ASSIGN_OR_RETURN(auto map, res->AsMap());
ABSL_ASSIGN_OR_RETURN(auto alg, GetSigningAlgorithm(map));
protected_header.alg = alg;
ABSL_ASSIGN_OR_RETURN(auto certificate_chain, GetCertificateChain(map));
protected_header.certificate_chain = std::move(certificate_chain);
return protected_header;
}
void EncodeTstToken(const TstToken& tst_token, cppbor::Map& m) {
m.add(cppbor::Tstr("val"), cppbor::Bstr(tst_token.val));
}
absl::StatusOr<TstToken> DecodeTstToken(cbor::MapView map) {
ABSL_ASSIGN_OR_RETURN(auto val, map.GetByteString("val"));
return TstToken{.val = std::string(val)};
}
void EncodeTstContainer(const TstContainer& tst_container, cppbor::Map& m) {
cppbor::Array tstTokens;
for (const auto& tstToken : tst_container.tst_tokens) {
cppbor::Map tstTokenMap;
EncodeTstToken(tstToken, tstTokenMap);
tstTokens.add(std::move(tstTokenMap));
}
m.add(cppbor::Tstr("tstTokens"), std::move(tstTokens));
}
absl::StatusOr<TstContainer> DecodeTstContainer(cbor::MapView map) {
ABSL_ASSIGN_OR_RETURN(auto tst_tokens, map.GetArray("tstTokens"));
TstContainer tst_container;
for (int i = 0; i < tst_tokens.size(); ++i) {
ABSL_ASSIGN_OR_RETURN(auto tst_token, tst_tokens.GetMap(i));
ABSL_ASSIGN_OR_RETURN(auto tst_token_decoded, DecodeTstToken(tst_token));
tst_container.tst_tokens.push_back(std::move(tst_token_decoded));
}
return tst_container;
}
void EncodeUnprotectedHeader(const UnprotectedHeader& unprotected_header,
cppbor::Map& m) {
if (unprotected_header.sig_tst2.has_value()) {
cppbor::Map tstContainer;
EncodeTstContainer(*unprotected_header.sig_tst2, tstContainer);
m.add(cppbor::Tstr(kTimestampV2HeaderKey), std::move(tstContainer));
}
if (!unprotected_header.ocsp_responses.empty()) {
cppbor::Array ocsp_responses;
for (const auto& ocsp_response : unprotected_header.ocsp_responses) {
ocsp_responses.add(cppbor::Bstr(ocsp_response));
}
cppbor::Map rvals;
rvals.add(cppbor::Tstr(kOcspValsKey), std::move(ocsp_responses));
m.add(cppbor::Tstr(kRvalsKey), std::move(rvals));
}
if (unprotected_header.pad.has_value()) {
m.add(cppbor::Tstr(kPadHeaderKey), cppbor::Bstr(*unprotected_header.pad));
}
if (unprotected_header.pad2.has_value()) {
m.add(cppbor::Tstr(kPad2HeaderKey), cppbor::Bstr(*unprotected_header.pad2));
}
}
absl::StatusOr<UnprotectedHeader> DecodeUnprotectedHeader(cbor::MapView map) {
UnprotectedHeader unprotected_header;
auto sig_tst2 = map.GetOptionalMap(kTimestampV2HeaderKey);
if (sig_tst2.has_value()) {
ABSL_ASSIGN_OR_RETURN(unprotected_header.sig_tst2,
DecodeTstContainer(*sig_tst2));
}
auto sig_tst = map.GetOptionalMap(kTimestampV1HeaderKey);
if (sig_tst.has_value()) {
ABSL_ASSIGN_OR_RETURN(unprotected_header.sig_tst,
DecodeTstContainer(*sig_tst));
}
unprotected_header.ocsp_responses = DecodeOcspResponses(map);
auto pad = map.GetOptionalByteString(kPadHeaderKey);
if (pad.has_value()) {
unprotected_header.pad = std::string(*pad);
}
auto pad2 = map.GetOptionalByteString(kPad2HeaderKey);
if (pad2.has_value()) {
unprotected_header.pad2 = std::string(*pad2);
}
auto certificate_chain = GetCertificateChain(map);
if (certificate_chain.ok()) {
unprotected_header.certificate_chain = *std::move(certificate_chain);
}
return unprotected_header;
}
std::string EncodeSig1Structure(const Sig1Structure& sig_structure) {
cppbor::Array s;
s.add(cppbor::Tstr(sig_structure.context));
s.add(cppbor::Bstr(sig_structure.body_protected));
s.add(cppbor::Bstr(sig_structure.external_aad));
s.add(cppbor::Bstr(sig_structure.payload));
return s.toString();
}
std::string EncodeCoseSign1TaggedStructure(const CoseSign1TaggedStructure& s) {
cppbor::Array array;
// Add protected_header.
array.add(cppbor::Bstr(s.protected_header));
// Add unprotected_header.
cppbor::Map unprotected_header_map;
EncodeUnprotectedHeader(s.unprotected_header, unprotected_header_map);
array.add(std::move(unprotected_header_map));
// Add payload.
if (s.payload.has_value()) {
array.add(cppbor::Bstr(*s.payload));
} else {
array.add(cppbor::Null());
}
// Add signature.
array.add(cppbor::Bstr(s.signature));
// tag 18 see https://datatracker.ietf.org/doc/html/rfc8152#section-4.2
cppbor::SemanticTag tagged_cbor(18, std::move(array));
return tagged_cbor.toString();
}
absl::StatusOr<CoseSign1TaggedStructure> DecodeCoseSign1TaggedStructure(
absl::string_view cbor) {
// COSE_Sign1 structure is a CBOR array of size 4.
// CDDL schema for the case of a single signer is:
//
// COSE_Sign1 = [
// protected: bstr,
// unprotected: map,
// payload: bstr / nil,
// signature: bstr
// ]
//
// See https://datatracker.ietf.org/doc/html/rfc9052 for more
// details.
ABSL_ASSIGN_OR_RETURN(auto res, cbor::Parse(cbor));
ABSL_ASSIGN_OR_RETURN(auto array, res->AsArray());
CoseSign1TaggedStructure s;
ABSL_ASSIGN_OR_RETURN(s.protected_header, array.GetByteString(0));
ABSL_ASSIGN_OR_RETURN(auto unprotect_header_cbor, array.GetMap(1));
ABSL_ASSIGN_OR_RETURN(s.unprotected_header,
DecodeUnprotectedHeader(unprotect_header_cbor));
ABSL_ASSIGN_OR_RETURN(auto is_payload_null, array.IsNull(2));
if (!is_payload_null) {
ABSL_ASSIGN_OR_RETURN(auto payload, array.GetByteString(2));
s.payload.emplace(payload);
}
// In COSE standard, there can also be an array of signatures instead of a
// single signature, if there are multiple signers. However, in C2PA we will
// always have a single signer.
ABSL_ASSIGN_OR_RETURN(auto signature, array.GetByteString(3));
s.signature = std::string(signature);
return s;
}
std::string CoseSign1TaggedStructure::signature_bstr() const {
return cppbor::Bstr(signature).toString();
}
} // namespace credentio