diff --git a/.github/workflows/build-and-publish.yaml b/.github/workflows/build-and-publish.yaml index 06b68b52..53669dc1 100644 --- a/.github/workflows/build-and-publish.yaml +++ b/.github/workflows/build-and-publish.yaml @@ -20,12 +20,12 @@ on: - CRITICAL (DO NOT use if JIRA ticket not raised) publish_to_maven: description: 'True to publish the artifacts to Maven repository, false to skip the step' - default: false + default: true required: false type: boolean java_version: type: string - default: '11' + default: '21' publish_vulnerabilities: type: string default: 'true' diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 4aad7e54..7df275d9 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -4,4 +4,6 @@ on: [pull_request, push, workflow_dispatch] jobs: build: uses: IABTechLab/uid2-shared-actions/.github/workflows/shared-build-and-test.yaml@v2 + with: + java_version: "21" secrets: inherit \ No newline at end of file diff --git a/pom.xml b/pom.xml index cf10e2df..f8bb6e42 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,11 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.uid2 uid2-shared - 7.17.9-alpha-142-SNAPSHOT + 7.20.0 ${project.groupId}:${project.artifactId} Library for all the shared uid2 operations https://github.com/IABTechLab/uid2docs @@ -37,8 +36,17 @@ snapshots-repo https://s01.oss.sonatype.org/content/repositories/snapshots - false - true + + false + + + true + + + + maven_central + Maven Central + https://repo.maven.apache.org/maven2/ @@ -67,7 +75,7 @@ software.amazon.awssdk bom - 2.20.69 + 2.27.2 pom import @@ -78,6 +86,13 @@ pom import + + com.azure + azure-sdk-bom + 1.2.26 + pom + import + @@ -85,7 +100,7 @@ com.uid2 uid2-attestation-api - 2.0.0-f968aec0e3 + 2.1.6 io.vertx @@ -131,17 +146,17 @@ ch.qos.logback logback-classic - 1.3.12 + 1.5.6 com.github.loki4j loki-logback-appender - 1.2.0 + 1.5.1 com.github.ben-manes.caffeine caffeine - 3.1.7 + 3.1.8 org.hashids @@ -151,17 +166,17 @@ com.amazonaws aws-java-sdk-s3 - 1.12.701 + 1.12.765 - + com.amazonaws aws-java-sdk-sts - 1.12.364 + 1.12.765 com.google.api-client google-api-client - 2.1.1 + 2.6.0 com.google.apis @@ -171,7 +186,7 @@ com.google.auth google-auth-library-oauth2-http - 1.14.0 + 1.23.0 com.google.cloud @@ -180,12 +195,10 @@ com.azure azure-security-attestation - 1.1.15 com.azure azure-core-http-netty - 1.13.11 co.nstant.in @@ -195,12 +208,12 @@ com.fasterxml.jackson.core jackson-databind - 2.12.7.1 + 2.14.2 org.projectlombok lombok - 1.18.30 + 1.18.34 org.apache.commons @@ -239,20 +252,20 @@ org.mockito - mockito-inline - 5.2.0 + mockito-core + 5.12.0 test org.mockito mockito-junit-jupiter - 5.10.0 + 5.12.0 test org.assertj assertj-core - 3.25.2 + 3.26.3 test @@ -264,19 +277,22 @@ maven-compiler-plugin 3.8.0 - 11 - 11 + 21 + 21 org.apache.maven.plugins maven-surefire-plugin 3.2.5 + + -XX:+EnableDynamicAgentLoading + org.sonatype.plugins nexus-staging-maven-plugin - 1.6.7 + 1.7.0 true ossrh @@ -343,7 +359,7 @@ org.jacoco jacoco-maven-plugin - 0.8.8 + 0.8.12 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 d657b136..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; @@ -29,11 +30,12 @@ public class AttestationResponseHandler { private final IAttestationProvider attestationProvider; private final String clientApiToken; + private final String operatorType; private final ApplicationVersion appVersion; 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; @@ -45,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; @@ -52,18 +55,21 @@ public class AttestationResponseHandler { public AttestationResponseHandler(Vertx vertx, String attestationEndpoint, String clientApiToken, + String operatorType, ApplicationVersion appVersion, IAttestationProvider attestationProvider, - Handler> responseWatcher, + Handler> responseWatcher, Proxy proxy) { - this(vertx, attestationEndpoint, clientApiToken, appVersion, attestationProvider, responseWatcher, proxy, new InstantClock(), null, null, 60000); + 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, @@ -73,6 +79,7 @@ public AttestationResponseHandler(Vertx vertx, this.attestationEndpoint = attestationEndpoint; this.encodedAttestationEndpoint = this.encodeStringUnicodeAttestationEndpoint(attestationEndpoint); this.clientApiToken = clientApiToken; + this.operatorType = operatorType; this.appVersion = appVersion; this.attestationProvider = attestationProvider; this.attestationToken = new AtomicReference<>(null); @@ -127,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); @@ -158,7 +161,8 @@ public void attest() throws IOException, AttestationResponseHandlerException { "attestation_request", Base64.getEncoder().encodeToString(attestationProvider.getAttestationRequest(publicKey, this.encodedAttestationEndpoint)), "public_key", Base64.getEncoder().encodeToString(publicKey), "application_name", appVersion.getAppName(), - "application_version", appVersion.getAppVersion() + "application_version", appVersion.getAppVersion(), + "operator_type", this.operatorType ); JsonObject components = new JsonObject(); for (Map.Entry kv : appVersion.getComponentVersions().entrySet()) { @@ -175,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); @@ -210,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); } @@ -237,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); } @@ -294,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(); } @@ -313,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; @@ -61,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]); @@ -106,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]); @@ -132,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. @@ -160,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]); @@ -189,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]); @@ -219,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]); @@ -237,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); @@ -256,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]); @@ -276,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]); @@ -298,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_attestUrl_in_attestation_request(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); @@ -329,14 +343,83 @@ public void attest_succeed_jsonRequest_includes_attestUrl_in_attestation_request 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")); + assertEquals(OPERATOR_TYPE, jsonBody.getString("operator_type")); verify(attestationProvider, times(1)).getAttestationRequest(any(), eq(ENCODED_ATTESTATION_ENDPOINT)); testContext.completeNow(); } - private AttestationResponseHandler getAttestationTokenRetriever(Vertx vertx) { - return new AttestationResponseHandler(vertx, ATTESTATION_ENDPOINT, "testApiKey", APP_VERSION, attestationProvider, responseWatcher, proxy, clock, mockHttpClient, mockAttestationTokenDecryptor, 250); + @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(); } diff --git a/version.json b/version.json index 68154af7..c25920a8 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{ "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", "version": "7.17", "publicReleaseRefSpec": [ "^refs/heads/master$", "^refs/heads/v\\d+(?:\\.\\d+)?$" ], "cloudBuild": { "setVersionVariables": true, "buildNumber": { "enabled": true, "includeCommitId": { "when": "always" } } } } +{ "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", "version": "7.20", "publicReleaseRefSpec": [ "^refs/heads/master$", "^refs/heads/v\\d+(?:\\.\\d+)?$" ], "cloudBuild": { "setVersionVariables": true, "buildNumber": { "enabled": true, "includeCommitId": { "when": "always" } } } }