| // 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 "crypto/default/timestamp_verifier.h" | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <iterator> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <vector> | |
| #include "absl/algorithm/container.h" | |
| #include "absl/base/no_destructor.h" | |
| #include "absl/log/check.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_matchers.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/time/time.h" | |
| #include "absl/types/span.h" | |
| #include "crypto/algorithms.h" | |
| #include "crypto/cbs_utils.h" | |
| #include "crypto/default/trust_store.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "openssl/base.h" | |
| #include "openssl/bio.h" | |
| #include "openssl/bytestring.h" | |
| #include "openssl/digest.h" | |
| #include "openssl/mem.h" | |
| #include "openssl/nid.h" | |
| #include "openssl/obj.h" | |
| #include "openssl/pem.h" | |
| #include "openssl/pki/verify.h" | |
| #include "openssl/x509.h" | |
| #include "proto/validation_result.pb.h" | |
| #include "tsp/constants.h" | |
| #include "tsp/parsed_timestamp_token.h" | |
| #include "tsp/test_helpers.h" | |
| #include "tsp/verified_timestamp.h" | |
| namespace credentio { | |
| namespace { | |
| using ::absl_testing::StatusIs; | |
| using ::testing::ElementsAre; | |
| using ::testing::Eq; | |
| using ::testing::HasSubstr; | |
| using ::testing::Return; | |
| constexpr absl::string_view kCaPem = R"(-----BEGIN CERTIFICATE----- | |
| MIIB3zCCAWagAwIBAgIUXJz+6VKGMVOVs+xupRS3Cne3or0wCgYIKoZIzj0EAwIw | |
| JjEkMCIGA1UEAwwbR29vZ2xlIEMyUEEgVGVzdGluZyBSb290IENBMCAXDTI1MDIw | |
| NDE1MTMyMFoYDzIxMjUwMTExMTUxMzIwWjAmMSQwIgYDVQQDDBtHb29nbGUgQzJQ | |
| QSBUZXN0aW5nIFJvb3QgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATWa/wOwQMD | |
| 5zNmXVNGbPJuptbvXrNBhgKZI6SHRzngJ4bV+SShgLDqFdtnDfwAO/nxHyACTNl6 | |
| sJ9ue407+HW6W1WEODD9a/jVYizAVkzUaEDElDEA5PPkegHqtVJuf8CjUzBRMB0G | |
| A1UdDgQWBBTTMOrQr2ny9gYyywPGUZGx/KENtzAfBgNVHSMEGDAWgBTTMOrQr2ny | |
| 9gYyywPGUZGx/KENtzAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA2cAMGQC | |
| MCsAxaLyEkp2nyQ2BtBoSuu52k3qfWLNAOHiaifoaxhQzDo1juNYuITM8I88PwVO | |
| QAIwJBRVRNysxvouMf7cQ2VSYTPc32BY/m38wnWS3K94l7eS3sANM9U++e8dgEtG | |
| U2Tv | |
| -----END CERTIFICATE-----)"; | |
| constexpr absl::string_view kTsaPem = R"(-----BEGIN CERTIFICATE----- | |
| MIIB1zCCAVygAwIBAgIUaUM3tWVucTzvVWUNY0HjFKBAEkwwCgYIKoZIzj0EAwIw | |
| JjEkMCIGA1UEAwwbR29vZ2xlIEMyUEEgVGVzdGluZyBSb290IENBMCAXDTI1MDIw | |
| NDIxNDIxNFoYDzIxMjUwMTExMjE0MjE0WjAiMSAwHgYDVQQDDBdHb29nbGUgQzJQ | |
| QSBUZXN0aW5nIFRTQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLlLssu8MJdD | |
| CS3UV4CPcI1l4lnQ7Y6V7jAaAXTt7wbRnN8lbjcMmfzY4mqhc6HMtFYqYj4UeO7t | |
| tjkvzI2PfGmjajBoMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEF | |
| BQcDCDAdBgNVHQ4EFgQUeWySrF5tKc2Cwe7AzkxALXVXyIwwHwYDVR0jBBgwFoAU | |
| 0zDq0K9p8vYGMssDxlGRsfyhDbcwCgYIKoZIzj0EAwIDaQAwZgIxAPwCKKGMymYV | |
| 8p2gXDDyJSMbHJnfHu/FvfeaWYF6a6twCCt7IFehJWbyg/f1p/3Q7QIxAK0yLI6L | |
| /FTQPRaB3nZBm4Ocw9QZ171qisDKd1wW83mjmFAp9aNFlFv2yQt9A1NeEQ== | |
| -----END CERTIFICATE-----)"; | |
| constexpr absl::string_view kTsaRsaPem = R"(-----BEGIN CERTIFICATE----- | |
| MIIDoDCCAyegAwIBAgIUDDONK2J0GuK8OSkZYI4sN4GGJaYwCgYIKoZIzj0EAwIw | |
| JjEkMCIGA1UEAwwbR29vZ2xlIEMyUEEgVGVzdGluZyBSb290IENBMCAXDTI1MDIw | |
| NTAwNDU1N1oYDzIxMjUwMTEyMDA0NTU3WjAiMSAwHgYDVQQDDBdHb29nbGUgQzJQ | |
| QSBUZXN0aW5nIFRTQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAO5N | |
| FYAVihiznVBVhnpoOIvz790xxqdpZXRYrCXkj9L2o07sjUza8cotpvDzd9W8EkPg | |
| CJSl0dJ4VeZ7wq4PH0azLgpWDppTVtUtzbYGWacvgWsgLQ96SqAmPYnuHwD+b+Wv | |
| q/p35JB3csFDpg2F9SKFHZH50KQJ4z+WLAKf9Gexfs0y8DsN3DtQgzaVF2vS7/SL | |
| jfFbnS1aHXoeKvPiu475pltkuw0velbXi8/cHIq3VuQOwxgyCIE4FuWJYYPMMu5j | |
| HJ0+bBtDI+Nn8rWmN7hpOV1Lo268gzwR2HtGmdM1LABj+RY8nBdyXJmfAY9kqm/D | |
| VuEVrsGdIOYiAw7iHLvjc4CfNaKs6l9bhTppsVydLZE1fKuMFvmSs5Gc9yIermeu | |
| jmoPB6QSVJh76mKOug7BmEzLHipFRxfDOmdr+eP/5w3L1ZOA+nEG2XWVICbIH/ND | |
| rteeFCGJsDV7wDx6Yhpfvjh8A5qP9Z41MW8CtqpzNakivtVaDKmULZ/dyKeI8BZ5 | |
| DvEg/6mpwN6Eu06czep0tO4LQ/l3935w3d/lkpn1yKDu9nm02uvVBEqwZW2lR5BT | |
| MeqxQTMrrbmoi0mjsyX+5XmsUzAzBFlZ9HJi14p6vd70FJOlRXttGxCxiLC/QkmN | |
| krFnFH7v1bhbRGGtKqttUK4yogdnxaOhZ6gr/s45AgMBAAGjajBoMA4GA1UdDwEB | |
| /wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAdBgNVHQ4EFgQU+rL+AIZ5 | |
| EsQVhIN6SfX34LSTS7YwHwYDVR0jBBgwFoAU0zDq0K9p8vYGMssDxlGRsfyhDbcw | |
| CgYIKoZIzj0EAwIDZwAwZAIwDrCJbyS2Z4yA+82o7slKap3VpBwqBW5xrYjIJyV3 | |
| IGzPI3i55bQb0wKRf6SMBHmCAjAfrVYauA+rpNxEYEIoM/ziaJ0xEbF/tw/JQyVE | |
| 0JW905o/hzXHO5LV3z/x0SYRPiY= | |
| -----END CERTIFICATE-----)"; | |
| constexpr absl::string_view kTsaWithIntermediatePem = | |
| R"(-----BEGIN CERTIFICATE----- | |
| MIIBvzCCAWSgAwIBAgIUImopbS3q7uF8Wz88f39/wfxAIYowCgYIKoZIzj0EAwIw | |
| LjEsMCoGA1UEAwwjR29vZ2xlIEMyUEEgVGVzdGluZyBJbnRlcm1lZGlhdGUgQ0Ew | |
| IBcNMjUwMzEzMTM1NzI2WhgPMjEyNTAyMTcxMzU3MjZaMCIxIDAeBgNVBAMMF0dv | |
| b2dsZSBDMlBBIFRlc3RpbmcgVFNBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE | |
| Z9YEmeJodxdGQdhJbSjIeccIbxjIQY5rPp/c6zqF4/mRr8VOdMur3r/r0Gyxju8k | |
| 7R+6/j5IjQwzci01Qs7W06NqMGgwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQM | |
| MAoGCCsGAQUFBwMIMB0GA1UdDgQWBBQGI2mf9lUe+oNSZE/WRQlwVGMgoTAfBgNV | |
| HSMEGDAWgBRmRE2zkZe58xIqg//9Iw76XyOLKjAKBggqhkjOPQQDAgNJADBGAiEA | |
| onFfvfflN96uF0ECnej2aZ7ikuyz+OsDEQVDQZNBTHMCIQCo5YiNm7EeeMexhDzM | |
| vVdDSKzUFmzJXGZAO/kkEQyNXw== | |
| -----END CERTIFICATE-----)"; | |
| constexpr absl::string_view kIntermediatePem = R"(-----BEGIN CERTIFICATE----- | |
| MIIByjCCAVGgAwIBAgIUVhW73Ry+Zn7eKwhnxTiyLDJ3cKowCgYIKoZIzj0EAwIw | |
| JjEkMCIGA1UEAwwbR29vZ2xlIEMyUEEgVGVzdGluZyBSb290IENBMCAXDTI1MDMx | |
| MzEzMzc0MVoYDzIxMjUwMjE3MTMzNzQxWjAuMSwwKgYDVQQDDCNHb29nbGUgQzJQ | |
| QSBUZXN0aW5nIEludGVybWVkaWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH | |
| A0IABJKYZuxD1F5Eqkgl+fqnubgtLr4MHlAh2p/7n8E/BOZuemrAAXVw7p27D1Oy | |
| n6RNyobJVRZHcm61i09hfTKw/NajUzBRMB0GA1UdDgQWBBRmRE2zkZe58xIqg//9 | |
| Iw76XyOLKjAfBgNVHSMEGDAWgBTTMOrQr2ny9gYyywPGUZGx/KENtzAPBgNVHRMB | |
| Af8EBTADAQH/MAoGCCqGSM49BAMCA2cAMGQCMBqHAcfcOv21PmRMWchgZ37ZRoor | |
| 4+XlKPnh+L0gBBRykqI49VvvpgxtCC2jVA2BnQIwZf0DOnk6c4lUvSzrEJ7+ToR1 | |
| dJMoNuufOTVoMQdojCLwnH4GrwID0qsdXJm8X0XY | |
| -----END CERTIFICATE-----)"; | |
| constexpr absl::string_view kClaimSignerPem = R"(-----BEGIN CERTIFICATE----- | |
| MIICDzCCAZWgAwIBAgIUR8DAqLjMOExo0KJS3DGC4Qsy79kwCgYIKoZIzj0EAwIw | |
| JjEkMCIGA1UEAwwbR29vZ2xlIEMyUEEgVGVzdGluZyBSb290IENBMCAXDTI1MDIy | |
| MzA1MDMwMFoYDzIxMjUwMTMwMDUwMzAwWjBbMSYwJAYDVQQDDB1Hb29nbGUgVGVz | |
| dCBDMlBBIENsYWltIFNpZ25lcjEdMBsGA1UECgwUR29vZ2xlIEMyUEEgVGVzdCBP | |
| cmcxEjAQBgNVBAsMCVRlc3QgVW5pdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA | |
| BKw1/4qPpbSgEWNUy7B5R48dxnsx92d5lqh7HY204TpvmxQgukcwavpW8XBxSdYr | |
| F+XEgNStkprWpnkoKWNpShyjajBoMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8E | |
| DDAKBggrBgEFBQcDBDAdBgNVHQ4EFgQU6wle6tElLcBSIwsoBICb7gInPiQwHwYD | |
| VR0jBBgwFoAU0zDq0K9p8vYGMssDxlGRsfyhDbcwCgYIKoZIzj0EAwIDaAAwZQIw | |
| YNgGpaaZxI2km45t/Wd+njvQlF0JJ4+9GvZ0bDQWH7kjAQTmKrkxuNMT77cA2IMW | |
| AjEArZqFNmE/5yv8tzHbPP73c3n8BPUmjIpZR5A2nzjfhPzpClFp70lkO+mTu82d | |
| ULnv | |
| -----END CERTIFICATE-----)"; | |
| class MockParsedTimestampToken : public ParsedTimestampToken { | |
| public: | |
| MOCK_METHOD(absl::Status, VerifySignature, (), (const, override)); | |
| MOCK_METHOD(absl::StatusOr<HashAlgorithm>, GetMessageImprintHashAlgorithm, (), | |
| (const, override)); | |
| MOCK_METHOD(absl::StatusOr<std::vector<std::string>>, GetCertificateChain, (), | |
| (const, override)); | |
| MOCK_METHOD(absl::StatusOr<absl::string_view>, GetSigningCertificateV2Bytes, | |
| (), (const, override)); | |
| MOCK_METHOD(absl::StatusOr<absl::string_view>, GetTstInfoBytes, (), | |
| (const, override)); | |
| }; | |
| std::string GetSigningCertificateV2Value(absl::string_view attribute_bytes) { | |
| CBS cbs = FromStringView(attribute_bytes); | |
| CBS seq, oid, set; | |
| CHECK(CBS_get_asn1(&cbs, &seq, CBS_ASN1_SEQUENCE)); | |
| CHECK(CBS_get_asn1(&seq, &oid, CBS_ASN1_OBJECT)); | |
| CHECK(CBS_get_asn1(&seq, &set, CBS_ASN1_SET)); | |
| CBS val_elem; | |
| CHECK(CBS_get_any_asn1_element(&set, &val_elem, nullptr, nullptr)); | |
| return std::string(ToStringView(val_elem)); | |
| } | |
| constexpr absl::string_view kSha384OidTxt = "2.16.840.1.101.3.4.2.2"; | |
| constexpr absl::string_view kMessageImprintHash = "fake message imprint"; | |
| constexpr absl::string_view kValidTimestamp = "20240101000000Z"; | |
| absl::Time ValidTimestampTime() { | |
| static absl::Time time = []() { | |
| absl::Time time; | |
| std::string err; | |
| CHECK(absl::ParseTime(kGenTimeFormat, kValidTimestamp, &time, &err)) << err; | |
| return time; | |
| }(); | |
| return time; | |
| } | |
| bssl::UniquePtr<X509> ParseCert(absl::string_view pem) { | |
| CBS cbs = FromStringView(pem); | |
| bssl::UniquePtr<BIO> bio(BIO_new_mem_buf(CBS_data(&cbs), CBS_len(&cbs))); | |
| CHECK(bio != nullptr); | |
| bssl::UniquePtr<X509> x509( | |
| PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr)); | |
| CHECK(x509 != nullptr); | |
| return x509; | |
| } | |
| std::string RawDer(X509* x509) { | |
| uint8_t* der_ptr = nullptr; | |
| size_t der_len = i2d_X509(x509, &der_ptr); | |
| bssl::UniquePtr<uint8_t> der(der_ptr); | |
| return std::string(reinterpret_cast<char*>(der_ptr), der_len); | |
| } | |
| std::string RawDer(absl::string_view pem) { | |
| return RawDer(ParseCert(pem).get()); | |
| } | |
| 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; | |
| } | |
| absl::string_view ValidNonce() { | |
| static absl::NoDestructor<std::string> nonce(EncodeDerInteger(0xf00dbabe)); | |
| return *nonce; | |
| } | |
| std::string SigningCertificateV2AttributeFromPem( | |
| absl::Span<const absl::string_view> pems) { | |
| std::vector<bssl::UniquePtr<X509>> certs; | |
| for (absl::string_view pem : pems) { | |
| certs.push_back(ParseCert(pem)); | |
| } | |
| std::vector<X509*> ptr_vec; | |
| absl::c_transform(certs, std::back_inserter(ptr_vec), | |
| [](const auto& cert) { return cert.get(); }); | |
| return CreateSigningCertificateV2Attribute(absl::MakeSpan(ptr_vec), | |
| HashAlgorithm::kSha256); | |
| } | |
| class TimestampVerifierTest : public testing::Test { | |
| protected: | |
| TimestampVerifierTest() : no_trust_checks_(/*tsa_roots=*/nullptr) {} | |
| void SetupMockDefaults(MockParsedTimestampToken& mock_token, | |
| const std::vector<std::string>& cert_chain, | |
| absl::string_view v2_val, absl::string_view tst_info) { | |
| ON_CALL(mock_token, GetCertificateChain()) | |
| .WillByDefault(Return(cert_chain)); | |
| ON_CALL(mock_token, GetMessageImprintHashAlgorithm()) | |
| .WillByDefault(Return(HashAlgorithm::kSha384)); | |
| ON_CALL(mock_token, VerifySignature()) | |
| .WillByDefault(Return(absl::OkStatus())); | |
| ON_CALL(mock_token, GetSigningCertificateV2Bytes()) | |
| .WillByDefault(Return(v2_val)); | |
| ON_CALL(mock_token, GetTstInfoBytes()).WillByDefault(Return(tst_info)); | |
| } | |
| TimestampVerifier no_trust_checks_; | |
| }; | |
| TEST_F(TimestampVerifierTest, AcceptsRsaTimestamp) { | |
| std::string tst_info = CreateTstInfo(kValidTimestamp, kMessageImprintHash, | |
| kSha384OidTxt, ValidNonce()); | |
| std::string v2_attr = SigningCertificateV2AttributeFromPem({kTsaRsaPem}); | |
| std::string v2_val = GetSigningCertificateV2Value(v2_attr); | |
| std::string cert_der = RawDer(kTsaRsaPem); | |
| MockParsedTimestampToken mock_token; | |
| SetupMockDefaults(mock_token, {cert_der}, v2_val, tst_info); | |
| auto timestamp = no_trust_checks_.VerifyTimestampToken(mock_token); | |
| ASSERT_TRUE(timestamp.ok()); | |
| EXPECT_EQ(timestamp->asserted_time(), ValidTimestampTime()); | |
| EXPECT_THAT(timestamp->message_imprint_hash(), Eq(kMessageImprintHash)); | |
| EXPECT_THAT(timestamp->message_imprint_hash_algorithm(), | |
| Eq(HashAlgorithm::kSha384)); | |
| EXPECT_THAT(timestamp->nonce(), Eq(ValidNonce())); | |
| EXPECT_THAT(timestamp->tsa_certificate(), Eq(RawDer(kTsaRsaPem))); | |
| EXPECT_THAT(timestamp->certificate_chain(), ElementsAre(RawDer(kTsaRsaPem))); | |
| } | |
| TEST_F(TimestampVerifierTest, AcceptsEcdsaTimestamp) { | |
| std::string tst_info = CreateTstInfo(kValidTimestamp, kMessageImprintHash, | |
| kSha384OidTxt, ValidNonce()); | |
| std::string v2_attr = SigningCertificateV2AttributeFromPem({kTsaPem}); | |
| std::string v2_val = GetSigningCertificateV2Value(v2_attr); | |
| std::string cert_der = RawDer(kTsaPem); | |
| MockParsedTimestampToken mock_token; | |
| SetupMockDefaults(mock_token, {cert_der}, v2_val, tst_info); | |
| auto timestamp = no_trust_checks_.VerifyTimestampToken(mock_token); | |
| ASSERT_TRUE(timestamp.ok()); | |
| EXPECT_EQ(timestamp->asserted_time(), ValidTimestampTime()); | |
| EXPECT_THAT(timestamp->message_imprint_hash(), Eq(kMessageImprintHash)); | |
| EXPECT_THAT(timestamp->message_imprint_hash_algorithm(), | |
| Eq(HashAlgorithm::kSha384)); | |
| EXPECT_THAT(timestamp->nonce(), Eq(ValidNonce())); | |
| EXPECT_THAT(timestamp->tsa_certificate(), Eq(cert_der)); | |
| EXPECT_THAT(timestamp->certificate_chain(), ElementsAre(cert_der)); | |
| } | |
| TEST_F(TimestampVerifierTest, AcceptsTimestampWithIntermediateCertificate) { | |
| std::string tst_info = CreateTstInfo(kValidTimestamp, kMessageImprintHash, | |
| kSha384OidTxt, ValidNonce()); | |
| std::string v2_attr = | |
| SigningCertificateV2AttributeFromPem({kTsaWithIntermediatePem}); | |
| std::string v2_val = GetSigningCertificateV2Value(v2_attr); | |
| std::string cert_der = RawDer(kTsaWithIntermediatePem); | |
| std::string intermediate_der = RawDer(kIntermediatePem); | |
| MockParsedTimestampToken mock_token; | |
| SetupMockDefaults(mock_token, {cert_der, intermediate_der}, v2_val, tst_info); | |
| auto timestamp = no_trust_checks_.VerifyTimestampToken(mock_token); | |
| ASSERT_TRUE(timestamp.ok()); | |
| EXPECT_EQ(timestamp->asserted_time(), ValidTimestampTime()); | |
| EXPECT_THAT(timestamp->message_imprint_hash(), Eq(kMessageImprintHash)); | |
| EXPECT_THAT(timestamp->message_imprint_hash_algorithm(), | |
| Eq(HashAlgorithm::kSha384)); | |
| EXPECT_THAT(timestamp->nonce(), Eq(ValidNonce())); | |
| EXPECT_THAT(timestamp->tsa_certificate(), Eq(cert_der)); | |
| EXPECT_THAT(timestamp->certificate_chain(), | |
| ElementsAre(cert_der, intermediate_der)); | |
| } | |
| TEST_F(TimestampVerifierTest, GetSigningCertificateV2BytesFails) { | |
| MockParsedTimestampToken mock_token; | |
| EXPECT_CALL(mock_token, GetCertificateChain()) | |
| .WillOnce(Return(std::vector<std::string>{RawDer(kTsaRsaPem)})); | |
| EXPECT_CALL(mock_token, GetMessageImprintHashAlgorithm()) | |
| .WillOnce(Return(HashAlgorithm::kSha384)); | |
| EXPECT_CALL(mock_token, VerifySignature()).WillOnce(Return(absl::OkStatus())); | |
| EXPECT_CALL(mock_token, GetSigningCertificateV2Bytes()) | |
| .WillOnce(Return(absl::InvalidArgumentError("Missing attribute"))); | |
| EXPECT_THAT( | |
| no_trust_checks_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kInvalidArgument, "Missing attribute")); | |
| } | |
| TEST_F(TimestampVerifierTest, InvalidTstInfoFails) { | |
| std::string v2_attr = SigningCertificateV2AttributeFromPem({kTsaRsaPem}); | |
| std::string v2_val = GetSigningCertificateV2Value(v2_attr); | |
| MockParsedTimestampToken mock_token; | |
| SetupMockDefaults(mock_token, {RawDer(kTsaRsaPem)}, v2_val, | |
| "THIS IS NOT BER OR DER OR CER OR AER OR ..."); | |
| EXPECT_THAT(no_trust_checks_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("could not read top-level sequence"))); | |
| } | |
| TEST_F(TimestampVerifierTest, SignatureVerificationFails) { | |
| MockParsedTimestampToken mock_token; | |
| EXPECT_CALL(mock_token, GetCertificateChain()) | |
| .WillOnce(Return(std::vector<std::string>{RawDer(kTsaPem)})); | |
| EXPECT_CALL(mock_token, GetMessageImprintHashAlgorithm()) | |
| .WillOnce(Return(HashAlgorithm::kSha384)); | |
| EXPECT_CALL(mock_token, VerifySignature()) | |
| .WillOnce(Return(absl::UnauthenticatedError("Signature failed"))); | |
| EXPECT_THAT(no_trust_checks_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kUnauthenticated, | |
| HasSubstr("Signature failed"))); | |
| } | |
| TEST_F(TimestampVerifierTest, EmptyChainFails) { | |
| MockParsedTimestampToken mock_token; | |
| EXPECT_CALL(mock_token, GetCertificateChain()) | |
| .WillOnce(Return(std::vector<std::string>{})); | |
| EXPECT_CALL(mock_token, GetMessageImprintHashAlgorithm()) | |
| .WillOnce(Return(HashAlgorithm::kSha384)); | |
| EXPECT_THAT(no_trust_checks_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kInternal, | |
| HasSubstr("TSA leaf cert missing"))); | |
| } | |
| TEST_F(TimestampVerifierTest, InvalidSigningCertificateAttribute) { | |
| MockParsedTimestampToken mock_token; | |
| EXPECT_CALL(mock_token, GetCertificateChain()) | |
| .WillOnce(Return(std::vector<std::string>{RawDer(kTsaRsaPem)})); | |
| EXPECT_CALL(mock_token, GetMessageImprintHashAlgorithm()) | |
| .WillOnce(Return(HashAlgorithm::kSha384)); | |
| EXPECT_CALL(mock_token, VerifySignature()).WillOnce(Return(absl::OkStatus())); | |
| EXPECT_CALL(mock_token, GetSigningCertificateV2Bytes()) | |
| .WillOnce(Return("THIS IS NOT A DER SEQUENCE")); | |
| EXPECT_THAT(no_trust_checks_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| "could not parse DER SEQUENCE for SigningCertificateV2 " | |
| "attribute value")); | |
| } | |
| TEST_F(TimestampVerifierTest, UnrecognizedSiginingCertificateHashAlgorithm) { | |
| CBB cbb; | |
| CHECK(CBB_init(&cbb, 1000)); | |
| CBB value_seq_cbb; | |
| CHECK(CBB_add_asn1(&cbb, &value_seq_cbb, CBS_ASN1_SEQUENCE)); | |
| CBB certs_cbb; | |
| CHECK(CBB_add_asn1(&value_seq_cbb, &certs_cbb, CBS_ASN1_SEQUENCE)); | |
| constexpr absl::string_view kFakeOid = "1.2.840.113549.1.800.888.8888"; | |
| CBS hash_cbs = FromStringView( | |
| "thisisnotarealhashbutthetestwontcarebecauseitwillfailduetothebadoidanywa" | |
| "ys"); | |
| CBB certs_entry_cbb; | |
| CHECK(CBB_add_asn1(&certs_cbb, &certs_entry_cbb, CBS_ASN1_SEQUENCE)); | |
| CBB obj_cbb; | |
| CHECK(CBB_add_asn1(&certs_entry_cbb, &obj_cbb, CBS_ASN1_OBJECT)); | |
| CHECK( | |
| CBB_add_asn1_oid_from_text(&obj_cbb, kFakeOid.data(), kFakeOid.length())); | |
| CHECK(CBB_flush(&certs_entry_cbb)); | |
| CHECK(CBB_add_asn1_octet_string(&certs_entry_cbb, CBS_data(&hash_cbs), | |
| CBS_len(&hash_cbs))); | |
| auto value_bytes = FinishToString(&cbb); | |
| ASSERT_TRUE(value_bytes.ok()); | |
| MockParsedTimestampToken mock_token; | |
| EXPECT_CALL(mock_token, GetCertificateChain()) | |
| .WillOnce(Return(std::vector<std::string>{RawDer(kTsaRsaPem)})); | |
| EXPECT_CALL(mock_token, GetMessageImprintHashAlgorithm()) | |
| .WillOnce(Return(HashAlgorithm::kSha384)); | |
| EXPECT_CALL(mock_token, VerifySignature()).WillOnce(Return(absl::OkStatus())); | |
| EXPECT_CALL(mock_token, GetSigningCertificateV2Bytes()) | |
| .WillOnce(Return(*value_bytes)); | |
| EXPECT_THAT(no_trust_checks_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| "unrecognized OID in `certs.hashAlgorithm` field of " | |
| "SigningCertificateV2 attribute value; OID value: " | |
| "1.2.840.113549.1.800.888.8888")); | |
| } | |
| TEST_F(TimestampVerifierTest, NoSigningCertificateHash) { | |
| CBB cbb; | |
| CHECK(CBB_init(&cbb, 1000)); | |
| CBB value_seq_cbb; | |
| CHECK(CBB_add_asn1(&cbb, &value_seq_cbb, CBS_ASN1_SEQUENCE)); | |
| CBB certs_cbb; | |
| CHECK(CBB_add_asn1(&value_seq_cbb, &certs_cbb, CBS_ASN1_SEQUENCE)); | |
| CBB certs_entry_cbb; | |
| CHECK(CBB_add_asn1(&certs_cbb, &certs_entry_cbb, CBS_ASN1_SEQUENCE)); | |
| CHECK(OBJ_nid2cbb(&certs_entry_cbb, NID_sha384)); | |
| CHECK(CBB_flush(&certs_entry_cbb)); | |
| auto value_bytes = FinishToString(&cbb); | |
| ASSERT_TRUE(value_bytes.ok()); | |
| MockParsedTimestampToken mock_token; | |
| EXPECT_CALL(mock_token, GetCertificateChain()) | |
| .WillOnce(Return(std::vector<std::string>{RawDer(kTsaRsaPem)})); | |
| EXPECT_CALL(mock_token, GetMessageImprintHashAlgorithm()) | |
| .WillOnce(Return(HashAlgorithm::kSha384)); | |
| EXPECT_CALL(mock_token, VerifySignature()).WillOnce(Return(absl::OkStatus())); | |
| EXPECT_CALL(mock_token, GetSigningCertificateV2Bytes()) | |
| .WillOnce(Return(*value_bytes)); | |
| EXPECT_THAT( | |
| no_trust_checks_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| "could not parse `certs.hash` field (DER OCTETSTRING) of " | |
| "SigningCertificateV2 attribute value")); | |
| } | |
| TEST_F(TimestampVerifierTest, UnsupportedSigningCertificateHashAlgorithm) { | |
| CBB cbb; | |
| CHECK(CBB_init(&cbb, 1000)); | |
| CBB value_seq_cbb; | |
| CHECK(CBB_add_asn1(&cbb, &value_seq_cbb, CBS_ASN1_SEQUENCE)); | |
| CBB certs_cbb; | |
| CHECK(CBB_add_asn1(&value_seq_cbb, &certs_cbb, CBS_ASN1_SEQUENCE)); | |
| CBS hash_cbs = FromStringView("Not a real hash"); | |
| CBB certs_entry_cbb; | |
| CHECK(CBB_add_asn1(&certs_cbb, &certs_entry_cbb, CBS_ASN1_SEQUENCE)); | |
| CHECK(OBJ_nid2cbb(&certs_entry_cbb, NID_sha1)); | |
| CHECK(CBB_flush(&certs_entry_cbb)); | |
| CHECK(CBB_add_asn1_octet_string(&certs_entry_cbb, CBS_data(&hash_cbs), | |
| CBS_len(&hash_cbs))); | |
| auto value_bytes = FinishToString(&cbb); | |
| ASSERT_TRUE(value_bytes.ok()); | |
| MockParsedTimestampToken mock_token; | |
| EXPECT_CALL(mock_token, GetCertificateChain()) | |
| .WillOnce(Return(std::vector<std::string>{RawDer(kTsaRsaPem)})); | |
| EXPECT_CALL(mock_token, GetMessageImprintHashAlgorithm()) | |
| .WillOnce(Return(HashAlgorithm::kSha384)); | |
| EXPECT_CALL(mock_token, VerifySignature()).WillOnce(Return(absl::OkStatus())); | |
| EXPECT_CALL(mock_token, GetSigningCertificateV2Bytes()) | |
| .WillOnce(Return(*value_bytes)); | |
| EXPECT_THAT(no_trust_checks_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| "unsupported hash algorithm in `certs.hashAlgorithm` " | |
| "field of SigningCertificateV2 attribute value: hash " | |
| "algorithm NID is not on C2PA allowlist: 64")); | |
| } | |
| class TrustChecksTest : public ::testing::Test { | |
| protected: | |
| TrustChecksTest() | |
| : tsa_roots_(LoadTestRoots().value()), verifier_(tsa_roots_.get()) {} | |
| absl::StatusOr<std::unique_ptr<bssl::VerifyTrustStore>> LoadTestRoots() { | |
| return LoadTrustStore(kCaPem); | |
| } | |
| void SetupMockDefaults(MockParsedTimestampToken& mock_token, | |
| const std::vector<std::string>& cert_chain, | |
| absl::string_view v2_val, absl::string_view tst_info) { | |
| ON_CALL(mock_token, GetCertificateChain()) | |
| .WillByDefault(Return(cert_chain)); | |
| ON_CALL(mock_token, GetMessageImprintHashAlgorithm()) | |
| .WillByDefault(Return(HashAlgorithm::kSha384)); | |
| ON_CALL(mock_token, VerifySignature()) | |
| .WillByDefault(Return(absl::OkStatus())); | |
| ON_CALL(mock_token, GetSigningCertificateV2Bytes()) | |
| .WillByDefault(Return(v2_val)); | |
| ON_CALL(mock_token, GetTstInfoBytes()).WillByDefault(Return(tst_info)); | |
| } | |
| const std::unique_ptr<bssl::VerifyTrustStore> tsa_roots_; | |
| TimestampVerifier verifier_; | |
| }; | |
| TEST_F(TrustChecksTest, Trusted) { | |
| constexpr absl::string_view kTime = "20250205000000Z"; | |
| std::string tst_info = | |
| CreateTstInfo(kTime, kMessageImprintHash, kSha384OidTxt); | |
| std::string v2_attr = SigningCertificateV2AttributeFromPem({kTsaPem}); | |
| std::string v2_val = GetSigningCertificateV2Value(v2_attr); | |
| std::string cert_der = RawDer(kTsaPem); | |
| MockParsedTimestampToken mock_token; | |
| SetupMockDefaults(mock_token, {cert_der}, v2_val, tst_info); | |
| auto verified_timestamp = verifier_.VerifyTimestampToken(mock_token); | |
| ASSERT_TRUE(verified_timestamp.ok()); | |
| EXPECT_THAT(verified_timestamp->asserted_time(), Eq(ParseGenTime(kTime))); | |
| } | |
| TEST_F(TrustChecksTest, TrustedWithIntermediate) { | |
| constexpr absl::string_view kTime = "20250514000000Z"; | |
| std::string tst_info = | |
| CreateTstInfo(kTime, kMessageImprintHash, kSha384OidTxt); | |
| std::string v2_attr = | |
| SigningCertificateV2AttributeFromPem({kTsaWithIntermediatePem}); | |
| std::string v2_val = GetSigningCertificateV2Value(v2_attr); | |
| std::string cert_der = RawDer(kTsaWithIntermediatePem); | |
| std::string intermediate_der = RawDer(kIntermediatePem); | |
| MockParsedTimestampToken mock_token; | |
| SetupMockDefaults(mock_token, {cert_der, intermediate_der}, v2_val, tst_info); | |
| auto verified_timestamp = verifier_.VerifyTimestampToken(mock_token); | |
| ASSERT_TRUE(verified_timestamp.ok()); | |
| EXPECT_THAT(verified_timestamp->asserted_time(), Eq(ParseGenTime(kTime))); | |
| } | |
| TEST_F(TrustChecksTest, TooManyIntermediateCerts) { | |
| constexpr absl::string_view kTime = "20250514000000Z"; | |
| std::string tst_info = | |
| CreateTstInfo(kTime, kMessageImprintHash, kSha384OidTxt); | |
| std::string v2_attr = | |
| SigningCertificateV2AttributeFromPem({kTsaWithIntermediatePem}); | |
| std::string v2_val = GetSigningCertificateV2Value(v2_attr); | |
| std::string cert_der = RawDer(kTsaWithIntermediatePem); | |
| std::string intermediate_der = RawDer(kIntermediatePem); | |
| MockParsedTimestampToken mock_token; | |
| // Chain length 4 (1 leaf + 3 intermediates) | |
| SetupMockDefaults( | |
| mock_token, | |
| {cert_der, intermediate_der, intermediate_der, intermediate_der}, v2_val, | |
| tst_info); | |
| EXPECT_THAT( | |
| verifier_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kUnauthenticated, | |
| HasSubstr("excessive timestamp certificate chain length"))); | |
| } | |
| TEST_F(TrustChecksTest, TrustedMissingIntermediate) { | |
| constexpr absl::string_view kTime = "20250514000000Z"; | |
| std::string tst_info = | |
| CreateTstInfo(kTime, kMessageImprintHash, kSha384OidTxt); | |
| std::string v2_attr = | |
| SigningCertificateV2AttributeFromPem({kTsaWithIntermediatePem}); | |
| std::string v2_val = GetSigningCertificateV2Value(v2_attr); | |
| std::string cert_der = RawDer(kTsaWithIntermediatePem); | |
| MockParsedTimestampToken mock_token; | |
| // Missing intermediate! | |
| SetupMockDefaults(mock_token, {cert_der}, v2_val, tst_info); | |
| EXPECT_THAT(verifier_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kUnauthenticated, | |
| HasSubstr("No matching issuer found"))); | |
| } | |
| TEST_F(TrustChecksTest, Untrusted) { | |
| constexpr absl::string_view kTime = "20250205000000Z"; | |
| std::string tst_info = | |
| CreateTstInfo(kTime, kMessageImprintHash, kSha384OidTxt); | |
| std::string dummy_cert = "DUMMY_CERT_DER"; | |
| // Compute SHA256 of dummy_cert using internal OpenSSL without hasher.h | |
| uint8_t hash[32]; | |
| unsigned int hash_len; | |
| CHECK(EVP_Digest(dummy_cert.data(), dummy_cert.length(), hash, &hash_len, | |
| EVP_sha256(), nullptr)); | |
| std::string digest(reinterpret_cast<char*>(hash), hash_len); | |
| CBB cbb; | |
| CHECK(CBB_init(&cbb, 1000)); | |
| CBB value_seq_cbb; | |
| CHECK(CBB_add_asn1(&cbb, &value_seq_cbb, CBS_ASN1_SEQUENCE)); | |
| CBB certs_cbb; | |
| CHECK(CBB_add_asn1(&value_seq_cbb, &certs_cbb, CBS_ASN1_SEQUENCE)); | |
| CBB certs_entry_cbb; | |
| CHECK(CBB_add_asn1(&certs_cbb, &certs_entry_cbb, CBS_ASN1_SEQUENCE)); | |
| CHECK(OBJ_nid2cbb(&certs_entry_cbb, NID_sha256)); | |
| CHECK(CBB_flush(&certs_entry_cbb)); | |
| CBS hash_cbs = FromStringView(digest); | |
| CHECK(CBB_add_asn1_octet_string(&certs_entry_cbb, CBS_data(&hash_cbs), | |
| CBS_len(&hash_cbs))); | |
| auto v2_val = FinishToString(&cbb); | |
| ASSERT_TRUE(v2_val.ok()); | |
| MockParsedTimestampToken mock_token; | |
| SetupMockDefaults(mock_token, {dummy_cert}, *v2_val, tst_info); | |
| EXPECT_THAT(verifier_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kUnauthenticated, | |
| HasSubstr("timestamp certificate chain"))); | |
| } | |
| TEST_F(TrustChecksTest, InvalidEku) { | |
| constexpr absl::string_view kTime = "20250205000000Z"; | |
| std::string tst_info = | |
| CreateTstInfo(kTime, kMessageImprintHash, kSha384OidTxt); | |
| std::string v2_attr = SigningCertificateV2AttributeFromPem({kClaimSignerPem}); | |
| std::string v2_val = GetSigningCertificateV2Value(v2_attr); | |
| std::string cert_der = RawDer(kClaimSignerPem); | |
| MockParsedTimestampToken mock_token; | |
| SetupMockDefaults(mock_token, {cert_der}, v2_val, tst_info); | |
| // The certificate has Email Protection EKU, which is not allowed for TSAs. | |
| EXPECT_THAT(verifier_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kUnauthenticated, | |
| HasSubstr("prohibited key usage"))); | |
| } | |
| TEST_F(TrustChecksTest, SigningCertificateV2AttributeLeafMismatch) { | |
| constexpr absl::string_view kTime = "21250204161300Z"; | |
| std::string tst_info = | |
| CreateTstInfo(kTime, kMessageImprintHash, kSha384OidTxt); | |
| std::string v2_attr = SigningCertificateV2AttributeFromPem({kTsaRsaPem}); | |
| std::string v2_val = GetSigningCertificateV2Value(v2_attr); | |
| std::string cert_der = RawDer(kTsaPem); | |
| MockParsedTimestampToken mock_token; | |
| SetupMockDefaults(mock_token, {cert_der}, v2_val, tst_info); | |
| EXPECT_THAT( | |
| verifier_.VerifyTimestampToken(mock_token), | |
| StatusIs( | |
| absl::StatusCode::kUnauthenticated, | |
| HasSubstr("TSA cert does not match SigningCertificateV2 attribute: " | |
| "certificate does not match EssCertIDV2 value"))); | |
| } | |
| TEST_F(TrustChecksTest, EmptySigningCertificateAttribute) { | |
| CBB cbb; | |
| CHECK(CBB_init(&cbb, 1000)); | |
| CBB value_seq_cbb; | |
| CHECK(CBB_add_asn1(&cbb, &value_seq_cbb, CBS_ASN1_SEQUENCE)); | |
| CBB certs_cbb; | |
| CHECK(CBB_add_asn1(&value_seq_cbb, &certs_cbb, CBS_ASN1_SEQUENCE)); | |
| auto value_bytes = FinishToString(&cbb); | |
| ASSERT_TRUE(value_bytes.ok()); | |
| constexpr absl::string_view kTime = "20250205000000Z"; | |
| std::string tst_info = | |
| CreateTstInfo(kTime, kMessageImprintHash, kSha384OidTxt); | |
| std::string cert_der = RawDer(kTsaRsaPem); | |
| MockParsedTimestampToken mock_token; | |
| SetupMockDefaults(mock_token, {cert_der}, *value_bytes, tst_info); | |
| EXPECT_THAT( | |
| verifier_.VerifyTimestampToken(mock_token), | |
| StatusIs( | |
| absl::StatusCode::kInvalidArgument, | |
| "SigningCertificateV2 signed attribute is empty; at minimum, " | |
| "TSAs must record their own leaf certificate in this attribute")); | |
| } | |
| TEST_F(TrustChecksTest, OutsideTsaCertValidityWindow) { | |
| // This time is outside the validity window of the test TSA certificate, | |
| // but inside the validity window of the test CA certificate. | |
| constexpr absl::string_view kTime = "20250204161300Z"; | |
| std::string tst_info = | |
| CreateTstInfo(kTime, kMessageImprintHash, kSha384OidTxt); | |
| std::string v2_attr = SigningCertificateV2AttributeFromPem({kTsaPem}); | |
| std::string v2_val = GetSigningCertificateV2Value(v2_attr); | |
| std::string cert_der = RawDer(kTsaPem); | |
| MockParsedTimestampToken mock_token; | |
| SetupMockDefaults(mock_token, {cert_der}, v2_val, tst_info); | |
| EXPECT_THAT( | |
| verifier_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kOutOfRange, HasSubstr("before notBefore"))); | |
| } | |
| TEST_F(TrustChecksTest, OutsideCaCertValidityWindow) { | |
| // This time is inside the validity window of the test TSA certificate, | |
| // but outside the validity window of the test CA certificate. | |
| constexpr absl::string_view kTime = "21250204161300Z"; | |
| std::string tst_info = | |
| CreateTstInfo(kTime, kMessageImprintHash, kSha384OidTxt); | |
| std::string v2_attr = SigningCertificateV2AttributeFromPem({kTsaPem}); | |
| std::string v2_val = GetSigningCertificateV2Value(v2_attr); | |
| std::string cert_der = RawDer(kTsaPem); | |
| MockParsedTimestampToken mock_token; | |
| SetupMockDefaults(mock_token, {cert_der}, v2_val, tst_info); | |
| EXPECT_THAT( | |
| verifier_.VerifyTimestampToken(mock_token), | |
| StatusIs(absl::StatusCode::kOutOfRange, HasSubstr("after notAfter"))); | |
| } | |
| } // namespace | |
| } // namespace credentio |