| // 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 |