blob: 91e6c1a3b52844057d0dc32cf07dca9479635501 [file]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include "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