Skip to content

Commit 7ad1038

Browse files
committed
AUT-2597 Add tests for ResilientOcspCertificateRevocationChecker
1 parent d69ce56 commit 7ad1038

File tree

3 files changed

+276
-2
lines changed

3 files changed

+276
-2
lines changed

src/main/java/eu/webeid/resilientocsp/ResilientOcspCertificateRevocationChecker.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ private void createAndAddRevocationInfoToList(Throwable throwable, List<Revocati
192192
revocationInfoList.addAll((exception.getValidationInfo().revocationInfoList()));
193193
return;
194194
}
195+
if (throwable instanceof ResilientUserCertificateRevokedException exception) {
196+
revocationInfoList.addAll((exception.getValidationInfo().revocationInfoList()));
197+
return;
198+
}
195199
revocationInfoList.add(new RevocationInfo(null, Map.ofEntries(
196200
Map.entry(RevocationInfo.KEY_OCSP_ERROR, throwable)
197201
)));

src/test/java/eu/webeid/ocsp/OcspCertificateRevocationCheckerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969

7070
// TODO Fix failing tests
7171
@Disabled
72-
class OcspCertificateRevocationCheckerTest extends AbstractTestWithValidator {
72+
public class OcspCertificateRevocationCheckerTest extends AbstractTestWithValidator {
7373

7474
private final OcspClient ocspClient = OcspClientImpl.build(Duration.ofSeconds(5));
7575
private X509Certificate estEid2018Cert;
@@ -366,7 +366,7 @@ private static byte[] getOcspResponseBytesFromResources() throws IOException {
366366
return getOcspResponseBytesFromResources("ocsp_response.der");
367367
}
368368

369-
private static byte[] getOcspResponseBytesFromResources(String resource) throws IOException {
369+
public static byte[] getOcspResponseBytesFromResources(String resource) throws IOException {
370370
try (final InputStream resourceAsStream = ClassLoader.getSystemResourceAsStream(resource)) {
371371
return toByteArray(resourceAsStream);
372372
}
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/*
2+
* Copyright (c) 2020-2025 Estonian Information System Authority
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
23+
package eu.webeid.resilientocsp;
24+
25+
import eu.webeid.ocsp.OcspCertificateRevocationChecker;
26+
import eu.webeid.ocsp.client.OcspClient;
27+
import eu.webeid.ocsp.exceptions.OCSPClientException;
28+
import eu.webeid.ocsp.service.OcspService;
29+
import eu.webeid.ocsp.service.OcspServiceProvider;
30+
import eu.webeid.resilientocsp.exceptions.ResilientUserCertificateOCSPCheckFailedException;
31+
import eu.webeid.resilientocsp.exceptions.ResilientUserCertificateRevokedException;
32+
import eu.webeid.resilientocsp.service.FallbackOcspService;
33+
import eu.webeid.security.authtoken.WebEidAuthToken;
34+
import eu.webeid.security.validator.AuthTokenValidator;
35+
import eu.webeid.security.validator.revocationcheck.RevocationInfo;
36+
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
37+
import io.github.resilience4j.retry.RetryConfig;
38+
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
39+
import org.bouncycastle.cert.ocsp.OCSPResp;
40+
import org.bouncycastle.cert.ocsp.RevokedStatus;
41+
import org.bouncycastle.cert.ocsp.SingleResp;
42+
import org.junit.jupiter.api.BeforeEach;
43+
import org.junit.jupiter.api.Disabled;
44+
import org.junit.jupiter.api.Test;
45+
46+
import java.net.URI;
47+
import java.security.cert.X509Certificate;
48+
import java.util.List;
49+
import java.util.Map;
50+
51+
import static eu.webeid.ocsp.OcspCertificateRevocationCheckerTest.getOcspResponseBytesFromResources;
52+
import static eu.webeid.security.testutil.AbstractTestWithValidator.VALID_AUTH_TOKEN;
53+
import static eu.webeid.security.testutil.AbstractTestWithValidator.VALID_CHALLENGE_NONCE;
54+
import static eu.webeid.security.testutil.AuthTokenValidators.getDefaultAuthTokenValidatorBuilder;
55+
import static eu.webeid.security.testutil.Certificates.getJaakKristjanEsteid2018Cert;
56+
import static eu.webeid.security.testutil.Certificates.getTestEsteid2018CA;
57+
import static org.assertj.core.api.Assertions.assertThat;
58+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
59+
import static org.junit.jupiter.api.Assertions.assertThrows;
60+
import static org.mockito.ArgumentMatchers.any;
61+
import static org.mockito.ArgumentMatchers.eq;
62+
import static org.mockito.Mockito.mock;
63+
import static org.mockito.Mockito.never;
64+
import static org.mockito.Mockito.verify;
65+
import static org.mockito.Mockito.when;
66+
67+
public class ResilientOcspCertificateRevocationCheckerTest {
68+
69+
private static final URI PRIMARY_URI = URI.create("http://primary.ocsp.test");
70+
private static final URI FALLBACK_URI = URI.create("http://fallback.ocsp.test");
71+
private static final URI SECOND_FALLBACK_URI = URI.create("http://second-fallback.ocsp.test");
72+
73+
private X509Certificate estEid2018Cert;
74+
private X509Certificate testEsteid2018CA;
75+
private OCSPResp ocspRespGood;
76+
77+
@BeforeEach
78+
void setUp() throws Exception {
79+
estEid2018Cert = getJaakKristjanEsteid2018Cert();
80+
testEsteid2018CA = getTestEsteid2018CA();
81+
ocspRespGood = new OCSPResp(getOcspResponseBytesFromResources("ocsp_response.der"));
82+
}
83+
84+
// TODO Rename to match the expected result
85+
@Test
86+
void whenMultipleValidationCalls_thenStaleListenersMutatePreviousResults() throws Exception {
87+
OcspClient ocspClient = mock(OcspClient.class);
88+
when(ocspClient.request(eq(PRIMARY_URI), any()))
89+
.thenThrow(new OCSPClientException("Primary OCSP service unavailable (call1)"))
90+
.thenThrow(new OCSPClientException("Primary OCSP service unavailable (call2)"));
91+
when(ocspClient.request(eq(FALLBACK_URI), any()))
92+
.thenThrow(new OCSPClientException("Fallback OCSP service unavailable (call1)"))
93+
.thenThrow(new OCSPClientException("Fallback OCSP service unavailable (call2)"));
94+
when(ocspClient.request(eq(SECOND_FALLBACK_URI), any()))
95+
.thenThrow(new OCSPClientException("Secondary fallback OCSP service unavailable (call1)"))
96+
.thenThrow(new OCSPClientException("Secondary fallback OCSP service unavailable (call2)"));
97+
ResilientOcspCertificateRevocationChecker resilientChecker = buildChecker(ocspClient, null, false);
98+
AuthTokenValidator validator = getDefaultAuthTokenValidatorBuilder()
99+
.withCertificateRevocationChecker(resilientChecker)
100+
.build();
101+
WebEidAuthToken authToken = validator.parse(VALID_AUTH_TOKEN);
102+
103+
ResilientUserCertificateOCSPCheckFailedException ex1 = assertThrows(ResilientUserCertificateOCSPCheckFailedException.class,
104+
() -> validator.validate(authToken, VALID_CHALLENGE_NONCE));
105+
List<RevocationInfo> revocationInfo1 = ex1.getValidationInfo().revocationInfoList();
106+
assertThat(revocationInfo1).hasSize(3);
107+
assertThat(revocationInfo1)
108+
.extracting(ri -> ((OCSPClientException) ri.ocspResponseAttributes().get("OCSP_ERROR")).getMessage())
109+
.containsExactly(
110+
"Primary OCSP service unavailable (call1)",
111+
"Fallback OCSP service unavailable (call1)",
112+
"Secondary fallback OCSP service unavailable (call1)"
113+
);
114+
ResilientUserCertificateOCSPCheckFailedException ex2 = assertThrows(ResilientUserCertificateOCSPCheckFailedException.class,
115+
() -> validator.validate(authToken, VALID_CHALLENGE_NONCE));
116+
List<RevocationInfo> revocationInfo2 = ex2.getValidationInfo().revocationInfoList();
117+
assertThat(revocationInfo2).hasSize(3);
118+
assertThat(revocationInfo2)
119+
.extracting(ri -> ((OCSPClientException) ri.ocspResponseAttributes().get("OCSP_ERROR")).getMessage())
120+
.containsExactly(
121+
"Primary OCSP service unavailable (call2)",
122+
"Fallback OCSP service unavailable (call2)",
123+
"Secondary fallback OCSP service unavailable (call2)"
124+
);
125+
assertThat(revocationInfo1).hasSize(3);
126+
assertThat(revocationInfo1)
127+
.extracting(ri -> ((OCSPClientException) ri.ocspResponseAttributes().get("OCSP_ERROR")).getMessage())
128+
.containsExactly(
129+
"Primary OCSP service unavailable (call1)",
130+
"Fallback OCSP service unavailable (call1)",
131+
"Secondary fallback OCSP service unavailable (call1)"
132+
);
133+
}
134+
135+
@Test
136+
void whenFirstFallbackReturnsRevoked_thenRevocationPropagatesWithoutSecondFallback() throws Exception {
137+
OCSPResp ocspRespRevoked = new OCSPResp(getOcspResponseBytesFromResources("ocsp_response_revoked.der"));
138+
139+
OcspClient ocspClient = mock(OcspClient.class);
140+
when(ocspClient.request(eq(PRIMARY_URI), any()))
141+
.thenThrow(new OCSPClientException("Primary OCSP service unavailable"));
142+
when(ocspClient.request(eq(FALLBACK_URI), any()))
143+
.thenReturn(ocspRespRevoked);
144+
when(ocspClient.request(eq(SECOND_FALLBACK_URI), any()))
145+
.thenReturn(ocspRespGood);
146+
147+
ResilientOcspCertificateRevocationChecker checker = buildChecker(ocspClient, null, false);
148+
149+
assertThatExceptionOfType(ResilientUserCertificateRevokedException.class)
150+
.isThrownBy(() -> checker.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA))
151+
.withMessage("User certificate has been revoked");
152+
153+
verify(ocspClient, never()).request(eq(SECOND_FALLBACK_URI), any());
154+
}
155+
156+
@Test
157+
void whenMaxAttemptsIsTwoAndAllCallsFail_thenRevocationInfoListShouldHaveFourElements() throws Exception {
158+
OcspClient ocspClient = mock(OcspClient.class);
159+
when(ocspClient.request(eq(PRIMARY_URI), any()))
160+
.thenThrow(new OCSPClientException());
161+
when(ocspClient.request(eq(FALLBACK_URI), any()))
162+
.thenThrow(new OCSPClientException());
163+
when(ocspClient.request(eq(SECOND_FALLBACK_URI), any()))
164+
.thenThrow(new OCSPClientException());
165+
166+
RetryConfig retryConfig = RetryConfig.custom()
167+
.maxAttempts(2)
168+
.build();
169+
170+
ResilientOcspCertificateRevocationChecker checker = buildChecker(ocspClient, retryConfig, false);
171+
ResilientUserCertificateOCSPCheckFailedException ex = assertThrows(ResilientUserCertificateOCSPCheckFailedException.class, () -> checker.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA));
172+
assertThat(ex.getValidationInfo().revocationInfoList().size()).isEqualTo(4);
173+
}
174+
175+
@Test
176+
@Disabled("Primary supplier has allowThisUpdateInPast disabled and that is checked before revocation, " +
177+
"which results in ResilientUserCertificateOCSPCheckFailedException")
178+
void whenMaxAttemptsIsTwoAndFirstCallFails_thenTwoCallsToPrimaryShouldBeRecorded() throws Exception {
179+
OcspClient ocspClient = mock(OcspClient.class);
180+
when(ocspClient.request(eq(PRIMARY_URI), any()))
181+
.thenThrow(new OCSPClientException("Primary OCSP service unavailable (call1)"))
182+
.thenReturn(ocspRespGood);
183+
184+
RetryConfig retryConfig = RetryConfig.custom()
185+
.maxAttempts(2)
186+
.build();
187+
188+
ResilientOcspCertificateRevocationChecker checker = buildChecker(ocspClient, retryConfig, false);
189+
List<RevocationInfo> revocationInfoList = checker.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA);
190+
assertThat(revocationInfoList.size()).isEqualTo(2);
191+
192+
Map<String, Object> firstResponseAttributes = revocationInfoList.get(0).ocspResponseAttributes();
193+
OCSPClientException ex1 = (OCSPClientException) firstResponseAttributes.get("OCSP_ERROR");
194+
assertThat(ex1.getMessage()).isEqualTo("Primary OCSP service unavailable (call1)");
195+
196+
Map<String, Object> secondResponseAttributes = revocationInfoList.get(1).ocspResponseAttributes();
197+
OCSPResp ocspResp = (OCSPResp) secondResponseAttributes.get("OCSP_RESPONSE");
198+
final BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResp.getResponseObject();
199+
final SingleResp certStatusResponse = basicResponse.getResponses()[0];
200+
assertThat(certStatusResponse.getCertStatus()).isEqualTo(org.bouncycastle.cert.ocsp.CertificateStatus.GOOD);
201+
}
202+
203+
@Test
204+
@Disabled("Primary supplier has allowThisUpdateInPast disabled and that is checked before revocation, " +
205+
"which results in ResilientUserCertificateOCSPCheckFailedException")
206+
void whenFirstCallSucceeds_thenRevocationInfoListShouldHaveOneElementAndItShouldHaveGoodStatus() throws Exception {
207+
OcspClient ocspClient = mock(OcspClient.class);
208+
when(ocspClient.request(eq(PRIMARY_URI), any()))
209+
.thenReturn(ocspRespGood);
210+
211+
ResilientOcspCertificateRevocationChecker checker = buildChecker(ocspClient, null, false);
212+
213+
List<RevocationInfo> revocationInfoList = checker.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA);
214+
assertThat(revocationInfoList.size()).isEqualTo(1);
215+
Map<String, Object> responseAttributes = revocationInfoList.get(0).ocspResponseAttributes();
216+
OCSPResp ocspResp = (OCSPResp) responseAttributes.get("OCSP_RESPONSE");
217+
final BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResp.getResponseObject();
218+
final SingleResp certStatusResponse = basicResponse.getResponses()[0];
219+
assertThat(certStatusResponse.getCertStatus()).isEqualTo(org.bouncycastle.cert.ocsp.CertificateStatus.GOOD);
220+
}
221+
222+
@Test
223+
@Disabled("Primary supplier has allowThisUpdateInPast disabled and that is checked before revocation, " +
224+
"which results in ResilientUserCertificateOCSPCheckFailedException")
225+
void whenFirstCallResultsInRevoked_thenRevocationInfoListShouldHaveOneElementAndItShouldHaveRevokedStatus() throws Exception {
226+
OcspClient ocspClient = mock(OcspClient.class);
227+
OCSPResp ocspRespRevoked = new OCSPResp(getOcspResponseBytesFromResources("ocsp_response_revoked.der"));
228+
when(ocspClient.request(eq(PRIMARY_URI), any()))
229+
.thenReturn(ocspRespRevoked);
230+
231+
ResilientOcspCertificateRevocationChecker checker = buildChecker(ocspClient, null, false);
232+
ResilientUserCertificateRevokedException ex = assertThrows(ResilientUserCertificateRevokedException.class, () -> checker.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA));
233+
List<RevocationInfo> revocationInfoList = ex.getValidationInfo().revocationInfoList();
234+
assertThat(revocationInfoList.size()).isEqualTo(1);
235+
Map<String, Object> responseAttributes = ex.getValidationInfo().revocationInfoList().get(0).ocspResponseAttributes();
236+
OCSPResp ocspResp = (OCSPResp) responseAttributes.get("OCSP_RESPONSE");
237+
final BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResp.getResponseObject();
238+
final SingleResp certStatusResponse = basicResponse.getResponses()[0];
239+
assertThat(certStatusResponse.getCertStatus()).isInstanceOf(RevokedStatus.class);
240+
}
241+
242+
private ResilientOcspCertificateRevocationChecker buildChecker(OcspClient ocspClient, RetryConfig retryConfig, boolean rejectUnknownOcspResponseStatus) throws Exception {
243+
FallbackOcspService secondFallbackService = mock(FallbackOcspService.class);
244+
when(secondFallbackService.getAccessLocation()).thenReturn(SECOND_FALLBACK_URI);
245+
when(secondFallbackService.doesSupportNonce()).thenReturn(false);
246+
247+
OcspService fallbackService = mock(OcspService.class);
248+
when(fallbackService.getAccessLocation()).thenReturn(FALLBACK_URI);
249+
when(fallbackService.doesSupportNonce()).thenReturn(false);
250+
251+
OcspService primaryService = mock(OcspService.class);
252+
when(primaryService.getAccessLocation()).thenReturn(PRIMARY_URI);
253+
when(primaryService.doesSupportNonce()).thenReturn(false);
254+
when(primaryService.getFallbackService()).thenReturn(fallbackService);
255+
256+
OcspServiceProvider ocspServiceProvider = mock(OcspServiceProvider.class);
257+
when(ocspServiceProvider.getService(any())).thenReturn(primaryService);
258+
when(ocspServiceProvider.getFallbackService(eq(FALLBACK_URI))).thenReturn(secondFallbackService);
259+
260+
return new ResilientOcspCertificateRevocationChecker(
261+
ocspClient,
262+
ocspServiceProvider,
263+
CircuitBreakerConfig.ofDefaults(),
264+
retryConfig,
265+
OcspCertificateRevocationChecker.DEFAULT_TIME_SKEW,
266+
OcspCertificateRevocationChecker.DEFAULT_THIS_UPDATE_AGE,
267+
rejectUnknownOcspResponseStatus
268+
);
269+
}
270+
}

0 commit comments

Comments
 (0)