| // 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 "tsp/timestamp_parsing.h" |
| |
| #include <cstddef> |
| #include <cstdint> |
| #include <string> |
| |
| #include "absl/base/no_destructor.h" |
| #include "absl/log/check.h" |
| #include "absl/status/status.h" |
| #include "absl/status/status_matchers.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/time/time.h" |
| #include "crypto/algorithms.h" |
| #include "crypto/cbs_utils.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "openssl/base.h" |
| #include "openssl/bytestring.h" |
| #include "openssl/mem.h" // IWYU pragma: keep, `bssl::UniquePtr` in base.h is incomplete |
| #include "testing/test_file_utils.h" |
| #include "tsp/constants.h" |
| #include "tsp/status_codes.h" |
| #include "tsp/test_helpers.h" |
| |
| namespace credentio { |
| namespace { |
| |
| using ::absl_testing::IsOk; |
| using ::absl_testing::IsOkAndHolds; |
| using ::absl_testing::StatusIs; |
| using ::testing::Test; |
| using ::testing::TestWithParam; |
| using ::testing::ValuesIn; |
| |
| constexpr absl::string_view kSha384OidTxt = "2.16.840.1.101.3.4.2.2"; |
| constexpr absl::string_view kSha256OidTxt = "2.16.840.1.101.3.4.2.1"; |
| |
| constexpr absl::string_view kMessageImprintHash = "fake message imprint"; |
| |
| constexpr absl::string_view kTestKeysDir = "c2pa/testing/testdata/keys/"; |
| |
| absl::string_view ValidNonce() { |
| static absl::NoDestructor<std::string> nonce(EncodeDerInteger(0xf00dbabe)); |
| return *nonce; |
| } |
| |
| std::string LoadTestCryptoAsset(absl::string_view short_path) { |
| std::string file_path = absl::StrCat(kTestKeysDir, short_path); |
| auto contents = credentio_testing::GetContents(file_path); |
| CHECK_OK(contents.status()); |
| return *contents; |
| } |
| |
| absl::Time ParseGenTime(absl::string_view timestamp) { |
| absl::Time time; |
| std::string parse_error; |
| CHECK(absl::ParseTime(kGenTimeFormat, timestamp, &time, &parse_error)) |
| << "ParseTime(\"" << kGenTimeFormat << "\", \"" << timestamp |
| << "\"): " << parse_error; |
| return time; |
| } |
| |
| class ParseTimestampRespTest : public Test { |
| public: |
| static void SetUpTestSuite() { |
| *time_stamp_token_ = CreateTimeStampTokenEcdsa( |
| {"NOT DER NOT ANYTHING WHATEVER DO NOT CARE THIS IS JUST JUNK FOR " |
| "TESTING"}, |
| LoadTestCryptoAsset("test_ca/tsa.key"), |
| LoadTestCryptoAsset("test_ca/tsa.pem")); |
| } |
| |
| protected: |
| absl::string_view time_stamp_token() { return *time_stamp_token_; } |
| |
| private: |
| static inline absl::NoDestructor<std::string> time_stamp_token_; |
| }; |
| |
| TEST_F(ParseTimestampRespTest, ValidTimestampResp) { |
| std::string timestamp_resp = CreateTimeStampResp(time_stamp_token()); |
| |
| EXPECT_THAT(ParseTimestampResp(timestamp_resp), |
| IsOkAndHolds(time_stamp_token())); |
| } |
| |
| TEST_F(ParseTimestampRespTest, FailedRespWithNoExplanation) { |
| std::string timestamp_resp = |
| CreateFailedTimeStampResp(TspPkiStatus::kRejection); |
| |
| EXPECT_THAT( |
| ParseTimestampResp(timestamp_resp), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| "TimestampResp.status contains an error; status=rejection")); |
| } |
| |
| TEST_F(ParseTimestampRespTest, FailedRespWithStatusString) { |
| std::string timestamp_resp = CreateFailedTimeStampResp( |
| TspPkiStatus::kRejection, |
| /*status_string=*/{"some explanation", "some other explanation"}); |
| |
| EXPECT_THAT( |
| ParseTimestampResp(timestamp_resp), |
| StatusIs( |
| absl::StatusCode::kInvalidArgument, |
| R"(TimestampResp.status contains an error; status=rejection; some explanation; some other explanation)")); |
| } |
| |
| TEST_F(ParseTimestampRespTest, FailedRespWithFailureInfo) { |
| std::string timestamp_resp = CreateFailedTimeStampResp( |
| TspPkiStatus::kRejection, {}, |
| {5 /* badDataFormat */, 14 /* timeNotAvailable */}); |
| EXPECT_THAT( |
| ParseTimestampResp(timestamp_resp), |
| StatusIs( |
| absl::StatusCode::kInvalidArgument, |
| R"(TimestampResp.status contains an error; status=rejection; (badDataFormat) the data submitted has the wrong format; (timeNotAvailable) the TSA's time source is not available)")); |
| } |
| |
| TEST(ParseTimeStampRespTest, NoSequence) { |
| constexpr absl::string_view kNotSequence = "invalid"; |
| |
| EXPECT_THAT( |
| ParseTimestampResp(kNotSequence), |
| StatusIs( |
| absl::StatusCode::kInvalidArgument, |
| "cannot parse top-level DER SEQUENCE while parsing TimeStampResp")); |
| } |
| |
| TEST(ParseTimeStampRespTest, InvalidPkiStatusInfo) { |
| CBB cbb; |
| CHECK(CBB_init(&cbb, 1024)); |
| CBB req; |
| CHECK(CBB_add_asn1(&cbb, &req, CBS_ASN1_SEQUENCE)); |
| CHECK(CBB_add_bytes(&req, reinterpret_cast<const uint8_t*>("12345"), 5)); |
| uint8_t* cbb_data; |
| size_t cbb_len; |
| CHECK(CBB_finish(&cbb, &cbb_data, &cbb_len)); |
| bssl::UniquePtr<uint8_t> cbb_data_uniq(cbb_data); |
| |
| EXPECT_THAT(ParseTimestampResp(absl::string_view( |
| reinterpret_cast<char*>(cbb_data), cbb_len)), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| "cannot parse TimeStampResp.status")); |
| } |
| |
| TEST(ParseTimeStampRespTest, InvalidStatusCode) { |
| CBB cbb; |
| CHECK(CBB_init(&cbb, 1024)); |
| CBB req; |
| CHECK(CBB_add_asn1(&cbb, &req, CBS_ASN1_SEQUENCE)); |
| CBB pki_status_info; |
| CHECK(CBB_add_asn1(&req, &pki_status_info, CBS_ASN1_SEQUENCE)); |
| CHECK(CBB_add_bytes(&pki_status_info, |
| reinterpret_cast<const uint8_t*>("12345"), 5)); |
| uint8_t* cbb_data; |
| size_t cbb_len; |
| CHECK(CBB_finish(&cbb, &cbb_data, &cbb_len)); |
| bssl::UniquePtr<uint8_t> cbb_data_uniq(cbb_data); |
| |
| EXPECT_THAT(ParseTimestampResp(absl::string_view( |
| reinterpret_cast<char*>(cbb_data), cbb_len)), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| "cannot parse TimeStampResp.status.status")); |
| } |
| |
| TEST(ParseTimeStampRespTest, InvalidTimestampToken) { |
| CBB cbb; |
| CHECK(CBB_init(&cbb, 1024)); |
| CBB req; |
| CHECK(CBB_add_asn1(&cbb, &req, CBS_ASN1_SEQUENCE)); |
| CBB pki_status_info; |
| CHECK(CBB_add_asn1(&req, &pki_status_info, CBS_ASN1_SEQUENCE)); |
| CHECK(CBB_add_asn1_int64( |
| &pki_status_info, static_cast<int64_t>(TspPkiStatus::kGrantedWithMods))); |
| CHECK(CBB_add_bytes(&req, reinterpret_cast<const uint8_t*>("12345"), 5)); |
| uint8_t* cbb_data; |
| size_t cbb_len; |
| CHECK(CBB_finish(&cbb, &cbb_data, &cbb_len)); |
| bssl::UniquePtr<uint8_t> cbb_data_uniq(cbb_data); |
| |
| EXPECT_THAT(ParseTimestampResp(absl::string_view( |
| reinterpret_cast<char*>(cbb_data), cbb_len)), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| "cannot extract TimeStampToken from TimeStampResp")); |
| } |
| |
| struct InvalidTimeFormatTestCase { |
| absl::string_view header_key; |
| absl::string_view timestamp; |
| absl::string_view expected_error_substr; |
| }; |
| |
| constexpr InvalidTimeFormatTestCase kInvalidFormatTestCases[] = { |
| {.timestamp = "6666666666666Z", |
| .expected_error_substr = "could not parse `genTime` value"}, |
| {.timestamp = "20240815094500.9999+0000Z", |
| .expected_error_substr = "could not parse `genTime` value"}, |
| {.timestamp = "20200521000000Z19920521000000Z19980521000000Z", |
| .expected_error_substr = |
| R"(`genTime` is not a valid timestamp (contains "Z" that is not at end))"}, |
| {.timestamp = "66666666666666", |
| .expected_error_substr = |
| "`genTime` value is not specified in UTC (needs \"Z\" suffix)"}, |
| {.timestamp = "6666666666666Z", |
| .expected_error_substr = "could not parse `genTime` value"}, |
| {.timestamp = "20240815094500.9999+0000Z", |
| .expected_error_substr = "could not parse `genTime` value"}, |
| {.timestamp = "20200521000000Z19920521000000Z19980521000000Z", |
| .expected_error_substr = |
| R"(`genTime` is not a valid timestamp (contains "Z" that is not at end))"}, |
| {.timestamp = "66666666666666", |
| .expected_error_substr = |
| "`genTime` value is not specified in UTC (needs \"Z\" suffix)"}, |
| }; |
| |
| using InvalidTimeFormatTest = TestWithParam<InvalidTimeFormatTestCase>; |
| |
| TEST_P(InvalidTimeFormatTest, RejectsInvalidFormats) { |
| std::string tst_info = |
| CreateTstInfo(GetParam().timestamp, kMessageImprintHash, kSha256OidTxt); |
| absl::Time time; |
| std::string message_imprint_hash; |
| HashAlgorithm hash_algorithm; |
| std::string nonce; |
| |
| EXPECT_THAT(ParseTstInfo(FromStringView(tst_info), &time, |
| &message_imprint_hash, &hash_algorithm, &nonce), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| testing::HasSubstr(GetParam().expected_error_substr))); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(ParseTstInfoTest, InvalidTimeFormatTest, |
| ValuesIn(kInvalidFormatTestCases)); |
| |
| struct ValidTimeFormatTestCase { |
| absl::string_view timestamp; |
| }; |
| |
| constexpr ValidTimeFormatTestCase kValidTimestampFormatTestCases[] = { |
| {.timestamp = "20230521091011.1337Z"}, |
| {.timestamp = "20230521000000Z"}, |
| {.timestamp = "20240101000000Z"}, |
| {.timestamp = "20251231235959.9999Z"}}; |
| |
| using ValidTimeFormatTest = TestWithParam<ValidTimeFormatTestCase>; |
| |
| TEST_P(ValidTimeFormatTest, AcceptsValidFormats) { |
| std::string tst_info = CreateTstInfo( |
| GetParam().timestamp, kMessageImprintHash, kSha384OidTxt, ValidNonce()); |
| |
| absl::Time time; |
| std::string message_imprint_hash; |
| HashAlgorithm hash_algorithm; |
| std::string nonce; |
| |
| EXPECT_THAT(ParseTstInfo(FromStringView(tst_info), &time, |
| &message_imprint_hash, &hash_algorithm, &nonce), |
| IsOk()); |
| EXPECT_EQ(time, ParseGenTime(GetParam().timestamp)); |
| EXPECT_EQ(message_imprint_hash, kMessageImprintHash); |
| EXPECT_EQ(hash_algorithm, HashAlgorithm::kSha384); |
| EXPECT_EQ(nonce, ValidNonce()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(ParseTstInfoTest, ValidTimeFormatTest, |
| ValuesIn(kValidTimestampFormatTestCases)); |
| |
| TEST(ParseTstInfoTest, InvalidVersion) { |
| CBB cbb; |
| CHECK(CBB_init(&cbb, 1024)); |
| CBB tst_info; |
| CHECK(CBB_add_asn1(&cbb, &tst_info, CBS_ASN1_SEQUENCE)); |
| // Version - should be INTEGER |
| CHECK(CBB_add_asn1_octet_string(&tst_info, nullptr, 0)); |
| |
| uint8_t* cbb_data; |
| size_t cbb_len; |
| CHECK(CBB_finish(&cbb, &cbb_data, &cbb_len)); |
| bssl::UniquePtr<uint8_t> cbb_data_uniq(cbb_data); |
| |
| absl::Time time; |
| std::string message_imprint_hash; |
| HashAlgorithm hash_algorithm; |
| std::string nonce; |
| |
| EXPECT_THAT( |
| ParseTstInfo(FromStringView(absl::string_view( |
| reinterpret_cast<char*>(cbb_data), cbb_len)), |
| &time, &message_imprint_hash, &hash_algorithm, &nonce), |
| StatusIs(absl::StatusCode::kInvalidArgument, "could not skip `version`")); |
| } |
| |
| TEST(ParseTstInfoTest, InvalidPolicy) { |
| CBB cbb; |
| CHECK(CBB_init(&cbb, 1024)); |
| CBB tst_info; |
| CHECK(CBB_add_asn1(&cbb, &tst_info, CBS_ASN1_SEQUENCE)); |
| // Version |
| CHECK(CBB_add_asn1_int64(&tst_info, 1)); |
| // Policy - should be OBJECT IDENTIFIER |
| CHECK(CBB_add_asn1_int64(&tst_info, 12345)); |
| |
| uint8_t* cbb_data; |
| size_t cbb_len; |
| CHECK(CBB_finish(&cbb, &cbb_data, &cbb_len)); |
| bssl::UniquePtr<uint8_t> cbb_data_uniq(cbb_data); |
| |
| absl::Time time; |
| std::string message_imprint_hash; |
| HashAlgorithm hash_algorithm; |
| std::string nonce; |
| |
| EXPECT_THAT( |
| ParseTstInfo(FromStringView(absl::string_view( |
| reinterpret_cast<char*>(cbb_data), cbb_len)), |
| &time, &message_imprint_hash, &hash_algorithm, &nonce), |
| StatusIs(absl::StatusCode::kInvalidArgument, "could not skip `policy`")); |
| } |
| |
| } // namespace |
| } // namespace credentio |