diff --git a/sgx-jvm/containers/core/Dockerfile b/sgx-jvm/containers/core/Dockerfile index 6a09dc5a1a..d110598548 100644 --- a/sgx-jvm/containers/core/Dockerfile +++ b/sgx-jvm/containers/core/Dockerfile @@ -67,7 +67,7 @@ ADD dependencies/proguard6.0beta1.tar.gz /usr/share/ # Expose ports for remote GDB and Java debugging, and test servers -EXPOSE 2000 5005 8080 9080 +EXPOSE 2000 5005 8080 8084 9080 # Environment diff --git a/sgx-jvm/remote-attestation/README-Phase2.md b/sgx-jvm/remote-attestation/README-Phase2.md new file mode 100644 index 0000000000..d86406105b --- /dev/null +++ b/sgx-jvm/remote-attestation/README-Phase2.md @@ -0,0 +1,107 @@ +Remote Attestation Phase 2 +========================== + +Phase 2 contains the following components: +- `ias-proxy`: This is the ISV, and is an authorised client of the Intel Attestation Service (IAS). +- `attestation-host`: This is a WAR running on an SGX-capable host with an enclave, and a client of `ias-proxy`. +- `attestation-challenger`: This is an executable JAR, and client of `attestation-host`. + +Building Instructions +--------------------- + +- Ensure that your user ID belongs to the `docker` group. This will enable you to run Docker as an unprivileged user. + +- Source the `environment` file: +```bash +$ . sgx-jvm/environment +``` + +- Build the Docker container: +```bash +$ cd sgx-jvm/containers/core +$ make +``` + +- Build the SGX SDK: +```bash +$ sx build linux-sgx clean all +``` + +- Build the SGX enclave: +```bash +$ sx build [-hp] remote-attestation/enclave clean all +``` +Add the `-hp` options to build in "pre-release" mode for SGX hardware. + +- Build the Attestation Host WAR: +```bash +$ sx build remote-attestation/attestation-host +``` + +- Build the JNI library for the Attestation Host: +```bash +$ sx build [-hp] remote-attestation/attestation-host/native clean all +``` +Add the `-hp` options to build in "pre-release" mode for SGX hardware. This setting +must match the setting used to build the SGX enclave, or they will be incompatible +at runtime. + +- Install our private key for Mutual-TLS with IAS: +```bash +$ cp client.key sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl +``` + +- Build the IAS Proxy: +```bash +$ cd sgx-jvm/remote-attestation +$ gradlew ias-proxy:build +``` + +- Build the Attestation Challenger: +```bash +$ cd sgx-jvm/remote-attestation +$ gradlew attestation-challenger:installDist +``` + +Execution Instructions +---------------------- + +- To launch the Attestation Host: +```bash +$ sx exec +$ cd sgx-jvm/remote-attestation/attestation-host +$ nohup ../gradlew [-Phardware=true] startHost >& OUT & +$ tail -f build/logs/attestation-host.log +``` +This can be shutdown again using: +```bash +$ ../gradlew stopHost +``` + +- To launch the IAS Proxy: +```bash +$ cd sgx-jvm/remote-attestation/ias-proxy +$ nohup ../gradlew startISV >& OUT & +$ tail -f build/logs/ias-proxy.log +``` +This can be shutdown again using: +```bash +$ ../gradlew stopISV +``` + +- To execute the Attestation Challenger: +```bash +$ cd sgx-jvm/remote-attestation/attestation-challenger/build/install/attestation-challenger +$ bin/attestation-challenger +``` +Use this executable's `--help` option for more information. + +When all of the components are working correctly, you should expect the challenger +to output something like: +```bash +$ bin/attestation-challenger +Report ID: 197283916372863387388037565359257649452 +Quote Status: OK +Timestamp: 2017-12-20T15:06:37.222956 +Secret provisioned successfully. +``` diff --git a/sgx-jvm/remote-attestation/README.md b/sgx-jvm/remote-attestation/README.md index 60ce64c6be..1ab70dd9f8 100644 --- a/sgx-jvm/remote-attestation/README.md +++ b/sgx-jvm/remote-attestation/README.md @@ -1,5 +1,7 @@ # Remote Attestation +![Flow between Challenger, Host, ISV and IAS](challenger-flow.png "Remote Attestation Flow") + ## Project Organisation * **Enclave** diff --git a/sgx-jvm/remote-attestation/attestation-challenger/build.gradle b/sgx-jvm/remote-attestation/attestation-challenger/build.gradle new file mode 100644 index 0000000000..7f7d6ebb8c --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-challenger/build.gradle @@ -0,0 +1,89 @@ +buildscript { + ext.keyStoreDir = "$buildDir/keystore" + ext.cli_version = '1.4' +} + +apply plugin: 'kotlin' +apply plugin: 'application' + +description 'Proof-of-concept Remote Attestation Challenger' + +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +sourceSets { + integrationTest { + kotlin { + compileClasspath += main.compileClasspath + test.compileClasspath + runtimeClasspath += main.runtimeClasspath + test.runtimeClasspath + //noinspection GroovyAssignabilityCheck + srcDir file('src/integration-test/kotlin') + } + } +} + +mainClassName = 'net.corda.attestation.challenger.Main' + +dependencies { + compile project(':attestation-common') + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + testCompile "junit:junit:$junit_version" + + compile "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version" + compile "com.fasterxml.jackson.core:jackson-core:$jackson_version" + compile "com.fasterxml.jackson.core:jackson-databind:$jackson_version" + compile "com.fasterxml.jackson.core:jackson-annotations:$jackson_version" + compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" + compile "org.apache.httpcomponents:httpclient:$httpclient_version" + compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + compile "org.apache.logging.log4j:log4j-core:$log4j_version" + compile "org.slf4j:jcl-over-slf4j:$slf4j_version" + compile "commons-cli:commons-cli:$cli_version" + testCompile project(path: ':attestation-common', configuration: 'testArtifacts') +} + +task createChallengerKeyStores(type: Exec) { + doFirst { + mkdir keyStoreDir + } + + inputs.dir "$projectDir/src/main/ssl/challenger" + outputs.files "$keyStoreDir/challenger.pfx" + workingDir keyStoreDir + commandLine "$projectDir/src/main/ssl/challenger/generate-keystore.sh" +} + +task createIntelKeyStores(type: Exec) { + doFirst { + mkdir keyStoreDir + } + + inputs.dir "$projectDir/src/main/ssl/intel" + outputs.files "$keyStoreDir/ias.pfx" + workingDir keyStoreDir + commandLine "$projectDir/src/main/ssl/intel/generate-keystores.sh" +} + +processResources { + dependsOn.addAll createChallengerKeyStores, createIntelKeyStores + from keyStoreDir +} + +tasks.withType(Test) { + // Enable "unlimited" encryption. + systemProperties["java.security.properties"] = "$projectDir/src/main/security.properties" +} + +jar { + manifest { + attributes( + 'Main-Class': mainClassName, + 'Class-Path': configurations.runtime.collect { it.getName() }.join(' '), + 'Automatic-Module-Name': 'net.corda.attestation.challenger' + ) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-challenger/src/main/kotlin/net/corda/attestation/challenger/AttestationResult.kt b/sgx-jvm/remote-attestation/attestation-challenger/src/main/kotlin/net/corda/attestation/challenger/AttestationResult.kt new file mode 100644 index 0000000000..464ac7be29 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-challenger/src/main/kotlin/net/corda/attestation/challenger/AttestationResult.kt @@ -0,0 +1,13 @@ +package net.corda.attestation.challenger + +import net.corda.attestation.message.ias.QuoteStatus +import java.security.interfaces.ECPublicKey +import java.time.LocalDateTime + +class AttestationResult( + val reportID: String, + val quoteStatus: QuoteStatus, + val peerPublicKey: ECPublicKey, + val platformInfo: ByteArray?, + val timestamp: LocalDateTime +) diff --git a/sgx-jvm/remote-attestation/attestation-challenger/src/main/kotlin/net/corda/attestation/challenger/Challenger.kt b/sgx-jvm/remote-attestation/attestation-challenger/src/main/kotlin/net/corda/attestation/challenger/Challenger.kt new file mode 100644 index 0000000000..d3531b868f --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-challenger/src/main/kotlin/net/corda/attestation/challenger/Challenger.kt @@ -0,0 +1,281 @@ +package net.corda.attestation.challenger + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.* +import net.corda.attestation.message.* +import net.corda.attestation.message.ias.ReportResponse +import org.apache.http.HttpStatus.* +import org.apache.http.client.config.CookieSpecs.* +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpPost +import org.apache.http.client.protocol.HttpClientContext +import org.apache.http.config.SocketConfig +import org.apache.http.entity.ContentType.APPLICATION_JSON +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.BasicCookieStore +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.bouncycastle.asn1.ASN1InputStream +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.DLSequence +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.URI +import java.nio.ByteBuffer +import java.security.* +import java.security.cert.* +import java.security.cert.Certificate +import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec +import java.util.* + +class Challenger( + private val keyPair: KeyPair, + private val enclaveHost: URI, + private val pkixParameters: PKIXParameters +) { + private companion object { + @JvmStatic + private val log: Logger = LoggerFactory.getLogger(Challenger::class.java) + + private const val AES_CMAC_FUNC = 1.toShort() + private const val tlvHeaderSize = 8 + + private val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setCookieSpec(STANDARD_STRICT) + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + private val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + } + + private val mapper = ObjectMapper().registerModule(JavaTimeModule()) + private val keyFactory: KeyFactory = KeyFactory.getInstance("EC") + private val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509") + private val transientKeyPair: KeyPair + private val ecParameters: ECParameterSpec + private val crypto = Crypto() + + private val cookies = BasicCookieStore() + + init { + ecParameters = (crypto.generateKeyPair().public as ECPublicKey).params + log.info("Elliptic Curve Parameters: {}", ecParameters) + + transientKeyPair = crypto.generateKeyPair() + } + + + fun attestToEnclave(): AttestationResult { + createHttpClient().use { client -> + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + // Send our public key, and receive the host's transient DH public key. + val challengeResponse: ChallengeResponse = try { + val challengeURI = enclaveHost.toString() + "/challenge" + log.info("Invoking host: {}", challengeURI) + + val challengeRequest = ChallengeRequest( + nonce = createNonce(), + gc = (keyPair.public as ECPublicKey).toLittleEndian() + ) + val httpRequest = HttpPost(challengeURI).apply { + entity = StringEntity(mapper.writeValueAsString(challengeRequest), APPLICATION_JSON) + } + client.execute(httpRequest, context).use { httpResponse -> + val statusCode = httpResponse.statusLine.statusCode + if (statusCode != SC_OK) { + throw ChallengerException("Challenge request to enclave failed (HTTP $statusCode)") + } + mapper.readValue(httpResponse.entity.content) + } + } catch (e: IOException) { + log.error("HTTP client error", e) + throw ChallengerException(e.message, e) + } + + val peerPublicKey = keyFactory.generatePublic(challengeResponse.ga.toBigEndianKeySpec(ecParameters)) as ECPublicKey + val smk = crypto.generateSMK(transientKeyPair.private, peerPublicKey) + + // Send our public key and signatures to the enclave. + val reportBody: ReportProxyResponse = try { + val attestURI = enclaveHost.toString() + "/attest" + log.info("Invoking host: {}", attestURI) + + val publicKey = transientKeyPair.public as ECPublicKey + val signatureGbGa = signatureOf(publicKey, peerPublicKey) + val gb = publicKey.toLittleEndian() + val signatureRequest = AttestationRequest( + gb = gb, + signatureGbGa = signatureGbGa, + aesCMAC = crypto.aesCMAC(smk, { aes -> + aes.update(gb) + aes.update(challengeResponse.spid.hexToBytes()) + aes.update(challengeResponse.quoteType.toLittleEndian()) + aes.update(AES_CMAC_FUNC.toLittleEndian()) + aes.update(signatureGbGa) + }) + ) + val httpRequest = HttpPost(attestURI).apply { + entity = StringEntity(mapper.writeValueAsString(signatureRequest), APPLICATION_JSON) + } + client.execute(httpRequest, context).use { httpResponse -> + val statusCode = httpResponse.statusLine.statusCode + if (statusCode != SC_OK) { + throw ChallengerException("Failed sending signatures to enclave (HTTP $statusCode)") + } + mapper.readValue(httpResponse.entity.content) + } + } catch (e: IOException) { + log.error("HTTP request error", e) + throw ChallengerException(e.message, e) + } + + // Check that this message really came from Intel. + validateSignature(reportBody) + + val reportResponse: ReportResponse = mapper.readValue(reportBody.report.inputStream()) + val platformInfo = reportResponse.platformInfoBlob?.removeHeader(tlvHeaderSize) + log.info("Attestation completed") + + // Successful response + return AttestationResult( + reportID = reportResponse.id, + quoteStatus = reportResponse.isvEnclaveQuoteStatus, + peerPublicKey = peerPublicKey, + platformInfo = platformInfo, + timestamp = reportResponse.timestamp + ) + } + } + + fun setSecret(secretValue: String, attestation: AttestationResult) { + val mk = crypto.generateMK(transientKeyPair.private, attestation.peerPublicKey) + val secretKey = crypto.generateSecretKey(transientKeyPair.private, attestation.peerPublicKey) + + try { + createHttpClient().use { client -> + val secretIV = crypto.createIV() + val secretData = crypto.encrypt(secretValue.toByteArray(), secretKey, secretIV) + val secretURI = enclaveHost.toString() + "/secret" + log.info("Invoking host: {}", secretURI) + + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val secretRequest = SecretRequest( + data = secretData.encryptedData(), + authTag = secretData.authenticationTag(), + iv = secretIV, + platformInfo = attestation.platformInfo, + aesCMAC = crypto.aesCMAC(mk, { aes -> + aes.update(attestation.platformInfo) + }) + ) + val httpRequest = HttpPost(secretURI).apply { + entity = StringEntity(mapper.writeValueAsString(secretRequest), APPLICATION_JSON) + } + client.execute(httpRequest, context).use { httpResponse -> + val statusCode = httpResponse.statusLine.statusCode + if (statusCode != SC_OK) { + throw ChallengerException("Failed sending secret to enclave (HTTP $statusCode)") + } + } + + log.info("Successfully provisioned to enclave") + } + } catch (e: IOException) { + log.error("HTTP client error", e) + throw ChallengerException(e.message, e) + } + } + + private fun createNonce(): String = UUID.randomUUID().let { uuid -> + String.format("%016x%016x", uuid.mostSignificantBits, uuid.leastSignificantBits) + } + + private fun createHttpClient(): CloseableHttpClient { + return HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { + socketConfig = httpSocketConfig + }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + private fun validateSignature(reportResponse: ReportProxyResponse) { + val certificatePath = try { + parseCertificates(reportResponse.certificatePath) + } catch (e: CertificateException) { + log.error("Failed to parse certificate from HTTP header '{}': {}", reportResponse.certificatePath, e.message) + throw e + } + + try { + val certValidator = CertPathValidator.getInstance("PKIX") + certValidator.validate(certificatePath, pkixParameters) + } catch (e: GeneralSecurityException) { + log.error("Certificate '{}' is invalid: {}", certificatePath, e.message) + throw e + } + + val signature = try { + Signature.getInstance("SHA256withRSA").apply { + initVerify(certificatePath.certificates[0]) + } + } catch (e: GeneralSecurityException) { + log.error("Failed to initialise signature: {}", e.message) + throw e + } + + try { + signature.update(reportResponse.report) + if (!signature.verify(reportResponse.signature.toByteArray().decodeBase64())) { + throw ChallengerException("Report failed IAS signature check") + } + } catch (e: SignatureException) { + log.error("Failed to parse signature from IAS: {}", e.message) + throw e + } + } + + private fun parseCertificates(iasCertificateHeader: String): CertPath { + val certificates = mutableListOf() + iasCertificateHeader.byteInputStream().use { input -> + while (input.available() > 0) { + certificates.add(certificateFactory.generateCertificate(input)) + } + } + return certificateFactory.generateCertPath(certificates) + } + + private fun signatureOf(publicKey: ECPublicKey, peerKey: ECPublicKey): ByteArray { + val signature = Signature.getInstance("SHA256WithECDSA").let { signer -> + signer.initSign(keyPair.private, crypto.random) + signer.update(publicKey.toLittleEndian()) + signer.update(peerKey.toLittleEndian()) + signer.sign() + } + return ByteBuffer.allocate(KEY_SIZE).let { buf -> + ASN1InputStream(signature).use { input -> + for (number in input.readObject() as DLSequence) { + val pos = (number as ASN1Integer).positiveValue.toLittleEndian(KEY_SIZE / 2) + buf.put(pos) + } + buf.array() + } + } + } + + private fun ByteArray.decodeBase64(): ByteArray = Base64.getDecoder().decode(this) + private fun ByteArray.removeHeader(headerSize: Int) = copyOfRange(headerSize, size) +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-challenger/src/main/kotlin/net/corda/attestation/challenger/ChallengerException.kt b/sgx-jvm/remote-attestation/attestation-challenger/src/main/kotlin/net/corda/attestation/challenger/ChallengerException.kt new file mode 100644 index 0000000000..55c3ec300f --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-challenger/src/main/kotlin/net/corda/attestation/challenger/ChallengerException.kt @@ -0,0 +1,5 @@ +package net.corda.attestation.challenger + +class ChallengerException(message: String?, cause: Throwable?) : Exception(message, cause) { + constructor(message: String) : this(message, null) +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-challenger/src/main/kotlin/net/corda/attestation/challenger/Main.kt b/sgx-jvm/remote-attestation/attestation-challenger/src/main/kotlin/net/corda/attestation/challenger/Main.kt new file mode 100644 index 0000000000..e74f8e480c --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-challenger/src/main/kotlin/net/corda/attestation/challenger/Main.kt @@ -0,0 +1,83 @@ +@file:JvmName("Main") +package net.corda.attestation.challenger + +import net.corda.attestation.message.ias.QuoteStatus +import org.apache.commons.cli.DefaultParser +import org.apache.commons.cli.HelpFormatter +import org.apache.commons.cli.Options +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.net.URI +import java.security.KeyPair +import java.security.KeyStore +import java.security.PrivateKey +import java.security.Security +import java.security.cert.PKIXRevocationChecker.Option.* +import java.security.cert.* +import java.util.* + +fun main(args: Array) { + val command = DefaultParser().parse(options, args) + if (command.hasOption("h")) { + HelpFormatter().printHelp("challenger", options) + return + } + + Security.addProvider(BouncyCastleProvider()) + + val keyStorePassword = command.getOptionValue("t", DEFAULT_PASSWORD).toCharArray() + val trustStorePassword = command.getOptionValue("k", DEFAULT_PASSWORD).toCharArray() + val challengerKeyPair = loadKeyStoreResource("challenger.pfx", keyStorePassword).getKeyPair("challenge", keyStorePassword) + + val iasStore = loadKeyStoreResource("ias.pfx", trustStorePassword) + val pkixParameters = PKIXParameters(iasStore.trustAnchorsFor("ias")).apply { + val rlChecker = CertPathValidator.getInstance("PKIX").revocationChecker as PKIXRevocationChecker + addCertPathChecker(rlChecker.apply { options = EnumSet.of(SOFT_FAIL) }) + } + + val hostname = command.getOptionValue("n", "localhost") + val port = command.getOptionValue("p", "8080") + val secretValue = command.getOptionValue("s", "And now for something very different indeed!") + + Challenger( + keyPair = challengerKeyPair, + enclaveHost = URI.create("http://$hostname:$port/host"), + pkixParameters = pkixParameters + ).apply { + val attestation = attestToEnclave() + println("Report ID: ${attestation.reportID}") + println("Quote Status: ${attestation.quoteStatus}") + println("Timestamp: ${attestation.timestamp}") + + if (attestation.quoteStatus == QuoteStatus.OK) { + setSecret(secretValue, attestation) + println("Secret provisioned successfully.") + } + } +} + +private const val DEFAULT_PASSWORD = "attestation" + +private val options = Options().apply { + addOption("t", "trustPassword", true, "Password for IAS trust store") + addOption("k", "keyPassword", true, "Password for Challenger's key store") + addOption("n", "hostname", true, "Hostname for Enclave/Host") + addOption("p", "port", true, "Port number for Enclave/Host") + addOption("s", "secret", true, "A secret string to be provisioned") + addOption("h", "help", false, "Displays usage") +} + +private fun loadKeyStoreResource(resourceName: String, password: CharArray, type: String = "PKCS12"): KeyStore { + return KeyStore.getInstance(type).apply { + Challenger::class.java.classLoader.getResourceAsStream(resourceName)?.use { input -> + load(input, password) + } + } +} + +private fun KeyStore.getKeyPair(alias: String, password: CharArray): KeyPair { + val privateKey = getKey(alias, password) as PrivateKey + return KeyPair(getCertificate(alias).publicKey, privateKey) +} + +private fun KeyStore.trustAnchorsFor(vararg aliases: String): Set + = aliases.map { alias -> TrustAnchor(getCertificate(alias) as X509Certificate, null) }.toSet() diff --git a/sgx-jvm/remote-attestation/attestation-challenger/src/main/resources/log4j2.xml b/sgx-jvm/remote-attestation/attestation-challenger/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..10ddd9b88b --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-challenger/src/main/resources/log4j2.xml @@ -0,0 +1,50 @@ + + + + + . + ${sys:attestation.home} + attestation-challenger + ${sys:log-path}/archive + info + debug + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sgx-jvm/remote-attestation/attestation-challenger/src/main/security.properties b/sgx-jvm/remote-attestation/attestation-challenger/src/main/security.properties new file mode 100644 index 0000000000..28b8893e70 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-challenger/src/main/security.properties @@ -0,0 +1 @@ +crypto.policy=unlimited diff --git a/sgx-jvm/remote-attestation/attestation-challenger/src/main/ssl/challenger/generate-keystore.sh b/sgx-jvm/remote-attestation/attestation-challenger/src/main/ssl/challenger/generate-keystore.sh new file mode 100755 index 0000000000..d04f1f4090 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-challenger/src/main/ssl/challenger/generate-keystore.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +STOREPASS=attestation +ALIAS=challenge + +openssl ecparam -name secp256r1 -genkey -noout -out privateKey.pem +openssl req -new -key privateKey.pem -x509 -out server.crt -days 1000 < aes.update(value) }) + + fun aesCMAC(key: ByteArray, update: (aes: Mac) -> Unit): ByteArray = Mac.getInstance("AESCMAC").let { aes -> + aes.init(SecretKeySpec(key, "AES")) + update(aes) + aes.doFinal() + } + + private fun createGCMParameters(iv: ByteArray) = GCMParameterSpec(gcmTagLength * 8, iv) + + fun createIV(): ByteArray = ByteArray(gcmIvLength).apply { random.nextBytes(this) } + + fun encrypt(data: ByteArray, secretKey: SecretKey, secretIV: ByteArray): ByteArray = Cipher.getInstance(AES_ALGORITHM).let { cip -> + cip.init(ENCRYPT_MODE, secretKey, createGCMParameters(secretIV), random) + cip.doFinal(data) + } + + @Suppress("UNUSED") + fun decrypt(data: ByteArray, secretKey: SecretKey, secretIV: ByteArray): ByteArray = Cipher.getInstance(AES_ALGORITHM).let { cip -> + cip.init(DECRYPT_MODE, secretKey, createGCMParameters(secretIV)) + cip.doFinal(data) + } + + fun generateSharedSecret(privateKey: PrivateKey, peerPublicKey: PublicKey): ByteArray { + return KeyAgreement.getInstance("ECDH").let { ka -> + ka.init(privateKey, random) + ka.doPhase(peerPublicKey, true) + ka.generateSecret() + } + } + + private fun generateKDK(sharedSecret: ByteArray) + = aesCMAC(ByteArray(macBlockSize), sharedSecret.reversedArray()).apply { log.debug("KDK: {}", toHexArrayString()) } + + private fun generateSMK(sharedSecret: ByteArray) + = aesCMAC(generateKDK(sharedSecret), smkValue).apply { log.debug("SMK: {}", toHexArrayString()) } + + fun generateSMK(privateKey: PrivateKey, peerPublicKey: PublicKey): ByteArray + = generateSMK(generateSharedSecret(privateKey, peerPublicKey)) + + private fun generateMK(sharedSecret: ByteArray) + = aesCMAC(generateKDK(sharedSecret), mkValue).apply { log.debug("MK: {}", toHexArrayString()) } + + fun generateMK(privateKey: PrivateKey, peerPublicKey: PublicKey): ByteArray + = generateMK(generateSharedSecret(privateKey, peerPublicKey)) + + private fun generateSK(sharedSecret: ByteArray) + = aesCMAC(generateKDK(sharedSecret), skValue).apply { log.debug("SK: {}", toHexArrayString()) } + + fun generateSecretKey(privateKey: PrivateKey, peerPublicKey: PublicKey): SecretKey + = SecretKeySpec(generateSK(generateSharedSecret(privateKey, peerPublicKey)), "AES") +} + +fun ByteArray.authenticationTag(): ByteArray = copyOfRange(size - Crypto.gcmTagLength, size) +fun ByteArray.encryptedData(): ByteArray = copyOf(size - Crypto.gcmTagLength) +fun ByteArray.toHexArrayString(): String = joinToString(prefix="[", separator=",", postfix="]", transform={ b -> String.format("0x%02x", b) }) diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/EndianUtils.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/EndianUtils.kt new file mode 100644 index 0000000000..3097b1a6e3 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/EndianUtils.kt @@ -0,0 +1,62 @@ +@file:JvmName("EndianUtils") +package net.corda.attestation + +import java.math.BigInteger +import java.nio.ByteBuffer +import java.nio.ByteOrder.LITTLE_ENDIAN +import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec +import java.security.spec.ECPoint +import java.security.spec.ECPublicKeySpec +import java.security.spec.KeySpec + +const val KEY_SIZE = 64 + +fun ByteArray.removeLeadingZeros(): ByteArray { + if (isEmpty() || this[0] != 0.toByte()) { return this } + for (i in 1 until size) { + if (this[i] != 0.toByte()) { + return copyOfRange(i, size) + } + } + return byteArrayOf() +} + +fun ByteArray.toPositiveInteger() = BigInteger(1, this) +fun ByteArray.toHexString(): String = toPositiveInteger().toString(16) + +fun BigInteger.toLittleEndian(size: Int = KEY_SIZE) = ByteArray(size).apply { + val leBytes = toByteArray().reversedArray() + System.arraycopy(leBytes, 0, this, 0, Math.min(size, leBytes.size)) +} + +fun BigInteger.toUnsignedBytes(): ByteArray = toByteArray().removeLeadingZeros() + +fun String.hexToBytes(): ByteArray = BigInteger(this, 16).toUnsignedBytes() + +fun ByteArray.toBigEndianKeySpec(ecParameters: ECParameterSpec): KeySpec { + if (size != KEY_SIZE) { + throw IllegalArgumentException("Public key has incorrect size ($size bytes)") + } + val ecPoint = ECPoint( + copyOf(size / 2).reversedArray().toPositiveInteger(), + copyOfRange(size / 2, size).reversedArray().toPositiveInteger() + ) + return ECPublicKeySpec(ecPoint, ecParameters) +} + +fun ECPublicKey.toLittleEndian(size: Int = KEY_SIZE): ByteArray { + val x = w.affineX.toByteArray().reversedArray() + val y = w.affineY.toByteArray().reversedArray() + return ByteArray(size).apply { + // Automatically discards any extra "most significant" last byte, which is assumed to be zero. + val half = size / 2 + System.arraycopy(x, 0, this, 0, Math.min(half, x.size)) + System.arraycopy(y, 0, this, half, Math.min(half, y.size)) + } +} + +fun Short.toLittleEndian(): ByteArray = ByteBuffer.allocate(2) + .order(LITTLE_ENDIAN) + .putShort(this) + .array() diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/JsonUtils.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/JsonUtils.kt new file mode 100644 index 0000000000..2a1e4f32c7 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/JsonUtils.kt @@ -0,0 +1,8 @@ +@file:JvmName("JsonUtils") +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import java.io.InputStream + +inline fun ObjectMapper.readValue(input: InputStream): T = readValue(input, T::class.java) +inline fun ObjectMapper.readValue(input: String): T = readValue(input, T::class.java) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/AttestationError.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/AttestationError.kt new file mode 100644 index 0000000000..dad91267f9 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/AttestationError.kt @@ -0,0 +1,7 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("message") +class AttestationError(@param:JsonProperty("message") val message: String) diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/AttestationRequest.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/AttestationRequest.kt new file mode 100644 index 0000000000..d27b36a0bf --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/AttestationRequest.kt @@ -0,0 +1,17 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("gb", "signatureGbGa", "aesCMAC") +class AttestationRequest( + // The challenger's public DH key (Little Endian) + @param:JsonProperty("gb") + val gb: ByteArray, + + @param:JsonProperty("signatureGbGa") + val signatureGbGa: ByteArray, + + @param:JsonProperty("aesCMAC") + val aesCMAC: ByteArray +) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ChallengeRequest.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ChallengeRequest.kt new file mode 100644 index 0000000000..7bd5065560 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ChallengeRequest.kt @@ -0,0 +1,17 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("gc", "nonce") +@JsonInclude(NON_NULL) +class ChallengeRequest( + // The challenger's permanent public key (Little Endian). + @param:JsonProperty("gc") + val gc: ByteArray, + + @param:JsonProperty("nonce") + val nonce: String +) diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ChallengeResponse.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ChallengeResponse.kt new file mode 100644 index 0000000000..44909dbb15 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ChallengeResponse.kt @@ -0,0 +1,17 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("ga", "spid", "quoteType") +class ChallengeResponse( + // The host's public DH key (Little Endian) + @param:JsonProperty("ga") + val ga: ByteArray, + + @param:JsonProperty("spid") + val spid: String, + + @param:JsonProperty("quoteType") + val quoteType: Short +) diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ReportProxyResponse.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ReportProxyResponse.kt new file mode 100644 index 0000000000..beb0e8ea34 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ReportProxyResponse.kt @@ -0,0 +1,19 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("signature", "certificatePath", "report") +@JsonInclude(NON_NULL) +class ReportProxyResponse( + @param:JsonProperty("signature") + val signature: String, + + @param:JsonProperty("certificatePath") + val certificatePath: String, + + @param:JsonProperty("report") + val report: ByteArray +) diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ReportRequest.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ReportRequest.kt new file mode 100644 index 0000000000..e6a392c723 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ReportRequest.kt @@ -0,0 +1,19 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("isvEnclaveQuote", "pseManifest", "nonce") +@JsonInclude(NON_NULL) +class ReportRequest( + @param:JsonProperty("isvEnclaveQuote") + val isvEnclaveQuote: ByteArray, + + @param:JsonProperty("pseManifest") + val pseManifest: ByteArray? = null, + + @param:JsonProperty("nonce") + val nonce: String? = null +) diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/SecretRequest.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/SecretRequest.kt new file mode 100644 index 0000000000..0454bda734 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/SecretRequest.kt @@ -0,0 +1,25 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("data", "authTag", "iv", "platformInfo", "aesCMAC") +@JsonInclude(NON_NULL) +class SecretRequest( + @param:JsonProperty("data") + val data: ByteArray, + + @param:JsonProperty("authTag") + val authTag: ByteArray, + + @param:JsonProperty("iv") + val iv: ByteArray, + + @param:JsonProperty("platformInfo") + val platformInfo: ByteArray? = null, + + @param:JsonProperty("aesCMAC") + val aesCMAC: ByteArray +) diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ServiceResponse.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ServiceResponse.kt new file mode 100644 index 0000000000..23a68aa6c4 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ServiceResponse.kt @@ -0,0 +1,13 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("spid", "quoteType") +class ServiceResponse( + @param:JsonProperty("spid") + val spid: String, + + @param:JsonProperty("quoteType") + val quoteType: Short +) diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ias/HexadecimalSerialisers.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ias/HexadecimalSerialisers.kt new file mode 100644 index 0000000000..ccc0ac55d7 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ias/HexadecimalSerialisers.kt @@ -0,0 +1,19 @@ +@file:JvmName("HexadecimalSerialisers") +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import net.corda.attestation.hexToBytes +import net.corda.attestation.toHexString + +class HexadecimalSerialiser : StdSerializer(ByteArray::class.java) { + override fun serialize(value: ByteArray, gen: JsonGenerator, provider: SerializerProvider) = gen.writeString(value.toHexString()) +} + +class HexadecimalDeserialiser : StdDeserializer(ByteArray::class.java) { + override fun deserialize(p: JsonParser, context: DeserializationContext) = p.valueAsString.hexToBytes() +} diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ias/ManifestStatus.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ias/ManifestStatus.kt new file mode 100644 index 0000000000..ec707be9cc --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ias/ManifestStatus.kt @@ -0,0 +1,10 @@ +package net.corda.attestation.message.ias + +enum class ManifestStatus { + OK, + UNKNOWN, + INVALID, + OUT_OF_DATE, + REVOKED, + RL_VERSION_MISMATCH +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ias/QuoteStatus.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ias/QuoteStatus.kt new file mode 100644 index 0000000000..c84d27fec8 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ias/QuoteStatus.kt @@ -0,0 +1,11 @@ +package net.corda.attestation.message.ias + +enum class QuoteStatus { + OK, + SIGNATURE_INVALID, + GROUP_REVOKED, + SIGNATURE_REVOKED, + KEY_REVOKED, + SIGRL_VERSION_MISMATCH, + GROUP_OUT_OF_DATE +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ias/ReportResponse.kt b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ias/ReportResponse.kt new file mode 100644 index 0000000000..bdff14718e --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/main/kotlin/net/corda/attestation/message/ias/ReportResponse.kt @@ -0,0 +1,58 @@ +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.annotation.JsonFormat +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import java.time.LocalDateTime + +@JsonPropertyOrder( + "nonce", + "id", + "timestamp", + "epidPseudonym", + "isvEnclaveQuoteStatus", + "isvEnclaveQuoteBody", + "pseManifestStatus", + "pseManifestHash", + "platformInfoBlob", + "revocationReason" +) +@JsonInclude(NON_NULL) +class ReportResponse( + @param:JsonProperty("id") + val id: String, + + @param:JsonProperty("isvEnclaveQuoteStatus") + val isvEnclaveQuoteStatus: QuoteStatus, + + @param:JsonProperty("isvEnclaveQuoteBody") + val isvEnclaveQuoteBody: ByteArray, + + @param:JsonProperty("platformInfoBlob") + @get:JsonSerialize(using = HexadecimalSerialiser::class) + @get:JsonDeserialize(using = HexadecimalDeserialiser::class) + val platformInfoBlob: ByteArray? = null, + + @param:JsonProperty("revocationReason") + val revocationReason: Int? = null, + + @param:JsonProperty("pseManifestStatus") + val pseManifestStatus: ManifestStatus? = null, + + @param:JsonProperty("pseManifestHash") + val pseManifestHash: String? = null, + + @param:JsonProperty("nonce") + val nonce: String? = null, + + @param:JsonProperty("epidPseudonym") + val epidPseudonym: ByteArray? = null, + + @param:JsonProperty("timestamp") + @field:JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS", timezone = "UTC") + val timestamp: LocalDateTime +) diff --git a/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/ByteUtils.kt b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/ByteUtils.kt new file mode 100644 index 0000000000..3a6ab4a563 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/ByteUtils.kt @@ -0,0 +1,14 @@ +@file:JvmName("ByteUtils") +package net.corda.attestation + +import java.util.* + +fun byteArray(size: Int, of: Int) = ByteArray(size, { of.toByte() }) + +fun unsignedByteArrayOf(vararg values: Int) = ByteArray(values.size).apply { + for (i in 0 until values.size) { + this[i] = values[i].toByte() + } +} + +fun ByteArray.toBase64(): String = Base64.getEncoder().encodeToString(this) diff --git a/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/CryptoProvider.kt b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/CryptoProvider.kt new file mode 100644 index 0000000000..7043d91d4c --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/CryptoProvider.kt @@ -0,0 +1,19 @@ +package net.corda.attestation + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.security.Security + +@Suppress("UNUSED") +class CryptoProvider : TestRule { + override fun apply(statement: Statement, description: Description?): Statement { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(BouncyCastleProvider()) + } + return statement + } + + val crypto: Crypto by lazy { Crypto() } +} diff --git a/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/CryptoTest.kt b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/CryptoTest.kt new file mode 100644 index 0000000000..2da7bb57e2 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/CryptoTest.kt @@ -0,0 +1,103 @@ +package net.corda.attestation + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.nio.charset.StandardCharsets.* +import java.security.KeyFactory +import java.security.interfaces.ECPublicKey + +class CryptoTest { + private lateinit var keyFactory: KeyFactory + private lateinit var crypto: Crypto + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Before + fun setup() { + keyFactory = KeyFactory.getInstance("EC") + crypto = cryptoProvider.crypto + } + + @Test + fun testKeyConversions() { + val keyPair = crypto.generateKeyPair() + val ecPublicKey = keyPair.public as ECPublicKey + val ecParameters = ecPublicKey.params + val bytes = ecPublicKey.toLittleEndian() + val resultKey = keyFactory.generatePublic(bytes.toBigEndianKeySpec(ecParameters)) as ECPublicKey + assertEquals(ecPublicKey, resultKey) + assertArrayEquals(ecPublicKey.encoded, resultKey.encoded) + } + + @Test + fun testSharedSecret() { + val keyPairA = crypto.generateKeyPair() + val keyPairB = crypto.generateKeyPair() + + val secretA = crypto.generateSharedSecret(keyPairA.private, keyPairB.public) + val secretB = crypto.generateSharedSecret(keyPairB.private, keyPairA.public) + assertArrayEquals(secretB, secretA) + assertEquals(32, secretA.size) + } + + @Test + fun testEncryption() { + val keyPairA = crypto.generateKeyPair() + val keyPairB = crypto.generateKeyPair() + val secretKeyA = crypto.generateSecretKey(keyPairA.private, keyPairB.public) + val secretKeyB = crypto.generateSecretKey(keyPairB.private, keyPairA.public) + assertEquals(secretKeyA, secretKeyB) + assertArrayEquals(secretKeyA.encoded, secretKeyB.encoded) + + val iv = crypto.createIV() + val data = crypto.encrypt("Sooper secret string value!".toByteArray(), secretKeyA, iv) + assertEquals("Sooper secret string value!", String(crypto.decrypt(data, secretKeyB, iv), UTF_8)) + } + + @Test + fun testAesCMAC() { + val messageBytes = "Hello World".toByteArray() + val keyBytes = "0123456789012345".toByteArray() + val cmac = crypto.aesCMAC(keyBytes, messageBytes) + assertArrayEquals("3AFAFFFC4EB9274ABD6C9CC3D8B6984A".hexToBytes(), cmac) + } + + @Test + fun testToHexArrayString() { + val bytes = unsignedByteArrayOf(0xf5, 0x04, 0x83, 0x71) + assertEquals("[0xf5,0x04,0x83,0x71]", bytes.toHexArrayString()) + } + + @Test + fun `test vectors from RFC4493 and NIST 800-38B`() { + // Key as defined on https://tools.ietf.org/html/rfc4493.html#appendix-A + val testKey = "2b7e151628aed2a6abf7158809cf4f3c".hexToBytes() + + // Example 1: len = 0 + val out0 = crypto.aesCMAC(testKey, ByteArray(0)) + assertArrayEquals("bb1d6929e95937287fa37d129b756746".hexToBytes(), out0) + + // Example 2: len = 16 + val out16 = crypto.aesCMAC(testKey, "6bc1bee22e409f96e93d7e117393172a".hexToBytes()) + assertArrayEquals("070a16b46b4d4144f79bdd9dd04a287c".hexToBytes(), out16) + + // Example 3: len = 40 + val messageBytes40 = ("6bc1bee22e409f96e93d7e117393172a" + + "ae2d8a571e03ac9c9eb76fac45af8e51" + + "30c81c46a35ce411").hexToBytes() + val out40 = crypto.aesCMAC(testKey, messageBytes40) + assertArrayEquals("dfa66747de9ae63030ca32611497c827".hexToBytes(), out40) + + // Example 4: len = 64 + val messageBytes64 = ("6bc1bee22e409f96e93d7e117393172a" + + "ae2d8a571e03ac9c9eb76fac45af8e51" + + "30c81c46a35ce411e5fbc1191a0a52ef" + + "f69f2445df4f9b17ad2b417be66c3710").hexToBytes() + val out64 = crypto.aesCMAC(testKey, messageBytes64) + assertArrayEquals("51f0bebf7e3b9d92fc49741779363cfe".hexToBytes(), out64) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/EndianUtilsTest.kt b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/EndianUtilsTest.kt new file mode 100644 index 0000000000..b9abd157df --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/EndianUtilsTest.kt @@ -0,0 +1,86 @@ +package net.corda.attestation + +import org.junit.Assert.* +import org.junit.Test +import sun.security.ec.ECPublicKeyImpl +import java.math.BigInteger +import java.security.KeyPairGenerator +import java.security.interfaces.ECPublicKey +import java.security.spec.ECGenParameterSpec +import java.security.spec.ECPoint + +class EndianUtilsTest { + @Test + fun testBigIntegerToLittleEndian() { + val source = BigInteger("8877665544332211", 16) + assertArrayEquals(unsignedByteArrayOf(0x11), source.toLittleEndian(1)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22), source.toLittleEndian(2)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33), source.toLittleEndian(3)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44), source.toLittleEndian(4)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44, 0x55), source.toLittleEndian(5)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44, 0x55, 0x66), source.toLittleEndian(6)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77), source.toLittleEndian(7)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88), source.toLittleEndian(8)) + } + + @Test + fun testRemovingLeadingZeros() { + assertArrayEquals(byteArrayOf(), byteArrayOf().removeLeadingZeros()) + assertArrayEquals(byteArrayOf(), byteArrayOf(0x00, 0x00, 0x00, 0x00).removeLeadingZeros()) + assertArrayEquals(byteArrayOf(0x7F, 0x63), byteArrayOf(0x00, 0x00, 0x7F, 0x63).removeLeadingZeros()) + assertArrayEquals(byteArrayOf(0x7F, 0x43), byteArrayOf(0x7F, 0x43).removeLeadingZeros()) + } + + @Test + fun testUnsignedBytes() { + val source = BigInteger("FFAA", 16) + assertArrayEquals(unsignedByteArrayOf(0xFF, 0xAA), source.toUnsignedBytes()) + } + + @Test + fun testToHexString() { + val source = unsignedByteArrayOf(0xFF, 0xAA, 0x00) + assertEquals("FFAA00", source.toHexString().toUpperCase()) + } + + @Test + fun testShortToLittleEndian() { + assertArrayEquals(unsignedByteArrayOf(0x0F, 0x00), 15.toShort().toLittleEndian()) + assertArrayEquals(unsignedByteArrayOf(0xFF, 0xFF), 65535.toShort().toLittleEndian()) + assertArrayEquals(unsignedByteArrayOf(0x00, 0x01), 256.toShort().toLittleEndian()) + } + + @Test + fun testLittleEndianPublicKey() { + val ecParameters = (KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1")) }.generateKeyPair().public as ECPublicKey).params + val publicKey = ECPublicKeyImpl(ECPoint(BigInteger.TEN, BigInteger.ONE), ecParameters) + + val littleEndian2 = publicKey.toLittleEndian(2) + assertArrayEquals(byteArrayOf(0x0A, 0x01), littleEndian2) + val littleEndian4 = publicKey.toLittleEndian(4) + assertArrayEquals(byteArrayOf(0x0A, 0x00, 0x01, 0x00), littleEndian4) + val littleEndian8 = publicKey.toLittleEndian(8) + assertArrayEquals(byteArrayOf(0x0A, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00), littleEndian8) + } + + @Test + fun testLittleEndianUnsigned() { + val veryBigNumber1 = BigInteger("FFFFFFFFEEEEEEEE", 16) + val veryBigNumber2 = BigInteger("AAAAAAAABBBBBBBB", 16) + val ecParameters = (KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1")) }.generateKeyPair().public as ECPublicKey).params + val publicKey = ECPublicKeyImpl(ECPoint(veryBigNumber1, veryBigNumber2), ecParameters) + + val littleEndian16 = publicKey.toLittleEndian(16) + assertArrayEquals(byteArray(4, 0xEE) + .plus(byteArray(4, 0xFF) + .plus(byteArray(4, 0xBB)) + .plus(byteArray(4, 0xAA))), littleEndian16) + } + + @Test + fun testLittleEndianUnderflow() { + val number = BigInteger("007FEDCB", 16) + val littleEndian = number.toLittleEndian(4) + assertArrayEquals(unsignedByteArrayOf(0xcb, 0xed, 0x7f, 0x00), littleEndian) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/KeyStoreProvider.kt b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/KeyStoreProvider.kt new file mode 100644 index 0000000000..87b827e71b --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/KeyStoreProvider.kt @@ -0,0 +1,36 @@ +package net.corda.attestation + +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.security.KeyPair +import java.security.KeyStore +import java.security.PrivateKey +import java.security.cert.TrustAnchor +import java.security.cert.X509Certificate + +@Suppress("UNUSED") +class KeyStoreProvider(private val storeName: String, private val storePassword: String) : TestRule { + private lateinit var keyStore: KeyStore + + private fun loadKeyStoreResource(resourceName: String, password: CharArray, type: String = "PKCS12"): KeyStore { + return KeyStore.getInstance(type).apply { + KeyStoreProvider::class.java.classLoader.getResourceAsStream(resourceName)?.use { input -> + load(input, password) + } + } + } + + override fun apply(statement: Statement, description: Description?): Statement { + keyStore = loadKeyStoreResource(storeName, storePassword.toCharArray()) + return statement + } + + fun getKeyPair(alias: String, password: String): KeyPair { + val privateKey = keyStore.getKey(alias, password.toCharArray()) as PrivateKey + return KeyPair(keyStore.getCertificate(alias).publicKey, privateKey) + } + + fun trustAnchorsFor(vararg aliases: String): Set + = aliases.map { alias -> TrustAnchor(keyStore.getCertificate(alias) as X509Certificate, null) }.toSet() +} diff --git a/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/AttestationErrorTest.kt b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/AttestationErrorTest.kt new file mode 100644 index 0000000000..49e95f3b81 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/AttestationErrorTest.kt @@ -0,0 +1,43 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class AttestationErrorTest { + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialise() { + val error = AttestationError("") + val str = mapper.writeValueAsString(error) + assertEquals("""{"message":""}""", str) + } + + @Test + fun testSerialiseEmpty() { + val request = AttestationError("") + val str = mapper.writeValueAsString(request) + assertEquals("""{"message":""}""", str) + } + + @Test + fun testDeserialise() { + val str = """{"message":""}""" + val error = mapper.readValue(str, AttestationError::class.java) + assertEquals("", error.message) + } + + @Test + fun testDeserialiseEmpty() { + val str = """{"message":""}""" + val error = mapper.readValue(str, AttestationError::class.java) + assertEquals("", error.message) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/AttestationRequestTest.kt b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/AttestationRequestTest.kt new file mode 100644 index 0000000000..9b2ea6a381 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/AttestationRequestTest.kt @@ -0,0 +1,45 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.toBase64 +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class AttestationRequestTest { + private companion object { + private val publicKeyData = byteArrayOf(0x3F, 0x2B, 0x52, 0x31) + private val publicKeyBase64 = publicKeyData.toBase64() + private val signatureData = byteArrayOf(0x31, 0x35, 0x5D, 0x1A, 0x27, 0x44) + private val signatureBase64 = signatureData.toBase64() + private val aesCMACData = byteArrayOf(0x7C, 0x62, 0x50, 0x2B, 0x47, 0x0E) + private val aesCMACBase64 = aesCMACData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialise() { + val attestation = AttestationRequest( + gb = publicKeyData, + signatureGbGa = signatureData, + aesCMAC = aesCMACData + ) + val str = mapper.writeValueAsString(attestation) + assertEquals("""{"gb":"$publicKeyBase64","signatureGbGa":"$signatureBase64","aesCMAC":"$aesCMACBase64"}""", str) + } + + @Test + fun testDeserialise() { + val str = """{"gb":"$publicKeyBase64","signatureGbGa":"$signatureBase64","aesCMAC":"$aesCMACBase64"}""" + val attestation = mapper.readValue(str, AttestationRequest::class.java) + assertArrayEquals(publicKeyData, attestation.gb) + assertArrayEquals(signatureData, attestation.signatureGbGa) + assertArrayEquals(aesCMACData, attestation.aesCMAC) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ChallengeRequestTest.kt b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ChallengeRequestTest.kt new file mode 100644 index 0000000000..d64fead726 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ChallengeRequestTest.kt @@ -0,0 +1,36 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.toBase64 +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class ChallengeRequestTest { + private companion object { + private val publicKeyData = byteArrayOf(0x1F, 0x0A, 0x22, 0x37) + private val publicKeyBase64 = publicKeyData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialise() { + val challenge = ChallengeRequest(publicKeyData, "") + val str = mapper.writeValueAsString(challenge) + assertEquals("""{"gc":"$publicKeyBase64","nonce":""}""", str) + } + + @Test + fun testDeserialise() { + val str = """{"gc":"$publicKeyBase64","nonce":""}""" + val challenge = mapper.readValue(str, ChallengeRequest::class.java) + assertArrayEquals(publicKeyData, challenge.gc) + assertEquals("", challenge.nonce) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ChallengeResponseTest.kt b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ChallengeResponseTest.kt new file mode 100644 index 0000000000..a9a351d431 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ChallengeResponseTest.kt @@ -0,0 +1,38 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.toBase64 +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class ChallengeResponseTest { + private companion object { + private const val SPID = "0123456789ABCDEF" + private val gaData = byteArrayOf(0x10, 0x00, 0x22, 0x00) + private val gaBase64 = gaData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialise() { + val challenge = ChallengeResponse(ga = gaData, spid = SPID, quoteType = 1) + val str = mapper.writeValueAsString(challenge) + assertEquals("""{"ga":"$gaBase64","spid":"$SPID","quoteType":1}""", str) + } + + @Test + fun testDeserialise() { + val str = """{"ga":"$gaBase64","spid":"$SPID","quoteType":1}""" + val challenge = mapper.readValue(str, ChallengeResponse::class.java) + assertArrayEquals(gaData, challenge.ga) + assertEquals(SPID, challenge.spid) + assertEquals(1.toShort(), challenge.quoteType) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ReportProxyResponseTest.kt b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ReportProxyResponseTest.kt new file mode 100644 index 0000000000..ccdf7808b9 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ReportProxyResponseTest.kt @@ -0,0 +1,49 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.toBase64 +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class ReportProxyResponseTest { + private companion object { + private val reportData = byteArrayOf(0x51, 0x62, 0x43, 0x24, 0x75, 0x4D) + private val reportBase64 = reportData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialiseBasic() { + val response = ReportProxyResponse( + signature = "", + certificatePath = "", + report = reportData + ) + val str = mapper.writeValueAsString(response) + assertEquals("{" + + "\"signature\":\"\"," + + "\"certificatePath\":\"\"," + + "\"report\":\"$reportBase64\"" + + "}", str) + } + + @Test + fun testDeserialiseBasic() { + val str = """{ + "signature":"", + "certificatePath":"", + "report":"$reportBase64" + }""" + val response = mapper.readValue(str, ReportProxyResponse::class.java) + assertEquals("", response.signature) + assertEquals("", response.certificatePath) + assertArrayEquals(reportData, response.report) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ReportRequestTest.kt b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ReportRequestTest.kt new file mode 100644 index 0000000000..ebb0b39677 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ReportRequestTest.kt @@ -0,0 +1,62 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.toBase64 +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class ReportRequestTest { + private companion object { + private val quoteData = byteArrayOf(0x41, 0x42, 0x43, 0x44, 0x45, 0x46) + private val quoteBase64 = quoteData.toBase64() + + private val manifestData = byteArrayOf(0x55, 0x72, 0x19, 0x5B) + private val manifestBase64 = manifestData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialise() { + val request = ReportRequest( + isvEnclaveQuote = quoteData, + pseManifest = manifestData, + nonce = "" + ) + val str = mapper.writeValueAsString(request) + assertEquals("""{"isvEnclaveQuote":"$quoteBase64","pseManifest":"$manifestBase64","nonce":""}""", str) + } + + @Test + fun testSerialiseEmpty() { + val request = ReportRequest( + isvEnclaveQuote = byteArrayOf() + ) + val str = mapper.writeValueAsString(request) + assertEquals("""{"isvEnclaveQuote":""}""", str) + } + + @Test + fun testDeserialise() { + val str = """{"isvEnclaveQuote":"$quoteBase64","pseManifest":"$manifestBase64","nonce":""}""" + val request = mapper.readValue(str, ReportRequest::class.java) + assertArrayEquals(quoteData, request.isvEnclaveQuote) + assertArrayEquals(manifestData, request.pseManifest) + assertEquals("", request.nonce) + } + + @Test + fun testDeserialiseQuoteOnly() { + val str = """{"isvEnclaveQuote":"$quoteBase64"}""" + val request = mapper.readValue(str, ReportRequest::class.java) + assertArrayEquals(quoteData, request.isvEnclaveQuote) + assertNull(request.pseManifest) + assertNull(request.nonce) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ServiceResponseTest.kt b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ServiceResponseTest.kt new file mode 100644 index 0000000000..297e0e91ec --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ServiceResponseTest.kt @@ -0,0 +1,37 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.toBase64 +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class ServiceResponseTest { + private companion object { + private const val SPID = "0123456789ABCDEF" + private val gaData = byteArrayOf(0x10, 0x00, 0x22, 0x00) + private val gaBase64 = gaData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialise() { + val service = ServiceResponse(spid = SPID, quoteType = 1) + val str = mapper.writeValueAsString(service) + assertEquals("""{"spid":"$SPID","quoteType":1}""", str) + } + + @Test + fun testDeserialise() { + val str = """{"spid":"$SPID","quoteType":1}""" + val service = mapper.readValue(str, ServiceResponse::class.java) + assertEquals(SPID, service.spid) + assertEquals(1.toShort(), service.quoteType) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ias/ReportResponseTest.kt b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ias/ReportResponseTest.kt new file mode 100644 index 0000000000..4b760fd5a4 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-common/src/test/kotlin/net/corda/attestation/message/ias/ReportResponseTest.kt @@ -0,0 +1,129 @@ +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.message.ias.ManifestStatus.* +import net.corda.attestation.message.ias.QuoteStatus.* +import net.corda.attestation.toBase64 +import net.corda.attestation.unsignedByteArrayOf +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class ReportResponseTest { + private companion object { + private val iso8601Time = "2017-11-08T18:19:27.123456" + private val testTimestamp = LocalDateTime.from(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(iso8601Time)) + + private val quoteBodyData = byteArrayOf(0x61, 0x62, 0x63, 0x64, 0x65, 0x66) + private val quoteBodyBase64 = quoteBodyData.toBase64() + + private val platformInfoData = unsignedByteArrayOf(0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef) + + private val pseudonymData = byteArrayOf(0x63, 0x18, 0x33, 0x72) + private val pseudonymBase64 = pseudonymData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper().registerModule(JavaTimeModule()) + } + + @Test + fun testSerialiseBasic() { + val response = ReportResponse( + id = "197283916372863387388037565359257649452", + isvEnclaveQuoteStatus = QuoteStatus.OK, + isvEnclaveQuoteBody = quoteBodyData, + timestamp = testTimestamp + ) + val str = mapper.writeValueAsString(response) + assertEquals("{" + + "\"id\":\"197283916372863387388037565359257649452\"," + + "\"timestamp\":\"$iso8601Time\"," + + "\"isvEnclaveQuoteStatus\":\"OK\"," + + "\"isvEnclaveQuoteBody\":\"$quoteBodyBase64\"" + + "}", str) + } + + @Test + fun testSerialiseFull() { + val response = ReportResponse( + id = "197283916372863387388037565359257649452", + isvEnclaveQuoteStatus = GROUP_OUT_OF_DATE, + isvEnclaveQuoteBody = quoteBodyData, + platformInfoBlob = platformInfoData, + revocationReason = 1, + pseManifestStatus = INVALID, + pseManifestHash = "", + nonce = "", + epidPseudonym = pseudonymData, + timestamp = testTimestamp + ) + val str = mapper.writeValueAsString(response) + assertEquals("{" + + "\"nonce\":\"\"," + + "\"id\":\"197283916372863387388037565359257649452\"," + + "\"timestamp\":\"$iso8601Time\"," + + "\"epidPseudonym\":\"$pseudonymBase64\"," + + "\"isvEnclaveQuoteStatus\":\"GROUP_OUT_OF_DATE\"," + + "\"isvEnclaveQuoteBody\":\"$quoteBodyBase64\"," + + "\"pseManifestStatus\":\"INVALID\"," + + "\"pseManifestHash\":\"\"," + + "\"platformInfoBlob\":\"123456789abcdef\"," + + "\"revocationReason\":1" + + "}", str) + } + + @Test + fun testDeserialiseBasic() { + val str = """{ + "id":"197283916372863387388037565359257649452", + "isvEnclaveQuoteStatus":"OK", + "isvEnclaveQuoteBody":"$quoteBodyBase64", + "timestamp":"$iso8601Time" + }""" + val response = mapper.readValue(str, ReportResponse::class.java) + assertEquals("197283916372863387388037565359257649452", response.id) + assertEquals(QuoteStatus.OK, response.isvEnclaveQuoteStatus) + assertArrayEquals(quoteBodyData, response.isvEnclaveQuoteBody) + assertNull(response.platformInfoBlob) + assertNull(response.revocationReason) + assertNull(response.pseManifestStatus) + assertNull(response.pseManifestHash) + assertNull(response.nonce) + assertNull(response.epidPseudonym) + assertEquals(testTimestamp, response.timestamp) + } + + @Test + fun testDeserialiseFull() { + val str = """{ + "id":"197283916372863387388037565359257649452", + "isvEnclaveQuoteStatus":"GROUP_OUT_OF_DATE", + "isvEnclaveQuoteBody":"$quoteBodyBase64", + "platformInfoBlob":"0123456789ABCDEF", + "revocationReason":1, + "pseManifestStatus":"OK", + "pseManifestHash":"", + "nonce":"", + "epidPseudonym":"$pseudonymBase64", + "timestamp":"$iso8601Time" + }""" + val response = mapper.readValue(str, ReportResponse::class.java) + assertEquals("197283916372863387388037565359257649452", response.id) + assertEquals(QuoteStatus.GROUP_OUT_OF_DATE, response.isvEnclaveQuoteStatus) + assertArrayEquals(quoteBodyData, response.isvEnclaveQuoteBody) + assertArrayEquals(platformInfoData, response.platformInfoBlob) + assertEquals(1, response.revocationReason) + assertEquals(ManifestStatus.OK, response.pseManifestStatus) + assertEquals("", response.pseManifestHash) + assertEquals("", response.nonce) + assertArrayEquals(pseudonymData, response.epidPseudonym) + assertEquals(testTimestamp, response.timestamp) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-host/Makefile b/sgx-jvm/remote-attestation/attestation-host/Makefile new file mode 100644 index 0000000000..d84f912332 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/Makefile @@ -0,0 +1,29 @@ +.PHONY: host host-native docs clean \ + unit-tests integration-tests + +# === GENERAL PARAMETERS ========================================================================== + +SHELL = /bin/bash +MAKEFILE_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +GRADLE_HOME ?= $(MAKEFILE_DIR)/../.gradle/ +GRADLE ?= $(MAKEFILE_DIR)/../gradlew -g $(GRADLE_HOME) + +# === PSEUDO TARGETS ============================================================================== + +host: + $(GRADLE) compileKotlin + +host-native: + make -C native all + +docs: + $(GRADLE) cleanDokka dokka + +unit-tests: + $(GRADLE) --rerun-tasks -q -Pdebug=$(DEBUG) test + +integration-tests: + $(GRADLE) --rerun-tasks -q -Pdebug=$(DEBUG) integrationTest + +clean: + $(GRADLE) clean diff --git a/sgx-jvm/remote-attestation/attestation-host/build.gradle b/sgx-jvm/remote-attestation/attestation-host/build.gradle new file mode 100644 index 0000000000..a76ce2205d --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/build.gradle @@ -0,0 +1,185 @@ +buildscript { + ext.keyStoreDir = "$buildDir/keystore" + ext.nativeBuildDir = "$projectDir/native/build" + ext.enclaveBuildDir = "$projectDir/../enclave/build" + + ext.hardware = project.hasProperty("hardware") && (ext.hardware == "1" || ext.hardware == "yes" || ext.hardware == "true") + ext.debug = project.hasProperty("debug") && (ext.debug == "1" || ext.debug == "yes" || ext.debug == "true") + + if (!project.hasProperty("debugPort")) { + ext.debugPort = 5005 + } else { + ext.debugPort = Integer.parseInt(ext.debugPort.toString()) + } + + if (ext.debug) { + ext.debugArgs = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,timeout=10000,address=$debugPort" + } else { + ext.debugArgs = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort" + } +} + +apply from: 'utilities.gradle' +apply plugin: 'kotlin' +apply plugin: 'war' +apply plugin: 'org.akhikhl.gretty' + +description 'Proof-of-Concept Remote Attestation Host' + +import org.akhikhl.gretty.AppStartTask +import org.akhikhl.gretty.AppStopTask + +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +sourceSets { + integrationTest { + kotlin { + compileClasspath += main.compileClasspath + test.compileClasspath + runtimeClasspath += main.runtimeClasspath + test.runtimeClasspath + //noinspection GroovyAssignabilityCheck + srcDir file('src/integration-test/kotlin') + } + } +} + +dependencies { + compile project(':attestation-common') + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + testCompile "junit:junit:$junit_version" + + compile "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version" + compile "org.jboss.resteasy:resteasy-jaxrs:$resteasy_version" + compile "org.jboss.resteasy:resteasy-jackson2-provider:$resteasy_version" + compile "org.jboss.resteasy:resteasy-servlet-initializer:$resteasy_version" + compile "com.fasterxml.jackson.core:jackson-core:$jackson_version" + compile "com.fasterxml.jackson.core:jackson-databind:$jackson_version" + compile "com.fasterxml.jackson.core:jackson-annotations:$jackson_version" + compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" + compile "org.apache.httpcomponents:httpclient:$httpclient_version" + compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + compile "org.apache.logging.log4j:log4j-core:$log4j_version" + runtime "org.apache.logging.log4j:log4j-web:$log4j_version" + compile "org.slf4j:jcl-over-slf4j:$slf4j_version" + + testCompile project(path: ':attestation-common', configuration: 'testArtifacts') +} + +task createMockKeyStores(type: Exec) { + doFirst { + mkdir keyStoreDir + } + + inputs.dir "$projectDir/src/main/ssl/mockisv" + outputs.files "$keyStoreDir/dummyIAS.pfx", "$keyStoreDir/dummyIAS-trust.pfx" + workingDir keyStoreDir + commandLine "$projectDir/src/main/ssl/mockisv/generate-keystores.sh" +} + +processResources { + dependsOn createMockKeyStores + from keyStoreDir +} + +tasks.withType(Test) { + // Enable "unlimited" encryption. + systemProperties["java.security.properties"] = "$projectDir/src/main/security.properties" + + // Location of JNI object. + systemProperties["java.library.path"] = nativeBuildDir + + // Location of enclave object. + systemProperties["corda.sgx.enclave.path"] = enclaveBuildDir + + // Allow us to connect to JVM within enclave + jvmArgs debugArgs +} + +task integrationTest(type: Test) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath + systemProperties["test.httpPort"] = testHttpPort +} + +gretty { + httpPort = testHttpPort + contextPath = "/" + servletContainer = 'tomcat8' + logDir = "$buildDir/logs" + logFileName = "gretty-test" + integrationTestTask = 'integrationTest' + jvmArgs = [ + "-Dorg.jboss.logging.provider=slf4j", + "-Djava.security.properties=$projectDir/src/main/security.properties", + "-Dattestation.home=$buildDir/logs", + "-Disv.host=localhost:$testHttpPort/mockisv", + "-Djava.library.path=$nativeBuildDir", + "-Dcorda.sgx.enclave.path=$enclaveBuildDir", + ] +} + +task('startHost', type: AppStartTask, dependsOn: war) { + prepareServerConfig { + httpPort = hostHttpPort + servletContainer = 'tomcat8' + logDir = "$buildDir/logs" + logFileName = "gretty-host" + jvmArgs = [ + "-Dorg.jboss.logging.provider=slf4j", + "-Djava.security.properties=$projectDir/src/main/security.properties", + "-Dattestation.home=$buildDir/logs", + "-Disv.host=localhost:$isvHttpPort", + "-Djava.library.path=$nativeBuildDir", + "-Dcorda.sgx.enclave.path=$enclaveBuildDir", + ] + } + + prepareWebAppConfig { + contextPath = "/" + inplace = false + } + + interactive = false +} + +task("stopHost", type: AppStopTask) + +task cleanEnclave(type: Exec) { + commandLine containerArgs("enclave", "clean") +} + +task cleanJniLibrary(type: Exec) { + commandLine containerArgs("attestation-host/native", "clean") +} + +clean.dependsOn.addAll cleanEnclave, cleanJniLibrary + +task buildEnclave(type: Exec) { + commandLine containerArgs("enclave", "all") +} + +task buildJniLibrary(type: Exec, dependsOn: [buildEnclave, classes]) { + commandLine containerArgs("attestation-host/native", "all") +} + +build.dependsOn.addAll buildJniLibrary + +task runUnitTestsInContainer(type: Task, dependsOn: buildJniLibrary) { + doLast { containerDebugWait(projectDir, "attestation-host", "unit-tests") } +} + +task runIntegrationTestsInContainer(type: Task, dependsOn: buildJniLibrary) { + doLast { containerDebugWait(projectDir, "attestation-host", "integration-tests") } +} + +task debugUnitTestsInContainer(type: Task, dependsOn: buildJniLibrary) { + doLast { containerDebugWait(projectDir, "attestation-host", "DEBUG=1", "unit-tests") } +} + +task debugIntegrationTestsInContainer(type: Task, dependsOn: buildJniLibrary) { + doLast { containerDebugWait(projectDir, "attestation-host", "DEBUG=1", "integration-tests") } +} diff --git a/sgx-jvm/remote-attestation/attestation-host/native/.gitignore b/sgx-jvm/remote-attestation/attestation-host/native/.gitignore new file mode 100644 index 0000000000..8762f1c2da --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/native/.gitignore @@ -0,0 +1 @@ +wrapper.hpp diff --git a/sgx-jvm/remote-attestation/attestation-host/native/Makefile b/sgx-jvm/remote-attestation/attestation-host/native/Makefile new file mode 100644 index 0000000000..aefb00000d --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/native/Makefile @@ -0,0 +1,115 @@ +.PHONY: all clean + +# === GENERAL PARAMETERS ========================================================================== + +SHELL = /bin/bash +MAKEFILE_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +MODE ?= DEBUG # or RELEASE + +GRADLE_FILE = $(MAKEFILE_DIR)/../build.gradle +NAME = corda_sgx_ra +VERSION := $(shell sed -n "s/^version = '\([^']*\)'.*/\\1/p" $(GRADLE_FILE)) +PLATFORM := $(shell uname -s | tr [:upper:] [:lower:]) + +OUT_DIR = $(MAKEFILE_DIR)/build +OBJ_DIR = $(MAKEFILE_DIR)/obj + +HOST_LIBRARY = $(OUT_DIR)/lib$(NAME).so + +# === BUILD PARAMETERS ============================================================================ + +ifeq ($(PLATFORM),linux) + JDK_HOME ?= $(dir $(word 1,$(wildcard /usr/lib/jvm/*/))) + JDK_INC_DIRS = -I$(JDK_HOME)include -I$(JDK_HOME)include/linux +endif + +LOGGING_DEFS = +ifeq ($(LOGGING),TRUE) + LOGGING_DEFS = -DLOGGING +endif + +CPP = g++ + +CPPFLAGS_BASE = $(JDK_INC_DIRS) -Wall -fPIC \ + $(SGX_DEFS) $(LOGGING_DEFS) +CPPFLAGS_DEBUG = $(CPPFLAGS_BASE) -g -O0 -DDEBUG +CPPFLAGS_RELEASE = $(CPPFLAGS_BASE) -s -DNDEBUG + +LDFLAGS_BASE = \ + -shared \ + -Wl,-soname,lib$(NAME).so \ + -Wl,-rpath,$(SGX_LIB_DIR):../../linux-sgx/build/linux \ + -Wl,-z,defs \ + -Wl,--no-as-needed \ + -L$(SGX_LIB_DIR) \ + -l$(URTS_LIB) \ + -l$(CAPABLE_LIB) \ + -l$(UAE_SERVICE_LIB) \ + -lpthread +LDFLAGS_DEBUG = $(LDFLAGS_BASE) +LDFLAGS_RELEASE = $(LDFLAGS_BASE) -s + +# === SGX-SPECIFIC BUILD PARAMETERS =============================================================== + +SGX_SDK := $(MAKEFILE_DIR)/../../../linux-sgx +include $(MAKEFILE_DIR)/../../enclave/sgx.mk + +# === MODE-SPECIFIC BUILD PARAMETERS ============================================================== + +ifeq ($(subst release,RELEASE,$(MODE)),RELEASE) + CPPFLAGS = $(CPPFLAGS_RELEASE) $(SGX_CPPFLAGS_RELEASE) + LDFLAGS = $(LDFLAGS_RELEASE) +else + CPPFLAGS = $(CPPFLAGS_DEBUG) $(SGX_CPPFLAGS_DEBUG) + LDFLAGS = $(LDFLAGS_DEBUG) +endif + +# === PSEUDO TARGETS ============================================================================== + +all: $(HOST_LIBRARY) + +clean: + @rm -f $(JNI_HEADERS) + @$(RM) -rf $(OUT_DIR) + @$(RM) -rf $(OBJ_DIR) + +# === HOST ======================================================================================== + +HOST_SOURCES = $(wildcard *.cpp) +HOST_OBJECTS = $(addprefix $(OBJ_DIR)/, $(HOST_SOURCES:.cpp=.o)) + +ENCLAVE_PROJECT = $(MAKEFILE_DIR)/../../enclave +ENCLAVE_OBJECTS = $(ENCLAVE_PROJECT)/obj/enclave_u.o +ENCLAVE_INC_DIR = $(MAKEFILE_DIR)/../../enclave/rpc + +JNI_HEADERS = wrapper.hpp +JNI_SOURCES = $(JNI_HEADERS:.hpp=.cpp) +JNI_OBJECTS = $(addprefix $(OBJ_DIR)/, $(JNI_SOURCES:.cpp=.o)) +CLASSPATH = $(MAKEFILE_DIR)/../build/classes/kotlin/main + +$(HOST_LIBRARY): $(HOST_OBJECTS) $(ENCLAVE_OBJECTS) $(JNI_OBJECTS) | $(OUT_DIR) + $(CPP) $(LDFLAGS) -o $@ $^ \ + $(SGX_LIB_DIR)/lib$(UKEY_EXCHNG).a + +$(HOST_OBJECTS): $(HOST_SOURCES) | $(OBJ_DIR) + +$(ENCLAVE_OBJECTS): + make -C $(ENCLAVE_PROJECT) obj-untrusted + +$(JNI_OBJECTS): $(JNI_SOURCES) | $(JNI_HEADERS) + +$(JNI_HEADERS): | $(JNI_SOURCES) + javah -o $@ -cp $(CLASSPATH) net.corda.attestation.host.sgx.bridge.wrapper.NativeWrapper + +$(OBJ_DIR)/%.o: %.cpp + @mkdir -p $(@D) + $(CPP) $(CPPFLAGS) -I$(ENCLAVE_INC_DIR) -o $@ -c $< + +# === BUILD DIRECTORIES =========================================================================== + +$(OUT_DIR): + @mkdir -p $(OUT_DIR) + +$(OBJ_DIR): + @mkdir -p $(OBJ_DIR) diff --git a/sgx-jvm/remote-attestation/attestation-host/native/enclave-manager.cpp b/sgx-jvm/remote-attestation/attestation-host/native/enclave-manager.cpp new file mode 100644 index 0000000000..11ef5365ae --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/native/enclave-manager.cpp @@ -0,0 +1,95 @@ +#include + +#include +#include +#include + +#include "enclave-manager.hpp" +#include "logging.hpp" + +// Instantiate a new enclave from a signed enclave binary, and return the +// identifier of the instance. +sgx_enclave_id_t create_enclave( + const char *path, + bool use_platform_services, + sgx_status_t *result, + sgx_launch_token_t *token +) { + int updated = 0; // Indication of whether the launch token was updated. + sgx_enclave_id_t enclave_id = 0; // The identifier of the created enclave. + + // If the launch token is empty, then create a new enclave. Otherwise, try + // to re-activate the existing enclave. `SGX_DEBUG_FLAG` is automatically + // set to 1 in debug mode, and 0 in release mode. + sgx_status_t status = sgx_create_enclave( + path, SGX_DEBUG_FLAG, token, &updated, &enclave_id, NULL + ); + + LOG(enclave_id, status, 0, "sgx_create_enclave()"); + + // Store the return value of the operation. + if (NULL != result) { + *result = status; + } + + // Return the identifier of the created enclave. Remember that if `status` + // is `SGX_ERROR_ENCLAVE_LOST`, the enclave should be destroyed and then + // re-created. + return (SGX_SUCCESS == status) ? enclave_id : 0; +} + +// Destroy enclave if currently loaded. +bool destroy_enclave(sgx_enclave_id_t enclave_id) { + if (enclave_id != 0){ + // Attempt to destroy the enclave if we are provided with a valid + // enclave identifier. + sgx_status_t status = sgx_destroy_enclave(enclave_id); + + LOG(enclave_id, status, 0, "sgx_destroy_enclave()"); + + return SGX_SUCCESS == status; + } + return false; +} + +// Check the status of the SGX device on the current machine. +sgx_device_status_t get_device_status(void) { +#if SGX_SIM == 1 +#pragma message "get_device_status() is being simulated" + // If in simulation mode, simulate device capabilities. + return SGX_ENABLED; +#endif + + // Try to retrieve the current status of the SGX device. + sgx_device_status_t status; + sgx_status_t ret = sgx_cap_enable_device(&status); + + LOG(0, ret, 0, "sgx_cap_enable_device() = { status = %x }", status); + + if (SGX_SUCCESS != ret) { + return SGX_DISABLED; + } + + return status; +} + +// Report which extended Intel EPID Group the client uses by default. +uint32_t get_extended_group_id(sgx_status_t *result) { + uint32_t egid; + + // The extended EPID group identifier is indicative of which attestation + // service the client is supposed to be communicating with. Currently, only + // a value of zero is supported, which is referring to Intel. The user + // should verify the retrieved extended group identifier, as any other + // value than zero will be disregarded by the service provider. + sgx_status_t status = sgx_get_extended_epid_group_id(&egid); + + LOG(0, status, 0, "sgx_get_extended_epid_group_id() = %u", egid); + + // Store the return value of the operation. + if (NULL != result) { + *result = status; + } + + return egid; +} diff --git a/sgx-jvm/remote-attestation/attestation-host/native/enclave-manager.hpp b/sgx-jvm/remote-attestation/attestation-host/native/enclave-manager.hpp new file mode 100644 index 0000000000..81396b6ccb --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/native/enclave-manager.hpp @@ -0,0 +1,55 @@ +#ifndef __ENCLAVE_MANAGER_H__ +#define __ENCLAVE_MANAGER_H__ + +#include +#include + +/** + * Instantiate a new enclave from a signed enclave binary, and return the + * identifier of the instance. + * + * @param path The file name of the signed enclave binary to load. + * @param use_platform_services If true, Intel's platform services are used to + * add extra protection against replay attacks during nonce generation and to + * provide a trustworthy monotonic counter. + * @param result Variable receiving the result of the operation, if not NULL. + * @param token Pointer to launch token; cannot be NULL. + * + * @return The identifier of the created enclave. + */ +sgx_enclave_id_t create_enclave( + const char *path, + bool use_platform_services, + sgx_status_t *result, + sgx_launch_token_t *token +); + +/** + * Destroy enclave if currently loaded. + * + * @param enclave_id The identifier of the enclave to destroy. + * + * @return True if the enclave was active and got destroyed. False otherwise. + */ +bool destroy_enclave( + sgx_enclave_id_t enclave_id +); + +/** + * Check the status of the SGX device on the current machine. + */ +sgx_device_status_t get_device_status(void); + +/** + * Report which extended Intel EPID Group the client uses by default. The key + * used to sign a Quote will be a member of the extended EPID Group reported in + * this API. The application will typically use this value to tell the ISV + * Service Provider which group to use during remote attestation. + * + * @param result Variable receiving the result of the operation, if not NULL. + * + * @return The extended EPID group identifier. + */ +uint32_t get_extended_group_id(sgx_status_t *result); + +#endif /* __ENCLAVE_MANAGER_H__ */ diff --git a/sgx-jvm/remote-attestation/attestation-host/native/jni.hpp b/sgx-jvm/remote-attestation/attestation-host/native/jni.hpp new file mode 100644 index 0000000000..1de7aa4f1f --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/native/jni.hpp @@ -0,0 +1,13 @@ +#ifndef __JNI_HPP__ +#define __JNI_HPP__ + +#include + +#define NATIVE_WRAPPER(return_type, method) \ + JNIEXPORT return_type JNICALL \ + Java_net_corda_attestation_host_sgx_bridge_wrapper_NativeWrapper_##method + +#define KLASS(name) \ + ("net/corda/attestation/host/sgx/bridge/wrapper/" name) + +#endif /* __JNI_HPP__ */ diff --git a/sgx-jvm/remote-attestation/attestation-host/native/logging.cpp b/sgx-jvm/remote-attestation/attestation-host/native/logging.cpp new file mode 100644 index 0000000000..a798565038 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/native/logging.cpp @@ -0,0 +1,32 @@ +#include + +#include "logging.hpp" + +void log( + sgx_enclave_id_t enclave_id, + sgx_status_t status, + sgx_ra_context_t context, + const char *message, + ... +) { + char mode[4] = { 0 }; + mode[0] = (SGX_SIM == 0) ? 'H' : 'S'; + mode[1] = (SGX_DEBUG == 0) ? 'R' : 'D'; + mode[2] = (SGX_PRERELEASE == 0) ? 'x' : 'P'; + mode[3] = 0; + + char buffer[1024]; + va_list args; + va_start(args, message); + vsnprintf(buffer, sizeof(buffer), message, args); + va_end(args); + + printf( + "SGX(id=%lx,status=%x,ctx=%u,mode=%s): %s\n", + (uint64_t)enclave_id, + (uint32_t)status, + (uint32_t)context, + mode, + buffer + ); +} diff --git a/sgx-jvm/remote-attestation/attestation-host/native/logging.hpp b/sgx-jvm/remote-attestation/attestation-host/native/logging.hpp new file mode 100644 index 0000000000..cc24228edc --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/native/logging.hpp @@ -0,0 +1,32 @@ +#ifndef __LOGGING_HPP__ +#define __LOGGING_HPP__ + +#include + +#include +#include + +#ifdef LOGGING +#define LOG(enclave_id, status, context, message, ...) \ + log(enclave_id, (sgx_status_t)(status), context, message, ##__VA_ARGS__) +#else +#define LOG(enclave_id, status, context, message, ...) ; +#endif + +/** + * Log message to standard output. + * + * @param enclave_id The enclave identifier. + * @param status The outcome of the last SGX operation. + * @param context The remote attestation context. + * @param message The message. + */ +void log( + sgx_enclave_id_t enclave_id, + sgx_status_t status, + sgx_ra_context_t context, + const char *message, + ... +); + +#endif /* __LOGGING_HPP__ */ diff --git a/sgx-jvm/remote-attestation/attestation-host/native/remote-attestation.cpp b/sgx-jvm/remote-attestation/attestation-host/native/remote-attestation.cpp new file mode 100644 index 0000000000..e25354aca9 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/native/remote-attestation.cpp @@ -0,0 +1,285 @@ +#include +#include + +#include + +#include "enclave_u.h" +#include "logging.hpp" +#include "remote-attestation.hpp" + +// Initialize the remote attestation. +sgx_status_t initialize_remote_attestation( + // Inputs + sgx_enclave_id_t enclave_id, + bool use_platform_services, + sgx_ec256_public_t *key_challenger, + + // Outputs + sgx_ra_context_t *context +) { + sgx_status_t ret; + + // Perform ECALL into the application enclave to initialize the remote + // attestation. The resulting attestation context will be stored in the + // variable referenced by the `context` parameter. + sgx_status_t _ret = initializeRemoteAttestation( + enclave_id, &ret, use_platform_services, key_challenger, context + ); + + LOG(enclave_id, _ret | ret, *context, "initialize_remote_attestation()"); + + // If the ECALL itself failed, report why. Otherwise, return the status of + // the underlying function call. + return (SGX_SUCCESS != _ret) ? _ret : ret; +} + +// Clean up and finalize the remote attestation process. +sgx_status_t finalize_remote_attestation( + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context +) { + sgx_status_t ret; + + // Perform ECALL into the application enclave to close the current + // attestation context and tidy up. + sgx_status_t _ret = finalizeRemoteAttestation(enclave_id, &ret, context); + + LOG(enclave_id, _ret | ret, context, "finalize_remote_attestation()"); + + // If the ECALL itself failed, report why. Otherwise, return the status of + // the underlying function call. + return (SGX_SUCCESS != _ret) ? _ret : ret; +} + +// Retrieve the application enclave's public key and the platform's group +// identifier. +sgx_status_t get_public_key_and_group_identifier( + // Inputs + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context, + + // Outputs + sgx_ec256_public_t *public_key, + sgx_epid_group_id_t *group_id, + + // Retry logic + int max_retry_count, + unsigned int retry_wait_in_secs +) { + sgx_status_t ret; + sgx_ra_msg1_t message; + + // It is generally recommended that the caller should wait (typically + // several seconds to tens of seconds) and retry `sgx_ra_get_msg1()` if + // `SGX_ERROR_BUSY` is returned. + int retry_count = max_retry_count; + + while (retry_count-- >= 0) { + // Using an ECALL proxy to `sgx_ra_get_ga()` in the `sgx_tkey_exchange` + // library to retrieve the public key of the application enclave. + ret = sgx_ra_get_msg1(context, enclave_id, sgx_ra_get_ga, &message); + + LOG(enclave_id, ret, context, "sgx_ra_get_msg1()"); + + if (SGX_ERROR_BUSY == ret) { + // Wait before retrying... + sleep(retry_wait_in_secs); + } else if (SGX_SUCCESS != ret) { + return ret; + } else { + break; + } + } + + // Store the public key; components X and Y, each 256 bits long. + if (NULL != public_key) { + memcpy(public_key, &message.g_a, sizeof(sgx_ec256_public_t)); + } + + // Store the EPID group identifier. Note, this is not the same as the + // extended group identifier. + if (NULL != group_id) { + memcpy(group_id, &message.gid, sizeof(sgx_epid_group_id_t)); + } + + return ret; +} + +// Process details received from challenger via the service provider, and +// generate quote. +sgx_status_t process_challenger_details_and_generate_quote( + // Inputs + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context, + sgx_ec256_public_t *challenger_public_key, + sgx_spid_t *service_provider_id, + uint16_t quote_type, + uint16_t key_derivation_function, + sgx_ec256_signature_t *signature, + sgx_mac_t *challenger_mac, + uint32_t revocation_list_size, + uint8_t *revocation_list, + + // Outputs + sgx_mac_t *enclave_mac, + sgx_ec256_public_t *enclave_public_key, + sgx_ps_sec_prop_desc_t *security_properties, + uint8_t **quote, + size_t *quote_size, + + // Retry logic + int max_retry_count, + unsigned int retry_wait_in_secs +) { + sgx_status_t ret = SGX_SUCCESS; + size_t msg_in_size = sizeof(sgx_ra_msg2_t) + revocation_list_size; + sgx_ra_msg2_t *msg_in = (sgx_ra_msg2_t*)malloc(msg_in_size); + sgx_ra_msg3_t *msg_out = NULL; + uint32_t msg_out_size; + + if (NULL == msg_in) { + return SGX_ERROR_OUT_OF_MEMORY; + } + + // Populate input message (message 2 in the Intel attestation flow). + memcpy(&msg_in->g_b, challenger_public_key, sizeof(sgx_ec256_public_t)); + memcpy(&msg_in->spid, service_provider_id, sizeof(sgx_spid_t)); + msg_in->quote_type = quote_type; + msg_in->kdf_id = key_derivation_function; + memcpy(&msg_in->sign_gb_ga, signature, sizeof(sgx_ec256_signature_t)); + memcpy(&msg_in->mac, challenger_mac, sizeof(sgx_mac_t)); + msg_in->sig_rl_size = revocation_list_size; + if (revocation_list_size > 0) { + memcpy(&msg_in->sig_rl, revocation_list, revocation_list_size); + } + + // Nullify outputs. + *quote = NULL; + + // It is generally recommended that the caller should wait (typically + // several seconds to tens of seconds) and retry `sgx_ra_proc_msg2()` if + // `SGX_ERROR_BUSY` is returned. + int retry_count = max_retry_count; + + while (retry_count-- >= 0) { + // Using an ECALL proxy to `sgx_ra_proc_msg2_trusted()` in the + // `sgx_tkey_exchange` library to process the incoming details from the + // challenger, and `sgx_ra_get_msg3_trusted()` in the same library to + // generate the quote. + ret = sgx_ra_proc_msg2( + context, + enclave_id, + sgx_ra_proc_msg2_trusted, + sgx_ra_get_msg3_trusted, + msg_in, + sizeof(sgx_ra_msg2_t) + revocation_list_size, + &msg_out, + &msg_out_size + ); + + LOG(enclave_id, ret, context, "sgx_ra_proc_msg2()"); + + if (SGX_ERROR_BUSY == ret) { + // Wait before retrying... + sleep(retry_wait_in_secs); + } else { + break; + } + } + + // Populate outputs from the returned message structure. + if (NULL != msg_out) { + memcpy(enclave_mac, &msg_out->mac, sizeof(sgx_mac_t)); + memcpy(enclave_public_key, &msg_out->g_a, sizeof(sgx_ec256_public_t)); + + size_t z_sec_prop = sizeof(sgx_ps_sec_prop_desc_t); + memcpy(security_properties, &msg_out->ps_sec_prop, z_sec_prop); + } + + // Populate the quote structure. + if (NULL != msg_out) { + *quote_size = msg_out_size - offsetof(sgx_ra_msg3_t, quote); + *quote = (uint8_t*)malloc(*quote_size); + if (NULL != quote) { + memcpy(*quote, &msg_out->quote, *quote_size); + } + } else { + *quote = NULL; + } + + // The output message is generated by the library and thus has to be freed + // upon completion. + free(msg_out); + + // Allocated due to the variable size revocation list. Free up the + // temporary structure. + free(msg_in); + + // Check if the malloc() call for the output quote failed above; if it did, + // it was due to an out-of-memory condition. + if (NULL == quote && SGX_SUCCESS == ret) { + return SGX_ERROR_OUT_OF_MEMORY; + } + + return ret; +} + +sgx_status_t verify_attestation_response( + // Inputs + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context, + uint8_t *message, + size_t message_size, + uint8_t *cmac, + size_t cmac_size, + uint8_t *secret, + size_t secret_size, + uint8_t *gcm_iv, + uint8_t *gcm_mac, + size_t gcm_mac_size, + + // Outputs + uint8_t *sealed_secret, + size_t *sealed_secret_size, + sgx_status_t *cmac_status +) { + // Check the generated CMAC from the service provider. + sgx_status_t ret = SGX_SUCCESS; + sgx_status_t _ret = verifyCMAC( + enclave_id, &ret, context, message, message_size, cmac, cmac_size + ); + + *cmac_status = ret; + LOG(enclave_id, _ret, context, "verify_cmac() = %x", (uint32_t)ret); + + // Abort if call failed. Otherwise, forward the outcome to the caller. + if (SGX_SUCCESS != _ret) { + return _ret; + } + + // Try to decrypt and verify the attestation response. + _ret = verifyAttestationResponse( + enclave_id, &ret, context, secret, secret_size, + gcm_iv, gcm_mac, gcm_mac_size, + sealed_secret, sizeof(sgx_sealed_data_t) + secret_size + ); + + LOG( + enclave_id, _ret, context, + "verify_attestation_response() = %x", + (uint32_t)ret + ); + + // Abort if unable to verify attestation response. + if (SGX_SUCCESS != (_ret | ret)) { + return (SGX_SUCCESS != _ret) ? _ret : ret; + } + + // Return sealed secret if requested. The buffer is populated by the ECALL + // above, if sealed_secret is non-null. + if (NULL != sealed_secret_size) { + *sealed_secret_size = sizeof(sgx_sealed_data_t) + secret_size; + } + + return SGX_SUCCESS; +} diff --git a/sgx-jvm/remote-attestation/attestation-host/native/remote-attestation.hpp b/sgx-jvm/remote-attestation/attestation-host/native/remote-attestation.hpp new file mode 100644 index 0000000000..caebbd3750 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/native/remote-attestation.hpp @@ -0,0 +1,181 @@ +#ifndef __REMOTE_ATTESTATION_H__ +#define __REMOTE_ATTESTATION_H__ + +#include +#include +#include + +/** + * Initialize the remote attestation. + * + * @param enclave_id The identifier of the enclave facilitating the remote + * attestation. + * @param use_platform_services If true, the enclave establishes a session with + * the PSE before initializing the attestation context. This provides + * additional nonce replay protection and a reliable monotonic counter. + * @param key_challenger ECDSA public key of the challenger with the 8 magic + * bytes removed, and X and Y components changed to little-endian. + * @param context The variable receiving the context constructed during + * initialization. + * + * @return Status code indicative of the outcome of the operation. + */ +sgx_status_t initialize_remote_attestation( + // Inputs + sgx_enclave_id_t enclave_id, + bool use_platform_services, + sgx_ec256_public_t *key_challenger, + + // Outputs + sgx_ra_context_t* context +); + +/** + * Clean up and finalize the remote attestation process. + * + * @param enclave_id The identifier of the enclave facilitating the remote + * attestation. + * @param context The context constructed during initialization. + * + * @return SGX_SUCCESS if successful, or SGX_ERROR_INVALID_PARAMETER if + * an invalid context is provided. + */ +sgx_status_t finalize_remote_attestation( + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context +); + +/** + * Get the public key of the application enclave, and the identifier of the + * EPID group the platform belongs to. + * + * @param enclave_id The identifier of the application enclave. + * @param context The context constructed during initialization. + * @param public_key Variable receiving the elliptic curve public key of the + * application enclave, based on the NIST P-256 elliptic curve.. + * @param group_id Variable receiving the identifier of the platform's EPID + * group. + * @param max_retry_count The maximum number of times to retry the operation. + * @param retry_wait_in_secs The number of seconds to wait between each retry. + * + * @return SGX_SUCCESS if successful, otherwise an error code indicative of + * what went wrong. + */ +sgx_status_t get_public_key_and_group_identifier( + // Inputs + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context, + + // Outputs + sgx_ec256_public_t *public_key, + sgx_epid_group_id_t *group_id, + + // Retry logic + int max_retry_count, + unsigned int retry_wait_in_secs +); + +/** + * Process details received from the challenger via the service provider, and + * generate quote. If the service provider accepts the quote, negotiated + * session keys between the application enclave and the challenger are ready + * for use. However, if it fails, the application should notify the service + * provider of the error or the service provider needs a time-out mechanism to + * terminate the remote attestation transaction when it does not receive the + * message. + * + * @param enclave_id The identifier of the application enclave. + * @param context The context constructed during initialization. + * @param challenger_public_key The public key of the challenger. + * @param service_provider_id The identifier of the service provider. + * @param quote_type Indicates the quote type, linkable (1) or unlinkable (0). + * @param key_derivation_function The ID of the key derivation function used. + * @param signature An ECDSA signature over the challenger and application + * enclave's public keys. + * @param mac A 128-bit AES-CMAC generated by the service provider. + * @param revocation_list_size The size of revocation_list, in bytes. + * @param revocation_list The signature revocation list certificate of the + * Intel EPID group identified by the group identifier in message 1. + * @param enclave_mac AES-CMAC generated by the enclave. + * @param enclave_public_key Variable receiving the public key of the + * application enclave. + * @param security_properties Variable receiving the security properties of the + * Intel SGX platform service. If the security property information is not + * required in the remote attestation and key exchange process, this field will + * be all zeros. + * @param quote Variable receiving a pointer to the quote returned from + * sgx_get_quote. This should be freed after use. + * @param quote_size Variable receiving the size of the quote. + * @param max_retry_count The maximum number of times to retry the operation. + * @param retry_wait_in_secs The number of seconds to wait between each retry. + * + * @return SGX_SUCCESS if successful, otherwise an error code indicative of + * what went wrong. + */ +sgx_status_t process_challenger_details_and_generate_quote( + // Inputs + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context, + sgx_ec256_public_t *challenger_public_key, + sgx_spid_t *service_provider_id, + uint16_t quote_type, + uint16_t key_derivation_function, + sgx_ec256_signature_t *signature, + sgx_mac_t *challenger_mac, + uint32_t revocation_list_size, + uint8_t *revocation_list, + + // Outputs + sgx_mac_t *enclave_mac, + sgx_ec256_public_t *enclave_public_key, + sgx_ps_sec_prop_desc_t *security_properties, + uint8_t **quote, + size_t *quote_size, + + // Retry logic + int max_retry_count, + unsigned int retry_wait_in_secs +); + +/** + * Verify attestation response from service provider. + * + * @param enclave_id The identifier of the application enclave. + * @param context The context constructed during initialization. + * @param message The received attestation result message. + * @param message_size The size of the attestation result message. + * @param cmac The CMAC computed over the attestation result message. + * @param cmac_size The size of the CMAC. + * @param secret The encrypted secret provisioned by the challenger. + * @param secret_size The size of the encrypted secret. + * @param gcm_iv The 12-byte initialization vector used in the decryption. + * @param gcm_mac The GCM-MAC generated over the secret. + * @param gcm_mac_size The size of the GCM-MAC. + * @param sealed_secret Pre-allocated buffer receiving the sealed secret. + * @param sealed_secret_size The size of the sealed secret. + * @param cmac_status The variable receiving the outcome of the CMAC check. + * + * @return SGX_SUCCESS if successful, otherwise an error code indicative of + * what went wrong. + */ +sgx_status_t verify_attestation_response( + // Inputs + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context, + uint8_t *message, + size_t message_size, + uint8_t *cmac, + size_t cmac_size, + uint8_t *secret, + size_t secret_size, + uint8_t *gcm_iv, + uint8_t *gcm_mac, + size_t gcm_mac_size, + + // Outputs + uint8_t *sealed_secret, + size_t *sealed_secret_size, + sgx_status_t *cmac_status +); + +#endif /* __REMOTE_ATTESTATION_H__ */ diff --git a/sgx-jvm/remote-attestation/attestation-host/native/sealing.cpp b/sgx-jvm/remote-attestation/attestation-host/native/sealing.cpp new file mode 100644 index 0000000000..be64121190 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/native/sealing.cpp @@ -0,0 +1,15 @@ +#include "enclave_u.h" +#include "sealing.hpp" + +// Check whether the application enclave is able to unseal a secret. +sgx_status_t unseal_secret( + sgx_enclave_id_t enclave_id, + uint8_t *sealed_secret, + size_t sealed_secret_size +) { + sgx_status_t status = SGX_SUCCESS; + sgx_status_t ret = unsealSecret( + enclave_id, &status, sealed_secret, sealed_secret_size + ); + return SGX_SUCCESS != ret ? ret : status; +} diff --git a/sgx-jvm/remote-attestation/attestation-host/native/sealing.hpp b/sgx-jvm/remote-attestation/attestation-host/native/sealing.hpp new file mode 100644 index 0000000000..2ba607aa3f --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/native/sealing.hpp @@ -0,0 +1,24 @@ +#ifndef __SEALING_H__ +#define __SEALING_H__ + +#include +#include + +/** + * Check whether the application enclave is able to unseal a persisted, sealed + * secret. + * + * @param enclave_id The identifier of the application enclave. + * @param sealed_secret The pre-existing, sealed secret. + * @param sealed_secret_size The size of the sealed secret. + * + * @return An indication of whether or not the enclave was able to unseal the + * secret. + */ +sgx_status_t unseal_secret( + sgx_enclave_id_t enclave_id, + uint8_t *sealed_secret, + size_t sealed_secret_size +); + +#endif /* __SEALING_H__ */ diff --git a/sgx-jvm/remote-attestation/attestation-host/native/wrapper.cpp b/sgx-jvm/remote-attestation/attestation-host/native/wrapper.cpp new file mode 100644 index 0000000000..b2170e7b8a --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/native/wrapper.cpp @@ -0,0 +1,394 @@ +#include +#include + +#include +#include + +#include "wrapper.hpp" +#include "jni.hpp" + +#include "logging.hpp" +#include "enclave-manager.hpp" +#include "remote-attestation.hpp" +#include "sealing.hpp" + +NATIVE_WRAPPER(jint, getDeviceStatus) + (JNIEnv *, jobject) +{ + // Get the status of the SGX device on the local machine. + return get_device_status(); +} + +NATIVE_WRAPPER(jobject, getExtendedGroupIdentifier) + (JNIEnv *env, jobject) +{ + // FindClass/GetMethodID both throw an exception upon failure, so we don't + // need to perform any further NULL-checks. + jclass klass = env->FindClass(KLASS("ExtendedGroupIdentifierResult")); + jmethodID cid = env->GetMethodID(klass, "", "(IJ)V"); + if (cid == NULL) { return NULL; } + + // Get the extended EPID group identifier from SGX. + sgx_status_t status = SGX_ERROR_UNEXPECTED; + uint32_t extended_group_id = get_extended_group_id(&status); + + // Construct and return ExtendedGroupIdentifierResult(identifier, status). + return env->NewObject(klass, cid, extended_group_id, status); +} + +NATIVE_WRAPPER(jobject, createEnclave) + (JNIEnv *env, jobject, jstring path, jboolean use_platform_services, + jbyteArray in_launch_token) +{ + // FindClass/GetMethodID both throw an exception upon failure, so we don't + // need to perform any further NULL-checks. + jclass klass = env->FindClass(KLASS("EnclaveResult")); + jmethodID cid = env->GetMethodID(klass, "", "(J[BJ)V"); + if (cid == NULL) { return NULL; } + + // Marshall inputs. + const char *n_path = env->GetStringUTFChars(path, NULL); + sgx_status_t status; + + // Initialize launch token. + sgx_launch_token_t launch_token = { 0 }; + env->GetByteArrayRegion( + in_launch_token, 0, sizeof(sgx_launch_token_t), (jbyte*)&launch_token + ); + + // Create the enclave. + sgx_enclave_id_t enclave_id = create_enclave( + n_path, (bool)use_platform_services, &status, + &launch_token + ); + + // Construct resulting launch token (could be the same as the input). + jbyteArray _launch_token = env->NewByteArray(sizeof(sgx_launch_token_t)); + env->SetByteArrayRegion( + _launch_token, 0, sizeof(sgx_launch_token_t), (jbyte*)&launch_token + ); + + // Free up memory. + env->ReleaseStringUTFChars(path, n_path); + + // Construct and return EnclaveResult(identifier, launch_token, status). + return env->NewObject(klass, cid, enclave_id, _launch_token, status); +} + +NATIVE_WRAPPER(jboolean, destroyEnclave) + (JNIEnv *, jobject, jlong enclave_id) +{ + // Destroy the enclave if a valid identifier has been passed in. + if (enclave_id != 0) { + return destroy_enclave((sgx_enclave_id_t)enclave_id); + } else { + return false; + } +} + +NATIVE_WRAPPER(jobject, initializeRemoteAttestation) + (JNIEnv *env, jobject, jlong enclave_id, jboolean use_platform_services, + jbyteArray in_key_challenger) +{ + // FindClass/GetMethodID both throw an exception upon failure, so we don't + // need to perform any further NULL-checks. + jclass klass = env->FindClass(KLASS("InitializationResult")); + jmethodID cid = env->GetMethodID(klass, "", "(IJ)V"); + if (cid == NULL) { return NULL; } + + // Marshall the public key passed in from the JVM. + sgx_ec256_public_t key_challenger = { 0 }; + env->GetByteArrayRegion( + in_key_challenger, 0, sizeof(sgx_ec256_public_t), + (jbyte*)&key_challenger + ); + + // Initialize the remote attestation context. + sgx_ra_context_t context; + sgx_status_t status = initialize_remote_attestation( + enclave_id, use_platform_services, &key_challenger, &context + ); + + // Construct and return InitializationResult(context, status). + return env->NewObject(klass, cid, context, status); +} + +NATIVE_WRAPPER(jlong, finalizeRemoteAttestation) + (JNIEnv *, jobject, jlong enclave_id, jint context) +{ + // Finalize the remote attestation + return finalize_remote_attestation(enclave_id, context); +} + +NATIVE_WRAPPER(jobject, getPublicKeyAndGroupIdentifier) + (JNIEnv *env, jobject, jlong enclave_id, jint context, jint max_retry_count, + jint retry_wait_in_secs) +{ + // FindClass/GetMethodID both throw an exception upon failure, so we don't + // need to perform any further NULL-checks. + jclass klass = env->FindClass(KLASS("PublicKeyAndGroupIdentifier")); + jmethodID cid = env->GetMethodID(klass, "", "([BIJ)V"); + if (cid == NULL) { return NULL; } + + // Get the public key of the application enclave, and the group identifier + // of the platform. + sgx_ec256_public_t public_key; + sgx_epid_group_id_t group_id; + sgx_status_t status = get_public_key_and_group_identifier( + enclave_id, context, &public_key, &group_id, max_retry_count, + retry_wait_in_secs + ); + + // Cast group identifier into an unsigned integer. + uint32_t gid = *((uint32_t*)group_id); + + // Create managed array used to return the enclave's public key. + jbyteArray _public_key = env->NewByteArray(sizeof(sgx_ec256_public_t)); + if (NULL == _public_key) { + // Out of memory - abort + return NULL; + } + + // Copy public key bytes over to managed array. + env->SetByteArrayRegion( + _public_key, 0, sizeof(sgx_ec256_public_t), (jbyte*)&public_key + ); + + // Return PublicKeyAndGroupIdentifier(publicKey, groupIdentifier, status). + return env->NewObject(klass, cid, _public_key, gid, status); +} + +NATIVE_WRAPPER(jobject, processServiceProviderDetailsAndGenerateQuote) + (JNIEnv *env, jobject, jlong enclave_id, jint context, + jbyteArray in_challenger_public_key, jbyteArray in_service_provider_id, + jshort quote_type, jshort key_derivation_function, jbyteArray in_signature, + jbyteArray in_mac, jint revocation_list_size, + jbyteArray in_revocation_list, jint max_retry_count, + jint retry_wait_in_secs) +{ + // FindClass/GetMethodID both throw an exception upon failure, so we don't + // need to perform any further NULL-checks. + jclass klass = env->FindClass(KLASS("QuoteResult")); + jmethodID cid = env->GetMethodID(klass, "", "([B[B[B[BJ)V"); + if (cid == NULL) { return NULL; } + + // Marshal inputs. + sgx_ec256_public_t challenger_public_key; + sgx_spid_t service_provider_id; + sgx_ec256_signature_t signature; + sgx_mac_t mac; + uint8_t *revocation_list = (uint8_t*)malloc(revocation_list_size); + + // Check if there's enough free memory to allocate a buffer for the + // revocation list. + if (NULL == revocation_list) { + return NULL; + } + + env->GetByteArrayRegion( + in_challenger_public_key, 0, sizeof(sgx_ec256_public_t), + (jbyte*)&challenger_public_key + ); + env->GetByteArrayRegion( + in_service_provider_id, 0, sizeof(sgx_spid_t), + (jbyte*)&service_provider_id + ); + env->GetByteArrayRegion( + in_signature, 0, sizeof(sgx_ec256_signature_t), + (jbyte*)&signature + ); + env->GetByteArrayRegion( + in_mac, 0, sizeof(sgx_mac_t), + (jbyte*)&mac + ); + env->GetByteArrayRegion( + in_revocation_list, 0, revocation_list_size, (jbyte*)revocation_list + ); + + // Output variables. + sgx_mac_t enclave_mac = { 0 }; + sgx_ec256_public_t enclave_public_key = { 0 }; + sgx_ps_sec_prop_desc_t security_properties = { 0 }; + uint8_t *quote = NULL; + size_t quote_size = 0; + + // Process details received from challenger via the service provider, and + // generate quote. + sgx_status_t status = process_challenger_details_and_generate_quote( + // Inputs + enclave_id, context, &challenger_public_key, &service_provider_id, + quote_type, key_derivation_function, &signature, &mac, + revocation_list_size, revocation_list, + // Outputs + &enclave_mac, &enclave_public_key, &security_properties, "e, + "e_size, + // Retry logic + max_retry_count, retry_wait_in_secs + ); + + LOG( + enclave_id, status, context, + "process_challenger_details_and_generate_quote() = quote(size=%u)", + quote_size + ); + + // Create output objects. + jbyteArray _enclave_mac = env->NewByteArray(sizeof(sgx_mac_t)); + env->SetByteArrayRegion( + _enclave_mac, 0, sizeof(sgx_mac_t), (jbyte*)&enclave_mac + ); + jbyteArray _enclave_public_key = env->NewByteArray( + sizeof(sgx_ec256_public_t) + ); + env->SetByteArrayRegion( + _enclave_public_key, 0, sizeof(sgx_ec256_public_t), + (jbyte*)&enclave_public_key + ); + jbyteArray _security_properties = env->NewByteArray( + sizeof(sgx_ps_sec_prop_desc_t) + ); + env->SetByteArrayRegion( + _security_properties, 0, sizeof(sgx_ps_sec_prop_desc_t), + (jbyte*)&security_properties + ); + + jbyteArray _quote = NULL; + + // Free up memory. + if (NULL != quote) { + _quote = env->NewByteArray(quote_size); + env->SetByteArrayRegion(_quote, 0, quote_size, (jbyte*)quote); + free(quote); + } else { + _quote = env->NewByteArray(0); + } + free(revocation_list); + + // Return QuoteResult(mac, publicKey, securityProperties, quote, status). + return env->NewObject( + klass, cid, + _enclave_mac, _enclave_public_key, _security_properties, _quote, status + ); +} + +NATIVE_WRAPPER(jobject, verifyAttestationResponse) + (JNIEnv *env, jobject, jlong enclave_id, jint context, + jbyteArray message, jbyteArray cmac, jbyteArray secret, + jbyteArray gcm_iv, jbyteArray gcm_mac) +{ + // FindClass/GetMethodID both throw an exception upon failure, so we don't + // need to perform any further NULL-checks. + jclass klass = env->FindClass(KLASS("VerificationResult")); + jmethodID cid = env->GetMethodID(klass, "", "([BJJ)V"); + if (cid == NULL) { return NULL; } + + // Get buffer sizes. + size_t message_size = (message == NULL) ? 0 : env->GetArrayLength(message); + size_t cmac_size = (cmac == NULL) ? 0 : env->GetArrayLength(cmac); + size_t secret_size = (secret == NULL) ? 0 : env->GetArrayLength(secret); + size_t gcm_mac_size = (gcm_mac == NULL) ? 0 : env->GetArrayLength(gcm_mac); + + // Allocate buffers. + uint8_t *_message = (uint8_t*)malloc(message_size); + uint8_t *_cmac = (uint8_t*)malloc(cmac_size); + uint8_t *_secret = (uint8_t*)malloc(secret_size); + uint8_t *_gcm_iv = (uint8_t*)calloc(1, SGX_AESGCM_IV_SIZE); + uint8_t *_gcm_mac = (uint8_t*)malloc(gcm_mac_size); + + // Length of secret is preserved during encryption, but prepend header. + size_t _sealed_secret_size = sizeof(sgx_sealed_data_t) + secret_size; + uint8_t *_sealed_secret = (uint8_t*)malloc(_sealed_secret_size); + + // Check if we ran out of memory. + if (NULL == _message || NULL == _cmac || NULL == _secret || + NULL == _gcm_iv || NULL == _gcm_mac || NULL == _sealed_secret) { + free(_message); + free(_cmac); + free(_secret); + free(_gcm_iv); + free(_gcm_mac); + free(_sealed_secret); + return NULL; + } + + // Marshal inputs. + if (message != NULL) { + env->GetByteArrayRegion(message, 0, message_size, (jbyte*)_message); + } + if (cmac != NULL) { + env->GetByteArrayRegion(cmac, 0, cmac_size, (jbyte*)_cmac); + } + if (secret != NULL) { + env->GetByteArrayRegion(secret, 0, secret_size, (jbyte*)_secret); + } + if (gcm_iv != NULL) { + env->GetByteArrayRegion(gcm_iv, 0, SGX_AESGCM_IV_SIZE, (jbyte*)_gcm_iv); + } + if (gcm_mac != NULL) { + env->GetByteArrayRegion(gcm_mac, 0, gcm_mac_size, (jbyte*)_gcm_mac); + } + + // Verify the attestation response received from the service provider. + sgx_status_t cmac_status = SGX_SUCCESS; + sgx_status_t status = verify_attestation_response( + enclave_id, context, _message, message_size, _cmac, cmac_size, + _secret, secret_size, _gcm_iv, _gcm_mac, gcm_mac_size, _sealed_secret, + &_sealed_secret_size, &cmac_status + ); + + // Free temporary allocations. + free(_message); + free(_cmac); + free(_secret); + free(_gcm_iv); + free(_gcm_mac); + + // Marshal outputs. + jbyteArray sealed_secret; + if (NULL != _sealed_secret) { + sealed_secret = env->NewByteArray(_sealed_secret_size); + env->SetByteArrayRegion( + sealed_secret, 0, _sealed_secret_size, (jbyte*)_sealed_secret + ); + free(_sealed_secret); + } else { + sealed_secret = env->NewByteArray(0); + } + + // Return VerificationResult(sealedSecret, cmacValidationStatus, status). + return env->NewObject(klass, cid, sealed_secret, cmac_status, status); +} + +NATIVE_WRAPPER(jlong, unsealSecret) + (JNIEnv *env, jobject, jlong enclave_id, jbyteArray sealed_secret) +{ + // Check if we've actually got a sealed secret to unseal. + uint8_t *_sealed_secret = NULL; + size_t sealed_secret_size = env->GetArrayLength(sealed_secret); + if (0 == sealed_secret_size) { + return SGX_ERROR_INVALID_PARAMETER; + } + + // Allocate buffer. + _sealed_secret = (uint8_t*)malloc(sealed_secret_size); + + // Check if we ran out of memory. + if (NULL == _sealed_secret) { + return 0; + } + + // Marshal inputs. + env->GetByteArrayRegion( + sealed_secret, 0, sealed_secret_size, (jbyte*)_sealed_secret + ); + + // Try to unseal the secret. + sgx_status_t result = unseal_secret( + enclave_id, _sealed_secret, sealed_secret_size + ); + + // Free temporary allocations. + free(_sealed_secret); + + return result; +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/AttestOnlyIT.kt b/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/AttestOnlyIT.kt new file mode 100644 index 0000000000..a0720241d4 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/AttestOnlyIT.kt @@ -0,0 +1,116 @@ +package net.corda.attestation.host + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.Crypto +import net.corda.attestation.CryptoProvider +import net.corda.attestation.message.AttestationError +import net.corda.attestation.message.AttestationRequest +import net.corda.attestation.readValue +import net.corda.attestation.toLittleEndian +import org.apache.http.HttpHeaders.CONTENT_TYPE +import org.apache.http.HttpStatus.* +import org.apache.http.client.CookieStore +import org.apache.http.client.config.CookieSpecs.STANDARD_STRICT +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpPost +import org.apache.http.client.protocol.HttpClientContext +import org.apache.http.config.SocketConfig +import org.apache.http.entity.ContentType.APPLICATION_JSON +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.BasicCookieStore +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.message.BasicHeader +import org.apache.http.util.EntityUtils +import org.junit.* +import org.junit.Assert.* +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.KeyPair +import java.security.interfaces.ECPublicKey + +class AttestOnlyIT { + private companion object { + private val httpPort = Integer.getInteger("test.httpPort") + } + private lateinit var httpClient: CloseableHttpClient + private lateinit var cookies: CookieStore + private lateinit var keyPair: KeyPair + private lateinit var mapper: ObjectMapper + private lateinit var crypto: Crypto + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Before + fun setup() { + crypto = cryptoProvider.crypto + mapper = ObjectMapper().registerModule(JavaTimeModule()) + cookies = BasicCookieStore() + keyPair = crypto.generateKeyPair() + + val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setCookieSpec(STANDARD_STRICT) + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + httpClient = HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun `attest without payload`() { + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val httpRequest = HttpPost("http://localhost:$httpPort/host/attest").apply { + addHeader(BasicHeader(CONTENT_TYPE, APPLICATION_JSON.mimeType)) + } + val responseBody = httpClient.execute(httpRequest, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_BAD_REQUEST, response.statusLine.statusCode) + output + } + + val error: AttestationError = mapper.readValue(responseBody) + assertEquals("Message is missing", error.message) + } + + @Test + fun `test attest requires challenge first`() { + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val attestRequest = AttestationRequest( + gb = (keyPair.public as ECPublicKey).toLittleEndian(), + signatureGbGa = byteArrayOf(), + aesCMAC = byteArrayOf() + ) + val httpRequest = HttpPost("http://localhost:$httpPort/host/attest").apply { + entity = StringEntity(mapper.writeValueAsString(attestRequest), APPLICATION_JSON) + } + val responseBody = httpClient.execute(httpRequest, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_UNAUTHORIZED, response.statusLine.statusCode) + output + } + + val error: AttestationError = mapper.readValue(responseBody) + assertEquals("No response from our challenge", error.message) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/AttestationIT.kt b/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/AttestationIT.kt new file mode 100644 index 0000000000..eb4ef17eb8 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/AttestationIT.kt @@ -0,0 +1,174 @@ +package net.corda.attestation.host + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.* +import net.corda.attestation.message.* +import org.apache.http.HttpStatus.* +import org.apache.http.client.CookieStore +import org.apache.http.client.config.CookieSpecs.STANDARD_STRICT +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpPost +import org.apache.http.client.protocol.HttpClientContext +import org.apache.http.config.SocketConfig +import org.apache.http.entity.ContentType.APPLICATION_JSON +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.BasicCookieStore +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.util.EntityUtils +import org.junit.* +import org.junit.Assert.* +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.* +import java.security.cert.* +import java.security.cert.Certificate +import java.security.cert.PKIXRevocationChecker.Option.* +import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec +import java.util.* + +class AttestationIT { + private companion object { + private val httpPort = Integer.getInteger("test.httpPort") + private val revocationListOptions = EnumSet.of(SOFT_FAIL, PREFER_CRLS, NO_FALLBACK) + private const val AES_CMAC_FUNC = 1.toShort() + } + private lateinit var certificateFactory: CertificateFactory + private lateinit var httpClient: CloseableHttpClient + private lateinit var cookies: CookieStore + private lateinit var challengerKeyPair: KeyPair + private lateinit var transientKeyPair: KeyPair + private lateinit var ecParameters: ECParameterSpec + private lateinit var pkixParameters: PKIXParameters + private lateinit var challengeResponse: ChallengeResponse + private lateinit var peerPublicKey: ECPublicKey + private lateinit var smk: ByteArray + private lateinit var mapper: ObjectMapper + private lateinit var crypto: Crypto + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Rule + @JvmField + val signatureProvider = SignatureProvider(cryptoProvider) + + @Rule + @JvmField + val keyStoreProvider = KeyStoreProvider("dummyIAS-trust.pfx", "attestation") + + @Before + fun setup() { + cookies = BasicCookieStore() + crypto = cryptoProvider.crypto + certificateFactory = CertificateFactory.getInstance("X.509") + challengerKeyPair = crypto.generateKeyPair() + transientKeyPair = crypto.generateKeyPair() + ecParameters = (transientKeyPair.public as ECPublicKey).params + mapper = ObjectMapper().registerModule(JavaTimeModule()) + + pkixParameters = PKIXParameters(keyStoreProvider.trustAnchorsFor("ias")).apply { + val rlChecker = CertPathValidator.getInstance("PKIX").revocationChecker as PKIXRevocationChecker + addCertPathChecker(rlChecker.apply { options = revocationListOptions }) + } + + val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setCookieSpec(STANDARD_STRICT) + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + httpClient = HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + challengerKeyPair = crypto.generateKeyPair() + val challengeRequest = ChallengeRequest((challengerKeyPair.public as ECPublicKey).toLittleEndian(), "nonce-value") + val request = HttpPost("http://localhost:$httpPort/host/challenge").apply { + entity = StringEntity(mapper.writeValueAsString(challengeRequest), APPLICATION_JSON) + } + val response = httpClient.execute(request, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + challengeResponse = mapper.readValue(response) + + val keyFactory = KeyFactory.getInstance("EC") + peerPublicKey = keyFactory.generatePublic(challengeResponse.ga.toBigEndianKeySpec(ecParameters)) as ECPublicKey + smk = crypto.generateSMK(transientKeyPair.private, peerPublicKey) + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun postAttestation() { + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val gb = (transientKeyPair.public as ECPublicKey).toLittleEndian() + val signatureGbGa = signatureProvider.signatureOf(challengerKeyPair.private, transientKeyPair.public as ECPublicKey, peerPublicKey) + val attestRequest = AttestationRequest( + gb = gb, + signatureGbGa = signatureGbGa, + aesCMAC = crypto.aesCMAC(smk, { aes -> + aes.update(gb) + aes.update(challengeResponse.spid.hexToBytes()) + aes.update(challengeResponse.quoteType.toLittleEndian()) + aes.update(AES_CMAC_FUNC.toLittleEndian()) + aes.update(signatureGbGa) + }) + ) + val httpRequest = HttpPost("http://localhost:$httpPort/host/attest").apply { + entity = StringEntity(mapper.writeValueAsString(attestRequest), APPLICATION_JSON) + } + val responseBody = httpClient.execute(httpRequest, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + validateSignature(mapper.readValue(responseBody)) + } + + private fun validateSignature(reportResponse: ReportProxyResponse) { + val certificatePath = parseCertificates(reportResponse.certificatePath) + CertPathValidator.getInstance("PKIX").apply { + validate(certificatePath, pkixParameters) + } + + Signature.getInstance("SHA256withRSA").apply { + initVerify(certificatePath.certificates[0]) + update(reportResponse.report) + if (!verify(reportResponse.signature.toByteArray().decodeBase64())) { + throw IllegalArgumentException("Incorrect response signature") + } + } + } + + private fun parseCertificates(iasCertificateHeader: String): CertPath { + val certificates = mutableListOf() + iasCertificateHeader.byteInputStream().use { input -> + while (input.available() > 0) { + certificates.add(certificateFactory.generateCertificate(input)) + } + } + return certificateFactory.generateCertPath(certificates) + } + + private fun ByteArray.decodeBase64(): ByteArray = Base64.getDecoder().decode(this) +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/ChallengeIT.kt b/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/ChallengeIT.kt new file mode 100644 index 0000000000..34f40396c4 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/ChallengeIT.kt @@ -0,0 +1,104 @@ +package net.corda.attestation.host + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.* +import net.corda.attestation.message.AttestationError +import net.corda.attestation.message.ChallengeRequest +import net.corda.attestation.message.ChallengeResponse +import org.apache.http.HttpStatus.* +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpPost +import org.apache.http.config.SocketConfig +import org.apache.http.entity.ContentType.APPLICATION_JSON +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.util.EntityUtils +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.KeyFactory +import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec + +class ChallengeIT { + private companion object { + private val httpPort = Integer.getInteger("test.httpPort") + } + private lateinit var httpClient: CloseableHttpClient + private lateinit var ecParameters: ECParameterSpec + private lateinit var keyFactory: KeyFactory + private lateinit var mapper: ObjectMapper + private lateinit var crypto: Crypto + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Before + fun setup() { + mapper = ObjectMapper() + crypto = cryptoProvider.crypto + keyFactory = KeyFactory.getInstance("EC") + ecParameters = (crypto.generateKeyPair().public as ECPublicKey).params + val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + httpClient = HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun challenge() { + val keyPair = crypto.generateKeyPair() + val challengeRequest = ChallengeRequest((keyPair.public as ECPublicKey).toLittleEndian(), "nonce-value") + val request = HttpPost("http://localhost:$httpPort/host/challenge").apply { + entity = StringEntity(mapper.writeValueAsString(challengeRequest), APPLICATION_JSON) + } + val response = httpClient.execute(request).use { response -> + val output = EntityUtils.toString(response.entity) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + + val challengeResponse: ChallengeResponse = mapper.readValue(response) + keyFactory.generatePublic(challengeResponse.ga.toBigEndianKeySpec(ecParameters)) + } + + + @Test + fun testHugeNonce() { + val keyPair = crypto.generateKeyPair() + val challengeRequest = ChallengeRequest( + gc = (keyPair.public as ECPublicKey).toLittleEndian(), + nonce = "1234567890123456789012345678901234" + ) + val httpRequest = HttpPost("http://localhost:$httpPort/host/challenge").apply { + entity = StringEntity(mapper.writeValueAsString(challengeRequest), APPLICATION_JSON) + } + val responseBody = httpClient.execute(httpRequest).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_BAD_REQUEST, response.statusLine.statusCode) + output + } + + val error: AttestationError = mapper.readValue(responseBody) + assertEquals("Nonce is too large: maximum 32 digits", error.message) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/SecretIT.kt b/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/SecretIT.kt new file mode 100644 index 0000000000..af29e20524 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/SecretIT.kt @@ -0,0 +1,153 @@ +package net.corda.attestation.host + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.* +import net.corda.attestation.message.AttestationRequest +import net.corda.attestation.message.ChallengeRequest +import net.corda.attestation.message.ChallengeResponse +import net.corda.attestation.message.SecretRequest +import org.apache.http.HttpStatus.SC_OK +import org.apache.http.client.CookieStore +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpPost +import org.apache.http.client.protocol.HttpClientContext +import org.apache.http.config.SocketConfig +import org.apache.http.entity.ContentType.APPLICATION_JSON +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.BasicCookieStore +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.util.EntityUtils +import org.junit.* +import org.junit.Assert.* +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.KeyFactory +import java.security.KeyPair +import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec + +class SecretIT { + private companion object { + private val httpPort = Integer.getInteger("test.httpPort") + private const val AES_CMAC_FUNC = 1.toShort() + } + private lateinit var httpClient: CloseableHttpClient + private lateinit var ecParameters: ECParameterSpec + private lateinit var keyFactory: KeyFactory + private lateinit var mapper: ObjectMapper + private lateinit var crypto: Crypto + private lateinit var cookies: CookieStore + private lateinit var transientKeyPair: KeyPair + private lateinit var challengerKeyPair: KeyPair + private lateinit var peerPublicKey: ECPublicKey + private lateinit var smk: ByteArray + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Rule + @JvmField + val signatureProvider = SignatureProvider(cryptoProvider) + + @Before + fun setup() { + mapper = ObjectMapper() + cookies = BasicCookieStore() + crypto = cryptoProvider.crypto + keyFactory = KeyFactory.getInstance("EC") + transientKeyPair = crypto.generateKeyPair() + ecParameters = (transientKeyPair.public as ECPublicKey).params + challengerKeyPair = crypto.generateKeyPair() + + val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + httpClient = HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val challengeRequest = ChallengeRequest((challengerKeyPair.public as ECPublicKey).toLittleEndian(), "nonce-value") + var httpRequest = HttpPost("http://localhost:$httpPort/host/challenge").apply { + entity = StringEntity(mapper.writeValueAsString(challengeRequest), APPLICATION_JSON) + } + val response = httpClient.execute(httpRequest, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + val challengeResponse = mapper.readValue(response) + + val keyFactory = KeyFactory.getInstance("EC") + peerPublicKey = keyFactory.generatePublic(challengeResponse.ga.toBigEndianKeySpec(ecParameters)) as ECPublicKey + smk = crypto.generateSMK(transientKeyPair.private, peerPublicKey) + + val gb = (transientKeyPair.public as ECPublicKey).toLittleEndian() + val signatureGbGa = signatureProvider.signatureOf(challengerKeyPair.private, transientKeyPair.public as ECPublicKey, peerPublicKey) + val attestRequest = AttestationRequest( + gb = gb, + signatureGbGa = signatureGbGa, + aesCMAC = crypto.aesCMAC(smk, { aes -> + aes.update(gb) + aes.update(challengeResponse.spid.hexToBytes()) + aes.update(challengeResponse.quoteType.toLittleEndian()) + aes.update(AES_CMAC_FUNC.toLittleEndian()) + aes.update(signatureGbGa) + }) + ) + httpRequest = HttpPost("http://localhost:$httpPort/host/attest").apply { + entity = StringEntity(mapper.writeValueAsString(attestRequest), APPLICATION_JSON) + } + httpClient.execute(httpRequest, context).use { httpResponse -> + val output = EntityUtils.toString(httpResponse.entity, UTF_8) + assertEquals(output, SC_OK, httpResponse.statusLine.statusCode) + output + } + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun postSecret() { + val secretKey = crypto.generateSecretKey(transientKeyPair.private, peerPublicKey) + val secretIV = crypto.createIV() + val secretData = crypto.encrypt("And now for something completely different!".toByteArray(), secretKey, secretIV) + + val platformInfo: ByteArray? = null + val mk = crypto.generateMK(transientKeyPair.private, peerPublicKey) + val secretRequest = SecretRequest( + platformInfo = platformInfo, + aesCMAC = crypto.aesCMAC(mk, { aes -> + aes.update(platformInfo) + }), + data = secretData.encryptedData(), + authTag = secretData.authenticationTag(), + iv = secretIV + ) + val request = HttpPost("http://localhost:$httpPort/host/secret").apply { + entity = StringEntity(mapper.writeValueAsString(secretRequest), APPLICATION_JSON) + } + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + httpClient.execute(request, context).use { response -> + val output = EntityUtils.toString(response.entity) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/SignatureProvider.kt b/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/SignatureProvider.kt new file mode 100644 index 0000000000..52fbabbcd1 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/integration-test/kotlin/net/corda/attestation/host/SignatureProvider.kt @@ -0,0 +1,39 @@ +package net.corda.attestation.host + +import net.corda.attestation.CryptoProvider +import net.corda.attestation.KEY_SIZE +import net.corda.attestation.toLittleEndian +import org.bouncycastle.asn1.ASN1InputStream +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.DLSequence +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.nio.ByteBuffer +import java.security.PrivateKey +import java.security.Signature +import java.security.interfaces.ECPublicKey + +class SignatureProvider(private val cryptoProvider: CryptoProvider) : TestRule { + override fun apply(stmt: Statement, description: Description): Statement { + return stmt + } + + fun signatureOf(signingKey: PrivateKey, localKey: ECPublicKey, remoteKey: ECPublicKey): ByteArray { + val signature = Signature.getInstance("SHA256WithECDSA").let { signer -> + signer.initSign(signingKey, cryptoProvider.crypto.random) + signer.update(localKey.toLittleEndian()) + signer.update(remoteKey.toLittleEndian()) + signer.sign() + } + return ByteBuffer.allocate(KEY_SIZE).let { buf -> + ASN1InputStream(signature).use { input -> + for (number in input.readObject() as DLSequence) { + val pos = (number as ASN1Integer).positiveValue.toLittleEndian(KEY_SIZE / 2) + buf.put(pos) + } + buf.array() + } + } + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/HostException.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/HostException.kt new file mode 100644 index 0000000000..b5a0c1ddde --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/HostException.kt @@ -0,0 +1,5 @@ +package net.corda.attestation.host + +open class HostException(message: String, cause: Throwable?) : Exception(message, cause) { + constructor(message: String) : this(message, null) +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/AttestationEnclave.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/AttestationEnclave.kt new file mode 100644 index 0000000000..05a5ebbab0 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/AttestationEnclave.kt @@ -0,0 +1,81 @@ +package net.corda.attestation.host.sgx + +import net.corda.attestation.host.sgx.enclave.ECKey +import net.corda.attestation.host.sgx.enclave.Enclave +import net.corda.attestation.host.sgx.enclave.SgxStatus +import net.corda.attestation.host.sgx.entities.AttestationResult +import net.corda.attestation.host.sgx.entities.Quote +import net.corda.attestation.host.sgx.sealing.SealedSecret +import net.corda.attestation.host.sgx.system.GroupIdentifier + +typealias AttestationContext = Int + +/** + * Enclave used in remote attestation. + */ +interface AttestationEnclave : Enclave { + + /** + * The platform services offer an architectural enclave which provides a + * trusted time source and a monotonic counter, which in turn can be used + * for replay protection during nonce generation and for securely + * calculating the length of time for which a secret shall be valid. + */ + val usePlatformServices: Boolean + + /** + * Create a context for the remote attestation and key exchange process. + * + * @param challengerKey The elliptic curve public key of the challenger + * (NIST P-256 elliptic curve). + * + * @throws SgxException If unable to create context. + */ + fun initializeKeyExchange(challengerKey: ECKey) + + /** + * Finalize the remote attestation and key exchange process. + */ + fun finalizeKeyExchange() + + /** + * Get the public key of the application enclave, based on NIST P-256 + * elliptic curve, and the identifier of the EPID group to which the + * platform belongs. + */ + fun getPublicKeyAndGroupIdentifier(): Pair + + /** + * Process the response from the challenger and generate a quote for the + * final step of the attestation process. + * + * @param challengerDetails Details received from the challenger. + */ + fun processChallengerDetailsAndGenerateQuote(challengerDetails: ChallengerDetails): Quote + + /** + * Verify the attestation response received from the service provider. + * + * @param attestationResult The received attestation response. + * + * @return A pair of (1) the outcome of the validation of the CMAC over + * the security manifest, and (2) the sealed secret, if successful. + * + * @throws SgxException If unable to verify the response and seal the + * secret. + */ + fun verifyAttestationResponse( + attestationResult: AttestationResult + ): Pair + + /** + * Attempt to unseal a secret inside the enclave and report the outcome of + * the operation. + * + * @param sealedSecret The sealed secret provisioned by the challenger. + * + * @return A status code indicative of the outcome of the operation. + */ + fun unseal(sealedSecret: SealedSecret): SgxStatus + +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/ChallengerDetails.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/ChallengerDetails.kt new file mode 100644 index 0000000000..d999ccdf34 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/ChallengerDetails.kt @@ -0,0 +1,55 @@ +package net.corda.attestation.host.sgx + +import net.corda.attestation.host.sgx.enclave.ECKey +import net.corda.attestation.host.sgx.entities.QuoteType + +/** + * Information retrieved from message 2 in the Intel remote attestation flow. + */ +class ChallengerDetails( + /** + * The public elliptic curve key of the challenger, based on the NIST + * P-256 elliptic curve. + */ + val publicKey: ECKey, + + /** + * The identifier of the service provider, also known as SPID. + */ + val serviceProviderIdentifier: ByteArray, + + /** + * Indicates the quote type, i.e., whether it is linkable or + * un-linkable. + */ + val quoteType: QuoteType, + + /** + * The identifier of the key derivation function. + */ + val keyDerivationFunctionIdentifier: Short, + + /** + * An ECDSA signature of (g_b||g_a), using the challenger's ECDSA + * private key corresponding to the public key specified in the + * sgx_ra_init function, where g_b is the public elliptic curve key of + * the challenger and g_a is the public key of application enclave, + * provided by the application enclave, in the remote attestation and + * key exchange message (message 1). + */ + val signature: ByteArray, + + /** + * The 128-bit AES-CMAC generated by the service provider. See + * [sgx_ra_msg2_t](https://software.intel.com/en-us/node/709237) + * for more details on its derivation. + */ + val messageAuthenticationCode: ByteArray, + + /** + * The Intel EPID signature revocation list certificate of the Intel + * EPID group identified by the group identifier in the remote + * attestation and key exchange message (message 1). + */ + val signatureRevocationList: ByteArray +) diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/NativeAttestationEnclave.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/NativeAttestationEnclave.kt new file mode 100644 index 0000000000..df94a2836f --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/NativeAttestationEnclave.kt @@ -0,0 +1,204 @@ +package net.corda.attestation.host.sgx.bridge + +import net.corda.attestation.host.sgx.AttestationContext +import net.corda.attestation.host.sgx.AttestationEnclave +import net.corda.attestation.host.sgx.ChallengerDetails +import net.corda.attestation.host.sgx.bridge.enclave.NativeEnclave +import net.corda.attestation.host.sgx.bridge.wrapper.NativeWrapper +import net.corda.attestation.host.sgx.enclave.ECKey +import net.corda.attestation.host.sgx.enclave.SgxException +import net.corda.attestation.host.sgx.enclave.SgxStatus +import net.corda.attestation.host.sgx.entities.AttestationException +import net.corda.attestation.host.sgx.entities.AttestationResult +import net.corda.attestation.host.sgx.entities.Quote +import net.corda.attestation.host.sgx.sealing.SealedSecret +import net.corda.attestation.host.sgx.system.GroupIdentifier +import net.corda.attestation.host.sgx.system.SgxSystem +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.nio.file.Path +import kotlin.concurrent.withLock + +/** + * Enclave used in remote attestation. + */ +class NativeAttestationEnclave @JvmOverloads constructor( + enclavePath: Path, + override val usePlatformServices: Boolean = false +) : NativeEnclave(enclavePath, usePlatformServices), AttestationEnclave { + + private companion object { + + @JvmStatic + private val log: Logger = LoggerFactory.getLogger(NativeAttestationEnclave::class.java) + + } + + private val maxRetryCount: Int = 5 + + private val retryDelayInSeconds: Int = 5 + + private var context: AttestationContext? = null + + /** + * Create a context for the remote attestation and key exchange process. + * + * @param challengerKey The elliptic curve public key of the challenger + * (NIST P-256 elliptic curve). + * + * @throws SgxException If unable to create context. + */ + override fun initializeKeyExchange(challengerKey: ECKey) { + lock.withLock { + val result = NativeWrapper.initializeRemoteAttestation( + identifier, + usePlatformServices, + challengerKey.bytes + ) + val status = SgxSystem.statusFromCode(result.result) + context = result.context + if (status != SgxStatus.SUCCESS) { + throw SgxException(status, identifier, context) + } + } + } + + /** + * Clean up and finalize the remote attestation process. + */ + override fun finalizeKeyExchange() { + lock.withLock { + val oldContext = context + if (oldContext != null) { + val code = NativeWrapper.finalizeRemoteAttestation( + identifier, + oldContext + ) + context = null + val status = SgxSystem.statusFromCode(code) + if (status != SgxStatus.SUCCESS) { + throw SgxException(status, identifier, oldContext) + } + } + } + } + + /** + * Get the public key of the application enclave, based on NIST P-256 + * elliptic curve, and the identifier of the EPID group the platform + * belongs to. + */ + override fun getPublicKeyAndGroupIdentifier(): Pair { + lock.withLock { + val context = context ?: throw AttestationException("Not initialized.") + val result = NativeWrapper.getPublicKeyAndGroupIdentifier( + identifier, + context, + maxRetryCount, + retryDelayInSeconds + ) + val status = SgxSystem.statusFromCode(result.result) + if (status != SgxStatus.SUCCESS) { + throw SgxException(status, identifier, context) + } + + return Pair(ECKey.fromBytes(result.publicKey), result.groupIdentifier) + } + } + + /** + * Process the response from the challenger and generate a quote for the + * final step of the attestation process. + * + * @param challengerDetails Details from the challenger. + */ + override fun processChallengerDetailsAndGenerateQuote( + challengerDetails: ChallengerDetails + ): Quote { + lock.withLock { + val context = context ?: throw AttestationException("Not initialized.") + val result = NativeWrapper.processServiceProviderDetailsAndGenerateQuote( + identifier, + context, + challengerDetails.publicKey.bytes, + challengerDetails.serviceProviderIdentifier, + challengerDetails.quoteType.value, + challengerDetails.keyDerivationFunctionIdentifier, + challengerDetails.signature, + challengerDetails.messageAuthenticationCode, + challengerDetails.signatureRevocationList.size, + challengerDetails.signatureRevocationList, + maxRetryCount, + retryDelayInSeconds + ) + val status = SgxSystem.statusFromCode(result.result) + if (status != SgxStatus.SUCCESS) { + throw SgxException(status, identifier, context) + } + return Quote( + result.messageAuthenticationCode, + ECKey.fromBytes(result.publicKey), + result.securityProperties, + result.payload + ) + } + } + + /** + * Verify the attestation response received from the service provider. + * + * @param attestationResult The received attestation response. + * + * @return A pair of (1) the outcome of the validation of the CMAC over + * the security manifest, and (2) the sealed secret, if successful. + * + * @throws SgxException If unable to verify the response or seal the + * secret. + */ + override fun verifyAttestationResponse( + attestationResult: AttestationResult + ): Pair { + lock.withLock { + val context = context ?: throw AttestationException("Not initialized.") + val result = NativeWrapper.verifyAttestationResponse( + identifier, + context, + attestationResult.attestationResultMessage, + attestationResult.aesCMAC, + attestationResult.secret, + attestationResult.secretIV, + attestationResult.secretHash + ) + val cmacValidationStatus = SgxSystem.statusFromCode( + result.cmacValidationStatus + ) + + if (cmacValidationStatus != SgxStatus.SUCCESS) { + if (attestationResult.aesCMAC.isEmpty()) { + log.warn("No CMAC available") + } else { + log.warn("Failed to validate AES-CMAC ({}).", cmacValidationStatus.name) + } + } + + val status = SgxSystem.statusFromCode(result.result) + if (status != SgxStatus.SUCCESS) { + throw SgxException(status, identifier, context) + } + + return Pair(cmacValidationStatus, result.secret) + } + } + + /** + * Attempt to unseal a secret inside the enclave and report the outcome of + * the operation. + */ + override fun unseal(sealedSecret: SealedSecret): SgxStatus { + lock.withLock { + val result = NativeWrapper.unsealSecret(identifier, sealedSecret) + return SgxSystem.statusFromCode(result) + } + } + +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/enclave/NativeEnclave.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/enclave/NativeEnclave.kt new file mode 100644 index 0000000000..3f780dcd44 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/enclave/NativeEnclave.kt @@ -0,0 +1,100 @@ +package net.corda.attestation.host.sgx.bridge.enclave + +import net.corda.attestation.host.sgx.bridge.system.NativeSgxSystem +import net.corda.attestation.host.sgx.bridge.wrapper.LaunchToken +import net.corda.attestation.host.sgx.bridge.wrapper.NativeWrapper +import net.corda.attestation.host.sgx.bridge.wrapper.newLaunchToken +import net.corda.attestation.host.sgx.enclave.Enclave +import net.corda.attestation.host.sgx.enclave.EnclaveIdentifier +import net.corda.attestation.host.sgx.enclave.SgxStatus +import net.corda.attestation.host.sgx.system.SgxSystem +import java.nio.file.Path +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +/** + * Representation of an enclave on an SGX-enabled system. + * + * @param enclavePath The path to the signed enclave binary. + * @param usePlatformServices Whether or not to leverage Intel's platform + * services (for replay protection in nonce generation, etc.). + */ +open class NativeEnclave @JvmOverloads constructor( + private val enclavePath: Path, + private val usePlatformServices: Boolean = false +) : Enclave { + + /** + * Lock for ensuring single entry of enclave calls. + */ + protected val lock: Lock = ReentrantLock() + + private var enclaveId: EnclaveIdentifier = 0 + + private var launchToken: LaunchToken = newLaunchToken() + + /** + * The SGX-enabled system on which this enclave is running. + */ + override val system: SgxSystem + get() = NativeSgxSystem() + + /** + * The enclave identifier. + */ + override val identifier: EnclaveIdentifier + get() = enclaveId + + /** + * Create enclave used for remote attestation, and consequently for secret + * sealing and unsealing. + */ + override fun create(): SgxStatus { + lock.withLock { + val result = NativeWrapper.createEnclave( + enclavePath.toString(), // The path to the signed enclave binary + usePlatformServices, // Whether to use Intel's services + launchToken // New or pre-existing launch token. + ) + val status = SgxSystem.statusFromCode(result.result) + if (status == SgxStatus.ERROR_ENCLAVE_LOST) { + // If the enclave was lost, we need to destroy it. Not doing so + // will result in EPC memory leakage that could prevent + // subsequent enclaves from loading. + destroy() + } + enclaveId = result.identifier + launchToken = result.token + return status + } + } + + /** + * Destroy enclave if running. + */ + override fun destroy(): Boolean { + lock.withLock { + if (enclaveId != 0L) { + // Only attempt to destroy enclave if one has been created + val result = NativeWrapper.destroyEnclave(enclaveId) + enclaveId = 0L + launchToken = newLaunchToken() + return result + } + return true + } + } + + /** + * Check whether the enclave has been run before or not. + */ + override fun isFresh(): Boolean { + lock.withLock { + val nullByte = 0.toByte() + return identifier == 0L && + (0 until launchToken.size).all { launchToken[it] == nullByte } + } + } + +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/system/NativeSgxSystem.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/system/NativeSgxSystem.kt new file mode 100644 index 0000000000..de0b99e379 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/system/NativeSgxSystem.kt @@ -0,0 +1,41 @@ +package net.corda.attestation.host.sgx.bridge.system + +import net.corda.attestation.host.sgx.bridge.wrapper.NativeWrapper +import net.corda.attestation.host.sgx.enclave.SgxException +import net.corda.attestation.host.sgx.enclave.SgxStatus +import net.corda.attestation.host.sgx.entities.AttestationException +import net.corda.attestation.host.sgx.system.ExtendedGroupIdentifier +import net.corda.attestation.host.sgx.system.SgxDeviceStatus +import net.corda.attestation.host.sgx.system.SgxSystem + +/** + * Query system properties of an SGX-enabled environment. + */ +class NativeSgxSystem : SgxSystem { + + /** + * Check if the client platform is enabled for Intel SGX. The application + * must be run with administrator privileges to get the status + * successfully. + * + * @return The current status of the SGX device. + */ + override fun getDeviceStatus(): SgxDeviceStatus { + return SgxSystem.deviceStatusFromCode(NativeWrapper.getDeviceStatus()) + } + + /** + * Get the extended Intel EPID Group the client uses by default. The key + * used to sign a quote will be a member of the this group. + */ + override fun getExtendedGroupIdentifier(): ExtendedGroupIdentifier { + val result = NativeWrapper.getExtendedGroupIdentifier() + val status = SgxSystem.statusFromCode(result.result) + if (status != SgxStatus.SUCCESS) { + throw SgxException(status) + } + return SgxSystem.extendedGroupIdentifier(result.extendedGroupIdentifier) + ?: throw AttestationException("Invalid extended EPID group") + } + +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/EnclaveResult.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/EnclaveResult.kt new file mode 100644 index 0000000000..07e5ef39d8 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/EnclaveResult.kt @@ -0,0 +1,24 @@ +package net.corda.attestation.host.sgx.bridge.wrapper + +import net.corda.attestation.host.sgx.enclave.EnclaveIdentifier + +/** + * The result of a call to [NativeWrapper.createEnclave]. + */ +class EnclaveResult( + /** + * The identifier of the created enclave, if any. + */ + val identifier: EnclaveIdentifier, + + /** + * The launch token of the enclave. + */ + val token: LaunchToken, + + /** + * The output status code of the enclave creation (of type [SgxStatus] + * downstream). + */ + val result: Long +) diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/ExtendedGroupIdentifierResult.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/ExtendedGroupIdentifierResult.kt new file mode 100644 index 0000000000..2ad0567cd8 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/ExtendedGroupIdentifierResult.kt @@ -0,0 +1,17 @@ +package net.corda.attestation.host.sgx.bridge.wrapper + +/** + * The result of a call to [NativeWrapper.getExtendedGroupIdentifier]. + */ +data class ExtendedGroupIdentifierResult( + /** + * The extended Intel EPID group identifier (of type + * [ExtendedGroupIdentifier] downstream). + */ + val extendedGroupIdentifier: Int, + + /** + * The result of the operation (of type [SgxStatus] downstream). + */ + val result: Long +) diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/InitializationResult.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/InitializationResult.kt new file mode 100644 index 0000000000..61c8cec37c --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/InitializationResult.kt @@ -0,0 +1,18 @@ +package net.corda.attestation.host.sgx.bridge.wrapper + +import net.corda.attestation.host.sgx.AttestationContext + +/** + * The result of a call to [NativeWrapper.initializeRemoteAttestation]. + */ +data class InitializationResult( + /** + * The context returned from the call to sgx_ra_init(). + */ + val context: AttestationContext, + + /** + * The result of the operation (of type [SgxStatus] downstream). + */ + val result: Long +) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/LaunchToken.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/LaunchToken.kt new file mode 100644 index 0000000000..8a3daed10c --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/LaunchToken.kt @@ -0,0 +1,13 @@ +package net.corda.attestation.host.sgx.bridge.wrapper + +/** + * An opaque type used to hold enclave launch information. Used by + * sgx_create_enclave to initialize an enclave. The license is generated by the + * launch enclave. + */ +typealias LaunchToken = ByteArray + +/** + * Create a new launch token. + */ +fun newLaunchToken(): LaunchToken = ByteArray(1024) diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/NativeWrapper.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/NativeWrapper.kt new file mode 100644 index 0000000000..ce757c96c0 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/NativeWrapper.kt @@ -0,0 +1,99 @@ +package net.corda.attestation.host.sgx.bridge.wrapper + +import net.corda.attestation.host.sgx.AttestationContext +import net.corda.attestation.host.sgx.enclave.EnclaveIdentifier +import net.corda.attestation.host.sgx.sealing.ProvisionedSecret +import net.corda.attestation.host.sgx.sealing.SealedSecret + +@Suppress("KDocMissingDocumentation") +internal object NativeWrapper { + + init { + System.loadLibrary("corda_sgx_ra") + } + + // enclave-management.cpp + + @Throws(Exception::class) + external fun getDeviceStatus(): Int + + @Throws(Exception::class) + external fun getExtendedGroupIdentifier(): ExtendedGroupIdentifierResult + + @Throws(Exception::class) + external fun createEnclave( + path: String, + usePlatformServices: Boolean, + token: LaunchToken + ): EnclaveResult + + @Throws(Exception::class) + external fun destroyEnclave( + id: Long + ): Boolean + + // remote-attestation.cpp + + @Throws(Exception::class) + external fun initializeRemoteAttestation( + enclaveIdentifier: EnclaveIdentifier, + usePlatformServices: Boolean, + challengerKey: ByteArray + ): InitializationResult + + @Throws(Exception::class) + external fun finalizeRemoteAttestation( + enclaveIdentifier: EnclaveIdentifier, + context: AttestationContext + ): Long + + @Throws(Exception::class) + external fun getPublicKeyAndGroupIdentifier( + enclaveIdentifier: EnclaveIdentifier, + context: AttestationContext, + maxRetryCount: Int, + retryWaitInSeconds: Int + ): PublicKeyAndGroupIdentifier + + @Throws(Exception::class) + external fun processServiceProviderDetailsAndGenerateQuote( + enclaveIdentifier: EnclaveIdentifier, + context: AttestationContext, + publicKey: ByteArray, + serviceProviderIdentifier: ByteArray, + quoteType: Short, + keyDerivationFunctionIdentifier: Short, + signature: ByteArray, + messageAuthenticationCode: ByteArray, + signatureRevocationSize: Int, + signatureRevocationList:ByteArray, + maxRetryCount: Int, + retryWaitInSeconds: Int + ): QuoteResult + + @Throws(Exception::class) + external fun verifyAttestationResponse( + enclaveIdentifier: EnclaveIdentifier, + context: AttestationContext, + attestationResultMessage: ByteArray?, + aesCmac: ByteArray, + secret: ByteArray, + gcmIV: ByteArray, + gcmMac: ByteArray + ): VerificationResult + + // sealing.cpp + + @Throws(Exception::class) + external fun sealSecret( + enclaveIdentifier: EnclaveIdentifier, + provisionedSecret: ProvisionedSecret + ): SealingOperationResult + + @Throws(Exception::class) + external fun unsealSecret( + enclaveIdentifier: EnclaveIdentifier, + sealedSecret: SealedSecret + ): Long + +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/PubicKeyAndGroupIdentifier.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/PubicKeyAndGroupIdentifier.kt new file mode 100644 index 0000000000..92308c5403 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/PubicKeyAndGroupIdentifier.kt @@ -0,0 +1,24 @@ +package net.corda.attestation.host.sgx.bridge.wrapper + +import net.corda.attestation.host.sgx.system.GroupIdentifier + +/** + * The result of a call to [NativeWrapper.getPublicKeyAndGroupIdentifier]. + */ +class PublicKeyAndGroupIdentifier( + /** + * The public elliptic curve key of the application enclave (of type + * [ECKey] downstream). + */ + val publicKey: ByteArray, + + /** + * The identifier of the EPID group to which the enclave belongs. + */ + val groupIdentifier: GroupIdentifier, + + /** + * The result of the operation (of type [SgxStatus] downstream). + */ + val result: Long +) diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/QuoteResult.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/QuoteResult.kt new file mode 100644 index 0000000000..f39c520cfb --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/QuoteResult.kt @@ -0,0 +1,40 @@ +package net.corda.attestation.host.sgx.bridge.wrapper + +/** + * The result of a call to + * [NativeWrapper.processServiceProviderDetailsAndGenerateQuote]. + */ +class QuoteResult( + /** + * The 128-bit AES-CMAC generated by the application enclave. See + * [sgx_ra_msg3_t](https://software.intel.com/en-us/node/709238) for + * more details on its derivation. + */ + val messageAuthenticationCode: ByteArray, + + /** + * The public elliptic curve key of the application enclave (of type + * [ECKey] downstream). + */ + val publicKey: ByteArray, + + /** + * Security property of the Intel SGX Platform Service. If the Intel + * SGX Platform Service security property information is not required + * in the remote attestation and key exchange process, this field will + * be all zeros. The buffer is 256 bytes long. + */ + val securityProperties: ByteArray, + + /** + * Quote returned from sgx_get_quote. More details about how the quote + * is derived can be found in Intel's documentation: + * [sgx_ra_msg3_t](https://software.intel.com/en-us/node/709238) + */ + val payload: ByteArray, + + /** + * The result of the operation (of type [SgxStatus] downstream). + */ + val result: Long +) diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/SealingOperationResult.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/SealingOperationResult.kt new file mode 100644 index 0000000000..f9dd616be4 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/SealingOperationResult.kt @@ -0,0 +1,19 @@ +package net.corda.attestation.host.sgx.bridge.wrapper + +import net.corda.attestation.host.sgx.sealing.SealedSecret + +/** + * The result of a call to [NativeWrapper.sealSecret]. + */ +class SealingOperationResult( + /** + * The sealed secret, if any. + */ + val sealedSecret: SealedSecret, + + /** + * The output result of the operation (of type [SealingResult] + * downstream). + */ + val result: Long +) diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/VerificationResult.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/VerificationResult.kt new file mode 100644 index 0000000000..88d01750a4 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/bridge/wrapper/VerificationResult.kt @@ -0,0 +1,25 @@ +package net.corda.attestation.host.sgx.bridge.wrapper + +import net.corda.attestation.host.sgx.sealing.SealedSecret + +/** + * The result of a call to [NativeWrapper.verifyAttestationResponse]. + */ +class VerificationResult( + /** + * The sealed secret returned if the attestation result was + * successfully verified. + */ + val secret: SealedSecret, + + /** + * The outcome of the validation of the CMAC over the attestation + * result message (of type [SgxStatus] downstream). + */ + val cmacValidationStatus: Long, + + /** + * The result of the operation (of type [SgxStatus] downstream). + */ + val result: Long +) diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/enclave/ECKey.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/enclave/ECKey.kt new file mode 100644 index 0000000000..812de19c30 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/enclave/ECKey.kt @@ -0,0 +1,43 @@ +package net.corda.attestation.host.sgx.enclave + +/** + * Public key based on NIST P-256 elliptic curve + * + * @param componentX The 256-bit X component of the key. + * @param componentY The 256-bit Y component of the key. + */ +class ECKey( + componentX: ByteArray, + componentY: ByteArray +) { + + /** + * The bytes constituting the elliptic curve public key. + */ + val bytes: ByteArray = componentX.plus(componentY) + + companion object { + private const val KEY_SIZE = 64 + + /** + * Create a public key from a byte array. + * + * @param bytes The 64 bytes forming the NIST P-256 elliptic curve. + */ + fun fromBytes(bytes: ByteArray): ECKey { + if (bytes.size != KEY_SIZE) { + throw Exception("Expected $KEY_SIZE bytes, but got ${bytes.size}") + } + val componentX = bytes.copyOfRange(0, KEY_SIZE / 2) + val componentY = bytes.copyOfRange(KEY_SIZE / 2, KEY_SIZE) + return ECKey(componentX, componentY) + } + + } + +} + +/** + * Short-hand generator for supplying a constant X or Y component. + */ +fun ecKeyComponent(vararg bytes: Int) = bytes.map { it.toByte() }.toByteArray() diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/enclave/Enclave.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/enclave/Enclave.kt new file mode 100644 index 0000000000..b7bb56ab34 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/enclave/Enclave.kt @@ -0,0 +1,87 @@ +package net.corda.attestation.host.sgx.enclave + +import net.corda.attestation.host.sgx.system.SgxSystem + +/** + * The identifier of an enclave. + */ +typealias EnclaveIdentifier = Long + +/** + * Representation an enclave. + */ +interface Enclave { + /** + * The SGX-enabled system on which this enclave is running. + */ + val system: SgxSystem + + /** + * The enclave identifier. + */ + val identifier: EnclaveIdentifier + + /** + * Create enclave used for remote attestation, and consequently for secret + * sealing and unsealing. + */ + fun create(): SgxStatus + + /** + * Destroy enclave if running. + */ + fun destroy(): Boolean + + /** + * Destroy and re-create the enclave. This is normally done if the enclave + * is lost due to a power transition or similar events. + */ + fun recreate(): SgxStatus { + destroy() + return create() + } + + /** + * Check whether the enclave has been run before or not. + */ + fun isFresh(): Boolean + + /** + * Check whether an enclave has already been created and initialized. + * Otherwise, try to create required enclave or re-create one in the cases + * where an older one has been lost due to a power transition or similar. + * + * @throws SgxException If unable to create enclave. + * @throws SgxUnavailableException If SGX is unavailable or for some reason + * disabled. + */ + fun activate() { + // First, make sure SGX is available and that it is enabled. Under some + // circumstances, a reboot may be required to enable SGX. In either + // case, as long as the extensions aren't enabled, an + // [SgxUnavailableException] will be thrown. + system.ensureAvailable() + + // If the enclave has already been created and is active, we are good + // to proceed. + var status = create() + if (status == SgxStatus.SUCCESS) { + return + } + + // Check if an attestation enclave was previously created. If it was + // and it is no longer available, recreate one to the same + // specification. Note: Losing an enclave is normally the result of a + // power transition. + if (status == SgxStatus.ERROR_ENCLAVE_LOST) { + status = recreate() + if (status != SgxStatus.SUCCESS) { + throw SgxException(status, identifier) + } + return + } + + // Some other error occurred, let's abort + throw SgxException(status, identifier) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/enclave/SgxException.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/enclave/SgxException.kt new file mode 100644 index 0000000000..e6d1f005b5 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/enclave/SgxException.kt @@ -0,0 +1,37 @@ +package net.corda.attestation.host.sgx.enclave + +import net.corda.attestation.host.HostException +import net.corda.attestation.host.sgx.AttestationContext + +/** + * Exception raised whenever there's a problem creating, destroying or + * interacting with an enclave or SGX. + * + * @property status The status or outcome of an SGX operation. + * @property enclaveIdentifier The identifier of the enclave, if available. + * @property context The established remote attestation context, if available. + */ +class SgxException @JvmOverloads constructor( + val status: SgxStatus, + private val enclaveIdentifier: EnclaveIdentifier? = null, + private val context: AttestationContext? = null +) : HostException(status.message) { + /** + * Human readable representation of the exception. + */ + override fun toString(): String { + val message = super.toString() + val identifierString = if (enclaveIdentifier != null) { + "0x${java.lang.Long.toHexString(enclaveIdentifier)}" + } else { + "null" + } + val contextString = if (context != null) { + "0x${java.lang.Integer.toHexString(context)}" + } else { + "null" + } + return "$message (enclave=$identifierString, " + + "context=$contextString, status=${status.name})" + } +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/enclave/SgxStatus.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/enclave/SgxStatus.kt new file mode 100644 index 0000000000..bda173fa3e --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/enclave/SgxStatus.kt @@ -0,0 +1,289 @@ +package net.corda.attestation.host.sgx.enclave + +/** + * The status of an SGX operation + * + * @property code The native status code returned from the SGX API. + * @property message A human readable representation of the state. + */ +enum class SgxStatus(val code: Long, val message: String) { + /** + * Success. + */ + SUCCESS(0x0000, "Success"), + + /** + * Unexpected error. + */ + ERROR_UNEXPECTED(0x0001, "Unexpected error"), + + /** + * The parameter is incorrect. + */ + ERROR_INVALID_PARAMETER(0x0002, "The parameter is incorrect"), + + /** + * Not enough memory is available to complete this operation. + */ + ERROR_OUT_OF_MEMORY(0x0003, "Not enough memory is available to complete this operation"), + + /** + * Enclave lost after power transition or used in child process created by linux:fork(). + */ + ERROR_ENCLAVE_LOST(0x0004, "Enclave lost after power transition or used in child process created by linux:fork()"), + + /** + * SGX API is invoked in incorrect order or state. + */ + ERROR_INVALID_STATE(0x0005, "SGX API is invoked in incorrect order or state"), + + /** + * The ECALL/OCALL index is invalid. + */ + ERROR_INVALID_FUNCTION(0x1001, "The ecall/ocall index is invalid"), + + /** + * The enclave is out of TCS. + */ + ERROR_OUT_OF_TCS(0x1003, "The enclave is out of TCS"), + + /** + * The enclave is crashed. + */ + ERROR_ENCLAVE_CRASHED(0x1006, "The enclave is crashed"), + + /** + * The ECALL is not allowed at this time, e.g. ECALL is blocked by the dynamic entry table, or nested ECALL is not allowed during initialization. + */ + ERROR_ECALL_NOT_ALLOWED(0x1007, "The ECALL is not allowed at this time, e.g. ECALL is blocked by the dynamic entry table, or nested ECALL is not allowed during initialization"), + + /** + * The OCALL is not allowed at this time, e.g. OCALL is not allowed during exception handling. + */ + ERROR_OCALL_NOT_ALLOWED(0x1008, "The OCALL is not allowed at this time, e.g. OCALL is not allowed during exception handling"), + + /** + * The enclave is running out of stack. + */ + ERROR_STACK_OVERRUN(0x1009, "The enclave is running out of stack"), + + /** + * The enclave image has one or more undefined symbols. + */ + ERROR_UNDEFINED_SYMBOL(0x2000, "The enclave image has one or more undefined symbols"), + + /** + * The enclave image is not correct. + */ + ERROR_INVALID_ENCLAVE(0x2001, "The enclave image is not correct."), + + /** + * The enclave identifier is invalid. + */ + ERROR_INVALID_ENCLAVE_ID(0x2002, "The enclave identifier is invalid."), + + /** + * The signature is invalid. + */ + ERROR_INVALID_SIGNATURE(0x2003, "The signature is invalid"), + + /** + * The enclave is signed as product enclave, and can not be created as debuggable enclave. + */ + ERROR_NDEBUG_ENCLAVE(0x2004, "The enclave is signed as product enclave, and can not be created as debuggable enclave"), + + /** + * Not enough EPC is available to load the enclave. + */ + ERROR_OUT_OF_EPC(0x2005, "Not enough EPC is available to load the enclave"), + + /** + * Cannot open SGX device. + */ + ERROR_NO_DEVICE(0x2006, "Cannot open SGX device"), + + /** + * Page mapping failed in driver. + */ + ERROR_MEMORY_MAP_CONFLICT(0x2007, "Page mapping failed in driver"), + + /** + * The metadata is incorrect. + */ + ERROR_INVALID_METADATA(0x2009, "The metadata is incorrect"), + + /** + * Device is busy, mostly EINIT failed. + */ + ERROR_DEVICE_BUSY(0x200c, "Device is busy, mostly EINIT failed"), + + /** + * Metadata version is inconsistent between uRTS and sgx_sign, or uRTS is incompatible with current platform. + */ + ERROR_INVALID_VERSION(0x200d, "Metadata version is inconsistent between uRTS and sgx_sign, or uRTS is incompatible with current platform"), + + /** + * The target enclave 32/64 bit mode or SIM/HW mode is incompatible with the mode of current uRTS. + */ + ERROR_MODE_INCOMPATIBLE(0x200e, "The target enclave 32/64 bit mode or SIM/HW mode is incompatible with the mode of current uRTS"), + + /** + * Cannot open enclave file. + */ + ERROR_ENCLAVE_FILE_ACCESS(0x200f, "Cannot open enclave file"), + + /** + * The MiscSelct/MiscMask settings are not correct. + */ + ERROR_INVALID_MISC(0x2010, "The MiscSelct/MiscMask settings are not correct"), + + /** + * Indicates verification error for reports, sealed data, etc. + */ + ERROR_MAC_MISMATCH(0x3001, "Indicates verification error for reports, sealed data, etc"), + + /** + * The enclave is not authorized. + */ + ERROR_INVALID_ATTRIBUTE(0x3002, "The enclave is not authorized"), + + /** + * The CPU SVN is beyond platform's CPU SVN value. + */ + ERROR_INVALID_CPUSVN(0x3003, "The CPU SVN is beyond platform's CPU SVN value"), + + /** + * The ISV SVN is greater than the enclave's ISV SVN. + */ + ERROR_INVALID_ISVSVN(0x3004, "The ISV SVN is greater than the enclave's ISV SVN"), + + /** + * The key name is an unsupported value. + */ + ERROR_INVALID_KEYNAME(0x3005, "The key name is an unsupported value"), + + /** + * Indicates AESM didn't respond or the requested service is not supported. + */ + ERROR_SERVICE_UNAVAILABLE(0x4001, "Indicates AESM didn't respond or the requested service is not supported"), + + /** + * The request to AESM timed out. + */ + ERROR_SERVICE_TIMEOUT(0x4002, "The request to AESM timed out"), + + /** + * Indicates EPID blob verification error. + */ + ERROR_AE_INVALID_EPIDBLOB(0x4003, "Indicates EPID blob verification error"), + + /** + * Enclave has no privilege to get launch token. + */ + ERROR_SERVICE_INVALID_PRIVILEGE(0x4004, "Enclave has no privilege to get launch token"), + + /** + * The EPID group membership is revoked. + */ + ERROR_EPID_MEMBER_REVOKED(0x4005, "The EPID group membership is revoked"), + + /** + * SGX needs to be updated. + */ + ERROR_UPDATE_NEEDED(0x4006, "SGX needs to be updated"), + + /** + * Network connection or proxy settings issue is encountered. + */ + ERROR_NETWORK_FAILURE(0x4007, "Network connection or proxy settings issue is encountered"), + + /** + * Session is invalid or ended by server. + */ + ERROR_AE_SESSION_INVALID(0x4008, "Session is invalid or ended by server"), + + /** + * Requested service is temporarily not available. + */ + ERROR_BUSY(0x400a, "Requested service is temporarily not available"), + + /** + * Monotonic Counter doesn't exist or has been invalidated. + */ + ERROR_MC_NOT_FOUND(0x400c, "Monotonic Counter doesn't exist or has been invalidated"), + + /** + * Caller doesn't have access to specified VMC. + */ + ERROR_MC_NO_ACCESS_RIGHT(0x400d, "Caller doesn't have access to specified VMC"), + + /** + * Monotonic counters are used up. + */ + ERROR_MC_USED_UP(0x400e, "Monotonic counters are used up"), + + /** + * Monotonic counters exceeds quota limitation. + */ + ERROR_MC_OVER_QUOTA(0x400f, "Monotonic counters exceeds quota limitation"), + + /** + * Key derivation function doesn't match during key exchange. + */ + ERROR_KDF_MISMATCH(0x4011, "Key derivation function doesn't match during key exchange"), + + /** + * EPID provisioning failed due to platform not being recognized by backend server. + */ + ERROR_UNRECOGNIZED_PLATFORM(0x4012, "EPID provisioning failed due to platform not being recognized by backend server"), + + /** + * Not privileged to perform this operation. + */ + ERROR_NO_PRIVILEGE(0x5002, "Not privileged to perform this operation"), + + /** + * The file is in a bad state. + */ + ERROR_FILE_BAD_STATUS(0x7001, "The file is in a bad state, run sgx_clearerr to try and fix it"), + + /** + * The KeyID field is all zeros, cannot re-generate the encryption key. + */ + ERROR_FILE_NO_KEY_ID(0x7002, "The KeyID field is all zeros, cannot re-generate the encryption key"), + + /** + * The current file name is different then the original file name (not allowed due to potential substitution attack). + */ + ERROR_FILE_NAME_MISMATCH(0x7003, "The current file name is different then the original file name (not allowed due to potential substitution attack)"), + + /** + * The file is not an SGX file. + */ + ERROR_FILE_NOT_SGX_FILE(0x7004, "The file is not an SGX file"), + + /** + * A recovery file cannot be opened, so flush operation cannot continue (only used when no EXXX is returned). + */ + ERROR_FILE_CANT_OPEN_RECOVERY_FILE(0x7005, "A recovery file cannot be opened, so flush operation cannot continue (only used when no EXXX is returned)"), + + /** + * A recovery file cannot be written, so flush operation cannot continue (only used when no EXXX is returned). + */ + ERROR_FILE_CANT_WRITE_RECOVERY_FILE(0x7006, "A recovery file cannot be written, so flush operation cannot continue (only used when no EXXX is returned)"), + + /** + * When opening the file, recovery is needed, but the recovery process failed. + */ + ERROR_FILE_RECOVERY_NEEDED(0x7007, "When opening the file, recovery is needed, but the recovery process failed"), + + /** + * fflush operation (to disk) failed (only used when no EXXX is returned). + */ + ERROR_FILE_FLUSH_FAILED(0x7008, "fflush operation (to disk) failed (only used when no EXXX is returned)"), + + /** + * fclose operation (to disk) failed (only used when no EXXX is returned). + */ + ERROR_FILE_CLOSE_FAILED(0x7009, "fclose operation (to disk) failed (only used when no EXXX is returned)"), +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/AttestationException.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/AttestationException.kt new file mode 100644 index 0000000000..3c0ec76ba9 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/AttestationException.kt @@ -0,0 +1,8 @@ +package net.corda.attestation.host.sgx.entities + +import net.corda.attestation.host.HostException + +/** + * Exception thrown during remote attestation. + */ +class AttestationException(message: String) : HostException(message) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/AttestationResult.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/AttestationResult.kt new file mode 100644 index 0000000000..2272aa2dc4 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/AttestationResult.kt @@ -0,0 +1,33 @@ +package net.corda.attestation.host.sgx.entities + +import net.corda.attestation.host.sgx.sealing.ProvisionedSecret + +/** + * The outcome of a remote attestation process. + */ +class AttestationResult( + /** + * The received attestation result message. + */ + val attestationResultMessage: ByteArray?, + + /** + * The CMAC over the attestation result message. + */ + val aesCMAC: ByteArray, + + /** + * Provisioned, encrypted secret if the attestation was successful. + */ + val secret: ProvisionedSecret, + + /** + * The initialization vector used as part of the decryption. + */ + val secretIV: ByteArray, + + /** + * The GCM MAC returned as part of the attestation response. + */ + val secretHash: ByteArray +) diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/AttestationStatus.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/AttestationStatus.kt new file mode 100644 index 0000000000..bb93dc7134 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/AttestationStatus.kt @@ -0,0 +1,20 @@ +package net.corda.attestation.host.sgx.entities + +/** + * The status of the remote attestation process. + * + * @property message A human readable representation of the state. + */ +enum class AttestationStatus(val message: String) { + + /** + * The remote attestation was successful. + */ + SUCCESS("Remote attestation succeeded."), + + /** + * The remote attestation failed. + */ + FAIL("Remote attestation failed."), + +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/Quote.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/Quote.kt new file mode 100644 index 0000000000..7a7ce6e8c6 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/Quote.kt @@ -0,0 +1,36 @@ +package net.corda.attestation.host.sgx.entities + +import net.corda.attestation.host.sgx.enclave.ECKey + +/** + * Signed report from the application enclave. The corresponds to message 3 in + * the Intel remote attestation flow. + */ +class Quote( + /** + * The 128-bit AES-CMAC generated by the application enclave. See + * [sgx_ra_msg3_t](https://software.intel.com/en-us/node/709238) for + * more details on its derivation. + */ + val messageAuthenticationCode: ByteArray, + + /** + * The public elliptic curve key of the application enclave. + */ + val publicKey: ECKey, + + /** + * Security property of the Intel SGX Platform Service. If the Intel + * SGX Platform Service security property information is not required + * in the remote attestation and key exchange process, this field will + * be all zeros. The buffer is 256 bytes long. + */ + val securityProperties: ByteArray, + + /** + * Quote returned from sgx_get_quote. More details about how the quote + * is derived can be found in Intel's documentation: + * [sgx_ra_msg3_t](https://software.intel.com/en-us/node/709238) + */ + val payload: ByteArray +) diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/QuoteStatus.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/QuoteStatus.kt new file mode 100644 index 0000000000..724b04fcde --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/QuoteStatus.kt @@ -0,0 +1,71 @@ +package net.corda.attestation.host.sgx.entities + +/** + * The status of an enclave's quote as it has been processed by the Intel + * Attestation service. + * + * @property description A human-readable description of the status code. + */ +enum class QuoteStatus(val description: String) { + + /** + * EPID signature of the ISV enclave quote was verified correctly and the + * TCB level of the SGX platform is up-to- date. + */ + OK("EPID signature of the ISV enclave quote was verified correctly and " + + "the TCB level of the SGX platform is up-to-date."), + + /** + * EPID signature of the ISV enclave quote was invalid. The content of the + * quote is not trustworthy. + */ + SIGNATURE_INVALID("EPID signature of the ISV enclave quote was invalid."), + + /** + * The EPID group has been revoked. When this value is returned, the + * revocation reason field of the Attestation Verification Report will + * contain a revocation reason code for this EPID group as reported in the + * EPID Group CRL. The content of the quote is not trustworthy. + */ + GROUP_REVOKED("The EPID group has been revoked."), + + /** + * The EPID private key used to sign the quote has been revoked by + * signature. The content of the quote is not trustworthy. + */ + SIGNATURE_REVOKED("The EPID private key used to sign the quote has been " + + "revoked by signature."), + + /** + * The EPID private key used to sign the quote has been directly revoked + * (not by signature). The content of the quote is not trustworthy. + */ + KEY_REVOKED("The EPID private key used to sign the quote has been " + + "directly revoked (not by signature)."), + + /** + * SigRL version in ISV enclave quote does not match the most recent + * version of the SigRL. In rare situations, after SP retrieved the SigRL + * from IAS and provided it to the platform, a newer version of the SigRL + * is made available. As a result, the Attestation Verification Report will + * indicate SIGRL_VERSION_MISMATCH. SP can retrieve the most recent version + * of SigRL from the IAS and request the platform to perform remote + * attestation again with the most recent version of SigRL. If the platform + * keeps failing to provide a valid quote matching with the most recent + * version of the SigRL, the content of the quote is not trustworthy. + */ + SIGRL_VERSION_MISMATCH("SigRL version in ISV enclave quote does not " + + "match the most recent version of the SigRL."), + + /** + * The EPID signature of the ISV enclave quote has been verified correctly, + * but the TCB level of SGX platform is outdated. The platform has not been + * identified as compromised and thus it is not revoked. It is up to the + * Service Provider to decide whether or not to trust the content of the + * quote. + */ + GROUP_OUT_OF_DATE("The EPID signature of the ISV enclave quote has " + + "been verified correctly, but the TCB level of SGX platform " + + "is outdated.") + +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/QuoteType.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/QuoteType.kt new file mode 100644 index 0000000000..c86c31b9d5 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/entities/QuoteType.kt @@ -0,0 +1,29 @@ +package net.corda.attestation.host.sgx.entities + +/** + * The type of quote used in the attestation. + * + * @property value The native value of the quote type. + */ +enum class QuoteType(val value: Short) { + /** + * Unlinkable is random value-based, meaning that having two quotes you + * cannot identify whether they are from the same source or not. + */ + UNLINKABLE(0), + + /** + * Linkable is name-based, meaning that having two quotes you can identify + * if they come from the same enclave or not. Note that you can not + * determine which enclave it is though. + */ + LINKABLE(1); + + companion object { + fun forValue(value: Short): QuoteType = when(value) { + 0.toShort() -> UNLINKABLE + 1.toShort() -> LINKABLE + else -> throw IllegalArgumentException("Unknown QuoteType '$value'") + } + } +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/sealing/SealingException.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/sealing/SealingException.kt new file mode 100644 index 0000000000..619ca0ea63 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/sealing/SealingException.kt @@ -0,0 +1,10 @@ +package net.corda.attestation.host.sgx.sealing + +/** + * Exception raised whenever there is a problem with a sealing operation. + * + * @property status The status or outcome of the operation. + */ +class SealingException( + val status: SealingResult +) : Exception(status.message) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/sealing/SealingResult.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/sealing/SealingResult.kt new file mode 100644 index 0000000000..79457d40fd --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/sealing/SealingResult.kt @@ -0,0 +1,21 @@ +package net.corda.attestation.host.sgx.sealing + +/** + * The outcome of a performed sealing operation. + * + * @property code The underlying status code. + * @property message A human readable representation of the state. + */ +enum class SealingResult(val code: Long, val message: String) { + + /** + * Sealing was successful. + */ + SUCCESS(0, "Sealing was successful."), + + /** + * Sealing was unsuccessful. + */ + FAIL(1, "Failed to seal secret."), + +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/sealing/SecretManager.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/sealing/SecretManager.kt new file mode 100644 index 0000000000..72a5ad4c63 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/sealing/SecretManager.kt @@ -0,0 +1,69 @@ +package net.corda.attestation.host.sgx.sealing + +import net.corda.attestation.host.sgx.AttestationEnclave +import net.corda.attestation.host.sgx.enclave.SgxStatus + +/** + * Representation of a provisioned secret. + */ +typealias ProvisionedSecret = ByteArray + +/** + * Representation of a sealed secret. + */ +typealias SealedSecret = ByteArray + +/** + * Manager for storing and managing secrets. + * + * @property enclave The facilitating attestation enclave. + */ +open class SecretManager( + private val enclave: AttestationEnclave +) { + + /** + * Check that an existing secret (if available) is valid and hasn't + * expired. + */ + fun isValid(): Boolean { + // First off, check whether we actually have access to a secret + if (!hasSecret()) { + return false + } + + // Then, ensure that we can unseal the secret, that the lease has not + // expired, etc. + val result = unsealSecret() + return result == SgxStatus.SUCCESS + } + + /** + * Retrieve sealed secret, or null if not available. + */ + open fun getSealedSecret(): SealedSecret? = null + + /** + * Persist the sealed secret to disk or similar, for future use. + * + * @param sealedSecret The secret sealed to the enclave's context. + */ + open fun persistSealedSecret(sealedSecret: SealedSecret) { } + + /** + * Check whether we have a secret persisted already. + */ + private fun hasSecret(): Boolean { + return getSealedSecret() != null + } + + /** + * Check if we can unseal an existing secret. + */ + private fun unsealSecret(): SgxStatus { + val sealedSecret = getSealedSecret() + ?: return SgxStatus.ERROR_INVALID_PARAMETER + return enclave.unseal(sealedSecret) + } + +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/ExtendedGroupIdentifier.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/ExtendedGroupIdentifier.kt new file mode 100644 index 0000000000..1b288787dd --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/ExtendedGroupIdentifier.kt @@ -0,0 +1,10 @@ +package net.corda.attestation.host.sgx.system + +enum class ExtendedGroupIdentifier(val value: Int) { + + /** + * Indicates that we are using Intel's Attestation Service. + */ + INTEL(0), + +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/GroupIdentifier.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/GroupIdentifier.kt new file mode 100644 index 0000000000..f5fa3e34a8 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/GroupIdentifier.kt @@ -0,0 +1,13 @@ +package net.corda.attestation.host.sgx.system + +/** + * The Intel EPID group identifier. + */ +typealias GroupIdentifier = Int + +/** + * Get the string representation of the group identifier. + */ +fun GroupIdentifier.value(): String = Integer + .toHexString(this) + .padStart(8, '0') diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/SgxDeviceStatus.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/SgxDeviceStatus.kt new file mode 100644 index 0000000000..1df6379839 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/SgxDeviceStatus.kt @@ -0,0 +1,60 @@ +package net.corda.attestation.host.sgx.system + +/** + * The status of the SGX device on the current machine. + * + * @property code The native status code returned from the SGX API. + * @property message A human readable representation of the state. + */ +enum class SgxDeviceStatus(val code: Int, val message: String) { + /** + * The platform is enabled for Intel SGX. + */ + ENABLED(0, "SGX device is available and enabled"), + + /** + * This platform is disabled for Intel SGX. It is configured to be enabled + * after the next reboot. + */ + DISABLED_REBOOT_REQUIRED(1, "Rebooted required"), + + /** + * The operating system does not support UEFI enabling of the Intel SGX + * device. If UEFI is supported by the operating system in general, but + * support for enabling the Intel SGX device does not exist, this function + * returns the more general [DISABLED]. + */ + DISABLED_LEGACY_OS(2, "Operating system with EFI support required"), + + /** + * This platform is disabled for Intel SGX. More details about the ability + * to enable Intel SGX are unavailable. There may be cases when Intel SGX + * can be enabled manually in the BIOS. + */ + DISABLED(3, "SGX device is not available"), + + /** + * The platform is disabled for Intel SGX but can be enabled using the + * Software Control Interface. + */ + DISABLED_SCI_AVAILABLE(4, "Needs enabling using the SCI"), + + /** + * The platform is disabled for Intel SGX but can be enabled manually + * through the BIOS menu. The Software Control Interface is not available + * to enable Intel SGX on this platform. + */ + DISABLED_MANUAL_ENABLE(5, "Needs enabling through the BIOS menu"), + + /** + * The detected version of Windows 10 is incompatible with Hyper-V. Intel + * SGX cannot be enabled on the target machine unless Hyper-V is disabled. + */ + DISABLED_HYPERV_ENABLED(6, "Hyper-V must be disabled"), + + + /** + * Intel SGX is not supported by this processor. + */ + DISABLED_UNSUPPORTED_CPU(7, "SGX not supported by processor"), +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/SgxSystem.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/SgxSystem.kt new file mode 100644 index 0000000000..fa9c12343f --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/SgxSystem.kt @@ -0,0 +1,76 @@ +package net.corda.attestation.host.sgx.system + +import net.corda.attestation.host.sgx.enclave.SgxStatus +import net.corda.attestation.host.sgx.entities.AttestationException +import net.corda.attestation.host.sgx.entities.QuoteStatus + +/** + * Query system properties of an SGX-enabled environment. + */ +interface SgxSystem { + + companion object { + + /** + * Get [SgxDeviceStatus] from numeric code. + */ + fun deviceStatusFromCode(code: Int): SgxDeviceStatus = + enumValues().first { it.code == code } + + /** + * Get [SgxStatus] from numeric code. + */ + fun statusFromCode(code: Long): SgxStatus = + enumValues().first { it.code == code } + + /** + * Get [ExtendedGroupIdentifier] from a numeric identifier. + */ + fun extendedGroupIdentifier(id: Int): ExtendedGroupIdentifier? = + enumValues(). + firstOrNull { it.value == id } + + /** + * Get [QuoteStatus] from string. + */ + fun quoteStatusFromString( + code: String + ): QuoteStatus { + return enumValues() + .firstOrNull { it.name == code } + ?: throw AttestationException( + "Invalid quote status code '$code'") + } + + } + + /** + * Check if the client platform is enabled for Intel SGX. The application + * must be run with administrator privileges to get the status + * successfully. + * + * @return The current status of the SGX device. + */ + fun getDeviceStatus(): SgxDeviceStatus + + /** + * Get the extended Intel EPID Group the client uses by default. The key + * used to sign a quote will be a member of the this group. + */ + fun getExtendedGroupIdentifier(): ExtendedGroupIdentifier + + /** + * Check if SGX is available and enabled in the current runtime + * environment. + * + * @throws SgxUnavailableException If SGX is unavailable or for some reason + * disabled. + */ + fun ensureAvailable() { + val status = getDeviceStatus() + if (status != SgxDeviceStatus.ENABLED) { + throw SgxUnavailableException(status) + } + } + +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/SgxUnavailableException.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/SgxUnavailableException.kt new file mode 100644 index 0000000000..4f686c1002 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/sgx/system/SgxUnavailableException.kt @@ -0,0 +1,11 @@ +package net.corda.attestation.host.sgx.system + + +/** + * Exception raised if SGX for some reason is unavailable on the system. + * + * @property status The status of the SGX device. + */ +class SgxUnavailableException( + val status: SgxDeviceStatus +) : Exception(status.message) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/AttestationHost.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/AttestationHost.kt new file mode 100644 index 0000000000..d628de73a2 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/AttestationHost.kt @@ -0,0 +1,352 @@ +package net.corda.attestation.host.web + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.* +import net.corda.attestation.host.sgx.AttestationEnclave +import net.corda.attestation.host.sgx.ChallengerDetails +import net.corda.attestation.host.sgx.bridge.NativeAttestationEnclave +import net.corda.attestation.host.sgx.enclave.ECKey +import net.corda.attestation.host.sgx.enclave.SgxException +import net.corda.attestation.host.sgx.enclave.SgxStatus +import net.corda.attestation.host.sgx.entities.AttestationResult +import net.corda.attestation.host.sgx.entities.QuoteType +import net.corda.attestation.host.sgx.system.value +import net.corda.attestation.message.* +import org.apache.http.HttpResponse +import org.apache.http.client.CookieStore +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpGet +import org.apache.http.client.methods.HttpPost +import org.apache.http.client.protocol.HttpClientContext +import org.apache.http.config.SocketConfig +import org.apache.http.entity.ContentType.APPLICATION_JSON +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.BasicCookieStore +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.util.EntityUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.URI +import java.nio.file.Paths +import java.security.* +import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec +import java.util.* +import java.util.concurrent.ExecutorService +import javax.servlet.ServletContext +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse.* +import javax.servlet.http.HttpSession +import javax.ws.rs.* +import javax.ws.rs.container.AsyncResponse +import javax.ws.rs.container.Suspended +import javax.ws.rs.core.Context +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import javax.ws.rs.core.UriBuilder + +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@Path("/host") +class AttestationHost { + private companion object { + @JvmStatic + private val log: Logger = LoggerFactory.getLogger(AttestationHost::class.java) + + private const val AES_CMAC_FUNC = 1.toShort() + private const val maxNonceLength = 32 + private const val enclaveAttr = "Enclave" + private const val challengerKeyAttr = "Challenger-Key" + private const val challengerNonceAttr = "Challenger-Nonce" + private const val challengeResponseAttr = "Challenge-Response" + private const val platformGIDAttr = "Platform-GroupID" + private const val conversationAttr = "Conversation-Cookies" + private val isvHost: URI = URI.create("http://${System.getProperty("isv.host", "localhost:8080")}") + private val enclavePath = Paths.get(System.getProperty("corda.sgx.enclave.path", ".")) + .resolve("corda_sgx_ra_enclave.so") + + private val mapper = ObjectMapper().registerModule(JavaTimeModule()) + private val keyFactory: KeyFactory = KeyFactory.getInstance("EC") + private val ecParameters: ECParameterSpec + private val crypto = Crypto() + + private val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + private val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + + init { + ecParameters = (crypto.generateKeyPair().public as ECPublicKey).params + log.info("Elliptic Curve Parameters: {}", ecParameters) + } + } + + @field:Context + private lateinit var httpRequest: HttpServletRequest + + @field:Context + private lateinit var servletContext: ServletContext + + private val executor: ExecutorService by lazy { + servletContext.getAttribute(ThreadPoolListener.threadPoolAttr) as ExecutorService + } + + @POST + @Path("/challenge") + fun provision(challenge: ChallengeRequest?, @Suspended async: AsyncResponse) { + if (challenge == null) { + throw BadRequestException(responseOf("Message is missing", SC_BAD_REQUEST)) + } + val session = httpRequest.session + log.info("Challenge - HTTP Session: {}", session.id) + + validateNonce(challenge.nonce) + + val challengerPublicKey = try { + keyFactory.generatePublic(challenge.gc.toBigEndianKeySpec(ecParameters)) as ECPublicKey + } catch (e: IllegalArgumentException) { + throw BadRequestException(responseOf(e.message ?: "", SC_BAD_REQUEST)) + } + session.setAttribute(challengerKeyAttr, challengerPublicKey) + session.setAttribute(challengerNonceAttr, challenge.nonce) + + val enclave = NativeAttestationEnclave(enclavePath).apply { + activate() + initializeKeyExchange(ECKey.fromBytes(challenge.gc)) + } + session.setAttribute(enclaveAttr, enclave) + + // Remember the HTTP session ID so that we can maintain a conversation + // across multiple requests between three HTTP servers. + val cookies = BasicCookieStore() + session.setAttribute(conversationAttr, cookies) + + executor.submit { + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + // Request basic service information from ISV. This data doesn't change. + val serviceResponse: ServiceResponse = try { + createHttpClient().use { client -> + val serviceURI = UriBuilder.fromUri(isvHost) + .path("/isv/service") + .build() + log.info("Invoking ISV: {}", serviceURI) + + val httpRequest = HttpGet(serviceURI) + client.execute(httpRequest, context).use { httpResponse -> + val statusCode = httpResponse.statusLine.statusCode + if (statusCode != SC_OK) { + async.resume(httpResponse.toResponse("Error from ISV Host (HTTP $statusCode)")) + return@submit + } + mapper.readValue(httpResponse.entity.content) + } + } + } catch (e: IOException) { + log.error("HTTP client error", e) + async.resume(responseOf("HTTP connection failed: ${e.message}", SC_FORBIDDEN)) + return@submit + } + + val (enclaveKey, platformGID) = enclave.getPublicKeyAndGroupIdentifier() + session.setAttribute(platformGIDAttr, platformGID) + + val challengeResponse = ChallengeResponse( + ga = enclaveKey.bytes, + spid = serviceResponse.spid, + quoteType = serviceResponse.quoteType + ) + session.setAttribute(challengeResponseAttr, challengeResponse) + async.resume(Response.ok(challengeResponse).build()) + } + } + + @POST + @Path("/attest") + fun attestation(attestation: AttestationRequest?, @Suspended async: AsyncResponse) { + if (attestation == null) { + throw BadRequestException(responseOf("Message is missing", SC_BAD_REQUEST)) + } + val session = httpRequest.session + log.info("Attestation - HTTP Session: {}", session.id) + + val challengeResponse: ChallengeResponse = session.requireAttribute(challengeResponseAttr, "No response from our challenge") + val challengeNonce: String = session.requireAttribute(challengerNonceAttr, "Challenger's nonce unavailable") + val platformGID = session.requireAttribute(platformGIDAttr, "Platform GID unavailable").value() + val cookies: CookieStore = session.requireAttribute(conversationAttr, "No existing HTTP session with ISV") + val enclave: AttestationEnclave = session.requireAttribute(enclaveAttr, "Enclave unavailable") + + executor.submit { + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + log.info("Platform GID: '{}'", platformGID) + + val iasReportBody: ByteArray = createHttpClient().use { client -> + /* + * First fetch the signature revocation list from the IAS Proxy. + */ + val revocationList = try { + val sigRlURI = UriBuilder.fromUri(isvHost) + .path("/isv/sigrl/{gid}") + .build(platformGID) + log.info("Invoking ISV: {}", sigRlURI) + + val getSigRL = HttpGet(sigRlURI) + client.execute(getSigRL, context).use { response -> + val statusCode = response.statusLine.statusCode + if (statusCode != SC_OK) { + async.resume(response.toResponse("Error from ISV Host (HTTP $statusCode)")) + return@submit + } + EntityUtils.toByteArray(response.entity).decodeBase64() + } + } catch (e: IOException) { + log.error("HTTP client error", e) + async.resume(responseOf("HTTP client error: ${e.message}")) + return@submit + } + + log.info("Fetched revocation list from IAS") + + /* + * Now tell the enclave to generate the quote. + */ + val quote = try { + val challengerDetails = ChallengerDetails( + publicKey = ECKey.fromBytes(attestation.gb), + serviceProviderIdentifier = challengeResponse.spid.hexToBytes(), + quoteType = QuoteType.forValue(challengeResponse.quoteType), + keyDerivationFunctionIdentifier = AES_CMAC_FUNC, + signature = attestation.signatureGbGa, + messageAuthenticationCode = attestation.aesCMAC, + signatureRevocationList = revocationList + ) + enclave.processChallengerDetailsAndGenerateQuote(challengerDetails) + } catch (e: Exception) { + log.error("Attestation error", e) + async.resume(responseOf("Attestation error: ${e.message}")) + return@submit + } + + /* + * Now pass the quote to the IAS Proxy for validation. + */ + val reportBody = try { + val reportURI = UriBuilder.fromUri(isvHost) + .path("/isv/report") + .build() + log.info("Invoking ISV: {}", reportURI) + + val reportRequest = ReportRequest( + isvEnclaveQuote = quote.payload, + pseManifest = quote.securityProperties, + nonce = challengeNonce + ) + val httpRequest = HttpPost(reportURI).apply { + entity = StringEntity(mapper.writeValueAsString(reportRequest), APPLICATION_JSON) + } + client.execute(httpRequest, context).use { httpResponse -> + val statusCode = httpResponse.statusLine.statusCode + if (statusCode != SC_OK) { + async.resume(httpResponse.toResponse("Error from ISV Host (HTTP $statusCode)")) + return@submit + } + EntityUtils.toByteArray(httpResponse.entity) + } + } catch (e: IOException) { + log.error("HTTP client error", e) + async.resume(responseOf("HTTP client error: ${e.message}")) + return@submit + } + + // Return this to the challenger for validation. + reportBody + } + + log.info("Received report from IAS") + + // Successful response. + async.resume(Response.ok(iasReportBody).build()) + } + } + + @POST + @Path("/secret") + fun secret(secret: SecretRequest?): Response { + if (secret == null) { + throw BadRequestException(responseOf("Message is missing", SC_BAD_REQUEST)) + } + + val session = httpRequest.session + log.info("Secret - HTTP Session: {}", session.id) + log.info("platformInfo: {}", secret.platformInfo) + log.info("aesCMAC: {}", secret.aesCMAC) + log.info("secret: {}", secret.data) + + val enclave: AttestationEnclave = session.requireAttribute(enclaveAttr, "Enclave unavailable") + val attestationResult = AttestationResult( + attestationResultMessage = secret.platformInfo, + aesCMAC = secret.aesCMAC, + secret = secret.data, + secretHash = secret.authTag, + secretIV = secret.iv + ) + val (cmacStatus, sealedSecret) = try { + enclave.verifyAttestationResponse(attestationResult) + } catch (e: SgxException) { + log.error("SGX enclave error", e) + return responseOf("Failed to validate request", SC_BAD_REQUEST) + } + + if (cmacStatus != SgxStatus.SUCCESS) { + log.error("CMAC validation failed: $cmacStatus") + return responseOf("Invalid CMAC status '$cmacStatus'", SC_BAD_REQUEST) + } + + // Successful response. + log.info("Sealed secret size: ${sealedSecret.size}") + return Response.ok().build() + } + + private fun validateNonce(n: String?) { + val nonce = n ?: return + if (nonce.length > maxNonceLength) { + throw BadRequestException(responseOf("Nonce is too large: maximum $maxNonceLength digits", SC_BAD_REQUEST)) + } + } + + private fun responseOf(message: String, statusCode: Int = SC_INTERNAL_SERVER_ERROR): Response = Response.status(statusCode) + .entity(AttestationError(message)) + .build() + + private fun HttpResponse.toResponse(message: String, statusCode: Int = statusLine.statusCode): Response { + return Response.status(statusCode) + .entity(AttestationError(message)) + .build() + } + + private fun createHttpClient(): CloseableHttpClient { + return HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { + socketConfig = httpSocketConfig + }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + private fun ByteArray.decodeBase64(): ByteArray = Base64.getDecoder().decode(this) + private inline fun HttpSession.requireAttribute(attrName: String, errorMessage: String): T + = getAttribute(attrName) as? T ?: throw NotAuthorizedException(responseOf(errorMessage, SC_UNAUTHORIZED)) +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/AttestationHostApplication.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/AttestationHostApplication.kt new file mode 100644 index 0000000000..6a97404252 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/AttestationHostApplication.kt @@ -0,0 +1,13 @@ +package net.corda.attestation.host.web + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.security.Security +import javax.ws.rs.ApplicationPath +import javax.ws.rs.core.Application + +@ApplicationPath("/") +class AttestationHostApplication : Application() { + init { + Security.addProvider(BouncyCastleProvider()) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/ExceptionHandler.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/ExceptionHandler.kt new file mode 100644 index 0000000000..9b62f42897 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/ExceptionHandler.kt @@ -0,0 +1,21 @@ +package net.corda.attestation.host.web + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import javax.ws.rs.WebApplicationException +import javax.ws.rs.core.Response +import javax.ws.rs.ext.ExceptionMapper +import javax.ws.rs.ext.Provider + +@Provider +class ExceptionHandler : ExceptionMapper { + private companion object { + @JvmStatic + private val log: Logger = LoggerFactory.getLogger(ExceptionHandler::class.java) + } + + override fun toResponse(e: WebApplicationException): Response { + log.error("HTTP Status: {}: {}", e.response.status, e.message) + return e.response + } +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/JacksonConfig.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/JacksonConfig.kt new file mode 100644 index 0000000000..91370b9791 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/JacksonConfig.kt @@ -0,0 +1,16 @@ +package net.corda.attestation.host.web + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import javax.ws.rs.Consumes +import javax.ws.rs.Produces +import javax.ws.rs.ext.ContextResolver +import javax.ws.rs.ext.Provider + +@Consumes("application/*+json", "text/json") +@Produces("application/*+json", "text/json") +@Provider +class JacksonConfig : ContextResolver { + private val mapper = ObjectMapper().registerModule(JavaTimeModule()) + override fun getContext(type: Class<*>?): ObjectMapper = mapper +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/ThreadPoolListener.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/ThreadPoolListener.kt new file mode 100644 index 0000000000..90f44b7da5 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/attestation/host/web/ThreadPoolListener.kt @@ -0,0 +1,42 @@ +package net.corda.attestation.host.web + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit.* +import javax.servlet.ServletContextEvent +import javax.servlet.ServletContextListener +import javax.servlet.annotation.WebListener + +/** + * Creates an @ApplicationScoped resource without having to use CDI. + */ +@WebListener +class ThreadPoolListener : ServletContextListener { + companion object { + const val threadPoolAttr = "Thread-Pool" + private val log: Logger = LoggerFactory.getLogger(ThreadPoolListener::class.java) + } + + private lateinit var pool: ExecutorService + + override fun contextInitialized(evt: ServletContextEvent) { + log.info("Creating thread pool") + pool = Executors.newCachedThreadPool() + evt.servletContext.setAttribute(threadPoolAttr, pool) + } + + override fun contextDestroyed(evt: ServletContextEvent) { + log.info("Destroying thread pool") + pool.shutdown() + try { + evt.servletContext.removeAttribute(threadPoolAttr) + if (!pool.awaitTermination(30, SECONDS)) { + pool.shutdownNow() + } + } catch (e: InterruptedException) { + log.error("Thread pool timed out on shutdown") + } + } +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/mockisv/MockISV.kt b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/mockisv/MockISV.kt new file mode 100644 index 0000000000..18f0bc389e --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/kotlin/net/corda/mockisv/MockISV.kt @@ -0,0 +1,112 @@ +package net.corda.mockisv + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.message.ReportProxyResponse +import net.corda.attestation.message.ReportRequest +import net.corda.attestation.message.ServiceResponse +import net.corda.attestation.message.ias.ManifestStatus +import net.corda.attestation.message.ias.QuoteStatus +import net.corda.attestation.message.ias.ReportResponse +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.ByteArrayOutputStream +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.KeyStore +import java.security.PrivateKey +import java.security.Signature +import java.security.cert.Certificate +import java.time.Clock +import java.time.LocalDateTime +import java.util.* +import javax.ws.rs.* +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response + +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@Path("/mockisv/isv") +class MockISV { + private companion object { + private val log: Logger = LoggerFactory.getLogger(MockISV::class.java) + private const val SPID = "84D402C36BA9EF9B0A86EF1A9CC8CE4F" + private const val linkableQuote = 1.toShort() + private const val QUOTE_BODY_SIZE = 432 + + private const val BEGIN_CERT = "-----BEGIN CERTIFICATE-----\n" + private const val END_CERT = "\n-----END CERTIFICATE-----\n" + private val platformInfo = byteArrayOf(0x12, 0x34, 0x56, 0x78, 0x9a.toByte(), 0xbc.toByte(), 0xde.toByte(), 0xf0.toByte(), 0x11, 0x22) + private const val signatureAlias = "ias" + private val storePassword = "attestation".toCharArray() + private val keyStore: KeyStore = KeyStore.getInstance("PKCS12").apply { + MockISV::class.java.classLoader.getResourceAsStream("dummyIAS.pfx")?.use { input -> + load(input, storePassword) + } + } + private val mapper = ObjectMapper().registerModule(JavaTimeModule()) + private val signingKey = keyStore.getKey(signatureAlias, storePassword) as PrivateKey + private val signingCertHeader: String = keyStore.getCertificateChain(signatureAlias).let { chain -> + StringBuilder().apply { + chain.forEach { cert -> append(cert.toPEM()) } + }.toString() + } + + private fun ByteArray.encodeBase64(): ByteArray = Base64.getEncoder().encode(this) + + private fun Certificate.toPEM(): String = ByteArrayOutputStream().let { out -> + out.write(BEGIN_CERT.toByteArray()) + out.write(encoded.encodeBase64()) + out.write(END_CERT.toByteArray()) + String(out.toByteArray(), UTF_8) + } + } + + @GET + @Path("/service") + fun challenge(): Response { + log.info("Mock Service Information") + return Response.ok(ServiceResponse(SPID, linkableQuote)).build() + } + + @GET + @Path("/sigrl/{gid}") + fun revocationList(@PathParam("gid") gid: String): Response { + log.info("Mock Signature Revocation List") + return Response.ok(if (gid.toLowerCase() == "00000000") "AAIADgAAAAEAAAABAAAAAGSf/es1h/XiJeCg7bXmX0S/NUpJ2jmcEJglQUI8VT5sLGU7iMFu3/UTCv9uPADal3LhbrQvhBa6+/dWbj8hnsE=" else "") + .build() + } + + @POST + @Path("/report") + fun getReport(request: ReportRequest): Response { + log.info("Mock IAS Report") + val report = ReportResponse( + id = "9497457846286849067596886882708771068", + isvEnclaveQuoteStatus = QuoteStatus.OK, + isvEnclaveQuoteBody = if (request.isvEnclaveQuote.size > QUOTE_BODY_SIZE) + request.isvEnclaveQuote.copyOf(QUOTE_BODY_SIZE) + else + request.isvEnclaveQuote, + pseManifestStatus = request.pseManifest?.toStatus(), + platformInfoBlob = platformInfo, + nonce = request.nonce, + timestamp = LocalDateTime.now(Clock.systemUTC()) + ) + val reportData = mapper.writeValueAsString(report).toByteArray() + val response = ReportProxyResponse( + signature = signatureOf(reportData).encodeBase64().toString(UTF_8), + certificatePath = signingCertHeader, + report = reportData + ) + return Response.ok(response).build() + } + + private fun signatureOf(data: ByteArray): ByteArray = Signature.getInstance("SHA256withRSA").let { signer -> + signer.initSign(signingKey) + signer.update(data) + signer.sign() + } + + private fun ByteArray.toStatus(): ManifestStatus + = if (this.isEmpty() || this[0] == 0.toByte()) ManifestStatus.INVALID else ManifestStatus.OK +} diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/resources/log4j2.xml b/sgx-jvm/remote-attestation/attestation-host/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..3ce15be5f8 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/resources/log4j2.xml @@ -0,0 +1,50 @@ + + + + + . + ${sys:attestation.home} + attestation-host + ${sys:log-path}/archive + info + debug + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/security.properties b/sgx-jvm/remote-attestation/attestation-host/src/main/security.properties new file mode 100644 index 0000000000..28b8893e70 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/security.properties @@ -0,0 +1 @@ +crypto.policy=unlimited diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/ssl/mockisv/generate-keystores.sh b/sgx-jvm/remote-attestation/attestation-host/src/main/ssl/mockisv/generate-keystores.sh new file mode 100755 index 0000000000..cd74f44f6b --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/ssl/mockisv/generate-keystores.sh @@ -0,0 +1,42 @@ +#!/bin/bash +set +o posix + +ALIAS=ias +KEYPASS=attestation +STOREPASS=attestation + +rm -f dummyIAS.pfx dummyIAS-trust.pfx + +CNF=`cat < + + + + + diff --git a/sgx-jvm/remote-attestation/attestation-host/src/main/webapp/WEB-INF/web.xml b/sgx-jvm/remote-attestation/attestation-host/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..5ef0bb1716 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,9 @@ + + + + 10 + + diff --git a/sgx-jvm/remote-attestation/attestation-host/utilities.gradle b/sgx-jvm/remote-attestation/attestation-host/utilities.gradle new file mode 100644 index 0000000000..2a613f7c51 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-host/utilities.gradle @@ -0,0 +1,61 @@ +String[] runArgs(List prefix, List args) { + return [ + *prefix, + "-Dcorda.sgx.enclave.path=/code/sgx-jvm/remote-attestation/enclave/build", + "-Djava.library.path=/code/sgx-jvm/remote-attestation/attestation-host/native/build", + "-Dattestation.home=/code/sgx-jvm/remote-attestation/attestation-host/build/logs", + "-Dfile.encoding=UTF-8", "-Duser.country=US", "-Duser.language=en", "-Duser.variant", + "-cp", sourceSets.main.runtimeClasspath.collect { it.toString() }.join(":"), + debugArgs, *args + ] +} + +String[] containerArgs(String project, String... args) { + def buildArgs = [ "bash", "$projectDir/../../tools/sx/sx", "build" ] + if (ext.hardware) { + buildArgs << "-hp" + } + return [ + *buildArgs, "remote-attestation/$project", *args + ] +} + +def execWait(String[] command, File directory, String ready) { + ProcessBuilder builder = new ProcessBuilder(command) + builder.redirectErrorStream(true) + builder.directory(directory) + Process process = builder.start() + + if (ready == null) { + process.waitFor() + return + } + + InputStream stdout = process.getInputStream() + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(stdout)) + + def line + while ((line = reader.readLine()) != null) { + println line + if (line.contains(ready)) { + println "Command $command is ready" + break + } + } + } finally { + stdout.close() + } +} + +def containerDebugWait(File directory, String project, String... args) { + def ready = "Listening for transport dt_socket at address:" + execWait(containerArgs(project, args), directory, ready) +} + +ext { + runArgs = this.&runArgs + containerArgs = this.&containerArgs + containerDebugWait = this.&containerDebugWait +} + diff --git a/sgx-jvm/remote-attestation/attestation-server/build.gradle b/sgx-jvm/remote-attestation/attestation-server/build.gradle index 76d95e5184..4d9428d1dc 100644 --- a/sgx-jvm/remote-attestation/attestation-server/build.gradle +++ b/sgx-jvm/remote-attestation/attestation-server/build.gradle @@ -11,11 +11,6 @@ buildscript { ext.keyStoreDir = "$buildDir/keystore" ext.httpsKeyStoreDir = "$buildDir/https-keystore" - - // Port numbers to launch the different components on. - ext.isvHttpPort = 8080 - ext.isvTestHttpPort = 9080 - ext.iasTestHttpsPort = 9443 } apply plugin: 'kotlin' @@ -46,7 +41,7 @@ sourceSets { dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" testCompile "junit:junit:$junit_version" compile "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version" @@ -57,6 +52,7 @@ dependencies { compile "com.fasterxml.jackson.core:jackson-databind:$jackson_version" compile "com.fasterxml.jackson.core:jackson-annotations:$jackson_version" compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" + compile "org.apache.httpcomponents:httpclient:$httpclient_version" compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" compile "org.apache.logging.log4j:log4j-core:$log4j_version" runtime "org.apache.logging.log4j:log4j-web:$log4j_version" @@ -66,9 +62,6 @@ dependencies { tasks.withType(Test) { // Enable "unlimited" encryption. systemProperties["java.security.properties"] = "$projectDir/src/integration-test/security.properties" - - // Set logging directory for all tests. - systemProperties["attestation.home"] = "$buildDir/logs" } task intelKeyStores(type: Exec) { @@ -92,7 +85,7 @@ task serviceKeyStore(type: Exec) { } processResources { - dependsOn = [ intelKeyStores, serviceKeyStore ] + dependsOn.addAll intelKeyStores, serviceKeyStore from keyStoreDir } @@ -101,7 +94,7 @@ task integrationTest(type: Test) { classpath = sourceSets.integrationTest.runtimeClasspath systemProperties["javax.net.ssl.keyStore"] = "$httpsKeyStoreDir/keystore" systemProperties["javax.net.ssl.keyStorePassword"] = "attestation" - systemProperties["test.isv.httpPort"] = isvTestHttpPort + systemProperties["test.isv.httpPort"] = testHttpPort } task httpsKeyStores(type: Exec) { @@ -119,7 +112,7 @@ project.afterEvaluate { } gretty { - httpPort = isvTestHttpPort + httpPort = testHttpPort contextPath = "/" servletContainer = 'tomcat8' logDir = "$buildDir/logs" diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/IASIT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/IASIT.kt index 8091ac9c21..d1db079279 100644 --- a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/IASIT.kt +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/IASIT.kt @@ -7,12 +7,14 @@ import org.apache.http.config.SocketConfig import org.apache.http.impl.client.CloseableHttpClient import org.apache.http.impl.client.HttpClients import org.apache.http.ssl.SSLContextBuilder +import org.apache.http.util.EntityUtils import org.junit.Before import org.junit.Ignore import org.junit.Test import java.net.URI import java.security.KeyStore import java.security.SecureRandom +import java.util.* import javax.ws.rs.core.UriBuilder @Ignore("This class exists only to probe IAS, and is not really a test at all.") @@ -57,6 +59,26 @@ class IASIT { keyStore = loadKeyStoreResource("isv.pfx", storePassword) } + @Test + fun testGID() { + createHttpClient().use { httpClient -> + val requestURI = UriBuilder.fromUri(iasHost) + .path("attestation/sgx/v2/sigrl/{gid}") + .build(String.format("%08x", 0xacc)) + println("URI: $requestURI") + val request = HttpGet(requestURI) + httpClient.execute(request).use { response -> + val statusCode = response.statusLine.statusCode + if (statusCode == SC_OK) { + val revocationList = EntityUtils.toByteArray(response.entity).decodeBase64() + println(revocationList.toHexArrayString()) + } else { + println("NOPE: $statusCode") + } + } + } + } + @Test fun huntGID() { createHttpClient().use { httpClient -> @@ -75,4 +97,6 @@ class IASIT { } } } + + private fun ByteArray.decodeBase64(): ByteArray = Base64.getDecoder().decode(this) } \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/resources/log4j2.xml b/sgx-jvm/remote-attestation/attestation-server/src/main/resources/log4j2.xml index 5711c1a9c8..5fb6448011 100644 --- a/sgx-jvm/remote-attestation/attestation-server/src/main/resources/log4j2.xml +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/resources/log4j2.xml @@ -2,6 +2,7 @@ + . ${sys:attestation.home} attestation-server ${sys:log-path}/archive diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/resources/logging.properties b/sgx-jvm/remote-attestation/attestation-server/src/main/resources/logging.properties new file mode 100644 index 0000000000..2f5ec24dd9 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/resources/logging.properties @@ -0,0 +1 @@ +handlers = org.slf4j.bridge.SLF4JBridgeHandler \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/service-key/generate-keystore.sh b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/service-key/generate-keystore.sh index 38f9982c3d..3e99f1f8bb 100755 --- a/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/service-key/generate-keystore.sh +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/service-key/generate-keystore.sh @@ -8,12 +8,12 @@ rm -f isv-svc.pfx openssl ecparam -name secp256r1 -genkey -noout -out privateKey.pem openssl req -new -key privateKey.pem -x509 -out server.crt -days 1000 < 0))) { return SGX_ERROR_INVALID_PARAMETER; } @@ -125,8 +127,9 @@ sgx_status_t verifyCMAC( // Perform 128-bit CMAC hash over the first four bytes of the status // obtained from the challenger. uint8_t computed_cmac[SGX_CMAC_MAC_SIZE] = { 0 }; + const uint8_t* safe_message = (message == NULL) ? safe_empty : message; CHECKED(sgx_rijndael128_cmac_msg( - &mk_key, message, message_size, &computed_cmac + &mk_key, safe_message, message_size, &computed_cmac )); // Compare the computed CMAC-SMK with the provided one. @@ -142,10 +145,10 @@ sgx_status_t verifyCMAC( // Verify attestation response from the challenger. sgx_status_t verifyAttestationResponse( sgx_ra_context_t context, - uint8_t *secret, + const uint8_t *secret, size_t secret_size, - uint8_t *gcm_iv, - uint8_t *gcm_mac, + const uint8_t *gcm_iv, + const uint8_t *gcm_mac, size_t gcm_mac_size, uint8_t *sealed_secret, size_t sealed_secret_size diff --git a/sgx-jvm/remote-attestation/enclave/enclave.edl b/sgx-jvm/remote-attestation/enclave/enclave.edl index 6019bf6256..4cb564b6ee 100644 --- a/sgx-jvm/remote-attestation/enclave/enclave.edl +++ b/sgx-jvm/remote-attestation/enclave/enclave.edl @@ -22,7 +22,7 @@ enclave { */ public sgx_status_t initializeRemoteAttestation( bool usePlatformServices, - [in] sgx_ec256_public_t *challengerKey, + [in] const sgx_ec256_public_t *challengerKey, [out] sgx_ra_context_t *context ); @@ -52,9 +52,9 @@ enclave { */ public sgx_status_t verifyCMAC( sgx_ra_context_t context, - [in,size=messageSize] uint8_t *message, + [in,size=messageSize] const uint8_t *message, size_t messageSize, - [in,size=cmacSize] uint8_t *cmac, + [in,size=cmacSize] const uint8_t *cmac, size_t cmacSize ); @@ -77,10 +77,10 @@ enclave { */ public sgx_status_t verifyAttestationResponse( sgx_ra_context_t context, - [in,size=secretSize] uint8_t *secret, + [in,size=secretSize] const uint8_t *secret, size_t secretSize, - [in,count=12] uint8_t *gcmIV, - [in,size=gcmMacSize] uint8_t *gcmMac, + [in,count=12] const uint8_t *gcmIV, + [in,size=gcmMacSize] const uint8_t *gcmMac, size_t gcmMacSize, [out,size=maxSealedSecretSize] uint8_t *sealedSecret, size_t maxSealedSecretSize diff --git a/sgx-jvm/remote-attestation/host/build.gradle b/sgx-jvm/remote-attestation/host/build.gradle index bfd7ffec32..4ddaee1a00 100644 --- a/sgx-jvm/remote-attestation/host/build.gradle +++ b/sgx-jvm/remote-attestation/host/build.gradle @@ -2,7 +2,6 @@ version = '0.1' buildscript { ext.dokka_version = "0.9.15" - ext.httpclient_version = "4.5.2" ext.nativeBuildDir = "$projectDir/native/build" ext.enclaveBuildDir = "$projectDir/../enclave/build" @@ -89,7 +88,6 @@ task integrationTest(type: Test) { tasks.withType(Test) { systemProperty "java.library.path", "$nativeBuildDir" systemProperty "corda.sgx.enclave.path", "$enclaveBuildDir" - systemProperty "attestation.home", "$buildDir/logs" jvmArgs "$debugArgs" } diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationResult.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationResult.kt index 5212c07ba6..383380d362 100644 --- a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationResult.kt +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationResult.kt @@ -1,7 +1,6 @@ package net.corda.sgx.attestation.entities import net.corda.sgx.sealing.ProvisionedSecret -import java.time.LocalDateTime /** * The outcome of a remote attestation process. @@ -9,7 +8,7 @@ import java.time.LocalDateTime class AttestationResult( /** - * THe status of the provided quote. + * The status of the provided quote. */ val quoteStatus: QuoteStatus, diff --git a/sgx-jvm/remote-attestation/ias-proxy/build.gradle b/sgx-jvm/remote-attestation/ias-proxy/build.gradle new file mode 100644 index 0000000000..dfbc85522e --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/build.gradle @@ -0,0 +1,152 @@ +buildscript { + ext.keyStoreDir = "$buildDir/keystore" + ext.httpsKeyStoreDir = "$buildDir/https-keystore" +} + +apply plugin: 'kotlin' +apply plugin: 'war' +apply plugin: 'org.akhikhl.gretty' + +description 'Proof-of-Concept IAS Proxy server' + +import org.akhikhl.gretty.AppStartTask +import org.akhikhl.gretty.AppStopTask + +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +sourceSets { + integrationTest { + kotlin { + compileClasspath += main.compileClasspath + test.compileClasspath + runtimeClasspath += main.runtimeClasspath + test.runtimeClasspath + //noinspection GroovyAssignabilityCheck + srcDir file('src/integration-test/kotlin') + } + } +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + testCompile "junit:junit:$junit_version" + + compile project(":attestation-common") + compile "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version" + compile "org.jboss.resteasy:resteasy-jaxrs:$resteasy_version" + compile "org.jboss.resteasy:resteasy-jackson2-provider:$resteasy_version" + compile "org.jboss.resteasy:resteasy-servlet-initializer:$resteasy_version" + compile "com.fasterxml.jackson.core:jackson-core:$jackson_version" + compile "com.fasterxml.jackson.core:jackson-databind:$jackson_version" + compile "com.fasterxml.jackson.core:jackson-annotations:$jackson_version" + compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" + compile "org.apache.httpcomponents:httpclient:$httpclient_version" + compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + compile "org.apache.logging.log4j:log4j-core:$log4j_version" + runtime "org.apache.logging.log4j:log4j-web:$log4j_version" + compile "org.slf4j:jcl-over-slf4j:$slf4j_version" + testCompile project(path: ':attestation-common', configuration: 'testArtifacts') +} + +task createIntelKeyStores(type: Exec) { + doFirst { + mkdir keyStoreDir + } + + inputs.dir "$projectDir/src/main/ssl/intel-ssl" + outputs.files "$keyStoreDir/isv.pfx", "$keyStoreDir/ias.pfx" + workingDir keyStoreDir + commandLine "$projectDir/src/main/ssl/intel-ssl/generate-keystores.sh" +} + +task createMockKeyStores(type: Exec) { + doFirst { + mkdir keyStoreDir + } + + inputs.dir "$projectDir/src/main/ssl/mockias" + outputs.files "$keyStoreDir/dummyIAS.pfx", "$keyStoreDir/dummyIAS-trust.pfx" + workingDir keyStoreDir + commandLine "$projectDir/src/main/ssl/mockias/generate-keystores.sh" +} + +processResources { + dependsOn = [createIntelKeyStores, createMockKeyStores] + from keyStoreDir +} + +task integrationTest(type: Test) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath + systemProperties["test.httpPort"] = testHttpPort +} + +task createHttpsKeyStores(type: Exec) { + doFirst { + mkdir httpsKeyStoreDir + } + + inputs.dir "$projectDir/src/integration-test/ssl" + outputs.dir httpsKeyStoreDir + workingDir httpsKeyStoreDir + commandLine "$projectDir/src/integration-test/ssl/generate-ssl.sh" +} + +project.afterEvaluate { + appBeforeIntegrationTest.dependsOn createHttpsKeyStores +} + +gretty { + httpPort = testHttpPort + contextPath = "/" + servletContainer = 'tomcat8' + logDir = "$buildDir/logs" + logFileName = "gretty-test" + integrationTestTask = 'integrationTest' + jvmArgs = [ + "-Dorg.jboss.logging.provider=slf4j", + "-Dnet.corda.IsvKeyStorePassword=attestation", + "-Djavax.net.ssl.keyStore=$httpsKeyStoreDir/keystore", + "-Djavax.net.ssl.keyStorePassword=attestation", + "-Djavax.net.ssl.trustStore=$httpsKeyStoreDir/truststore", + "-Djavax.net.ssl.trustStorePassword=attestation", + "-Dattestation.home=$buildDir/logs", + "-Dias.host=localhost:$iasTestHttpsPort", + ] + + httpsPort = iasTestHttpsPort + httpsEnabled = true + sslNeedClientAuth = true + sslKeyStorePath = "$httpsKeyStoreDir/keystore" + sslKeyStorePassword = 'attestation' + sslKeyManagerPassword = 'attestation' +} + +task('startISV', type: AppStartTask, dependsOn: war) { + prepareServerConfig { + httpPort = isvHttpPort + servletContainer = 'tomcat8' + logDir = "$buildDir/logs" + logFileName = "gretty-iasproxy" + jvmArgs = [ + "-Dorg.jboss.logging.provider=slf4j", + "-Dnet.corda.IsvKeyStorePassword=attestation", + "-Dias.host=test-as.sgx.trustedservices.intel.com", + "-Dattestation.home=$buildDir/logs", + ] + + httpsEnabled = false + } + + prepareWebAppConfig { + contextPath = "/" + inplace = false + } + + interactive = false +} + +task("stopISV", type: AppStopTask) diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/integration-test/kotlin/net/corda/attestation/ias/ReportProxyIT.kt b/sgx-jvm/remote-attestation/ias-proxy/src/integration-test/kotlin/net/corda/attestation/ias/ReportProxyIT.kt new file mode 100644 index 0000000000..05ae8f0f86 --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/integration-test/kotlin/net/corda/attestation/ias/ReportProxyIT.kt @@ -0,0 +1,110 @@ +package net.corda.attestation.ias + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.ias.message.ReportProxyResponse +import net.corda.attestation.ias.message.ReportRequest +import net.corda.attestation.readValue +import org.apache.http.HttpStatus.* +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpPost +import org.apache.http.config.SocketConfig +import org.apache.http.entity.ContentType.APPLICATION_JSON +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.util.EntityUtils +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.io.InputStream +import java.nio.charset.StandardCharsets.* + +class ReportProxyIT { + private companion object { + private val httpPort = Integer.getInteger("test.httpPort") + } + private lateinit var httpClient: CloseableHttpClient + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper().registerModule(JavaTimeModule()) + val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + httpClient = HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun testIASReportWithoutManifest() { + val quote = byteArrayOf(0x02, 0x04, 0x08, 0x10) + val requestMessage = ReportRequest( + isvEnclaveQuote = quote, + nonce = "0000000000000000" + ) + val request = HttpPost("http://localhost:$httpPort/isv/report").apply { + entity = StringEntity(mapper.writeValueAsString(requestMessage), APPLICATION_JSON) + } + val response = httpClient.execute(request).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + + val responseMessage: ReportProxyResponse = mapper.readValue(response) + assertTrue(responseMessage.signature.isNotEmpty()) + assertTrue(responseMessage.certificatePath.isNotEmpty()) + + val iasReport = toMap(responseMessage.report.inputStream()) + assertEquals("OK", iasReport["isvEnclaveQuoteStatus"]) + assertEquals("0000000000000000", iasReport["nonce"]) + assertNull(iasReport["pseManifestStatus"]) + } + + @Test + fun testIASReportWithManifest() { + val quote = byteArrayOf(0x02, 0x04, 0x08, 0x10) + val requestMessage = ReportRequest( + isvEnclaveQuote = quote, + pseManifest = byteArrayOf(0x63, 0x31, 0x0D, 0x5A), + nonce = "0000000000000000" + ) + val request = HttpPost("http://localhost:$httpPort/isv/report").apply { + entity = StringEntity(mapper.writeValueAsString(requestMessage), APPLICATION_JSON) + } + val response = httpClient.execute(request).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + + val responseMessage: ReportProxyResponse = mapper.readValue(response) + assertTrue(responseMessage.signature.isNotEmpty()) + assertTrue(responseMessage.certificatePath.isNotEmpty()) + + val iasReport = toMap(responseMessage.report.inputStream()) + assertEquals("OK", iasReport["isvEnclaveQuoteStatus"]) + assertEquals("0000000000000000", iasReport["nonce"]) + assertEquals("OK", iasReport["pseManifestStatus"]) + } + + private fun toMap(input: InputStream): Map + = mapper.readValue(input, object : TypeReference>() {}) +} diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/integration-test/kotlin/net/corda/attestation/ias/RevocationListProxyIT.kt b/sgx-jvm/remote-attestation/ias-proxy/src/integration-test/kotlin/net/corda/attestation/ias/RevocationListProxyIT.kt new file mode 100644 index 0000000000..cc5ff6e02c --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/integration-test/kotlin/net/corda/attestation/ias/RevocationListProxyIT.kt @@ -0,0 +1,57 @@ +package net.corda.attestation.ias + +import org.apache.http.HttpStatus.* +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpGet +import org.apache.http.config.SocketConfig +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.util.EntityUtils +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.nio.charset.StandardCharsets.* +import java.util.* + +class RevocationListProxyIT { + private companion object { + private val httpPort = Integer.getInteger("test.httpPort") + } + private lateinit var httpClient: CloseableHttpClient + + @Before + fun setup() { + val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + httpClient = HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun testIASRevocationList() { + val request = HttpGet("http://localhost:$httpPort/isv/sigrl/00000000") + val response = httpClient.execute(request).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + + val revocationList = Base64.getDecoder().decode(response) + assertTrue(revocationList.isNotEmpty()) + } +} diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/integration-test/ssl/generate-ssl.sh b/sgx-jvm/remote-attestation/ias-proxy/src/integration-test/ssl/generate-ssl.sh new file mode 100755 index 0000000000..538c5990ae --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/integration-test/ssl/generate-ssl.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +ALIAS=jetty +KEYPASS=attestation +STOREPASS=attestation + +rm -f keystore truststore + +# Generate the keystore and truststore that will allow us to enable HTTPS. +# Both keystore and truststore are expected to use password "attestation". + +keytool -keystore keystore -storetype pkcs12 -genkey -alias ${ALIAS} -dname CN=localhost -keyalg RSA -keypass ${KEYPASS} -storepass ${STOREPASS} +keytool -keystore keystore -storetype pkcs12 -export -alias ${ALIAS} -keyalg RSA -file jetty.cert -keypass ${KEYPASS} -storepass ${STOREPASS} +keytool -keystore truststore -storetype pkcs12 -import -alias ${ALIAS} -file jetty.cert -keypass ${KEYPASS} -storepass ${STOREPASS} < { + private companion object { + @JvmStatic + private val log: Logger = LoggerFactory.getLogger(ExceptionHandler::class.java) + } + + override fun toResponse(e: WebApplicationException): Response { + log.error("HTTP Status: {}: {}", e.response.status, e.message) + return e.response + } +} diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/IASProxy.kt b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/IASProxy.kt new file mode 100644 index 0000000000..a17f56c4aa --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/IASProxy.kt @@ -0,0 +1,214 @@ +package net.corda.attestation.ias + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.ias.message.ReportProxyResponse +import net.corda.attestation.ias.message.ReportRequest +import net.corda.attestation.message.AttestationError +import net.corda.attestation.message.ServiceResponse +import org.apache.http.HttpResponse +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpGet +import org.apache.http.client.methods.HttpPost +import org.apache.http.config.RegistryBuilder +import org.apache.http.config.SocketConfig +import org.apache.http.conn.socket.ConnectionSocketFactory +import org.apache.http.conn.ssl.SSLConnectionSocketFactory +import org.apache.http.entity.ContentType +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.ssl.SSLContextBuilder +import org.apache.http.util.EntityUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.URI +import java.net.URLDecoder +import java.security.KeyStore +import java.security.SecureRandom +import java.util.concurrent.ExecutorService +import javax.net.ssl.SSLException +import javax.servlet.ServletContext +import javax.servlet.http.HttpServletResponse.* +import javax.ws.rs.* +import javax.ws.rs.container.AsyncResponse +import javax.ws.rs.container.Suspended +import javax.ws.rs.core.Context +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import javax.ws.rs.core.UriBuilder + +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@Path("/isv") +class IASProxy { + private companion object { + @JvmStatic + private val log: Logger = LoggerFactory.getLogger(IASProxy::class.java) + private const val SPID = "84D402C36BA9EF9B0A86EF1A9CC8CE4F" + private const val linkableQuote = 1.toShort() + + private const val isvKeyAlias = "isv" + + private val mapper = ObjectMapper().registerModule(JavaTimeModule()) + private val random = SecureRandom() + + private val storePassword = (System.getProperty("net.corda.IsvKeyStorePassword") ?: "").toCharArray() + private val keyStore: KeyStore + private val iasHost: URI = URI.create("https://${System.getProperty("ias.host", "localhost:8443")}") + + private val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + private val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + + init { + keyStore = loadKeyStoreResource("isv.pfx", storePassword, "PKCS12") + } + + private fun loadKeyStoreResource(resourceName: String, password: CharArray, type: String = "PKCS12"): KeyStore { + return KeyStore.getInstance(type).apply { + IASProxy::class.java.classLoader.getResourceAsStream(resourceName)?.use { input -> + load(input, password) + } + } + } + } + + @field:Context + private lateinit var servletContext: ServletContext + + private val executor: ExecutorService by lazy { + servletContext.getAttribute(ThreadPoolListener.threadPoolAttr) as ExecutorService + } + + @GET + @Path("/service") + fun serviceParameters(): Response { + return Response.ok(ServiceResponse(SPID, linkableQuote)).build() + } + + @GET + @Path("/sigrl/{gid}") + fun proxyRevocationList(@PathParam("gid") platformGID: String, @Suspended async: AsyncResponse) { + log.info("Requesting revocation list for GID={}", platformGID) + + executor.submit { + val revocationListBase64: ByteArray = try { + createHttpClient().use { client -> + val sigRlURI = UriBuilder.fromUri(iasHost) + .path("attestation/sgx/v2/sigrl/{gid}") + .build(platformGID) + log.info("Invoking IAS: {}", sigRlURI) + + val getSigRL = HttpGet(sigRlURI) + client.execute(getSigRL).use { response -> + val statusCode = response.statusLine.statusCode + if (statusCode != SC_OK) { + log.error("HTTP {} error from IAS", statusCode) + async.resume(response.toResponse("Error from Intel Attestation Service (HTTP $statusCode)")) + return@submit + } + EntityUtils.toByteArray(response.entity) + } + } + } catch (e: SSLException) { + log.error("HTTPS error: ${e.message}") + async.resume(responseOf("HTTPS connection failed: ${e.message}", SC_FORBIDDEN)) + return@submit + } catch (e: IOException) { + log.error("HTTP client error", e) + async.resume(responseOf("HTTP client error: ${e.message}")) + return@submit + } + + // Successful response + async.resume(Response.ok(revocationListBase64).build()) + } + } + + @POST + @Path("/report") + fun proxyReport(reportRequest: ReportRequest?, @Suspended async: AsyncResponse) { + if (reportRequest == null) { + throw BadRequestException(responseOf("Message is missing", SC_BAD_REQUEST)) + } + + executor.submit { + val reportResponse = try { + createHttpClient().use { client -> + val reportURI = UriBuilder.fromUri(iasHost) + .path("attestation/sgx/v2/report") + .build() + log.info("Invoking IAS: {}", reportURI) + + val httpRequest = HttpPost(reportURI) + httpRequest.entity = StringEntity(mapper.writeValueAsString(reportRequest), ContentType.APPLICATION_JSON) + client.execute(httpRequest).use { httpResponse -> + if (httpResponse.statusLine.statusCode != SC_OK) { + async.resume(httpResponse.toResponse("Error from Intel Attestation Service")) + return@submit + } + ReportProxyResponse( + signature = httpResponse.requireHeader("X-IASReport-Signature"), + certificatePath = httpResponse.requireHeader("X-IASReport-Signing-Certificate").decodeURL(), + report = EntityUtils.toByteArray(httpResponse.entity) + ) + } + } + } catch (e: SSLException) { + log.error("HTTPS error: ${e.message}") + async.resume(responseOf("HTTPS connection failed: ${e.message}", SC_FORBIDDEN)) + return@submit + } catch (e: IOException) { + log.error("HTTP client error", e) + async.resume(responseOf("HTTP client error: ${e.message}")) + return@submit + } + + // Successful response + async.resume(Response.ok(reportResponse).build()) + } + } + + private fun responseOf(message: String, statusCode: Int = SC_INTERNAL_SERVER_ERROR): Response = Response.status(statusCode) + .entity(AttestationError(message)) + .build() + + private fun HttpResponse.toResponse(message: String, statusCode: Int = statusLine.statusCode): Response { + return Response.status(statusCode) + .entity(AttestationError(message)) + .apply { + val requestIdHeader = getFirstHeader("Request-ID") ?: return@apply + this.header(requestIdHeader.name, requestIdHeader.value) + } + .build() + } + + private fun HttpResponse.requireHeader(name: String): String + = (this.getFirstHeader(name) ?: throw ForbiddenException(toResponse("Response header '$name' missing", SC_FORBIDDEN))).value + + private fun createHttpClient(): CloseableHttpClient { + val sslContext = SSLContextBuilder() + .loadKeyMaterial(keyStore, storePassword, { _, _ -> isvKeyAlias }) + .setSecureRandom(random) + .build() + val registry = RegistryBuilder.create() + .register("https", SSLConnectionSocketFactory(sslContext, SSLConnectionSocketFactory.getDefaultHostnameVerifier())) + .build() + return HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager(registry).apply { + socketConfig = httpSocketConfig + }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + private fun String.decodeURL(): String = URLDecoder.decode(this, "UTF-8") +} diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/IASProxyApplication.kt b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/IASProxyApplication.kt new file mode 100644 index 0000000000..cc4df7ef92 --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/IASProxyApplication.kt @@ -0,0 +1,13 @@ +package net.corda.attestation.ias + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.security.Security +import javax.ws.rs.ApplicationPath +import javax.ws.rs.core.Application + +@ApplicationPath("/") +class IASProxyApplication : Application() { + init { + Security.addProvider(BouncyCastleProvider()) + } +} diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/JacksonConfig.kt b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/JacksonConfig.kt new file mode 100644 index 0000000000..1991fff43d --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/JacksonConfig.kt @@ -0,0 +1,17 @@ +package net.corda.attestation.ias + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import javax.ws.rs.Consumes +import javax.ws.rs.Produces +import javax.ws.rs.ext.ContextResolver +import javax.ws.rs.ext.Provider + +@Consumes("application/*+json", "text/json") +@Produces("application/*+json", "text/json") +@Provider +class JacksonConfig : ContextResolver { + private val mapper = ObjectMapper().registerModule(JavaTimeModule()) + override fun getContext(type: Class<*>?): ObjectMapper = mapper +} + diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/ThreadPoolListener.kt b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/ThreadPoolListener.kt new file mode 100644 index 0000000000..f4b3152481 --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/ThreadPoolListener.kt @@ -0,0 +1,42 @@ +package net.corda.attestation.ias + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit.* +import javax.servlet.ServletContextEvent +import javax.servlet.ServletContextListener +import javax.servlet.annotation.WebListener + +/** + * Creates an @ApplicationScoped resource without having to use CDI. + */ +@WebListener +class ThreadPoolListener : ServletContextListener { + companion object { + const val threadPoolAttr = "Thread-Pool" + private val log: Logger = LoggerFactory.getLogger(ThreadPoolListener::class.java) + } + + private lateinit var pool: ExecutorService + + override fun contextInitialized(evt: ServletContextEvent) { + log.info("Creating thread pool") + pool = Executors.newCachedThreadPool() + evt.servletContext.setAttribute(threadPoolAttr, pool) + } + + override fun contextDestroyed(evt: ServletContextEvent) { + log.info("Destroying thread pool") + pool.shutdown() + try { + evt.servletContext.removeAttribute(threadPoolAttr) + if (!pool.awaitTermination(30, SECONDS)) { + pool.shutdownNow() + } + } catch (e: InterruptedException) { + log.error("Thread pool timed out on shutdown") + } + } +} diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/message/ReportProxyResponse.kt b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/message/ReportProxyResponse.kt new file mode 100644 index 0000000000..a4064ef695 --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/message/ReportProxyResponse.kt @@ -0,0 +1,19 @@ +package net.corda.attestation.ias.message + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("signature", "certificatePath", "report") +@JsonInclude(NON_NULL) +class ReportProxyResponse( + @param:JsonProperty("signature") + val signature: String, + + @param:JsonProperty("certificatePath") + val certificatePath: String, + + @param:JsonProperty("report") + val report: ByteArray +) diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/message/ReportRequest.kt b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/message/ReportRequest.kt new file mode 100644 index 0000000000..5576385443 --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/attestation/ias/message/ReportRequest.kt @@ -0,0 +1,19 @@ +package net.corda.attestation.ias.message + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("isvEnclaveQuote", "pseManifest", "nonce") +@JsonInclude(NON_NULL) +class ReportRequest( + @param:JsonProperty("isvEnclaveQuote") + val isvEnclaveQuote: ByteArray, + + @param:JsonProperty("pseManifest") + val pseManifest: ByteArray? = null, + + @param:JsonProperty("nonce") + val nonce: String? = null +) diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/mockias/MockIAS.kt b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/mockias/MockIAS.kt new file mode 100644 index 0000000000..fd55c2d914 --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/mockias/MockIAS.kt @@ -0,0 +1,59 @@ +package net.corda.mockias + +import net.corda.attestation.ias.message.ReportRequest +import net.corda.attestation.message.ias.ManifestStatus +import net.corda.attestation.message.ias.QuoteStatus +import net.corda.attestation.message.ias.ReportResponse +import java.time.Clock +import java.time.LocalDateTime +import javax.servlet.http.HttpServletResponse.* +import javax.ws.rs.* +import javax.ws.rs.core.MediaType.* +import javax.ws.rs.core.Response + +@Path("/attestation/sgx/v2") +class MockIAS { + private companion object { + private const val requestID = "de305d5475b4431badb2eb6b9e546014" + private const val QUOTE_BODY_SIZE = 432 + + private val platformInfo = byteArrayOf(0x12, 0x34, 0x56, 0x78, 0x9a.toByte(), 0xbc.toByte(), 0xde.toByte(), 0xf0.toByte(), 0x11, 0x22) + } + + @GET + @Path("/sigrl/{gid}") + fun getSigRL(@PathParam("gid") gid: String): Response { + return Response.ok(if (gid.toLowerCase() == "00000000") "AAIADgAAAAEAAAABAAAAAGSf/es1h/XiJeCg7bXmX0S/NUpJ2jmcEJglQUI8VT5sLGU7iMFu3/UTCv9uPADal3LhbrQvhBa6+/dWbj8hnsE=" else "") + .header("Request-ID", requestID) + .build() + } + + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + @IASReport + @POST + @Path("/report") + fun getReport(req: ReportRequest?): Response { + val request = req ?: return Response.status(SC_BAD_REQUEST) + .header("Request-ID", requestID) + .build() + val report = ReportResponse( + id = "9497457846286849067596886882708771068", + isvEnclaveQuoteStatus = QuoteStatus.OK, + isvEnclaveQuoteBody = if (request.isvEnclaveQuote.size > QUOTE_BODY_SIZE) + request.isvEnclaveQuote.copyOf(QUOTE_BODY_SIZE) + else + request.isvEnclaveQuote, + pseManifestStatus = req.pseManifest?.toStatus(), + platformInfoBlob = platformInfo, + nonce = request.nonce, + timestamp = LocalDateTime.now(Clock.systemUTC()) + ) + return Response.ok(report) + .header("Request-ID", requestID) + .build() + } + + private fun ByteArray.toStatus(): ManifestStatus + = if (this.isEmpty() || this[0] == 0.toByte()) ManifestStatus.INVALID else ManifestStatus.OK +} diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/mockias/ReportSigner.kt b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/mockias/ReportSigner.kt new file mode 100644 index 0000000000..5d1cbad852 --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/mockias/ReportSigner.kt @@ -0,0 +1,72 @@ +@file:JvmName("ReportSigner") +package net.corda.mockias + +import net.corda.mockias.io.SignatureOutputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.net.URLEncoder +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.KeyStore +import java.security.PrivateKey +import java.security.Signature +import java.security.cert.Certificate +import java.util.* +import javax.ws.rs.NameBinding +import javax.ws.rs.ext.Provider +import javax.ws.rs.ext.WriterInterceptor +import javax.ws.rs.ext.WriterInterceptorContext + + +@Provider +@IASReport +class ReportSigner : WriterInterceptor { + private companion object { + private const val BEGIN_CERT = "-----BEGIN CERTIFICATE-----\n" + private const val END_CERT = "\n-----END CERTIFICATE-----\n" + private const val signatureAlias = "ias" + private val storePassword = "attestation".toCharArray() + private val keyStore: KeyStore = KeyStore.getInstance("PKCS12").apply { + ReportSigner::class.java.classLoader.getResourceAsStream("dummyIAS.pfx")?.use { input -> + load(input, storePassword) + } + } + private val signingKey = keyStore.getKey(signatureAlias, storePassword) as PrivateKey + private val signingCertHeader: String = keyStore.getCertificateChain(signatureAlias).let { chain -> + StringBuilder().apply { + chain.forEach { cert -> append(cert.toPEM()) } + }.toString().encodeURL() + } + + private fun ByteArray.encodeBase64(): ByteArray = Base64.getEncoder().encode(this) + private fun String.encodeURL(): String = URLEncoder.encode(this, "UTF-8") + + private fun Certificate.toPEM(): String = ByteArrayOutputStream().let { out -> + out.write(BEGIN_CERT.toByteArray()) + out.write(encoded.encodeBase64()) + out.write(END_CERT.toByteArray()) + String(out.toByteArray(), UTF_8) + } + } + + @Throws(IOException::class) + override fun aroundWriteTo(context: WriterInterceptorContext) { + val contentStream = context.outputStream + val baos = ByteArrayOutputStream() + val signature = Signature.getInstance("SHA256withRSA").apply { + initSign(signingKey) + } + context.outputStream = SignatureOutputStream(baos, signature) + try { + context.proceed() + context.headers?.apply { + add("X-IASReport-Signature", signature.sign().encodeBase64().toString(UTF_8)) + add("X-IASReport-Signing-Certificate", signingCertHeader) + } + } finally { + baos.writeTo(contentStream) + } + } +} + +@NameBinding +annotation class IASReport diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/mockias/io/SignatureOutputStream.kt b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/mockias/io/SignatureOutputStream.kt new file mode 100644 index 0000000000..7a5467a284 --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/kotlin/net/corda/mockias/io/SignatureOutputStream.kt @@ -0,0 +1,43 @@ +package net.corda.mockias.io + +import java.io.FilterOutputStream +import java.io.IOException +import java.io.OutputStream +import java.security.Signature +import java.security.SignatureException + +class SignatureOutputStream(wrapped: OutputStream, val signature: Signature) : FilterOutputStream(wrapped) { + @Throws(IOException::class) + override fun write(data: Int) { + try { + signature.update(data.toByte()) + } catch (e: SignatureException) { + throw IOException(e.message, e) + } + super.write(data) + } + + @Throws(IOException::class) + override fun write(data: ByteArray, offset: Int, length: Int) { + try { + signature.update(data, offset, length) + } catch (e: SignatureException) { + throw IOException(e.message, e) + } + super.out.write(data, offset, length) + } + + @Throws(IOException::class) + override fun flush() { + super.flush() + } + + @Throws(IOException::class) + override fun close() { + super.close() + } + + @Suppress("UNUSED") + @Throws(SignatureException::class) + fun sign(): ByteArray = signature.sign() +} diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/resources/log4j2.xml b/sgx-jvm/remote-attestation/ias-proxy/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..c677135318 --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/resources/log4j2.xml @@ -0,0 +1,50 @@ + + + + + . + ${sys:attestation.home} + ias-proxy + ${sys:log-path}/archive + info + debug + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl/.gitignore b/sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl/.gitignore new file mode 100644 index 0000000000..92409802c8 --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl/.gitignore @@ -0,0 +1 @@ +client.key diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl/AttestationReportSigningCACert.pem b/sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl/AttestationReportSigningCACert.pem new file mode 100644 index 0000000000..948b4c0cdd --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl/AttestationReportSigningCACert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV +BAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0 +YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy +MzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL +U2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD +DCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G +CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR+tXc8u1EtJzLA10Feu1Wg+p7e +LmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh +rgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT +L/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe +NpEJUmg4ktal4qgIAxk+QHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ +byinkNndn+Bgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H +afuVeLHcDsRp6hol4P+ZFIhu8mmbI1u0hH3W/0C2BuYXB5PC+5izFFh/nP0lc2Lf +6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM +RoOaX4AS+909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX +MFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50 +L0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW +BBR4Q3t2pn680K9+QjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9+Qjfr +NXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq +hkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir +IEqucRiJSSx+HjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi+ripMtPZ +sFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi +zLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra +Ud4APK0wZTGtfPXU7w+IBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA +152Sq049ESDz+1rRGc2NVEqh1KaGXmtXvqxXcTB+Ljy5Bw2ke0v8iGngFBPqCTVB +3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5+xmBc388v9Dm21HGfcC8O +DD+gT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R+mJTLwPXVMrv +DaVzWh5aiEx+idkSGMnX +-----END CERTIFICATE----- diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl/client.crt b/sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl/client.crt new file mode 100644 index 0000000000..0e1132974f --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl/client.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDhDCCAmygAwIBAgIJAMJY5H3wxR1xMA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMQswCQYDVQQKDAJSMzEQMA4GA1UE +AwwHUjMgVGVzdDAeFw0xNzEwMjUxMzEwMjJaFw0xODEwMjUxMzEwMjJaMEExCzAJ +BgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMQswCQYDVQQKDAJSMzEQMA4G +A1UEAwwHUjMgVGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALqc +lg7AXm5QPdJ/Yl3oUsIC+kfU4+OV6cvmJjAoThgah64h2bDjl7bYKnssSGbxZ2kH +21jTP6PxxB27XAPlkgGJEx0F7+Ss8Tq8XxtgfpARVNbQvwt4R7iujbeAS/1QIbJc +xpCCVsp4rAfWKV/DqzfBpSi/Rhdw5YmCfbsVZ/1GBY8kFDCI/KMLvZ67y1ZbqBYN +c146xp6uHxCiW5xhyOaIEgoefVyLCpK/SjzN9a3dQHyz07axOayjSRyK/gM8WDRU +K1+oiJOIvhp5zX7H5D5eRgksTNH3bwN24TVob63ltcs/N7MC/i09Omq+u9E99yo9 +7JqSCzwnanxwafLYVbcCAwEAAaN/MH0wCwYDVR0PBAQDAgKkMB0GA1UdDgQWBBQ0 +Rq9OPXGihG1iE6HUm7aMKorgeTAfBgNVHSMEGDAWgBQ0Rq9OPXGihG1iE6HUm7aM +KorgeTAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDwYDVR0TBAgwBgEB +/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAFUr+hlaixchIQZkzXzvCZFu+gfe0I6YJ +rZd9RKUJQfNteVoC29emEORQazjOn69Q2T9iNFy3COHQrzHhS0kZZEdm72283Yj5 +h6C/krEp784/cjWRObDcu8CP8o9FdFcXZb52NpC+xlTv9Re8rhV8NcitlGW2z3eW +9mBVePSvhilL0ssPXjfDQY2GALck5+ZxiA8gAmzvCkoI23fxUTwQ3u+qhjyPxjCk +kST2Ir7ilCNdeZTJErQhBtcQY+K39W5C98aVSJQDRDgeTT5nClBBMBlMrVE4ohaO +eMgJJkraVMDXiJqXXG1YccsoXb5ezmPCYFoTPOPtY7TyPMZb1Jxgag== +-----END CERTIFICATE----- diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl/generate-keystores.sh b/sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl/generate-keystores.sh new file mode 100755 index 0000000000..8a16ee0f30 --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl/generate-keystores.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +rm -f isv.pfx ias.pfx + +ALIAS=isv +KEYPASS=attestation +STOREPASS=attestation + +DIRHOME=$(dirname $0) +KEYFILE=${DIRHOME}/client.key +CRTFILE=${DIRHOME}/client.crt +INTEL_CRTFILE=${DIRHOME}/AttestationReportSigningCACert.pem + +openssl verify -x509_strict -purpose sslclient -CAfile ${CRTFILE} ${CRTFILE} + +if [ ! -r ${KEYFILE} ]; then + echo "Development private key missing. This is the key that IAS expects our HTTP client to be using for Mutual TLS." + exit 1 +fi + +openssl pkcs12 -export -out client.pfx -inkey ${KEYFILE} -in ${CRTFILE} -passout pass:${STOREPASS} + +keytool -importkeystore -srckeystore client.pfx -srcstoretype pkcs12 -destkeystore isv.pfx -deststoretype pkcs12 -srcstorepass ${STOREPASS} -deststorepass ${STOREPASS} + +keytool -keystore isv.pfx -storetype pkcs12 -changealias -alias 1 -destalias ${ALIAS} -storepass ${STOREPASS} + +rm -rf client.pfx + +## Generate trust store for Intel's certificate. +if [ -r ${INTEL_CRTFILE} ]; then + keytool -import -keystore ias.pfx -storetype pkcs12 -file ${INTEL_CRTFILE} -alias ias -storepass ${STOREPASS} < + + + + + diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/main/webapp/WEB-INF/web.xml b/sgx-jvm/remote-attestation/ias-proxy/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..5ef0bb1716 --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,9 @@ + + + + 10 + + diff --git a/sgx-jvm/remote-attestation/ias-proxy/src/test/kotlin/net/corda/mockias/io/SignatureOutputStreamTest.kt b/sgx-jvm/remote-attestation/ias-proxy/src/test/kotlin/net/corda/mockias/io/SignatureOutputStreamTest.kt new file mode 100644 index 0000000000..883981fc11 --- /dev/null +++ b/sgx-jvm/remote-attestation/ias-proxy/src/test/kotlin/net/corda/mockias/io/SignatureOutputStreamTest.kt @@ -0,0 +1,53 @@ +package net.corda.mockias.io + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.security.KeyPairGenerator +import java.security.Signature + +class SignatureOutputStreamTest { + + private lateinit var output: ByteArrayOutputStream + private lateinit var signedOutput: SignatureOutputStream + private lateinit var reference: Signature + + @Before + fun setup() { + val keyPairGenerator = KeyPairGenerator.getInstance("RSA") + val keyPair = keyPairGenerator.genKeyPair() + val signature = Signature.getInstance("SHA256withRSA").apply { + initSign(keyPair.private) + } + output = ByteArrayOutputStream() + signedOutput = SignatureOutputStream(output, signature) + reference = Signature.getInstance("SHA256withRSA").apply { + initSign(keyPair.private) + } + } + + @Test + fun testSignValue() { + signedOutput.write(-0x74) + signedOutput.write(0x00) + signedOutput.write(0x11) + reference.update(ByteArrayOutputStream().let { baos -> + baos.write(-0x74) + baos.write(0x00) + baos.write(0x11) + baos.toByteArray() + }) + assertArrayEquals(byteArrayOf(-0x74, 0x00, 0x11), output.toByteArray()) + assertArrayEquals(reference.sign(), signedOutput.sign()) + } + + @Test + fun testSignBuffer() { + val buffer = byteArrayOf(0x01, -0x7F, 0x64, -0x52, 0x00) + signedOutput.write(buffer) + reference.update(buffer) + assertArrayEquals(buffer, output.toByteArray()) + assertArrayEquals(reference.sign(), signedOutput.sign()) + } +} diff --git a/sgx-jvm/remote-attestation/settings.gradle b/sgx-jvm/remote-attestation/settings.gradle index ebd2861e7a..fba1d024a7 100644 --- a/sgx-jvm/remote-attestation/settings.gradle +++ b/sgx-jvm/remote-attestation/settings.gradle @@ -1,3 +1,11 @@ rootProject.name = 'remote-attestation' + +// Phase 1 include 'attestation-server' include 'host' + +// Phase 2 +include 'attestation-common' +include 'attestation-challenger' +include 'attestation-host' +include 'ias-proxy' diff --git a/sgx-jvm/tools/sx/sx b/sgx-jvm/tools/sx/sx index 298fbd0e97..8aeaa16624 100755 --- a/sgx-jvm/tools/sx/sx +++ b/sgx-jvm/tools/sx/sx @@ -27,6 +27,7 @@ HSM_PROFILE=dev_sim # Runtime variables ISV_PORT=${PORT:-9080} +HOST_PORT=${HOST_PORT:-8080} LINES=${LINES:-50} # Debug variables @@ -303,7 +304,7 @@ in_container() { mei0_device="--device /dev/mei0" fi fi - ports="-p ${gdb_port}:${gdb_port} -p ${jdwp_port}:${jdwp_port}" + ports="-p ${gdb_port}:${gdb_port} -p ${jdwp_port}:${jdwp_port} -p ${HOST_PORT}:${HOST_PORT}" local sock="/var/run/aesmd/aesm.socket" if [ -e "${sock}" ]; then aesm_socket="-v ${sock}:${sock}"