diff --git a/pom.xml b/pom.xml index 4637e30d..bf211285 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.uid2 uid2-shared - 7.19.0 + 7.19.2-alpha-151-SNAPSHOT ${project.groupId}:${project.artifactId} Library for all the shared uid2 operations https://github.com/IABTechLab/uid2docs @@ -285,6 +285,9 @@ org.apache.maven.plugins maven-surefire-plugin 3.2.5 + + -XX:+EnableDynamicAgentLoading + org.sonatype.plugins diff --git a/src/main/java/com/uid2/shared/attest/AttestationResponseCode.java b/src/main/java/com/uid2/shared/attest/AttestationResponseCode.java new file mode 100644 index 00000000..38359b25 --- /dev/null +++ b/src/main/java/com/uid2/shared/attest/AttestationResponseCode.java @@ -0,0 +1,7 @@ +package com.uid2.shared.attest; + +public enum AttestationResponseCode { + AttestationFailure, + RetryableFailure, + Success +} diff --git a/src/main/java/com/uid2/shared/attest/AttestationResponseHandler.java b/src/main/java/com/uid2/shared/attest/AttestationResponseHandler.java index ff230cd4..183a4f29 100644 --- a/src/main/java/com/uid2/shared/attest/AttestationResponseHandler.java +++ b/src/main/java/com/uid2/shared/attest/AttestationResponseHandler.java @@ -7,6 +7,7 @@ import io.vertx.core.Vertx; import io.vertx.core.json.Json; import io.vertx.core.json.JsonObject; +import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.utils.Pair; @@ -34,7 +35,7 @@ public class AttestationResponseHandler { private final AtomicReference attestationToken; private final AtomicReference optOutJwt; private final AtomicReference coreJwt; - private final Handler> responseWatcher; + private final Handler> responseWatcher; private final String attestationEndpoint; private final byte[] encodedAttestationEndpoint; private final IClock clock; @@ -46,6 +47,7 @@ public class AttestationResponseHandler { private Instant attestationTokenExpiresAt = Instant.MAX; private final Lock lock; private final AttestationTokenDecryptor attestationTokenDecryptor; + @Getter private final String appVersionHeader; private final int attestCheckMilliseconds; private final AtomicReference optOutUrl; @@ -56,17 +58,18 @@ public AttestationResponseHandler(Vertx vertx, String operatorType, ApplicationVersion appVersion, IAttestationProvider attestationProvider, - Handler> responseWatcher, + Handler> responseWatcher, Proxy proxy) { this(vertx, attestationEndpoint, clientApiToken, operatorType, appVersion, attestationProvider, responseWatcher, proxy, new InstantClock(), null, null, 60000); } + public AttestationResponseHandler(Vertx vertx, String attestationEndpoint, String clientApiToken, String operatorType, ApplicationVersion appVersion, IAttestationProvider attestationProvider, - Handler> responseWatcher, + Handler> responseWatcher, Proxy proxy, IClock clock, URLConnectionHttpClient httpClient, @@ -131,11 +134,7 @@ private void attestationExpirationCheck(long timerId) { } attest(); - } catch (AttestationResponseHandlerException e) { - notifyResponseWatcher(401, e.getMessage()); - LOGGER.info("Re-attest failed: ", e); - } catch (IOException e){ - notifyResponseWatcher(500, e.getMessage()); + } catch (AttestationResponseHandlerException | IOException e) { LOGGER.info("Re-attest failed: ", e); } finally { this.isAttesting.set(false); @@ -180,30 +179,32 @@ public void attest() throws IOException, AttestationResponseHandlerException { int statusCode = response.statusCode(); String responseBody = response.body(); - notifyResponseWatcher(statusCode, responseBody); - if (statusCode < 200 || statusCode >= 300) { - LOGGER.warn("attestation failed with UID2 Core returning statusCode={}", statusCode); - throw new AttestationResponseHandlerException(statusCode, "unexpected status code from uid core service"); + AttestationResponseCode responseCode = this.getAttestationResponseCodeFromHttpStatus(statusCode); + + notifyResponseWatcher(responseCode, responseBody); + + if (responseCode != AttestationResponseCode.Success) { + throw new AttestationResponseHandlerException(responseCode, "Non-success response from Core on attest"); } JsonObject responseJson = (JsonObject) Json.decodeValue(responseBody); if (isFailed(responseJson)) { - throw new AttestationResponseHandlerException(statusCode, "response did not return a successful status"); + throw new AttestationResponseHandlerException(AttestationResponseCode.RetryableFailure, "response did not return a successful status"); } JsonObject innerBody = responseJson.getJsonObject("body"); if (innerBody == null) { - throw new AttestationResponseHandlerException(statusCode, "response did not contain a body object"); + throw new AttestationResponseHandlerException(AttestationResponseCode.RetryableFailure, "response did not contain a body object"); } String atoken = getAttestationToken(innerBody); if (atoken == null) { - throw new AttestationResponseHandlerException(statusCode, "response json does not contain body.attestation_token"); + throw new AttestationResponseHandlerException(AttestationResponseCode.RetryableFailure, "response json does not contain body.attestation_token"); } String expiresAt = getAttestationTokenExpiresAt(innerBody); if (expiresAt == null) { - throw new AttestationResponseHandlerException(statusCode, "response json does not contain body.expiresAt"); + throw new AttestationResponseHandlerException(AttestationResponseCode.RetryableFailure, "response json does not contain body.expiresAt"); } atoken = new String(attestationTokenDecryptor.decrypt(Base64.getDecoder().decode(atoken), keyPair.getPrivate()), StandardCharsets.UTF_8); @@ -215,8 +216,8 @@ public void attest() throws IOException, AttestationResponseHandlerException { setOptoutURLFromResponse(innerBody); scheduleAttestationExpirationCheck(); - } catch (IOException ioe) { - throw ioe; + } catch (AttestationResponseHandlerException | IOException e) { + throw e; } catch (Exception e) { throw new AttestationResponseHandlerException(e); } @@ -242,10 +243,6 @@ public String getOptOutUrl() { return this.optOutUrl.get(); } - public String getAppVersionHeader() { - return this.appVersionHeader; - } - private void setAttestationTokenExpiresAt(String expiresAt) { this.attestationTokenExpiresAt = Instant.parse(expiresAt); } @@ -299,11 +296,15 @@ private static KeyPair generateKeyPair() throws NoSuchAlgorithmException { return gen.generateKeyPair(); } - private void notifyResponseWatcher(int statusCode, String responseBody) { + private void notifyResponseWatcher(AttestationResponseCode responseCode, String responseBody) { + if (responseCode != AttestationResponseCode.Success) { + LOGGER.warn("Received a non-success response code on Attestation: ResponseCode: {}, Message: {}", responseCode, responseBody); + } + this.lock.lock(); try { if (this.responseWatcher != null) - this.responseWatcher.handle(Pair.of(statusCode, responseBody)); + this.responseWatcher.handle(Pair.of(responseCode, responseBody)); } finally { lock.unlock(); } @@ -318,4 +319,16 @@ private byte[] encodeStringUnicodeAttestationEndpoint(String data) { ByteBuffer buffer = StandardCharsets.UTF_8.encode(data); return Arrays.copyOf(buffer.array(), buffer.limit()); } + + private AttestationResponseCode getAttestationResponseCodeFromHttpStatus(int httpStatus) { + if (httpStatus == 401 || httpStatus == 403) { + return AttestationResponseCode.AttestationFailure; + } + + if (httpStatus == 200) { + return AttestationResponseCode.Success; + } + + return AttestationResponseCode.RetryableFailure; + } } diff --git a/src/main/java/com/uid2/shared/attest/AttestationResponseHandlerException.java b/src/main/java/com/uid2/shared/attest/AttestationResponseHandlerException.java index 774fbc8d..b133a512 100644 --- a/src/main/java/com/uid2/shared/attest/AttestationResponseHandlerException.java +++ b/src/main/java/com/uid2/shared/attest/AttestationResponseHandlerException.java @@ -1,7 +1,10 @@ package com.uid2.shared.attest; +import lombok.Getter; + +@Getter public class AttestationResponseHandlerException extends Exception { - private int statusCode = 0; + private AttestationResponseCode responseCode; public AttestationResponseHandlerException(Throwable t) { super(t); @@ -11,12 +14,13 @@ public AttestationResponseHandlerException(String message) { super(message); } - public AttestationResponseHandlerException(int statusCode, String message) { - super("http status: " + String.valueOf(statusCode) + ", " + message); - this.statusCode = statusCode; + public AttestationResponseHandlerException(AttestationResponseCode responseCode, String message) { + super("AttestationResponseCode: " + String.valueOf(responseCode) + ", " + message); + this.responseCode = responseCode; } public boolean isAttestationFailure() { - return statusCode == 401; + return responseCode == AttestationResponseCode.AttestationFailure; } + } diff --git a/src/main/java/com/uid2/shared/attest/UidCoreClient.java b/src/main/java/com/uid2/shared/attest/UidCoreClient.java index fca09eaa..57b8acf0 100644 --- a/src/main/java/com/uid2/shared/attest/UidCoreClient.java +++ b/src/main/java/com/uid2/shared/attest/UidCoreClient.java @@ -80,9 +80,8 @@ private InputStream internalDownload(String path) throws CloudStorageException { } return inputStream; } catch (Exception e) { - throw new CloudStorageException("download " + path + " error: " + e.getMessage(), e); + throw new CloudStorageException("download error: " + e.getMessage(), e); } - } private InputStream readContentFromLocalFileSystem(String path, Proxy proxy) throws IOException { @@ -99,14 +98,6 @@ private InputStream getWithAttest(String path) throws IOException, AttestationRe HttpResponse httpResponse; httpResponse = sendHttpRequest(path, attestationToken); - // This should never happen, but keeping this part of the code just to be extra safe. - if (httpResponse.statusCode() == 401) { - LOGGER.info("Initial response from UID2 Core returned 401, performing attestation"); - attestationResponseHandler.attest(); - attestationToken = attestationResponseHandler.getAttestationToken(); - httpResponse = sendHttpRequest(path, attestationToken); - } - return Utils.convertHttpResponseToInputStream(httpResponse); } diff --git a/src/main/java/com/uid2/shared/secure/AttestationClientException.java b/src/main/java/com/uid2/shared/secure/AttestationClientException.java index ce43f61f..15096125 100644 --- a/src/main/java/com/uid2/shared/secure/AttestationClientException.java +++ b/src/main/java/com/uid2/shared/secure/AttestationClientException.java @@ -1,7 +1,12 @@ package com.uid2.shared.secure; -public class AttestationClientException extends AttestationException -{ +import lombok.Getter; + +@Getter +public class AttestationClientException extends AttestationException { + // This exception should be used when the error is as a result of invalid or bad data from the caller. + // It will result in a return code in the 400s + private final AttestationFailure attestationFailure; public AttestationClientException(Throwable cause) { @@ -14,7 +19,4 @@ public AttestationClientException(String message, AttestationFailure attestation this.attestationFailure = attestationFailure; } - public AttestationFailure getAttestationFailure() { - return this.attestationFailure; - } } diff --git a/src/main/java/com/uid2/shared/secure/AttestationException.java b/src/main/java/com/uid2/shared/secure/AttestationException.java index e6aa0077..175f0d8c 100644 --- a/src/main/java/com/uid2/shared/secure/AttestationException.java +++ b/src/main/java/com/uid2/shared/secure/AttestationException.java @@ -1,6 +1,10 @@ package com.uid2.shared.secure; public class AttestationException extends Exception { + // Used to indicate an error in the processing of Attestation due to internal server errors + // It will result in a response code of 500. + // If the error is as a result in invalid input from the caller, use the AttestationClientException + private final boolean isClientError; public boolean IsClientError() { diff --git a/src/main/java/com/uid2/shared/secure/AttestationFailure.java b/src/main/java/com/uid2/shared/secure/AttestationFailure.java index 2cfcd6b6..7eba1592 100644 --- a/src/main/java/com/uid2/shared/secure/AttestationFailure.java +++ b/src/main/java/com/uid2/shared/secure/AttestationFailure.java @@ -7,6 +7,10 @@ public enum AttestationFailure { BAD_CERTIFICATE, FORBIDDEN_ENCLAVE, UNKNOWN_ATTESTATION_URL, + INVALID_PROTOCOL, + INTERNAL_ERROR, + INVALID_TYPE, + RESPONSE_ENCRYPTION_ERROR, UNKNOWN; public String explain() { @@ -23,6 +27,14 @@ public String explain() { return "The enclave identifier is unknown"; case UNKNOWN_ATTESTATION_URL: return "The given attestation URL is unknown"; + case INVALID_PROTOCOL: + return "The given protocol is not valid"; + case INTERNAL_ERROR: + return "There was an internal processing error"; + case INVALID_TYPE: + return "Invalid Operator Type"; + case RESPONSE_ENCRYPTION_ERROR: + return "Error encrypting the response"; default: return "Unknown reason"; } diff --git a/src/main/java/com/uid2/shared/secure/AttestationResult.java b/src/main/java/com/uid2/shared/secure/AttestationResult.java index 2e3f239d..c4ee89ba 100644 --- a/src/main/java/com/uid2/shared/secure/AttestationResult.java +++ b/src/main/java/com/uid2/shared/secure/AttestationResult.java @@ -16,7 +16,7 @@ public AttestationResult(AttestationFailure reasonToFail) { } public AttestationResult(AttestationClientException exception) { - this.failure = AttestationFailure.UNKNOWN; + this.failure = exception.getAttestationFailure(); this.publicKey = null; this.enclaveId = "Failed attestation, enclave Id unknown"; this.attestationClientException = exception; diff --git a/src/main/java/com/uid2/shared/secure/BadFormatException.java b/src/main/java/com/uid2/shared/secure/BadFormatException.java deleted file mode 100644 index 731dd5df..00000000 --- a/src/main/java/com/uid2/shared/secure/BadFormatException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.uid2.shared.secure; - -public class BadFormatException extends Exception { - public BadFormatException(Throwable cause) { - super(cause); - } - public BadFormatException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/com/uid2/shared/secure/NitroCoreAttestationService.java b/src/main/java/com/uid2/shared/secure/NitroCoreAttestationService.java index 5488d176..700379c9 100644 --- a/src/main/java/com/uid2/shared/secure/NitroCoreAttestationService.java +++ b/src/main/java/com/uid2/shared/secure/NitroCoreAttestationService.java @@ -20,7 +20,7 @@ public class NitroCoreAttestationService implements ICoreAttestationService { private final String attestationUrl; - private Set allowedEnclaveIds; + private final Set allowedEnclaveIds; private final ICertificateProvider certificateProvider; private static final Logger LOGGER = LoggerFactory.getLogger(NitroCoreAttestationService.class); @@ -37,6 +37,8 @@ public void attest(byte[] attestationRequest, byte[] publicKey, Handler() - {{ + private static final ApplicationVersion APP_VERSION = new ApplicationVersion("appName", "appVersion", new HashMap() {{ put("Component1", "Value1"); put("Component2", "Value2"); }}); private final IAttestationProvider attestationProvider = mock(IAttestationProvider.class); - private final Handler> responseWatcher = mock(Handler.class); + private final Handler> responseWatcher = mock(Handler.class); private final IClock clock = mock(IClock.class); private final URLConnectionHttpClient mockHttpClient = mock(URLConnectionHttpClient.class); private Proxy proxy = CloudUtils.defaultProxy; @@ -62,31 +66,32 @@ void setUp() { @Test public void attest_succeed_attestationTokenSet(Vertx vertx, VertxTestContext testContext) throws Exception { - attestationResponseHandler = getAttestationTokenRetriever(vertx); + attestationResponseHandler = getAttestationResponseHandler(vertx); when(attestationProvider.isReady()).thenReturn(true); when(attestationProvider.getAttestationRequest(any(), any())).thenReturn(new byte[1]); HttpResponse mockHttpResponse = mock(HttpResponse.class); - String expectedResponseBody = "{\"body\": {\"attestation_token\": \"test\",\"expiresAt\": \"2023-08-03T09:09:30.608597Z\",\"attestation_jwt_optout\": \"\",\"attestation_jwt_core\": \"\"},\"status\": \"success\"}"; + String token = java.util.Base64.getEncoder().encodeToString("testToken".getBytes()); + String expectedResponseBody = "{\"body\": {\"attestation_token\": \"" + token + "\",\"expiresAt\": \"2023-08-03T09:09:30.608597Z\",\"attestation_jwt_optout\": \"\",\"attestation_jwt_core\": \"\"},\"status\": \"success\"}"; when(mockHttpResponse.body()).thenReturn(expectedResponseBody); when(mockHttpResponse.statusCode()).thenReturn(200); when(mockHttpClient.post(eq(ATTESTATION_ENDPOINT), any(String.class), any(HashMap.class))).thenReturn(mockHttpResponse); - when(mockAttestationTokenDecryptor.decrypt(any(), any())).thenReturn("test_attestation_token".getBytes(StandardCharsets.UTF_8)); + when(mockAttestationTokenDecryptor.decrypt(eq(java.util.Base64.getDecoder().decode(token.getBytes())), any())).thenReturn("test_attestation_token".getBytes(StandardCharsets.UTF_8)); when(clock.now()).thenReturn(Instant.parse("2023-08-01T00:00:00.111Z")); attestationResponseHandler.attest(); - Assertions.assertEquals("test_attestation_token", attestationResponseHandler.getAttestationToken()); - Assertions.assertEquals("appName=appVersion;Component1=Value1;Component2=Value2", attestationResponseHandler.getAppVersionHeader()); - verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody)); + assertEquals("test_attestation_token", attestationResponseHandler.getAttestationToken()); + assertEquals("appName=appVersion;Component1=Value1;Component2=Value2", attestationResponseHandler.getAppVersionHeader()); + verify(this.responseWatcher, times(1)).handle(Pair.of(AttestationResponseCode.Success, expectedResponseBody)); testContext.completeNow(); } @Test public void attest_currentTimeAfterTenMinsBeforeAttestationTokenExpiry_expiryCheckCallsAttestFixedIntervalUntilSuccess(Vertx vertx, VertxTestContext testContext) throws Exception { - attestationResponseHandler = getAttestationTokenRetriever(vertx); + attestationResponseHandler = getAttestationResponseHandler(vertx); when(attestationProvider.isReady()).thenReturn(true); when(attestationProvider.getAttestationRequest(any(), any())).thenReturn(new byte[1]); @@ -107,14 +112,18 @@ public void attest_currentTimeAfterTenMinsBeforeAttestationTokenExpiry_expiryChe testContext.awaitCompletion(1100, TimeUnit.MILLISECONDS); // Verify on httpClient because we can't mock attestationTokenRetriever verify(mockHttpClient, times(4)).post(eq(ATTESTATION_ENDPOINT), any(String.class), any(HashMap.class)); - verify(this.responseWatcher, times(2)).handle(Pair.of(200, expectedResponseBody)); - verify(this.responseWatcher, times(2)).handle(Pair.of(500, "bad")); + ArgumentCaptor> notifyArgument = ArgumentCaptor.forClass(Pair.class); + verify(this.responseWatcher, times(4)).handle(notifyArgument.capture()); + List> calls = notifyArgument.getAllValues(); + assertEquals(2, calls.stream().filter(c -> c.left() == AttestationResponseCode.Success && c.right().equals(expectedResponseBody)).count()); + assertEquals(2, calls.stream().filter(c -> c.left() == AttestationResponseCode.RetryableFailure && c.right().equals("bad")).count()); + testContext.completeNow(); } @Test public void attest_currentTimeAfterTenMinsBeforeAttestationTokenExpiry_expiryCheckDoesNotCallAttest(Vertx vertx, VertxTestContext testContext) throws Exception { - attestationResponseHandler = getAttestationTokenRetriever(vertx); + attestationResponseHandler = getAttestationResponseHandler(vertx); when(attestationProvider.isReady()).thenReturn(true); when(attestationProvider.getAttestationRequest(any(), any())).thenReturn(new byte[1]); @@ -133,13 +142,13 @@ public void attest_currentTimeAfterTenMinsBeforeAttestationTokenExpiry_expiryChe testContext.awaitCompletion(1, TimeUnit.SECONDS); // Verify on httpClient because we can't mock attestationTokenRetriever verify(mockHttpClient, times(1)).post(eq(ATTESTATION_ENDPOINT), any(String.class), any(HashMap.class)); - verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody)); + verify(this.responseWatcher, times(1)).handle(Pair.of(AttestationResponseCode.Success, expectedResponseBody)); testContext.completeNow(); } @Test public void attest_currentTimeAfterTenMinsBeforeAttestationTokenExpiry_providerNotReadyDoesNotCallAttest(Vertx vertx, VertxTestContext testContext) throws Exception { - attestationResponseHandler = getAttestationTokenRetriever(vertx); + attestationResponseHandler = getAttestationResponseHandler(vertx); // isReady will be called twice: // The first is by attest() with true returned so that expiration check will be scheduled. @@ -161,14 +170,14 @@ public void attest_currentTimeAfterTenMinsBeforeAttestationTokenExpiry_providerN testContext.awaitCompletion(1, TimeUnit.SECONDS); // Verify on httpClient because we can't mock attestationTokenRetriever verify(mockHttpClient, times(1)).post(eq(ATTESTATION_ENDPOINT), any(String.class), any(HashMap.class)); - verify(this.responseWatcher, only()).handle(Pair.of(200, expectedResponseBody)); - verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody)); + verify(this.responseWatcher, only()).handle(Pair.of(AttestationResponseCode.Success, expectedResponseBody)); + verify(this.responseWatcher, times(1)).handle(Pair.of(AttestationResponseCode.Success, expectedResponseBody)); testContext.completeNow(); } @Test public void attest_responseBodyHasNoAttestationToken_exceptionThrown(Vertx vertx, VertxTestContext testContext) throws IOException, AttestationException, AttestationResponseHandlerException, InterruptedException { - attestationResponseHandler = getAttestationTokenRetriever(vertx); + attestationResponseHandler = getAttestationResponseHandler(vertx); when(attestationProvider.isReady()).thenReturn(true); when(attestationProvider.getAttestationRequest(any(), any())).thenReturn(new byte[1]); @@ -190,15 +199,15 @@ public void attest_responseBodyHasNoAttestationToken_exceptionThrown(Vertx vertx AttestationResponseHandlerException result = Assertions.assertThrows(AttestationResponseHandlerException.class, () -> { attestationResponseHandler.attest(); }); - String expectedExceptionMessage = "com.uid2.shared.attest.AttestationResponseHandlerException: http status: 200, response json does not contain body.attestation_token"; - Assertions.assertEquals(expectedExceptionMessage, result.getMessage()); - verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody)); + String expectedExceptionMessage = "AttestationResponseCode: RetryableFailure, response json does not contain body.attestation_token"; + assertEquals(expectedExceptionMessage, result.getMessage()); + verify(this.responseWatcher, times(1)).handle(Pair.of(AttestationResponseCode.Success, expectedResponseBody)); testContext.completeNow(); } @Test public void attest_responseBodyHasNoExpiredAt_exceptionThrown(Vertx vertx, VertxTestContext testContext) throws IOException, AttestationException, AttestationResponseHandlerException, InterruptedException { - attestationResponseHandler = getAttestationTokenRetriever(vertx); + attestationResponseHandler = getAttestationResponseHandler(vertx); when(attestationProvider.isReady()).thenReturn(true); when(attestationProvider.getAttestationRequest(any(), any())).thenReturn(new byte[1]); @@ -220,16 +229,16 @@ public void attest_responseBodyHasNoExpiredAt_exceptionThrown(Vertx vertx, Vertx AttestationResponseHandlerException result = Assertions.assertThrows(AttestationResponseHandlerException.class, () -> { attestationResponseHandler.attest(); }); - String expectedExceptionMessage = "com.uid2.shared.attest.AttestationResponseHandlerException: http status: 200, response json does not contain body.expiresAt"; + String expectedExceptionMessage = "AttestationResponseCode: RetryableFailure, response json does not contain body.expiresAt"; - Assertions.assertEquals(expectedExceptionMessage, result.getMessage()); - verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody)); + assertEquals(expectedExceptionMessage, result.getMessage()); + verify(this.responseWatcher, times(1)).handle(Pair.of(AttestationResponseCode.Success, expectedResponseBody)); testContext.completeNow(); } @Test public void attest_providerNotReady_exceptionThrown(Vertx vertx, VertxTestContext testContext) throws Exception { - attestationResponseHandler = getAttestationTokenRetriever(vertx); + attestationResponseHandler = getAttestationResponseHandler(vertx); when(attestationProvider.isReady()).thenReturn(false); when(attestationProvider.getAttestationRequest(any(), any())).thenReturn(new byte[1]); @@ -238,18 +247,20 @@ public void attest_providerNotReady_exceptionThrown(Vertx vertx, VertxTestContex attestationResponseHandler.attest(); }); String expectedExceptionMessage = "attestation provider is not ready"; - Assertions.assertEquals(expectedExceptionMessage, result.getMessage()); + assertEquals(expectedExceptionMessage, result.getMessage()); testContext.completeNow(); } + @Test public void attest_succeed_optOutJwtSet(Vertx vertx, VertxTestContext testContext) throws Exception { - attestationResponseHandler = getAttestationTokenRetriever(vertx); + attestationResponseHandler = getAttestationResponseHandler(vertx); when(attestationProvider.isReady()).thenReturn(true); when(attestationProvider.getAttestationRequest(any(), any())).thenReturn(new byte[1]); - HttpResponse mockHttpResponse = mock(HttpResponse.class); String expectedResponseBody = "{\"body\": {\"attestation_token\": \"test\",\"expiresAt\": \"2023-08-03T09:09:30.608597Z\",\"attestation_jwt_optout\": \"test_jwt\",\"attestation_jwt_core\": \"\"},\"status\": \"success\"}"; + HttpResponse mockHttpResponse = mock(HttpResponse.class); + String expectedResponseBody = "{\"body\": {\"attestation_token\": \"test\",\"expiresAt\": \"2023-08-03T09:09:30.608597Z\",\"attestation_jwt_optout\": \"test_jwt\",\"attestation_jwt_core\": \"\"},\"status\": \"success\"}"; when(mockHttpResponse.body()).thenReturn(expectedResponseBody); when(mockHttpResponse.statusCode()).thenReturn(200); @@ -257,13 +268,14 @@ public void attest_succeed_optOutJwtSet(Vertx vertx, VertxTestContext testContex when(mockAttestationTokenDecryptor.decrypt(any(), any())).thenReturn("test_attestation_token".getBytes(StandardCharsets.UTF_8)); attestationResponseHandler.attest(); - Assertions.assertEquals("test_jwt", attestationResponseHandler.getOptOutJWT()); - verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody)); + assertEquals("test_jwt", attestationResponseHandler.getOptOutJWT()); + verify(this.responseWatcher, times(1)).handle(Pair.of(AttestationResponseCode.Success, expectedResponseBody)); testContext.completeNow(); } + @Test public void attest_succeed_coreJwtSet(Vertx vertx, VertxTestContext testContext) throws Exception { - attestationResponseHandler = getAttestationTokenRetriever(vertx); + attestationResponseHandler = getAttestationResponseHandler(vertx); when(attestationProvider.isReady()).thenReturn(true); when(attestationProvider.getAttestationRequest(any(), any())).thenReturn(new byte[1]); @@ -277,13 +289,14 @@ public void attest_succeed_coreJwtSet(Vertx vertx, VertxTestContext testContext) when(mockAttestationTokenDecryptor.decrypt(any(), any())).thenReturn("test_attestation_token".getBytes(StandardCharsets.UTF_8)); attestationResponseHandler.attest(); - Assertions.assertEquals("test_jwt_core", attestationResponseHandler.getCoreJWT()); - verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody)); + assertEquals("test_jwt_core", attestationResponseHandler.getCoreJWT()); + verify(this.responseWatcher, times(1)).handle(Pair.of(AttestationResponseCode.Success, expectedResponseBody)); testContext.completeNow(); } + @Test public void attest_succeed_jwtsNull(Vertx vertx, VertxTestContext testContext) throws Exception { - attestationResponseHandler = getAttestationTokenRetriever(vertx); + attestationResponseHandler = getAttestationResponseHandler(vertx); when(attestationProvider.isReady()).thenReturn(true); when(attestationProvider.getAttestationRequest(any(), any())).thenReturn(new byte[1]); @@ -299,13 +312,13 @@ public void attest_succeed_jwtsNull(Vertx vertx, VertxTestContext testContext) t attestationResponseHandler.attest(); Assertions.assertNull(attestationResponseHandler.getOptOutJWT()); Assertions.assertNull(attestationResponseHandler.getCoreJWT()); - verify(this.responseWatcher, times(1)).handle(Pair.of(200, expectedResponseBody)); + verify(this.responseWatcher, times(1)).handle(Pair.of(AttestationResponseCode.Success, expectedResponseBody)); testContext.completeNow(); } @Test - public void attest_succeed_jsonRequest_includes_expected_properties(Vertx vertx, VertxTestContext testContext) throws Exception{ - attestationResponseHandler = getAttestationTokenRetriever(vertx); + public void attest_succeed_jsonRequest_includes_expected_properties(Vertx vertx, VertxTestContext testContext) throws Exception { + attestationResponseHandler = getAttestationResponseHandler(vertx); when(attestationProvider.isReady()).thenReturn(true); when(attestationProvider.getAttestationRequest(any(), eq(ENCODED_ATTESTATION_ENDPOINT))).thenReturn(ENCODED_ATTESTATION_ENDPOINT); @@ -330,17 +343,83 @@ public void attest_succeed_jsonRequest_includes_expected_properties(Vertx vertx, String base64Content = jsonBody.getString("attestation_request"); byte[] data = Base64.decode(base64Content); String decodedUrl = new String(data, StandardCharsets.UTF_8); - Assertions.assertEquals(ATTESTATION_ENDPOINT, decodedUrl); + assertEquals(ATTESTATION_ENDPOINT, decodedUrl); Assertions.assertNotNull(jsonBody.getString("operator_type")); - Assertions.assertEquals(OPERATOR_TYPE, jsonBody.getString("operator_type")); + assertEquals(OPERATOR_TYPE, jsonBody.getString("operator_type")); verify(attestationProvider, times(1)).getAttestationRequest(any(), eq(ENCODED_ATTESTATION_ENDPOINT)); testContext.completeNow(); } - private AttestationResponseHandler getAttestationTokenRetriever(Vertx vertx) { + @ParameterizedTest + @ValueSource(ints = {401, 403}) + public void attest_response_throws_AttestationFailure_on_auth_failure(Integer responseCode, Vertx vertx, VertxTestContext testContext) throws Exception { + // Arrange + attestationResponseHandler = getAttestationResponseHandler(vertx); + + when(attestationProvider.isReady()).thenReturn(true); + when(attestationProvider.getAttestationRequest(any(), eq(ENCODED_ATTESTATION_ENDPOINT))).thenReturn(ENCODED_ATTESTATION_ENDPOINT); + + HttpResponse mockHttpResponse = mock(HttpResponse.class); + String expectedResponseBody = "Failed attestation"; + when(mockHttpResponse.body()).thenReturn(expectedResponseBody); + when(mockHttpResponse.statusCode()).thenReturn(responseCode); + + when(mockHttpClient.post(eq(ATTESTATION_ENDPOINT), any(String.class), any(HashMap.class))).thenReturn(mockHttpResponse); + + // Act + AttestationResponseHandlerException result = Assertions.assertThrows(AttestationResponseHandlerException.class, () -> { + attestationResponseHandler.attest(); + }); + + // Assert + assertEquals("AttestationResponseCode: AttestationFailure, Non-success response from Core on attest", result.getMessage()); + assertEquals(AttestationResponseCode.AttestationFailure, result.getResponseCode()); + assertTrue(result.isAttestationFailure()); + verify(this.responseWatcher, times(1)).handle(Pair.of(AttestationResponseCode.AttestationFailure, "Failed attestation")); + verify(this.mockAttestationTokenDecryptor, never()).decrypt(any(), any()); + Assertions.assertNull(attestationResponseHandler.getOptOutJWT()); + Assertions.assertNull(attestationResponseHandler.getCoreJWT()); + + testContext.completeNow(); + } + + @ParameterizedTest + @ValueSource(ints = {100, 199, 404, 500, 502, 503}) + public void attest_response_throws_AttestationRetryable(Integer responseCode, Vertx vertx, VertxTestContext testContext) throws Exception { + // Arrange + attestationResponseHandler = getAttestationResponseHandler(vertx); + + when(attestationProvider.isReady()).thenReturn(true); + when(attestationProvider.getAttestationRequest(any(), eq(ENCODED_ATTESTATION_ENDPOINT))).thenReturn(ENCODED_ATTESTATION_ENDPOINT); + + HttpResponse mockHttpResponse = mock(HttpResponse.class); + String expectedResponseBody = "Some error"; + when(mockHttpResponse.body()).thenReturn(expectedResponseBody); + when(mockHttpResponse.statusCode()).thenReturn(responseCode); + + when(mockHttpClient.post(eq(ATTESTATION_ENDPOINT), any(String.class), any(HashMap.class))).thenReturn(mockHttpResponse); + + // Act + AttestationResponseHandlerException result = Assertions.assertThrows(AttestationResponseHandlerException.class, () -> { + attestationResponseHandler.attest(); + }); + + // Assert + assertEquals("AttestationResponseCode: RetryableFailure, Non-success response from Core on attest", result.getMessage()); + assertEquals(AttestationResponseCode.RetryableFailure, result.getResponseCode()); + assertFalse(result.isAttestationFailure()); + verify(this.responseWatcher, times(1)).handle(Pair.of(AttestationResponseCode.RetryableFailure, "Some error")); + verify(this.mockAttestationTokenDecryptor, never()).decrypt(any(), any()); + Assertions.assertNull(attestationResponseHandler.getOptOutJWT()); + Assertions.assertNull(attestationResponseHandler.getCoreJWT()); + + testContext.completeNow(); + } + + private AttestationResponseHandler getAttestationResponseHandler(Vertx vertx) { return new AttestationResponseHandler(vertx, ATTESTATION_ENDPOINT, "testApiKey", OPERATOR_TYPE, APP_VERSION, attestationProvider, responseWatcher, proxy, clock, mockHttpClient, mockAttestationTokenDecryptor, 250); } } \ No newline at end of file diff --git a/src/test/java/com/uid2/shared/attest/UidCoreClientTest.java b/src/test/java/com/uid2/shared/attest/UidCoreClientTest.java index 8e1f1916..16927e56 100644 --- a/src/test/java/com/uid2/shared/attest/UidCoreClientTest.java +++ b/src/test/java/com/uid2/shared/attest/UidCoreClientTest.java @@ -62,18 +62,18 @@ public void Download_Succeed_RequestSentWithExpectedParameters() throws IOExcept @Test public void Download_AttestInternalFail_ExceptionThrown() throws IOException, AttestationResponseHandlerException { - AttestationResponseHandlerException exception = new AttestationResponseHandlerException(401, "test failure"); + AttestationResponseHandlerException exception = new AttestationResponseHandlerException(AttestationResponseCode.AttestationFailure, "test failure"); doThrow(exception).when(mockAttestationResponseHandler).attest(); CloudStorageException result = assertThrows(CloudStorageException.class, () -> { uidCoreClient.download("https://download"); }); - String expectedExceptionMessage = "download https://download error: http status: 401, test failure"; + String expectedExceptionMessage = "download error: AttestationResponseCode: AttestationFailure, test failure"; assertEquals(expectedExceptionMessage, result.getMessage()); } @Test - public void Download_Attest401_AttestCalledTwice() throws CloudStorageException, IOException, InterruptedException, AttestationResponseHandlerException { + public void Download_Attest401_getOptOut_NotCalled() throws CloudStorageException, IOException, AttestationResponseHandlerException { HttpResponse mockHttpResponse = mock(HttpResponse.class); when(mockHttpResponse.statusCode()).thenReturn(401); @@ -83,7 +83,7 @@ public void Download_Attest401_AttestCalledTwice() throws CloudStorageException, when(mockHttpClient.get(eq("https://download"), any(HashMap.class))).thenReturn(mockHttpResponse); uidCoreClient.download("https://download"); - verify(mockAttestationResponseHandler, times(2)).attest(); + verify(mockAttestationResponseHandler, times(1)).attest(); verify(mockAttestationResponseHandler, never()).getOptOutUrl(); }