blob: 46c985e627adb3c46f3153e2349551fa270101b6 [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 "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