diff --git a/key-attestation/java-example/src/main/java/com/fortanix/keyattestationstatementverifier/Verify.java b/key-attestation/java-example/src/main/java/com/fortanix/keyattestationstatementverifier/Verify.java index 64138ed..228e547 100644 --- a/key-attestation/java-example/src/main/java/com/fortanix/keyattestationstatementverifier/Verify.java +++ b/key-attestation/java-example/src/main/java/com/fortanix/keyattestationstatementverifier/Verify.java @@ -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; @@ -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(); @@ -86,9 +91,11 @@ public static void verify(List 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 { @@ -125,30 +132,40 @@ public static void verify(List authorityChain, X509Certificate public static void verify_cert_chain_signature(List 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 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.addCertPathChecker(rc); + pkixParams.setRevocationEnabled(verifyCrl); // 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"); } /** diff --git a/key-attestation/java-example/src/main/java/com/fortanix/keyattestationstatementverifier/certchecker/CertChecker.java b/key-attestation/java-example/src/main/java/com/fortanix/keyattestationstatementverifier/certchecker/CertChecker.java index 71719dd..070e4be 100644 --- a/key-attestation/java-example/src/main/java/com/fortanix/keyattestationstatementverifier/certchecker/CertChecker.java +++ b/key-attestation/java-example/src/main/java/com/fortanix/keyattestationstatementverifier/certchecker/CertChecker.java @@ -80,6 +80,7 @@ protected void verifyDistPoint(String errStrPrefix, X509Certificate cert, String errStrPrefix + " certificate cRLDistributionPoints extension wrong Name"); } + LOGGER.info("DONE"); } /** 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 17b92e4..1b43d81 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 @@ -3,18 +3,28 @@ import org.junit.Ignore; import org.junit.Test; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fortanix.keyattestationstatementverifier.types.json.KeyAttestationResponse; import static org.junit.Assert.*; +import java.io.BufferedReader; import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; import java.io.Reader; +import java.net.HttpURLConnection; import java.net.URL; import java.security.cert.X509Certificate; import java.util.List; +import java.util.UUID; + +import javax.net.ssl.HttpsURLConnection; 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"; @@ -88,7 +98,7 @@ public void verifyStatementFromJsonWithoutCrlCheck() throws Exception { */ @Ignore @Test - public void verifyStatementFullCheck() throws Exception { + public void verifyStatementFullCheckExample() throws Exception { String jsonPath = "Path to the KeyAttestationResponse json file"; Reader reader = new FileReader(jsonPath); ObjectMapper objectMapper = new ObjectMapper(); @@ -101,4 +111,87 @@ public void verifyStatementFullCheck() throws Exception { Verify.verify(decodedResponse, trustedRootCert, true); } + + /** + * This test tests the full process of creating a RSA key, get RSA key's key + * attestation statement and finally verify it. + * + * @throws Exception + */ + @Test + public void verifyStatementFullCheckOnlineAMER() throws Exception { + String appApiKeyString = System.getenv(JAVA_CI_AMER_APP_API_KEY); + String authString = "Basic " + appApiKeyString; + // create a key + String generateKeyUrl = FORTANIX_AMER_SAAS_SERVER_URL + "/crypto/v1/keys"; + String newRsaKeyName = UUID.randomUUID().toString(); + String generateKeyRequest = String.format( + "{\"name\":\"%s\",\"description\":\"\",\"obj_type\":\"RSA\",\"key_ops\":[\"APPMANAGEABLE\",\"SIGN\",\"VERIFY\"],\"key_size\":2048,\"pub_exponent\":65537,\"expirationDate\":null,\"enabled\":true,\"rsa\":{\"encryption_policy\":[{\"padding\":{\"OAEP\":{\"mgf\":{\"mgf1\":{}}}}}],\"signature_policy\":[{\"padding\":{\"PKCS1_V15\":{}}},{\"padding\":{\"PSS\":{\"mgf\":{\"mgf1\":{}}}}}]}}", + newRsaKeyName); + System.out.println( + String.format("Creating a new RSA key named '$s' through %s ...", newRsaKeyName, generateKeyUrl)); + String generateKeyResponseString = sendHttpRequest(generateKeyUrl, "POST", generateKeyRequest, authString); + ObjectMapper keyObjectMapper = new ObjectMapper(); + JsonNode jsonNode = keyObjectMapper.readTree(generateKeyResponseString); + String keyId = jsonNode.get("kid").asText(); + System.out.println(String.format("Created a new RSA key named '$s' with key id: %s", newRsaKeyName, keyId)); + + String getKeyAttestationUrl = FORTANIX_AMER_SAAS_SERVER_URL + "/crypto/v1/keys/key_attestation"; + String getKeyAttestationRequest = String.format("{\"key\":{\"kid\":\"%s\"}}", keyId); + System.out.println(String.format("Getting key attestation statement through $s ...", getKeyAttestationUrl)); + String keyAttestationResponseString = sendHttpRequest(getKeyAttestationUrl, "POST", getKeyAttestationRequest, authString); + System.out.println(String.format("Got key attestation statement")); + + // get the the key attestation statement of the key just created + ObjectMapper attestationObjectMapper = new ObjectMapper(); + KeyAttestationResponse decodedResponse = attestationObjectMapper.readValue(keyAttestationResponseString, KeyAttestationResponse.class); + + 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); + } + + public static String sendHttpRequest(String url, String method, String body, String authorization) + throws IOException { + URL apiUrl = new URL(url); + HttpsURLConnection connection = (HttpsURLConnection) apiUrl.openConnection(); + + try { + // Set the HTTP method + connection.setRequestMethod(method); + + // Set the Authorization header if provided + if (authorization != null && !authorization.isEmpty()) { + connection.setRequestProperty("Authorization", authorization); + } + + // Handle request body if provided + if (body != null && !body.isEmpty()) { + connection.setDoOutput(true); + connection.getOutputStream().write(body.getBytes("UTF-8")); + } + + int responseCode = connection.getResponseCode(); + if (responseCode >= 200 && responseCode < 300) { + // Successful response, read and return the response body + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + reader.close(); + return response.toString(); + } else { + // Non-2XX response, throw an exception + throw new IOException("HTTP request failed with response code: " + responseCode); + } + } finally { + connection.disconnect(); + } + } }