Skip to content

Commit

Permalink
Add online test for verify key attestation statement (#5)
Browse files Browse the repository at this point in the history
test: add online key attestation verification test

* ci: fix tests
* test: add online key attestation verification test targeting Fortanix SaaS AMER
* test: fix logging
* refactor: use sdkms ApiClient
  • Loading branch information
Taowyoo authored Oct 3, 2023
1 parent 0c016e3 commit a62f3b3
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 23 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/java-example-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ jobs:
fi
- name: Build with Maven
run: mvn -B package --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
Expand Down
6 changes: 6 additions & 0 deletions key-attestation/java-example/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fortanix/sdkms-client -->
<dependency>
<groupId>com.fortanix</groupId>
<artifactId>sdkms-client</artifactId>
<version>4.19.2244</version>
</dependency>
</dependencies>
<build>
<testResources>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,20 @@
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.CertPath;
import java.security.cert.CertPathBuilder;
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.PKIXParameters;
import java.security.cert.PKIXRevocationChecker;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
Expand Down Expand Up @@ -52,7 +57,7 @@ public final class Verify {
*/
public static void verify(KeyAttestationResponse keyAttestationResponse,
X509Certificate trustRootCa, boolean verifyCrl) throws Exception {
LOGGER.info(String.format("Verifying KeyAttestationResponse: %s", keyAttestationResponse.toString()));
LOGGER.info("Verifying KeyAttestationResponse ...");
// skip verifying attestationStatement.format since it's been covered by json
// decoding
String attestationStatementStr = keyAttestationResponse.getAttestationStatement().getStatement();
Expand Down Expand Up @@ -86,9 +91,11 @@ public static void verify(List<X509Certificate> authorityChain, X509Certificate
// certificate, so we need to manually check 'Fortanix DSM Key Attestation' is
// correctly singed by 'Fortanix DSM SaaS Key Attestation Authority' certificate
verify_cert_signature(attestationStatement, authorityChain.get(0));
LOGGER.info("DONE");

LOGGER.info("Checking if root certificate in `authorityChain` matches given trusted root certificate");
check_root_cert_match(authorityChain.get(authorityChain.size() - 1), trustRootCa);
LOGGER.info("DONE");
// verify each signature on authority certificate chain is correctly signed by
// it's parent
try {
Expand Down Expand Up @@ -125,30 +132,42 @@ public static void verify(List<X509Certificate> authorityChain, X509Certificate
public static void verify_cert_chain_signature(List<X509Certificate> chain, X509Certificate trust_ca,
boolean verifyCrl)
throws Exception {
LOGGER.info("Checking if root certificate in `authorityChain` matches given trusted root certificate");
LOGGER.info("Checking `authorityChain` signatures");
if (chain.isEmpty()) {
throw new KeyAttestationStatementVerifyException("Empty certificate chain");
}
// Create CertPath
CertificateFactory factory = CertificateFactory.getInstance("X.509", "BC");
CertPath certPath = factory.generateCertPath(chain);
// Because root CA does not contain CRL distribution point extension,
// CertPathValidator will throw error when CRL revocation check is enabled.
// As a result, root CA need to be removed from cert chain.
CertPath certPath = factory.generateCertPath(chain.subList(0, chain.size() - 1));

// Set up TrustAnchor using the last certificate as the root certificate
TrustAnchor trustAnchor = new TrustAnchor(trust_ca, null);
Set<TrustAnchor> trustAnchors = Collections.singleton(trustAnchor);

// Set up PKIXParameters
PKIXParameters params = new PKIXParameters(trustAnchors);
params.setRevocationEnabled(verifyCrl);
CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
PKIXRevocationChecker rc = (PKIXRevocationChecker) cpb.getRevocationChecker();
rc.setOptions(EnumSet.of(
PKIXRevocationChecker.Option.PREFER_CRLS, // prefer CLR over OCSP
PKIXRevocationChecker.Option.NO_FALLBACK)); // don't fall back to OCSP checking

PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(trustAnchors, new X509CertSelector());
pkixParams.setRevocationEnabled(verifyCrl);
if (verifyCrl){
pkixParams.addCertPathChecker(rc);
}

// Validate CertPath
CertPathValidator validator = CertPathValidator.getInstance("PKIX", "BC");
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
try {
validator.validate(certPath, params);
validator.validate(certPath, pkixParams);
} catch (CertPathValidatorException e) {
// Handle validation exception
throw new KeyAttestationStatementVerifyException("Certificate chain validation failed" + e.toString());
}
LOGGER.info("DONE");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ protected void verifyDistPoint(String errStrPrefix, X509Certificate cert, String
errStrPrefix
+ " certificate cRLDistributionPoints extension wrong Name");
}
LOGGER.info("DONE");
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package com.fortanix.keyattestationstatementverifier.types.json;

import com.fasterxml.jackson.annotation.JsonProperty;

public enum KeyAttestationStatementFormat {

/**
* The attestation statement is formatted as a DER-encoded X.509 certificate.
*/
@JsonProperty("x509_certificate")
X509_CERTIFICATE
x509_certificate
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public void testKeyAttestationResponseJsonEncodingDecoding() throws Exception {
List<String> authorityChain = Arrays.asList("certChain1", "certChain2");
String statementString = "statement1";
KeyAttestationStatement keyAttestationStatement = new KeyAttestationStatement(
KeyAttestationStatementFormat.X509_CERTIFICATE, statementString);
KeyAttestationStatementFormat.x509_certificate, statementString);

// Encode to JSON
KeyAttestationResponse response = new KeyAttestationResponse(authorityChain, keyAttestationStatement);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,43 @@
package com.fortanix.keyattestationstatementverifier;

import org.junit.Ignore;
import org.junit.Test;

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);
Expand Down Expand Up @@ -81,24 +102,103 @@ public void verifyStatementFromJsonWithoutCrlCheck() throws Exception {
}

/**
* This test is ignored because it's an example code for showing how to verify a
* real 'Fortanix DSM Key Attestation'
* 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
*/
@Ignore
@Test
public void verifyStatementFullCheck() throws Exception {
String jsonPath = "Path to the KeyAttestationResponse json file";
Reader reader = new FileReader(jsonPath);
ObjectMapper objectMapper = new ObjectMapper();
KeyAttestationResponse decodedResponse = objectMapper.readValue(reader, KeyAttestationResponse.class);
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());
System.exit(1);
}

// 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<Pair> queryParams = new ArrayList<>(); // query parameters
Object body = String.format("{\"key\":{\"kid\":\"%s\"}}", keyId);
Map<String, String> headerParams = new HashMap<>(); // header parameters
Map<String, Object> formParams = new HashMap<>(); // form parameters
String accept = "application/json";
String contentType = "application/json";
String[] authNames = new String[] { "bearerToken" };
GenericType<KeyAttestationResponse> returnType = new GenericType<KeyAttestationResponse>() {
};
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"));

Verify.verify(decodedResponse, trustedRootCert, true);
// Do verification
Verify.verify(keyAttestationResponse, trustedRootCert, true);
}

}

0 comments on commit a62f3b3

Please sign in to comment.