| // 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 "cose/ocsp_verifier.h" | |
| #include <memory> | |
| #include <string> | |
| #include <vector> | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_matchers.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/time/clock.h" | |
| #include "absl/time/time.h" | |
| #include "absl/types/span.h" | |
| #include "constants/status_codes.h" | |
| #include "crypto/default/pem.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "proto/validation_status.pb.h" | |
| #include "testing/pem.h" | |
| #include "tsp/verified_timestamp.h" | |
| namespace credentio { | |
| namespace { | |
| using ::testing::ElementsAre; | |
| using ::testing::Eq; | |
| using ::testing::IsEmpty; | |
| using ::testing::IsFalse; | |
| using ::testing::SizeIs; | |
| class MockCryptoReadHandler : public CryptoReadHandler { | |
| public: | |
| MOCK_METHOD(absl::StatusOr<std::unique_ptr<ParsedCertificates>>, | |
| ParseCertificatesDer, | |
| (absl::Span<const absl::string_view> certificates), | |
| (const, override)); | |
| MOCK_METHOD(absl::StatusOr<VerifiedTimestamp>, VerifyTimestamp, | |
| (absl::string_view cms), (const, override)); | |
| MOCK_METHOD(absl::StatusOr<OCSPRevocationStatus>, VerifyOcspResponse, | |
| (absl::string_view ocsp_response_der, | |
| absl::string_view certificate_der, | |
| absl::string_view issuer_certificate_der, | |
| absl::Time verify_time), | |
| (const, override)); | |
| MOCK_METHOD(TrustEnvironment, trust_environment, (), (const, override)); | |
| }; | |
| // Matches validation status codes. | |
| auto C2paStatusIs(auto status_code) { | |
| return ::testing::Property(&ValidationStatus::code, | |
| Eq(absl::StrCat(status_code))); | |
| } | |
| absl::StatusOr<absl::Time> ParseTime(absl::string_view time_str) { | |
| absl::Time time; | |
| std::string err; | |
| if (!absl::ParseTime(absl::RFC3339_full, time_str, &time, &err)) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Failed to parse time: ", err)); | |
| } | |
| return time; | |
| } | |
| TEST(OcspVerifierTest, RecordSkippedOcspCheck) { | |
| ValidationStatusSet status_set; | |
| RecordSkippedOcspCheck(&status_set); | |
| EXPECT_THAT(status_set.failures(), IsEmpty()); | |
| EXPECT_THAT(status_set.successes(), IsEmpty()); | |
| EXPECT_THAT(status_set.informationals(), | |
| ElementsAre(C2paStatusIs( | |
| InformationalStatusCode::kSigningCredentialOcspSkipped))); | |
| } | |
| TEST(OcspVerifierTest, SkippedWhenResponsesEmpty) { | |
| ValidationStatusSet status_set; | |
| std::vector<std::string> ocsp_responses = {}; | |
| std::vector<std::string> trust_chain = {"fake_cert_1", "fake_cert_2"}; | |
| absl::Time asserted_time = absl::Now(); | |
| MockCryptoReadHandler mock_crypto_handler; | |
| EXPECT_CALL(mock_crypto_handler, VerifyOcspResponse).Times(0); | |
| OcspVerifier verifier(&mock_crypto_handler); | |
| EXPECT_TRUE(verifier.VerifyOcspResponses(ocsp_responses, trust_chain, | |
| asserted_time, &status_set)); | |
| EXPECT_THAT(status_set.failures(), IsEmpty()); | |
| EXPECT_THAT(status_set.successes(), IsEmpty()); | |
| EXPECT_THAT(status_set.informationals(), | |
| ElementsAre(C2paStatusIs( | |
| InformationalStatusCode::kSigningCredentialOcspSkipped))); | |
| } | |
| TEST(OcspVerifierTest, SkippedWhenNoValidResponses) { | |
| ValidationStatusSet status_set; | |
| // Valid check config but fake inputs that won't match/decode successfully. | |
| std::vector<std::string> ocsp_responses = {"invalid_ocsp_data"}; | |
| std::vector<std::string> trust_chain = {"invalid_cert_1", "invalid_cert_2"}; | |
| absl::Time asserted_time = absl::Now(); | |
| MockCryptoReadHandler mock_crypto_handler; | |
| EXPECT_CALL(mock_crypto_handler, VerifyOcspResponse) | |
| .WillOnce(::testing::Return(absl::InvalidArgumentError("invalid ocsp"))); | |
| OcspVerifier verifier(&mock_crypto_handler); | |
| EXPECT_TRUE(verifier.VerifyOcspResponses(ocsp_responses, trust_chain, | |
| asserted_time, &status_set)); | |
| // Since CheckOCSP fails to identify revocation status, it will be neither | |
| // good nor revoked, hence recorded as skipped. | |
| EXPECT_THAT(status_set.failures(), IsEmpty()); | |
| EXPECT_THAT(status_set.successes(), IsEmpty()); | |
| EXPECT_THAT(status_set.informationals(), | |
| ElementsAre(C2paStatusIs( | |
| InformationalStatusCode::kSigningCredentialOcspSkipped))); | |
| } | |
| // certs/claim_signer_ocsp_2010_2024.pem | |
| static constexpr absl::string_view kSubjectCertPem = | |
| R"(-----BEGIN CERTIFICATE----- | |
| MIIBVjCB/qADAgECAhEAqY+Ga0uKJc71iAGCQ2xmSTAKBggqhkjOPQQDAjAUMRIw | |
| EAYDVQQDEwlSb290IENBIDEwHhcNMTAwMTAxMDAwMDAwWhcNMjQxMjMxMjM1OTU5 | |
| WjAcMRowGAYDVQQDExFDbGFpbSBTaWduZXIgT0NTUDBZMBMGByqGSM49AgEGCCqG | |
| SM49AwEHA0IABIekbtQdrGd0nLIoGkyxOxUHVDrdf4kCsoG1oy8hKUp0JX1nN6I5 | |
| Uki0Rdnjjp0lkhRC7pS0ltq3RzYF2QLH9j2jKTAnMA4GA1UdDwEB/wQEAwIHgDAV | |
| BgNVHSUEDjAMBgorBgEEAYPoXgIBMAoGCCqGSM49BAMCA0cAMEQCIEoOXctlLyTC | |
| 9ICtWt37D3UoLbtmGdPH/QKLuN5cEKmRAiABCe2qUdMzcrwR5rHJd2xQ8l4JZgv9 | |
| t7/qA0tV/kHm6g== | |
| -----END CERTIFICATE----- | |
| )"; | |
| // certs/root_ca1_cert.pem | |
| static constexpr absl::string_view kIssuerCertPem = | |
| R"(-----BEGIN CERTIFICATE----- | |
| MIIBaDCCAQ6gAwIBAgIQQjiBDyR9IFSiOk3YI9Vz6jAKBggqhkjOPQQDAjAUMRIw | |
| EAYDVQQDEwlSb290IENBIDEwHhcNMDAwMTAxMDAwMDAwWhcNMjAwMTAxMDAwMDAw | |
| WjAUMRIwEAYDVQQDEwlSb290IENBIDEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC | |
| AATGlEZ2h/3CeOl6anwJPvk9rY0SbQAUnM7ogiSCh2Xh8A4MA/ZFvFxLokfUS+Le | |
| Rew96qN9o6jC7EXGs5Dlcql2o0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ | |
| BAUwAwEB/zAdBgNVHQ4EFgQUoD7enXLkSPQ4NFbWMsyPJw8CkREwCgYIKoZIzj0E | |
| AwIDSAAwRQIgZHyZ6carq1CPrvQA87zDsfqVe1Hcy7/Hd2Pz8fTbn/YCIQC68wrO | |
| LL8RURusZlo0c7KhG/TBTG368XayFcJbTq5piA== | |
| -----END CERTIFICATE----- | |
| )"; | |
| // ocsp/good_2012.pem | |
| static constexpr absl::string_view kGoodOcspResponsePem = | |
| R"(-----BEGIN OCSP RESPONSE----- | |
| MIICeAoBAKCCAnEwggJtBgkrBgEFBQcwAQEEggJeMIICWjCBpKEbMBkxFzAVBgNV | |
| BAMTDk9DU1AgUmVzcG9uZGVyGA8yMDEyMDEwMTAwMDAwMFowdDByMEowCQYFKw4D | |
| AhoFAAQUH/QKbQfkANfLUhHO1/Ux3C+InIMEFLEaFwXj/kcm0icZbAOdlyD4YMUx | |
| AhEAqY+Ga0uKJc71iAGCQ2xmSYAAGA8yMDEyMDEwMTAwMDAwMFqgERgPMjAxMjEy | |
| MzEyMzU5NTlaMAoGCCqGSM49BAMCA0cAMEQCIFBWX+xMGwGlswHlzGoxUqNe4MQS | |
| 6M6rsH3WVhB6V1XqAiAH7ENBzOuqkiwuyn/Ll22OehLDOwhGkcPj4ngBH981X6CC | |
| AVowggFWMIIBUjCB+aADAgECAhEAylWc524QSfWaV4Y1C8VtVTAKBggqhkjOPQQD | |
| AjAUMRIwEAYDVQQDEwlSb290IENBIDEwHhcNMTAwMTAxMDAwMDAwWhcNMjQxMjMx | |
| MjM1OTU5WjAZMRcwFQYDVQQDEw5PQ1NQIFJlc3BvbmRlcjBZMBMGByqGSM49AgEG | |
| CCqGSM49AwEHA0IABBxOD6IfWwY2vJ/dQfo3iquqL7jqrliO6KquLxGJYOuhmGiX | |
| mgKhnXHv9fAF3xnb09BHZMqVvkW/Vjqx3VvWRjajJzAlMA4GA1UdDwEB/wQEAwIH | |
| gDATBgNVHSUEDDAKBggrBgEFBQcDCTAKBggqhkjOPQQDAgNIADBFAiAk4CANeeK7 | |
| R5RQ+lgZkgiegSUaFu4pxHJzQ8yK2XnsqQIhAMevOsBvD9R6o5AmZJgn/dDsNwGQ | |
| FuM38EKpIEjm+lH0 | |
| -----END OCSP RESPONSE----- | |
| )"; | |
| // ocsp/revoked_2012.pem | |
| static constexpr absl::string_view kRevokedOcspResponsePem = | |
| R"(-----BEGIN OCSP RESPONSE----- | |
| MIICjQoBAKCCAoYwggKCBgkrBgEFBQcwAQEEggJzMIICbzCBt6EbMBkxFzAVBgNV | |
| BAMTDk9DU1AgUmVzcG9uZGVyGA8yMDEyMDEwMTAwMDAwMFowgYYwgYMwSjAJBgUr | |
| DgMCGgUABBQf9AptB+QA18tSEc7X9THcL4icgwQUsRoXBeP+RybSJxlsA52XIPhg | |
| xTECEQCpj4ZrS4olzvWIAYJDbGZJoREYDzIwMTIwMTAxMDAwMDAwWhgPMjAxMjAx | |
| MDEwMDAwMDBaoBEYDzIwMTIxMjMxMjM1OTU5WjAKBggqhkjOPQQDAgNJADBGAiEA | |
| 1N0hbCk0zfZUIWYqwwPmymq4l6/6379wQClNBvr+bWcCIQC8LPYY2wwrPGUa/LRu | |
| hNmuv3wgUpapuqtrpRP9ool4S6CCAVowggFWMIIBUjCB+aADAgECAhEAylWc524Q | |
| SfWaV4Y1C8VtVTAKBggqhkjOPQQDAjAUMRIwEAYDVQQDEwlSb290IENBIDEwHhcN | |
| MTAwMTAxMDAwMDAwWhcNMjQxMjMxMjM1OTU5WjAZMRcwFQYDVQQDEw5PQ1NQIFJl | |
| c3BvbmRlcjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBxOD6IfWwY2vJ/dQfo3 | |
| iquqL7jqrliO6KquLxGJYOuhmGiXmgKhnXHv9fAF3xnb09BHZMqVvkW/Vjqx3VvW | |
| RjajJzAlMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDCTAKBggq | |
| hkjOPQQDAgNIADBFAiAk4CANeeK7R5RQ+lgZkgiegSUaFu4pxHJzQ8yK2XnsqQIh | |
| AMevOsBvD9R6o5AmZJgn/dDsNwGQFuM38EKpIEjm+lH0 | |
| -----END OCSP RESPONSE----- | |
| )"; | |
| TEST(OcspVerifierTest, MatchingGoodResponse) { | |
| auto subject_certs = LoadCertsFromPem(kSubjectCertPem); | |
| ABSL_ASSERT_OK(subject_certs); | |
| ASSERT_THAT(*subject_certs, SizeIs(1)); | |
| auto issuer_certs = LoadCertsFromPem(kIssuerCertPem); | |
| ABSL_ASSERT_OK(issuer_certs); | |
| ASSERT_THAT(*issuer_certs, SizeIs(1)); | |
| auto ocsp_response = LoadOcspResponseFromPem(kGoodOcspResponsePem); | |
| ABSL_ASSERT_OK(ocsp_response); | |
| ValidationStatusSet status_set; | |
| std::vector<std::string> ocsp_responses = {*ocsp_response}; | |
| std::vector<std::string> trust_chain = {(*subject_certs)[0], | |
| (*issuer_certs)[0]}; | |
| auto asserted_time = ParseTime("2012-06-01T00:00:00Z"); | |
| ABSL_ASSERT_OK(asserted_time); | |
| MockCryptoReadHandler mock_crypto_handler; | |
| EXPECT_CALL(mock_crypto_handler, | |
| VerifyOcspResponse(*ocsp_response, (*subject_certs)[0], | |
| (*issuer_certs)[0], *asserted_time)) | |
| .WillOnce(::testing::Return(OCSPRevocationStatus::kGood)); | |
| OcspVerifier verifier(&mock_crypto_handler); | |
| EXPECT_TRUE(verifier.VerifyOcspResponses(ocsp_responses, trust_chain, | |
| *asserted_time, &status_set)); | |
| EXPECT_THAT(status_set.failures(), IsEmpty()); | |
| EXPECT_THAT(status_set.informationals(), IsEmpty()); | |
| EXPECT_THAT(status_set.successes(), | |
| ElementsAre(C2paStatusIs( | |
| SuccessStatusCode::kSigningCredentialOcspNotRevoked))); | |
| } | |
| TEST(OcspVerifierTest, MatchingRevokedResponse) { | |
| auto subject_certs = LoadCertsFromPem(kSubjectCertPem); | |
| ABSL_ASSERT_OK(subject_certs); | |
| ASSERT_THAT(*subject_certs, SizeIs(1)); | |
| auto issuer_certs = LoadCertsFromPem(kIssuerCertPem); | |
| ABSL_ASSERT_OK(issuer_certs); | |
| ASSERT_THAT(*issuer_certs, SizeIs(1)); | |
| auto ocsp_response = LoadOcspResponseFromPem(kRevokedOcspResponsePem); | |
| ABSL_ASSERT_OK(ocsp_response); | |
| ValidationStatusSet status_set; | |
| std::vector<std::string> ocsp_responses = {*ocsp_response}; | |
| std::vector<std::string> trust_chain = {(*subject_certs)[0], | |
| (*issuer_certs)[0]}; | |
| auto asserted_time = ParseTime("2012-06-01T00:00:00Z"); | |
| ABSL_ASSERT_OK(asserted_time); | |
| MockCryptoReadHandler mock_crypto_handler; | |
| EXPECT_CALL(mock_crypto_handler, | |
| VerifyOcspResponse(*ocsp_response, (*subject_certs)[0], | |
| (*issuer_certs)[0], *asserted_time)) | |
| .WillOnce(::testing::Return(OCSPRevocationStatus::kRevoked)); | |
| OcspVerifier verifier(&mock_crypto_handler); | |
| EXPECT_THAT(verifier.VerifyOcspResponses(ocsp_responses, trust_chain, | |
| *asserted_time, &status_set), | |
| IsFalse()); | |
| EXPECT_THAT(status_set.failures(), | |
| ElementsAre(C2paStatusIs( | |
| FailureStatusCode::kSigningCredentialOcspRevoked))); | |
| EXPECT_THAT(status_set.informationals(), IsEmpty()); | |
| EXPECT_THAT(status_set.successes(), IsEmpty()); | |
| } | |
| TEST(OcspVerifierTest, MatchingRevokedResponseOnIntermediateCert) { | |
| auto subject_certs = LoadCertsFromPem(kSubjectCertPem); | |
| ABSL_ASSERT_OK(subject_certs); | |
| ASSERT_THAT(*subject_certs, SizeIs(1)); | |
| auto issuer_certs = LoadCertsFromPem(kIssuerCertPem); | |
| ABSL_ASSERT_OK(issuer_certs); | |
| ASSERT_THAT(*issuer_certs, SizeIs(1)); | |
| auto ocsp_response = LoadOcspResponseFromPem(kRevokedOcspResponsePem); | |
| ABSL_ASSERT_OK(ocsp_response); | |
| ValidationStatusSet status_set; | |
| std::vector<std::string> ocsp_responses = {*ocsp_response}; | |
| // Insert a fake leaf cert so that subject_certs[0] is considered an | |
| // intermediate cert. | |
| std::vector<std::string> trust_chain = {"fake_leaf_cert", (*subject_certs)[0], | |
| (*issuer_certs)[0]}; | |
| auto asserted_time = ParseTime("2012-06-01T00:00:00Z"); | |
| ABSL_ASSERT_OK(asserted_time); | |
| MockCryptoReadHandler mock_crypto_handler; | |
| EXPECT_CALL(mock_crypto_handler, | |
| VerifyOcspResponse(*ocsp_response, "fake_leaf_cert", | |
| (*subject_certs)[0], *asserted_time)) | |
| .WillOnce(::testing::Return(absl::InvalidArgumentError("no match"))); | |
| EXPECT_CALL(mock_crypto_handler, | |
| VerifyOcspResponse(*ocsp_response, (*subject_certs)[0], | |
| (*issuer_certs)[0], *asserted_time)) | |
| .WillOnce(::testing::Return(OCSPRevocationStatus::kRevoked)); | |
| OcspVerifier verifier(&mock_crypto_handler); | |
| EXPECT_THAT(verifier.VerifyOcspResponses(ocsp_responses, trust_chain, | |
| *asserted_time, &status_set), | |
| IsFalse()); | |
| EXPECT_THAT(status_set.failures(), | |
| ElementsAre(C2paStatusIs( | |
| FailureStatusCode::kSigningCredentialUntrusted))); | |
| EXPECT_THAT(status_set.informationals(), IsEmpty()); | |
| EXPECT_THAT(status_set.successes(), IsEmpty()); | |
| } | |
| TEST(OcspVerifierTest, MatchingGoodResponseOnIntermediateCert) { | |
| auto subject_certs = LoadCertsFromPem(kSubjectCertPem); | |
| ABSL_ASSERT_OK(subject_certs); | |
| ASSERT_THAT(*subject_certs, SizeIs(1)); | |
| auto issuer_certs = LoadCertsFromPem(kIssuerCertPem); | |
| ABSL_ASSERT_OK(issuer_certs); | |
| ASSERT_THAT(*issuer_certs, SizeIs(1)); | |
| auto ocsp_response = LoadOcspResponseFromPem(kGoodOcspResponsePem); | |
| ABSL_ASSERT_OK(ocsp_response); | |
| ValidationStatusSet status_set; | |
| std::vector<std::string> ocsp_responses = {*ocsp_response}; | |
| // Insert a fake leaf cert so that subject_certs[0] is considered an | |
| // intermediate cert. | |
| std::vector<std::string> trust_chain = {"fake_leaf_cert", (*subject_certs)[0], | |
| (*issuer_certs)[0]}; | |
| auto asserted_time = ParseTime("2012-06-01T00:00:00Z"); | |
| ABSL_ASSERT_OK(asserted_time); | |
| MockCryptoReadHandler mock_crypto_handler; | |
| EXPECT_CALL(mock_crypto_handler, | |
| VerifyOcspResponse(*ocsp_response, "fake_leaf_cert", | |
| (*subject_certs)[0], *asserted_time)) | |
| .WillOnce(::testing::Return(absl::InvalidArgumentError("no match"))); | |
| EXPECT_CALL(mock_crypto_handler, | |
| VerifyOcspResponse(*ocsp_response, (*subject_certs)[0], | |
| (*issuer_certs)[0], *asserted_time)) | |
| .WillOnce(::testing::Return(OCSPRevocationStatus::kGood)); | |
| OcspVerifier verifier(&mock_crypto_handler); | |
| EXPECT_TRUE(verifier.VerifyOcspResponses(ocsp_responses, trust_chain, | |
| *asserted_time, &status_set)); | |
| EXPECT_THAT(status_set.failures(), IsEmpty()); | |
| EXPECT_THAT(status_set.informationals(), | |
| ElementsAre(C2paStatusIs( | |
| InformationalStatusCode::kSigningCredentialOcspSkipped))); | |
| EXPECT_THAT(status_set.successes(), IsEmpty()); | |
| } | |
| } // namespace | |
| } // namespace credentio |