| // 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 <string> |
| |
| #include "absl/log/check.h" |
| #include "absl/status/status.h" |
| #include "absl/status/status_macros.h" |
| #include "absl/status/status_matchers.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/escaping.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/strings/substitute.h" |
| #include "cbor/cbor.h" |
| #include "cbor/parse.h" |
| #include "crypto/algorithms.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "testing/cbor_utils.h" |
| |
| namespace credentio { |
| namespace { |
| |
| using ::absl_testing::IsOkAndHolds; |
| using ::absl_testing::StatusIs; |
| |
| absl::StatusOr<Sig1Structure> DecodeSig1Structure(absl::string_view cbor) { |
| ABSL_ASSIGN_OR_RETURN(auto res, cbor::Parse(cbor)); |
| ABSL_ASSIGN_OR_RETURN(auto array, res->AsArray()); |
| Sig1Structure s; |
| |
| auto context = array.GetString(0); |
| if (!context.ok()) { |
| return absl::Status( |
| context.status().code(), |
| absl::StrCat("could not get `context`: ", context.status().message())); |
| } |
| s.context = std::string(*context); |
| |
| auto body_protected = array.GetByteString(1); |
| if (!body_protected.ok()) { |
| return absl::Status(body_protected.status().code(), |
| absl::StrCat("could not get `body_protected`: ", |
| body_protected.status().message())); |
| } |
| s.body_protected = std::string(*body_protected); |
| |
| auto external_aad = array.GetByteString(2); |
| if (!external_aad.ok()) { |
| return absl::Status(external_aad.status().code(), |
| absl::StrCat("could not get `external_aad`: ", |
| external_aad.status().message())); |
| } |
| s.external_aad = std::string(*external_aad); |
| |
| auto payload = array.GetByteString(3); |
| if (!payload.ok()) { |
| return absl::Status( |
| payload.status().code(), |
| absl::StrCat("could not get `payload`: ", payload.status().message())); |
| } |
| s.payload = std::string(*payload); |
| |
| return s; |
| } |
| |
| std::string Base64Unescape(absl::string_view base64) { |
| std::string bytes; |
| CHECK(absl::Base64Unescape(base64, &bytes)); |
| return bytes; |
| } |
| |
| // Asserts the value is a byte string, and equals to the `hex` argument in hex |
| // representation. |
| MATCHER_P(IsBytesEqHexStr, hex, "") { |
| auto hex_arg = absl::BytesToHexString(arg); |
| *result_listener << absl::StrCat("Hex strings ", hex, " and ", hex_arg, |
| " are not equal"); |
| return hex == hex_arg; |
| } |
| |
| TEST(SigStructureTest, Sig1StructureTestVectorSuccess) { |
| const Sig1Structure payload = { |
| .context = "Signature1", |
| .body_protected = "body_protected", |
| .external_aad = "external_aad", |
| .payload = "payload", |
| }; |
| |
| const std::string cbor = EncodeSig1Structure(payload); |
| EXPECT_THAT( |
| cbor, IsBytesEqHexStr( |
| "846a5369676e6174757265314e626f64795f70726f7465637465644c657874" |
| "65726e616c5f616164477061796c6f6164")); |
| |
| EXPECT_THAT(DecodeSig1Structure(cbor), IsOkAndHolds(payload)); |
| } |
| |
| TEST(SigStructureTest, ProtectedHeaderEdDsa) { |
| const ProtectedHeader payload = { |
| .alg = SigningAlgorithm::kEdDsa, |
| .certificate_chain = {"abcd"}, |
| }; |
| |
| auto cbor = EncodeProtectedHeader(payload); |
| ASSERT_TRUE(cbor.ok()); |
| EXPECT_THAT(DecodeProtectedHeader(*cbor), IsOkAndHolds(payload)); |
| } |
| |
| TEST(SigStructureTest, ProtectedHeaderEs256) { |
| const ProtectedHeader payload = { |
| .alg = SigningAlgorithm::kEs256, |
| .certificate_chain = {"abcd"}, |
| }; |
| |
| auto cbor = EncodeProtectedHeader(payload); |
| ASSERT_TRUE(cbor.ok()); |
| EXPECT_THAT(DecodeProtectedHeader(*cbor), IsOkAndHolds(payload)); |
| } |
| TEST(SigStructureTest, ProtectedHeaderEs384) { |
| const ProtectedHeader payload = { |
| .alg = SigningAlgorithm::kEs384, |
| .certificate_chain = {"abcd"}, |
| }; |
| |
| auto cbor = EncodeProtectedHeader(payload); |
| ASSERT_TRUE(cbor.ok()); |
| EXPECT_THAT(DecodeProtectedHeader(*cbor), IsOkAndHolds(payload)); |
| } |
| |
| TEST(SigStructureTest, ProtectedHeaderEs512) { |
| const ProtectedHeader payload = { |
| .alg = SigningAlgorithm::kEs512, |
| .certificate_chain = {"abcd"}, |
| }; |
| |
| auto cbor = EncodeProtectedHeader(payload); |
| ASSERT_TRUE(cbor.ok()); |
| EXPECT_THAT(DecodeProtectedHeader(*cbor), IsOkAndHolds(payload)); |
| } |
| |
| TEST(SigStructureTest, ProtectedHeaderPs256) { |
| const ProtectedHeader payload = { |
| .alg = SigningAlgorithm::kPs256, |
| .certificate_chain = {"abcd"}, |
| }; |
| |
| auto cbor = EncodeProtectedHeader(payload); |
| ASSERT_TRUE(cbor.ok()); |
| EXPECT_THAT(DecodeProtectedHeader(*cbor), IsOkAndHolds(payload)); |
| } |
| |
| TEST(SigStructureTest, ProtectedHeaderPs384) { |
| const ProtectedHeader payload = { |
| .alg = SigningAlgorithm::kPs384, |
| .certificate_chain = {"abcd"}, |
| }; |
| |
| auto cbor = EncodeProtectedHeader(payload); |
| ASSERT_TRUE(cbor.ok()); |
| EXPECT_THAT(DecodeProtectedHeader(*cbor), IsOkAndHolds(payload)); |
| } |
| |
| TEST(SigStructureTest, ProtectedHeaderPs512) { |
| const ProtectedHeader payload = { |
| .alg = SigningAlgorithm::kPs512, |
| .certificate_chain = {"abcd"}, |
| }; |
| |
| auto cbor = EncodeProtectedHeader(payload); |
| ASSERT_TRUE(cbor.ok()); |
| EXPECT_THAT(DecodeProtectedHeader(*cbor), IsOkAndHolds(payload)); |
| } |
| |
| TEST(SigStructureTest, ProtectedHeaderMultipleCerts) { |
| const ProtectedHeader payload = { |
| .alg = SigningAlgorithm::kPs512, |
| .certificate_chain = {"cert1", "cert2"}, |
| }; |
| |
| auto cbor = EncodeProtectedHeader(payload); |
| ASSERT_TRUE(cbor.ok()); |
| EXPECT_THAT(DecodeProtectedHeader(*cbor), IsOkAndHolds(payload)); |
| } |
| |
| TEST(SigStructureTest, ProtectedHeaderUnsupportedAlgorithm) { |
| // hex for {1: -100, 33: "abcd"} |
| auto cbor = absl::HexStringToBytes("a2013863214461626364"); |
| EXPECT_THAT(DecodeProtectedHeader(cbor), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| "unsupported COSE algorithm: -100")); |
| } |
| |
| TEST(SigStructureTest, CoseSign1TaggedStructureEncodeNullPayload) { |
| const CoseSign1TaggedStructure payload = { |
| .protected_header = "abc", |
| .signature = "def", |
| }; |
| const std::string cbor = EncodeCoseSign1TaggedStructure(payload); |
| EXPECT_THAT(DecodeCoseSign1TaggedStructure(cbor), IsOkAndHolds(payload)); |
| } |
| TEST(SigStructureTest, CoseSign1TaggedStructureEncodeNonNullPayload) { |
| const CoseSign1TaggedStructure payload = { |
| .protected_header = "abc", |
| .payload = "xyz", |
| .signature = "def", |
| }; |
| const std::string cbor = EncodeCoseSign1TaggedStructure(payload); |
| EXPECT_THAT(DecodeCoseSign1TaggedStructure(cbor), IsOkAndHolds(payload)); |
| } |
| TEST(SigStructureTest, CoseSign1TaggedStructureEncodeTstTokens) { |
| const CoseSign1TaggedStructure payload = { |
| .protected_header = "abc", |
| .unprotected_header = {.sig_tst2 = |
| TstContainer{.tst_tokens = {{.val = "val1"}, |
| {.val = "val2"}}}}, |
| .signature = "def", |
| }; |
| const std::string cbor = EncodeCoseSign1TaggedStructure(payload); |
| EXPECT_THAT(DecodeCoseSign1TaggedStructure(cbor), IsOkAndHolds(payload)); |
| } |
| |
| TEST(SigStructureTest, CoseSign1TaggedStructureEncodeOcspResponses) { |
| const CoseSign1TaggedStructure payload = { |
| .protected_header = "abc", |
| .unprotected_header = {.ocsp_responses = {"ocsp1", "ocsp2"}}, |
| .signature = "def", |
| }; |
| const std::string cbor = EncodeCoseSign1TaggedStructure(payload); |
| EXPECT_THAT(DecodeCoseSign1TaggedStructure(cbor), IsOkAndHolds(payload)); |
| } |
| |
| TEST(SigStructureTest, CoseSign1TaggedStructureEncodePad) { |
| const CoseSign1TaggedStructure payload = { |
| .protected_header = "abc", |
| .unprotected_header = {.pad = "padding"}, |
| .signature = "def", |
| }; |
| const std::string cbor = EncodeCoseSign1TaggedStructure(payload); |
| EXPECT_THAT(DecodeCoseSign1TaggedStructure(cbor), IsOkAndHolds(payload)); |
| } |
| |
| TEST(SigStructureTest, CoseSign1TaggedStructureDecodeSignature) { |
| auto protected_headers_b64 = |
| R"(cHJvdGVjdGVkX2hlYWRlcg==)"; // "protected_header" |
| auto timestamp_cms_b64 = R"(dGltZXN0YW1w)"; // "timestamp" |
| auto signature_b64 = R"(c2lnbmF0dXJl)"; // "signature" |
| auto signature = cbor::FromJson(absl::Substitute( |
| R"json(["b64'$0'", {"sigTst2": {"tstTokens": [{"val": "b64'$1'"}]}}, null, "b64'$2'"])json", |
| protected_headers_b64, timestamp_cms_b64, signature_b64)); |
| // Prepend the COSE_Sign1 tag (Major type 6 addl value 18). |
| signature = "\xD2" + signature; |
| CoseSign1TaggedStructure expected{ |
| .protected_header = Base64Unescape(protected_headers_b64), |
| .unprotected_header = |
| { |
| .sig_tst2 = |
| TstContainer{ |
| .tst_tokens = {{.val = |
| Base64Unescape(timestamp_cms_b64)}}, |
| }, |
| }, |
| .signature = Base64Unescape(signature_b64), |
| }; |
| EXPECT_THAT(DecodeCoseSign1TaggedStructure(signature), |
| IsOkAndHolds(expected)); |
| EXPECT_EQ(EncodeCoseSign1TaggedStructure(expected), signature); |
| }; |
| |
| TEST(SigStructureTest, DecodeUnprotectedHeaderContainingSigTst) { |
| auto protected_headers_b64 = |
| R"(cHJvdGVjdGVkX2hlYWRlcg==)"; // "protected_header" |
| auto timestamp_cms_b64 = R"(dGltZXN0YW1w)"; // "timestamp" |
| auto signature_b64 = R"(c2lnbmF0dXJl)"; // "signature" |
| auto signature = cbor::FromJson(absl::Substitute( |
| R"json(["b64'$0'", {"sigTst": {"tstTokens": [{"val": "b64'$1'"}]}}, null, "b64'$2'"])json", |
| protected_headers_b64, timestamp_cms_b64, signature_b64)); |
| // Prepend the COSE_Sign1 tag (Major type 6 addl value 18). |
| signature = "\xD2" + signature; |
| CoseSign1TaggedStructure expected{ |
| .protected_header = Base64Unescape(protected_headers_b64), |
| .unprotected_header = |
| { |
| .sig_tst = |
| TstContainer{ |
| .tst_tokens = {{.val = |
| Base64Unescape(timestamp_cms_b64)}}, |
| }, |
| }, |
| .signature = Base64Unescape(signature_b64), |
| }; |
| EXPECT_THAT(DecodeCoseSign1TaggedStructure(signature), |
| IsOkAndHolds(expected)); |
| } |
| |
| TEST(SigStructureTest, DecodeUnprotectedHeaderContainingCertificateChain) { |
| auto protected_headers_b64 = |
| R"(cHJvdGVjdGVkX2hlYWRlcg==)"; // "protected_header" |
| auto timestamp_cms_b64 = R"(dGltZXN0YW1w)"; // "timestamp" |
| auto signature_b64 = R"(c2lnbmF0dXJl)"; // "signature" |
| auto certificate_64 = R"(Y2VydGlmaWNhdGU=)"; // "certificate" |
| auto signature = cbor::FromJson(absl::Substitute( |
| R"json(["b64'$0'", {"sigTst": {"tstTokens": [{"val": "b64'$1'"}]}, "x5chain": ["b64'$2'"]}, null, "b64'$3'"])json", |
| protected_headers_b64, timestamp_cms_b64, certificate_64, signature_b64)); |
| // Prepend the COSE_Sign1 tag (Major type 6 addl value 18). |
| signature = "\xD2" + signature; |
| CoseSign1TaggedStructure expected{ |
| .protected_header = Base64Unescape(protected_headers_b64), |
| .unprotected_header = |
| { |
| .sig_tst = |
| TstContainer{ |
| .tst_tokens = {{.val = |
| Base64Unescape(timestamp_cms_b64)}}, |
| }, |
| .certificate_chain = {Base64Unescape(certificate_64)}, |
| }, |
| .signature = Base64Unescape(signature_b64), |
| }; |
| EXPECT_THAT(DecodeCoseSign1TaggedStructure(signature), |
| IsOkAndHolds(expected)); |
| } |
| |
| } // namespace |
| } // namespace credentio |