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