diff --git a/.github/workflows/java-example-ci.yml b/.github/workflows/java-example-ci.yml index 3162ce9..c36f210 100644 --- a/.github/workflows/java-example-ci.yml +++ b/.github/workflows/java-example-ci.yml @@ -5,34 +5,38 @@ name: Java CI with Maven for java-example on: push: - branches: [ "main" ] + branches: ["main"] paths: - - 'key-attestation/java-example/**' - - '.github/workflows/**' + - "key-attestation/java-example/**" + - ".github/workflows/**" pull_request: - branches: [ "main" ] + branches: ["main"] paths: - - 'key-attestation/java-example/**' - - '.github/workflows/**' + - "key-attestation/java-example/**" + - ".github/workflows/**" schedule: # Runs at 00:00 UTC every other day - - cron: '0 0 */2 * *' + - cron: "0 0 */2 * *" jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up JDK 8 - uses: actions/setup-java@v3 - with: - java-version: '8' - distribution: 'temurin' - cache: maven - - name: Check and Install Maven - run: | + - uses: actions/checkout@v3 + - name: Install faketime + run: | + sudo apt-get install -y faketime + + - name: Set up JDK 8 + uses: actions/setup-java@v3 + with: + java-version: "8" + distribution: "temurin" + cache: maven + + - name: Check and Install Maven + run: | if ! command -v mvn --version &> /dev/null then echo "Maven could not be found" @@ -43,13 +47,23 @@ jobs: else echo "Maven is already installed" fi - - name: Build with Maven - run: mvn -B package -Dstyle.color=always --file key-attestation/java-example/pom.xml - env: - AMER_APP_API_KEY: ${{ secrets.AMER_APP_API_KEY }} - - # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - - name: Update dependency graph - uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 - with: - directory: key-attestation/java-example + + - name: Run offline tests and build package + run: mvn -B -Dstyle.color=always package + working-directory: ./key-attestation/java-example + + - name: Run faketime tests + run: ./run_faketime_tests.sh + working-directory: ./key-attestation/java-example + + - name: Run online tests + run: mvn test -Dstyle.color=always -Dtest=VerifyTestLive + working-directory: ./key-attestation/java-example + env: + AMER_APP_API_KEY: ${{ secrets.AMER_APP_API_KEY }} + + # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive + - name: Update dependency graph + uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 + with: + directory: key-attestation/java-example diff --git a/key-attestation/java-example/README.md b/key-attestation/java-example/README.md index d884c5a..f98e1e6 100644 --- a/key-attestation/java-example/README.md +++ b/key-attestation/java-example/README.md @@ -4,19 +4,25 @@ This is a example java of how to verify **Fortanix DSM Key Attestation Statement ## Building -`mvn compile` +`mvn -B package` ## Testing -`mvn test` +- Run offline tests: + - `mvn test` +- Run online tests: + - `mvn test -Dtest=VerifyTestLive` +- Because the certificates stored in repo are already expired, to test the successful code path with [`faketime`](https://manpages.ubuntu.com/manpages/trusty/man1/faketime.1.html): + - `./run_faketime_tests.sh` ## Explanation The test code under [VerifyTest.java](src/test/java/com/fortanix/keyattestationstatementverifier/VerifyTest.java) -shows how to properly verify the **Fortanix DSM Key Attestation Statement** certificate: +shows how to properly verify the **Fortanix DSM Key Attestation Statement** certificate offline: -Online check -- `verifyStatementFullCheck`: Verify given `KeyAttestationResponse` in a JSON file, the `Fortanix Attestation and Provisioning Root CA` is downloaded from https://pki.fortanix.com in runtime. **Note**: This test is turned off since CRL and PKI server is not ready when this example code is created and certificates for testing are signed by fake CA. - -Offline check - `verifyStatementFromJsonWithoutCrlCheck`: Verify given `KeyAttestationResponse` in a JSON file, assuming the last certificate in authority chain is correct ROOT certificate and skipping CRL checks. + +The test code under [VerifyTestLive.java](src/test/java/com/fortanix/keyattestationstatementverifier/VerifyTest.java) +shows how to properly verify the **Fortanix DSM Key Attestation Statement** certificate online: + +- `verifyStatementFullCheckOnlineAMER`: Use https://amer.smartkey.io to create a RSA key and get attestation statement of it.Then it verifies the statement with root CA downloaded from https://pki.fortanix.com/Fortanix_Attestation_and_Provisioning_Root_CA.crt. \ No newline at end of file diff --git a/key-attestation/java-example/run_faketime_tests.sh b/key-attestation/java-example/run_faketime_tests.sh new file mode 100755 index 0000000..9c746c7 --- /dev/null +++ b/key-attestation/java-example/run_faketime_tests.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Check if faketime is installed +if ! command -v faketime &> /dev/null; then + echo "faketime is not installed. Please install it using 'sudo apt install faketime'." + exit 1 +fi + +# Tell tests that faketime is active +export FAKE_TIME_ACTIVE=1 + +# Run Maven tests with faketime active +faketime --exclude-monotonic '2023-09-15 10:00:00' mvn test -Dstyle.color=always -Dtest=VerifyTest \ No newline at end of file diff --git a/key-attestation/java-example/src/test/java/com/fortanix/keyattestationstatementverifier/VerifyTest.java b/key-attestation/java-example/src/test/java/com/fortanix/keyattestationstatementverifier/VerifyTest.java index 1fdd32c..99da7ac 100644 --- a/key-attestation/java-example/src/test/java/com/fortanix/keyattestationstatementverifier/VerifyTest.java +++ b/key-attestation/java-example/src/test/java/com/fortanix/keyattestationstatementverifier/VerifyTest.java @@ -5,39 +5,17 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fortanix.keyattestationstatementverifier.types.json.KeyAttestationResponse; -import com.fortanix.sdkms.v1.ApiClient; -import com.fortanix.sdkms.v1.ApiException; -import com.fortanix.sdkms.v1.Configuration; -import com.fortanix.sdkms.v1.Pair; -import com.fortanix.sdkms.v1.api.AuthenticationApi; -import com.fortanix.sdkms.v1.api.SecurityObjectsApi; -import com.fortanix.sdkms.v1.auth.ApiKeyAuth; -import com.fortanix.sdkms.v1.model.AuthResponse; -import com.fortanix.sdkms.v1.model.KeyObject; -import com.fortanix.sdkms.v1.model.ObjectType; - -import com.fortanix.sdkms.v1.model.SobjectRequest; - import static org.junit.Assert.*; import java.io.FileReader; import java.io.Reader; import java.net.URL; import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.UUID; - -import javax.ws.rs.core.GenericType; public class VerifyTest { - private static final String FORTANIX_AMER_SAAS_SERVER_URL = "https://amer.smartkey.io"; - private static final String JAVA_CI_AMER_APP_API_KEY = "AMER_APP_API_KEY"; private static final String VALID_STATEMENT_CERT_PEM = "key-attestation-statement.pem"; private static final String VALID_RESPONSE_JSON = "key-attestation-response.json"; - private static final boolean DEBUG = false; private URL getTestFileUrl(String fileName) throws Exception { URL resUrl = getClass().getClassLoader().getResource(fileName); @@ -71,12 +49,16 @@ public void verifyStatementFromPemWithoutCrlCheck() throws Exception { // NOTE: please replace with Root CA certificate if you already downloaded it // somewhere. X509Certificate trusted = cert_chain.get(cert_chain.size() - 1); - // because at time this code is written, CRL server is not setup, we turn of the - // CRL check - Exception exception = assertThrows(Exception.class, - () -> Verify.verify(authorityChain, cert_chain.get(0), trusted, false)); - assertTrue("certificates should already expired, exception: " + exception.toString(), - exception.toString().contains("certificate expired") || exception.toString().contains("NotAfter")); + + String fakeTimeEnv = System.getenv("FAKE_TIME_ACTIVE"); + if (fakeTimeEnv != null && "1".equals(fakeTimeEnv)) { + Verify.verify(authorityChain, cert_chain.get(0), trusted, false); + } else { + Exception exception = assertThrows(Exception.class, + () -> Verify.verify(authorityChain, cert_chain.get(0), trusted, false)); + assertTrue("certificates should already expired, exception: " + exception.toString(), + exception.toString().contains("certificate expired") || exception.toString().contains("NotAfter")); + } } /** @@ -99,112 +81,15 @@ public void verifyStatementFromJsonWithoutCrlCheck() throws Exception { // somewhere. List authorityChain = decodedResponse.getAuthorityChain(); X509Certificate trusted = Verify.readBase64EncodedCertificate(authorityChain.get(authorityChain.size() - 1)); - // because at time this code is written, CRL server is not setup, we turn of the - // CRL check - Exception exception = assertThrows(Exception.class, - () -> Verify.verify(decodedResponse, trusted, false)); - assertTrue("certificates should already expired, exception: " + exception.toString(), - exception.toString().contains("certificate expired") || exception.toString().contains("NotAfter")); - } - - /** - * This test tests the full process: - * 1. Creating a RSA key. - * 2. Get RSA key's key attestation statement. - * 3. Verify key attestation statement. - * - * @throws Exception - */ - @Test - public void verifyStatementFullCheckOnlineAMER() throws Exception { - // Setup a SDKMS API client - String appApiKeyString = System.getenv(JAVA_CI_AMER_APP_API_KEY); - ApiClient client = new ApiClient(); - - // Set the path of the server to talk to. - client.setBasePath(FORTANIX_AMER_SAAS_SERVER_URL); - // This optionally enables verbose logging in the API library. - client.setDebugging(DEBUG); - - // The default ApiClient (and its configured authorization) will be - // used for constructing the specific API objects, such as - // AuthenticationApi and SecurityObjectsApi. - Configuration.setDefaultApiClient(client); - - // When authenticating as an application, the API Key functions as - // the entire HTTP basic auth token. - client.setBasicAuthString(appApiKeyString); - - String bearerToken = null; - // Acquire a bearer token to use for other APIs. - try { - AuthResponse response = new AuthenticationApi().authorize(); - bearerToken = response.getAccessToken(); - if (DEBUG) { - System.err.printf("Received Bearer token %s\n", bearerToken); - } - - // Configure the client library to use the bearer token. - ApiKeyAuth bearerAuth = (ApiKeyAuth) client.getAuthentication("bearerToken"); - bearerAuth.setApiKey(bearerToken); - bearerAuth.setApiKeyPrefix("Bearer"); - } catch (ApiException e) { - System.err.println("Unable to authenticate: " + e.getMessage()); - throw e; - } - - // Create a RSA key - SecurityObjectsApi securityObjectsApi = new SecurityObjectsApi(); - SobjectRequest sobjectRequest = new SobjectRequest(); - String newRsaKeyName = UUID.randomUUID().toString(); - sobjectRequest.setName(newRsaKeyName); - sobjectRequest.setObjType(ObjectType.RSA); - sobjectRequest.setKeySize(2048); - System.out.println(String.format("Generating a new RSA key named '%s' ...", newRsaKeyName)); - KeyObject newRsaKeyObject = securityObjectsApi.generateSecurityObject(sobjectRequest); - String keyId = newRsaKeyObject.getKid(); - System.out.println(String.format("Generated a new RSA key named '%s' with key id: %s", newRsaKeyName, keyId)); - - // Get the the key attestation statement of the key just created - String path = "/crypto/v1/keys/key_attestation"; // API path - String method = "POST"; - List queryParams = new ArrayList<>(); // query parameters - Object body = String.format("{\"key\":{\"kid\":\"%s\"}}", keyId); - Map headerParams = new HashMap<>(); // header parameters - Map formParams = new HashMap<>(); // form parameters - String accept = "application/json"; - String contentType = "application/json"; - String[] authNames = new String[] { "bearerToken" }; - GenericType returnType = new GenericType() { - }; - System.out.println(String.format("Getting key attestation statement through ...")); - KeyAttestationResponse keyAttestationResponse = client.invokeAPI(path, method, queryParams, body, headerParams, - formParams, accept, contentType, authNames, returnType); - System.out.println("Got key attestation statement"); - - // Logout SDKMS ApiClient - if (bearerToken != null) { - // It is a good idea to terminate the session when you are done - // using it. This minimizes the window of time in which an attacker - // could steal bearer token and use it. - try { - new AuthenticationApi().terminate(); - } catch (ApiException e) { - System.err.println("Error logging out: " + e.getMessage()); - } - bearerToken = null; + String fakeTimeEnv = System.getenv("FAKE_TIME_ACTIVE"); + if (fakeTimeEnv != null && "1".equals(fakeTimeEnv)) { + Verify.verify(decodedResponse, trusted, false); + } else { + Exception exception = assertThrows(Exception.class, + () -> Verify.verify(decodedResponse, trusted, false)); + assertTrue("certificates should already expired, exception: " + exception.toString(), + exception.toString().contains("certificate expired") || exception.toString().contains("NotAfter")); } - - // Download Fortanix Root CA certificate - System.out.println("Downloading Fortanix Attestation and Provisioning Root CA certificate form: " - + Common.FORTANIX_ATTESTATION_AND_PROVISIONING_ROOT_CA_CERT_URL); - X509Certificate trustedRootCert = Common.getFortanixRootCaCertRemote( - Common.FORTANIX_ATTESTATION_AND_PROVISIONING_ROOT_CA_CERT_URL); - System.out.println(String.format("Downloaded Fortanix Attestation and Provisioning Root CA")); - - // Do verification - Verify.verify(keyAttestationResponse, trustedRootCert, true); } - } diff --git a/key-attestation/java-example/src/test/java/com/fortanix/keyattestationstatementverifier/VerifyTestLive.java b/key-attestation/java-example/src/test/java/com/fortanix/keyattestationstatementverifier/VerifyTestLive.java new file mode 100644 index 0000000..e6ac896 --- /dev/null +++ b/key-attestation/java-example/src/test/java/com/fortanix/keyattestationstatementverifier/VerifyTestLive.java @@ -0,0 +1,139 @@ +package com.fortanix.keyattestationstatementverifier; + +import org.junit.Test; + +import com.fortanix.keyattestationstatementverifier.types.json.KeyAttestationResponse; + +import com.fortanix.sdkms.v1.ApiClient; +import com.fortanix.sdkms.v1.ApiException; +import com.fortanix.sdkms.v1.Configuration; +import com.fortanix.sdkms.v1.Pair; +import com.fortanix.sdkms.v1.api.AuthenticationApi; +import com.fortanix.sdkms.v1.api.SecurityObjectsApi; +import com.fortanix.sdkms.v1.auth.ApiKeyAuth; +import com.fortanix.sdkms.v1.model.AuthResponse; +import com.fortanix.sdkms.v1.model.KeyObject; +import com.fortanix.sdkms.v1.model.ObjectType; + +import com.fortanix.sdkms.v1.model.SobjectRequest; + +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.ws.rs.core.GenericType; + +public class VerifyTestLive { + private static final String FORTANIX_AMER_SAAS_SERVER_URL = "https://amer.smartkey.io"; + private static final String JAVA_CI_AMER_APP_API_KEY = "AMER_APP_API_KEY"; + private static final boolean DEBUG = false; + + /** + * This test tests the full process: + * 1. Creating a RSA key. + * 2. Get RSA key's key attestation statement. + * 3. Verify key attestation statement. + * + * @throws Exception + */ + @Test + public void verifyStatementFullCheckOnlineAMER() throws Exception { + // Setup a SDKMS API client + String appApiKeyString = System.getenv(JAVA_CI_AMER_APP_API_KEY); + // Assert that the API key is not empty + if (appApiKeyString == null || appApiKeyString.isEmpty()) { + throw new IllegalArgumentException("Environment variable 'AMER_APP_API_KEY' is not set or is empty"); + } + + ApiClient client = new ApiClient(); + + // Set the path of the server to talk to. + client.setBasePath(FORTANIX_AMER_SAAS_SERVER_URL); + + // This optionally enables verbose logging in the API library. + client.setDebugging(DEBUG); + + // The default ApiClient (and its configured authorization) will be + // used for constructing the specific API objects, such as + // AuthenticationApi and SecurityObjectsApi. + Configuration.setDefaultApiClient(client); + + // When authenticating as an application, the API Key functions as + // the entire HTTP basic auth token. + client.setBasicAuthString(appApiKeyString); + + String bearerToken = null; + // Acquire a bearer token to use for other APIs. + try { + AuthResponse response = new AuthenticationApi().authorize(); + bearerToken = response.getAccessToken(); + if (DEBUG) { + System.err.printf("Received Bearer token %s\n", bearerToken); + } + + // Configure the client library to use the bearer token. + ApiKeyAuth bearerAuth = (ApiKeyAuth) client.getAuthentication("bearerToken"); + bearerAuth.setApiKey(bearerToken); + bearerAuth.setApiKeyPrefix("Bearer"); + } catch (ApiException e) { + System.err.println("Unable to authenticate: " + e.getMessage()); + throw e; + } + + // Create a RSA key + SecurityObjectsApi securityObjectsApi = new SecurityObjectsApi(); + SobjectRequest sobjectRequest = new SobjectRequest(); + String newRsaKeyName = UUID.randomUUID().toString(); + sobjectRequest.setName(newRsaKeyName); + sobjectRequest.setObjType(ObjectType.RSA); + sobjectRequest.setKeySize(2048); + System.out.println(String.format("Generating a new RSA key named '%s' ...", newRsaKeyName)); + KeyObject newRsaKeyObject = securityObjectsApi.generateSecurityObject(sobjectRequest); + String keyId = newRsaKeyObject.getKid(); + System.out.println(String.format("Generated a new RSA key named '%s' with key id: %s", newRsaKeyName, keyId)); + + // Get the the key attestation statement of the key just created + String path = "/crypto/v1/keys/key_attestation"; // API path + String method = "POST"; + List queryParams = new ArrayList<>(); // query parameters + Object body = String.format("{\"key\":{\"kid\":\"%s\"}}", keyId); + Map headerParams = new HashMap<>(); // header parameters + Map formParams = new HashMap<>(); // form parameters + String accept = "application/json"; + String contentType = "application/json"; + String[] authNames = new String[] { "bearerToken" }; + GenericType returnType = new GenericType() { + }; + System.out.println(String.format("Getting key attestation statement through ...")); + KeyAttestationResponse keyAttestationResponse = client.invokeAPI(path, method, queryParams, body, headerParams, + formParams, accept, contentType, authNames, returnType); + System.out.println("Got key attestation statement"); + + // Logout SDKMS ApiClient + if (bearerToken != null) { + // It is a good idea to terminate the session when you are done + // using it. This minimizes the window of time in which an attacker + // could steal bearer token and use it. + try { + new AuthenticationApi().terminate(); + } catch (ApiException e) { + System.err.println("Error logging out: " + e.getMessage()); + } + bearerToken = null; + } + + // Download Fortanix Root CA certificate + System.out.println("Downloading Fortanix Attestation and Provisioning Root CA certificate form: " + + Common.FORTANIX_ATTESTATION_AND_PROVISIONING_ROOT_CA_CERT_URL); + X509Certificate trustedRootCert = Common.getFortanixRootCaCertRemote( + Common.FORTANIX_ATTESTATION_AND_PROVISIONING_ROOT_CA_CERT_URL); + System.out.println(String.format("Downloaded Fortanix Attestation and Provisioning Root CA")); + + // Do verification + Verify.verify(keyAttestationResponse, trustedRootCert, true); + } + +}