| // 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 |