| // 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/absl_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 = "credentio/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); | |
| ABSL_CHECK_OK(contents.status()); | |
| return *contents; | |
| } | |
| absl::Time ParseGenTime(absl::string_view timestamp) { | |
| absl::Time time; | |
| std::string parse_error; | |
| ABSL_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; | |
| ABSL_CHECK(CBB_init(&cbb, 1024)); | |
| CBB req; | |
| ABSL_CHECK(CBB_add_asn1(&cbb, &req, CBS_ASN1_SEQUENCE)); | |
| ABSL_CHECK(CBB_add_bytes(&req, reinterpret_cast<const uint8_t*>("12345"), 5)); | |
| uint8_t* cbb_data; | |
| size_t cbb_len; | |
| ABSL_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; | |
| ABSL_CHECK(CBB_init(&cbb, 1024)); | |
| CBB req; | |
| ABSL_CHECK(CBB_add_asn1(&cbb, &req, CBS_ASN1_SEQUENCE)); | |
| CBB pki_status_info; | |
| ABSL_CHECK(CBB_add_asn1(&req, &pki_status_info, CBS_ASN1_SEQUENCE)); | |
| ABSL_CHECK(CBB_add_bytes(&pki_status_info, | |
| reinterpret_cast<const uint8_t*>("12345"), 5)); | |
| uint8_t* cbb_data; | |
| size_t cbb_len; | |
| ABSL_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; | |
| ABSL_CHECK(CBB_init(&cbb, 1024)); | |
| CBB req; | |
| ABSL_CHECK(CBB_add_asn1(&cbb, &req, CBS_ASN1_SEQUENCE)); | |
| CBB pki_status_info; | |
| ABSL_CHECK(CBB_add_asn1(&req, &pki_status_info, CBS_ASN1_SEQUENCE)); | |
| ABSL_CHECK(CBB_add_asn1_int64( | |
| &pki_status_info, static_cast<int64_t>(TspPkiStatus::kGrantedWithMods))); | |
| ABSL_CHECK(CBB_add_bytes(&req, reinterpret_cast<const uint8_t*>("12345"), 5)); | |
| uint8_t* cbb_data; | |
| size_t cbb_len; | |
| ABSL_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; | |
| ABSL_CHECK(CBB_init(&cbb, 1024)); | |
| CBB tst_info; | |
| ABSL_CHECK(CBB_add_asn1(&cbb, &tst_info, CBS_ASN1_SEQUENCE)); | |
| // Version - should be INTEGER | |
| ABSL_CHECK(CBB_add_asn1_octet_string(&tst_info, nullptr, 0)); | |
| uint8_t* cbb_data; | |
| size_t cbb_len; | |
| ABSL_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; | |
| ABSL_CHECK(CBB_init(&cbb, 1024)); | |
| CBB tst_info; | |
| ABSL_CHECK(CBB_add_asn1(&cbb, &tst_info, CBS_ASN1_SEQUENCE)); | |
| // Version | |
| ABSL_CHECK(CBB_add_asn1_int64(&tst_info, 1)); | |
| // Policy - should be OBJECT IDENTIFIER | |
| ABSL_CHECK(CBB_add_asn1_int64(&tst_info, 12345)); | |
| uint8_t* cbb_data; | |
| size_t cbb_len; | |
| ABSL_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 |