diff --git a/settings.gradle b/settings.gradle index 2e20226e98..43cc37c09d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -56,4 +56,3 @@ project(':hsm-tool').with { projectDir = file("$settingsDir/sgx-jvm/hsm-tool") } include 'perftestcordapp' - diff --git a/sgx-jvm/remote-attestation/attestation-server/README.md b/sgx-jvm/remote-attestation/attestation-server/README.md new file mode 100644 index 0000000000..024d4471ce --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/README.md @@ -0,0 +1,42 @@ +Remote Attestation ISV: Proof-Of-Concept +---------------------------------------- + +This initial version of the ISV expects to communicate with the [Attestation Host](../host), which should run on hardware +with a SGX enclave. The ISV also communicates with the Intel Attestation Service (IAS) over HTTPS with mutual TLS, which +requires it to contain our development private key. (We have already shared this key's public key with Intel, and IAS +expects to use it to authenticate us.) + +Please install this private key in PEM formt as `src/main/ssl/intel-ssl/client.key`. + +This ISV runs as a WAR file within Tomcat8, and implements the message flow as described in Intel's [end-to-end example](https://software.intel.com/en-us/articles/intel-software-guard-extensions-remote-attestation-end-to-end-example) +using JSON and HTTP. The use of HTTP here is mere convenience for our proof-of-concept; we anticipate using +something completely different when we integrate with Corda. + +Gradle/Tomcat integration is achieved using the [Gretty plugin](https://github.com/akhikhl/gretty). + +You will need OpenSSL installed so that Gradle can generate the keystores and truststores required by HTTPS and Mutual TLS. + +## Building the ISV + +From this project directory, execute the command: + +```bash +$ ../gradlew build integrationTest +``` + +## Running the ISV + +To launch the ISV as a daemon process listening on TCP/8080, execute: + +```bash +$ nohup ../gradlew startISV & +``` + +The ISV can then be shutdown using: + +```bash +$ ../gradlew stopISV +``` + +It will log messages to `build/logs/attestation-server.log`. + diff --git a/sgx-jvm/remote-attestation/attestation-server/build.gradle b/sgx-jvm/remote-attestation/attestation-server/build.gradle new file mode 100644 index 0000000000..76d95e5184 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/build.gradle @@ -0,0 +1,173 @@ +buildscript { + repositories { + mavenLocal() + mavenCentral() + jcenter() + } + + dependencies { + classpath 'org.akhikhl.gretty:gretty:2.0.0' + } + + 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' +apply plugin: 'war' +apply plugin: 'org.akhikhl.gretty' + +description 'Server side of SGX remote attestation process' + +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:$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.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" +} + +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) { + doFirst { + mkdir keyStoreDir + } + inputs.dir "$projectDir/src/main/ssl/intel-ssl" + outputs.dir keyStoreDir + workingDir keyStoreDir + commandLine "$projectDir/src/main/ssl/intel-ssl/generate-keystores.sh" +} + +task serviceKeyStore(type: Exec) { + doFirst { + mkdir keyStoreDir + } + inputs.dir "$projectDir/src/main/ssl/service-key" + outputs.dir keyStoreDir + workingDir keyStoreDir + commandLine "$projectDir/src/main/ssl/service-key/generate-keystore.sh" +} + +processResources { + dependsOn = [ intelKeyStores, serviceKeyStore ] + from keyStoreDir +} + +task integrationTest(type: Test) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath + systemProperties["javax.net.ssl.keyStore"] = "$httpsKeyStoreDir/keystore" + systemProperties["javax.net.ssl.keyStorePassword"] = "attestation" + systemProperties["test.isv.httpPort"] = isvTestHttpPort +} + +task httpsKeyStores(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 httpsKeyStores +} + +gretty { + httpPort = isvTestHttpPort + contextPath = "/" + servletContainer = 'tomcat8' + logDir = "$buildDir/logs" + logFileName = "gretty-test" + integrationTestTask = 'integrationTest' + jvmArgs = [ + "-Dorg.jboss.logging.provider=slf4j", + "-Djava.security.properties=$projectDir/src/integration-test/security.properties", + "-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-isv" + jvmArgs = [ + "-Dorg.jboss.logging.provider=slf4j", + "-Djava.security.properties=$projectDir/src/main/security.properties", + "-Djavax.net.ssl.keyStore=$keyStoreDir/isv.pfx", + "-Djavax.net.ssl.keyStorePassword=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/attestation-server/src/integration-test/kotlin/net/corda/attestation/CryptoProvider.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/CryptoProvider.kt new file mode 100644 index 0000000000..f0e13724c4 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/CryptoProvider.kt @@ -0,0 +1,16 @@ +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 + +class CryptoProvider : TestRule { + override fun apply(statement: Statement, description: Description?): Statement { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(BouncyCastleProvider()) + } + return statement + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/GetProvisioningIT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/GetProvisioningIT.kt new file mode 100644 index 0000000000..92fb37d443 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/GetProvisioningIT.kt @@ -0,0 +1,62 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.ChallengeResponse +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.Rule +import org.junit.Test + +class GetProvisioningIT { + private val httpPort = Integer.getInteger("test.isv.httpPort") + private lateinit var httpClient: CloseableHttpClient + private lateinit var mapper: ObjectMapper + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Before + fun setup() { + mapper = ObjectMapper() + 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 getProvisioning() { + val request = HttpGet("http://localhost:$httpPort/attest/provision") + val response = httpClient.execute(request).use { response -> + val output = EntityUtils.toString(response.entity) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + + val challenge = mapper.readValue(response, ChallengeResponse::class.java) + assertTrue(challenge.nonce.isNotEmpty()) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..8091ac9c21 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/IASIT.kt @@ -0,0 +1,78 @@ +package net.corda.attestation + +import org.apache.http.HttpStatus.SC_OK +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.ssl.SSLContextBuilder +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 javax.ws.rs.core.UriBuilder + +@Ignore("This class exists only to probe IAS, and is not really a test at all.") +class IASIT { + private val iasHost: URI = URI.create("https://test-as.sgx.trustedservices.intel.com") + private val storePassword = (System.getProperty("javax.net.ssl.keyStorePassword") ?: "").toCharArray() + private val random = SecureRandom() + + private lateinit var keyStore: KeyStore + + private val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + private val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + + private fun createHttpClient(): CloseableHttpClient { + val sslContext = SSLContextBuilder() + .loadKeyMaterial(keyStore, storePassword, { _, _ -> "isv" }) + .setSecureRandom(random) + .build() + return HttpClients.custom() + .setDefaultRequestConfig(httpRequestConfig) + .setDefaultSocketConfig(httpSocketConfig) + .setSSLContext(sslContext) + .build() + } + + private fun loadKeyStoreResource(resourceName: String, password: CharArray, type: String = "PKCS12"): KeyStore { + return KeyStore.getInstance(type).apply { + RemoteAttestation::class.java.classLoader.getResourceAsStream(resourceName)?.use { input -> + load(input, password) + } + } + } + + @Before + fun setup() { + keyStore = loadKeyStoreResource("isv.pfx", storePassword) + } + + @Test + fun huntGID() { + createHttpClient().use { httpClient -> + for (i in 1000..1999) { + val requestURI = UriBuilder.fromUri(iasHost) + .path("attestation/sgx/v2/sigrl/{gid}") + .build(String.format("%16x", i)) + val request = HttpGet(requestURI) + httpClient.execute(request).use { response -> + if (response.statusLine.statusCode == SC_OK) { + println("FOUND: $i") + } else { + println("NO: $i -> ${response.statusLine.statusCode}") + } + } + } + } + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/KeyStoreProvider.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/KeyStoreProvider.kt new file mode 100644 index 0000000000..74eaabc3f7 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/KeyStoreProvider.kt @@ -0,0 +1,30 @@ +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 + +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) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage0IT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage0IT.kt new file mode 100644 index 0000000000..b2e4cb2571 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage0IT.kt @@ -0,0 +1,62 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.Message0 +import org.apache.http.HttpStatus.SC_OK +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.* +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 + +class PostMessage0IT { + private val httpPort = Integer.getInteger("test.isv.httpPort") + private lateinit var httpClient: CloseableHttpClient + private lateinit var mapper: ObjectMapper + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Before + fun setup() { + mapper = ObjectMapper() + 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 postMsg0() { + val request = HttpPost("http://localhost:$httpPort/attest/msg0") + val msg0 = Message0(extendedGID = 0) + request.entity = StringEntity(mapper.writeValueAsString(msg0), APPLICATION_JSON) + httpClient.execute(request).use { response -> + val output = EntityUtils.toString(response.entity) + assertEquals(output, SC_OK, response.statusLine.statusCode) + } + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage1IT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage1IT.kt new file mode 100644 index 0000000000..03a7d330ce --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage1IT.kt @@ -0,0 +1,130 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.Message1 +import net.corda.attestation.message.Message2 +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.* +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.bouncycastle.asn1.ASN1EncodableVector +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.ASN1OutputStream +import org.bouncycastle.asn1.DLSequence +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.KeyFactory +import java.security.KeyPair +import java.security.Signature +import java.security.interfaces.ECPublicKey + +class PostMessage1IT { + private companion object { + private const val KEY_PASSWORD = "attestation" + private const val SERVICE_KEYSTORE = "isv-svc.pfx" + private val httpPort = Integer.getInteger("test.isv.httpPort") + } + private lateinit var httpClient: CloseableHttpClient + private lateinit var serviceKeyPair: KeyPair + private lateinit var keyPair: KeyPair + private lateinit var ecPublicKey: ECPublicKey + private lateinit var mapper: ObjectMapper + private lateinit var crypto: Crypto + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Rule + @JvmField + val keyStoreProvider = KeyStoreProvider(SERVICE_KEYSTORE, KEY_PASSWORD) + + @Before + fun setup() { + serviceKeyPair = keyStoreProvider.getKeyPair("isv-svc", KEY_PASSWORD) + mapper = ObjectMapper() + crypto = Crypto() + keyPair = crypto.generateKeyPair() + ecPublicKey = keyPair.public as ECPublicKey + 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 postMsg1() { + val ecParameters = ecPublicKey.params + val request = HttpPost("http://localhost:$httpPort/attest/msg1") + val msg1 = Message1(ga = ecPublicKey.toLittleEndian(), platformGID = "00000000") + request.entity = StringEntity(mapper.writeValueAsString(msg1), 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 msg2 = mapper.readValue(response, Message2::class.java) + assertEquals("84D402C36BA9EF9B0A86EF1A9CC8CE4F", msg2.spid.toUpperCase()) + assertEquals(KEY_SIZE, msg2.signatureGbGa.size) + assertEquals(80, msg2.revocationList.size) + + KeyFactory.getInstance("EC").generatePublic(msg2.gb.toBigEndianKeySpec(ecParameters)) + + val asn1Signature = ByteArrayOutputStream().let { baos -> + ASN1OutputStream(baos).apply { + writeObject(DLSequence(ASN1EncodableVector().apply { + add(ASN1Integer(msg2.signatureGbGa.copyOf(KEY_SIZE / 2).reversedArray().toPositiveInteger())) + add(ASN1Integer(msg2.signatureGbGa.copyOfRange(KEY_SIZE / 2, KEY_SIZE).reversedArray().toPositiveInteger())) + })) + } + baos.toByteArray() + } + val verified = Signature.getInstance("SHA256WithECDSA").let { verifier -> + verifier.initVerify(serviceKeyPair.public) + verifier.update(msg2.gb) + verifier.update(msg1.ga) + verifier.verify(asn1Signature) + } + assertTrue(verified) + } + + @Test + fun testEmptyRevocationList() { + val request = HttpPost("http://localhost:$httpPort/attest/msg1") + val msg1 = Message1(ga = ecPublicKey.toLittleEndian(), platformGID = "0000000b") + request.entity = StringEntity(mapper.writeValueAsString(msg1), 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 msg2 = mapper.readValue(response, Message2::class.java) + assertEquals(0, msg2.revocationList.size) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3IT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3IT.kt new file mode 100644 index 0000000000..984629a663 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3IT.kt @@ -0,0 +1,165 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +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.* +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.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.nio.charset.StandardCharsets.* +import java.security.* +import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec + +class PostMessage3IT { + private val httpPort = Integer.getInteger("test.isv.httpPort") + private lateinit var httpClient: CloseableHttpClient + private lateinit var cookies: CookieStore + private lateinit var keyPair: KeyPair + private lateinit var ecParameters: ECParameterSpec + private lateinit var msg2: Message2 + 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() + + @Before + fun setup() { + crypto = Crypto() + cookies = BasicCookieStore() + keyPair = crypto.generateKeyPair() + ecParameters = (keyPair.public as ECPublicKey).params + mapper = ObjectMapper().registerModule(JavaTimeModule()) + + 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 + } + + val request = HttpPost("http://localhost:$httpPort/attest/msg1") + val msg1 = Message1(ga = (keyPair.public as ECPublicKey).toLittleEndian(), platformGID = "00000000") + request.entity = StringEntity(mapper.writeValueAsString(msg1), 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 + } + msg2 = mapper.readValue(response, Message2::class.java) + + val keyFactory = KeyFactory.getInstance("EC") + peerPublicKey = keyFactory.generatePublic(msg2.gb.toBigEndianKeySpec(ecParameters)) as ECPublicKey + smk = crypto.generateSMK(keyPair.private, peerPublicKey) + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun postMsg3() { + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val request3 = HttpPost("http://localhost:$httpPort/attest/msg3") + val ga = (keyPair.public as ECPublicKey).toLittleEndian() + val quote = byteArrayOf(0x02, 0x04, 0x08, 0x10) + val msg3 = Message3( + aesCMAC = crypto.aesCMAC(smk, { aes -> + aes.update(ga) + aes.update(quote) + }), + ga = ga, + quote = quote + ) + request3.entity = StringEntity(mapper.writeValueAsString(msg3), APPLICATION_JSON) + val response3 = httpClient.execute(request3, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + val msg4 = mapper.readValue(response3, Message4::class.java) + assertEquals("OK", msg4.quoteStatus) + assertArrayEquals(quote, msg4.quoteBody) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22), msg4.platformInfo) + assertNull(msg4.securityManifestStatus) + + val cmac = crypto.aesCMAC(crypto.generateMK(keyPair.private, peerPublicKey), { aes -> + aes.update(msg4.platformInfo ?: byteArrayOf()) + }) + assertArrayEquals(cmac, msg4.aesCMAC) + + val secretKey = crypto.generateSecretKey(keyPair.private, peerPublicKey) + val secret = String(crypto.decrypt(msg4.secret.plus(msg4.secretHash), secretKey, msg4.secretIV), UTF_8) + assertEquals("And now for something completely different!", secret) + } + + @Test + fun testHugeNonce() { + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val request3 = HttpPost("http://localhost:$httpPort/attest/msg3") + val ga = (keyPair.public as ECPublicKey).toLittleEndian() + val quote = byteArrayOf(0x02, 0x04, 0x08, 0x10) + val msg3 = Message3( + aesCMAC = crypto.aesCMAC(smk, { aes -> + aes.update(ga) + aes.update(quote) + }), + ga = ga, + quote = quote, + nonce = "1234567890123456789012345678901234" + ) + request3.entity = StringEntity(mapper.writeValueAsString(msg3), APPLICATION_JSON) + val response3 = httpClient.execute(request3, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_BAD_REQUEST, response.statusLine.statusCode) + output + } + + val error = mapper.readValue(response3, AttestationError::class.java) + assertEquals("Nonce is too large: maximum 32 digits", error.message) + } + + private fun unsignedByteArrayOf(vararg values: Int) = ByteArray(values.size).apply { + for (i in 0 until values.size) { + this[i] = values[i].toByte() + } + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3OnlyIT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3OnlyIT.kt new file mode 100644 index 0000000000..5eee5f8f05 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3OnlyIT.kt @@ -0,0 +1,112 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.message.AttestationError +import net.corda.attestation.message.Message3 +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.* +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.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.KeyPair +import java.security.interfaces.ECPublicKey + +class PostMessage3OnlyIT { + private val httpPort = Integer.getInteger("test.isv.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() { + mapper = ObjectMapper().registerModule(JavaTimeModule()) + crypto = Crypto() + 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 `test msg3 without payload`() { + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val request3 = HttpPost("http://localhost:$httpPort/attest/msg3") + request3.addHeader(BasicHeader(CONTENT_TYPE, APPLICATION_JSON.mimeType)) + val response3 = httpClient.execute(request3, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_BAD_REQUEST, response.statusLine.statusCode) + output + } + + val error = mapper.readValue(response3, AttestationError::class.java) + assertEquals("Message is missing", error.message) + } + + @Test + fun `test Msg3 requires Msg1 first`() { + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val request3 = HttpPost("http://localhost:$httpPort/attest/msg3") + val msg3 = Message3( + aesCMAC = byteArrayOf(), + ga = (keyPair.public as ECPublicKey).toLittleEndian(), + securityManifest = byteArrayOf(), + quote = byteArrayOf() + ) + request3.entity = StringEntity(mapper.writeValueAsString(msg3), APPLICATION_JSON) + val response3 = httpClient.execute(request3, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_UNAUTHORIZED, response.statusLine.statusCode) + output + } + + val error = mapper.readValue(response3, AttestationError::class.java) + assertEquals("Secret key has not been calculated yet", error.message) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/ReportProxyIT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/ReportProxyIT.kt new file mode 100644 index 0000000000..8048694958 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/ReportProxyIT.kt @@ -0,0 +1,104 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.message.ias.* +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 +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.* + +class ReportProxyIT { + private val httpPort = Integer.getInteger("test.isv.httpPort") + private lateinit var httpClient: CloseableHttpClient + private lateinit var mapper: ObjectMapper + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @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 testIASReportWithManifest() { + val request = HttpPost("http://localhost:$httpPort/ias/report") + val quote = byteArrayOf(0x02, 0x04, 0x08, 0x10) + val requestMessage = ReportRequest( + isvEnclaveQuote = quote, + pseManifest = byteArrayOf(0x73, 0x42), + nonce = "0000000000000000" + ) + request.entity = StringEntity(mapper.writeValueAsString(requestMessage), ContentType.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 = mapper.readValue(response, ReportProxyResponse::class.java) + assertTrue(responseMessage.signature.isNotEmpty()) + assertTrue(responseMessage.certificatePath.isNotEmpty()) + + val iasReport = mapper.readValue(responseMessage.report.inputStream(), ReportResponse::class.java) + assertEquals(QuoteStatus.OK, iasReport.isvEnclaveQuoteStatus) + assertEquals("0000000000000000", iasReport.nonce) + assertEquals(ManifestStatus.OK, iasReport.pseManifestStatus) + } + + @Test + fun testIASReportWithoutManifest() { + val request = HttpPost("http://localhost:$httpPort/ias/report") + val quote = byteArrayOf(0x02, 0x04, 0x08, 0x10) + val requestMessage = ReportRequest( + isvEnclaveQuote = quote, + nonce = "0000000000000000" + ) + request.entity = StringEntity(mapper.writeValueAsString(requestMessage), ContentType.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 = mapper.readValue(response, ReportProxyResponse::class.java) + assertTrue(responseMessage.signature.isNotEmpty()) + assertTrue(responseMessage.certificatePath.isNotEmpty()) + + val iasReport = mapper.readValue(responseMessage.report.inputStream(), ReportResponse::class.java) + assertEquals(QuoteStatus.OK, iasReport.isvEnclaveQuoteStatus) + assertEquals("0000000000000000", iasReport.nonce) + assertNull(iasReport.pseManifestStatus) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/RevocationListProxyIT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/RevocationListProxyIT.kt new file mode 100644 index 0000000000..792f9c5891 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/RevocationListProxyIT.kt @@ -0,0 +1,59 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.ias.RevocationListProxyResponse +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.* + +class RevocationListProxyIT { + private val httpPort = Integer.getInteger("test.isv.httpPort") + private lateinit var httpClient: CloseableHttpClient + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + 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/ias/sigrl/0000000000000000") + 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 = mapper.readValue(response, RevocationListProxyResponse::class.java) + assertEquals("84D402C36BA9EF9B0A86EF1A9CC8CE4F", responseMessage.spid) + assertTrue(responseMessage.revocationList.isNotEmpty()) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/security.properties b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/security.properties new file mode 100644 index 0000000000..314cf7d792 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/security.properties @@ -0,0 +1,2 @@ +crypto.policy=unlimited + diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/ssl/generate-ssl.sh b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/ssl/generate-ssl.sh new file mode 100755 index 0000000000..538c5990ae --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/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} < 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-server/src/main/kotlin/net/corda/attestation/EndianUtils.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/EndianUtils.kt new file mode 100644 index 0000000000..bdd29f3862 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/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-server/src/main/kotlin/net/corda/attestation/ExceptionHandler.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/ExceptionHandler.kt new file mode 100644 index 0000000000..8866b94d36 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/ExceptionHandler.kt @@ -0,0 +1,21 @@ +package net.corda.attestation + +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-server/src/main/kotlin/net/corda/attestation/IASProxy.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/IASProxy.kt new file mode 100644 index 0000000000..fa531bbb71 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/IASProxy.kt @@ -0,0 +1,180 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.message.AttestationError +import net.corda.attestation.message.ias.ReportProxyResponse +import net.corda.attestation.message.ias.ReportRequest +import net.corda.attestation.message.ias.RevocationListProxyResponse +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.FileInputStream +import java.io.IOException +import java.net.URI +import java.net.URLDecoder +import java.security.KeyStore +import java.security.SecureRandom +import java.util.* +import javax.net.ssl.SSLException +import javax.servlet.http.HttpServletResponse.* +import javax.ws.rs.* +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("/ias") +class IASProxy { + private companion object { + @JvmStatic + private val log: Logger = LoggerFactory.getLogger(IASProxy::class.java) + @JvmStatic + private val SPID = "84D402C36BA9EF9B0A86EF1A9CC8CE4F" + + private val mapper = ObjectMapper().registerModule(JavaTimeModule()) + private val random = SecureRandom() + + private val storePassword = (System.getProperty("javax.net.ssl.keyStorePassword") ?: "").toCharArray() + private val keyStore: KeyStore + private val iasHost: URI = URI.create("https://${System.getProperty("ias.host", "localhost:8443")}") + private val isDummy = iasHost.host == "localhost" + private val isvKeyAlias = if (isDummy) "jetty" else "isv" + + 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 { + val keyStoreType = System.getProperty("javax.net.ssl.keyStoreType", "PKCS12") + keyStore = loadKeyStore("javax.net.ssl.keyStore", storePassword, keyStoreType) + } + + private fun loadKeyStore(propertyName: String, password: CharArray, type: String = "PKCS12"): KeyStore { + val fileName = System.getProperty(propertyName) ?: throw IllegalStateException("System property $propertyName not set") + return KeyStore.getInstance(type).apply { + FileInputStream(fileName).use { input -> this.load(input, password) } + } + } + } + + @GET + @Path("/sigrl/{gid}") + fun proxyRevocationList(@PathParam("gid") platformGID: String): Response { + val revocationList = try { + createHttpClient().use { client -> + val sigRlURI = UriBuilder.fromUri(iasHost) + .path("attestation/sgx/v2/sigrl/{gid}") + .build(platformGID) + val getSigRL = HttpGet(sigRlURI) + client.execute(getSigRL).use { response -> + if (response.statusLine.statusCode != SC_OK) { + return response.toResponse("Error from Intel Attestation Service (HTTP ${response.statusLine.statusCode})") + } + EntityUtils.toByteArray(response.entity).decodeBase64() + } + } + } catch (e: SSLException) { + log.error("HTTPS error: ${e.message}") + return responseOf("HTTPS connection failed: ${e.message}", SC_FORBIDDEN) + } catch (e: IOException) { + log.error("HTTP client error", e) + return responseOf("HTTP client error: ${e.message}") + } + + return Response.ok() + .entity(RevocationListProxyResponse(SPID, revocationList)) + .build() + } + + @POST + @Path("/report") + fun proxyReport(prq: ReportRequest?): Response { + val reportRequest = prq ?: return responseOf("Message is missing", SC_BAD_REQUEST) + + val reportResponse = try { + createHttpClient().use { client -> + val reportURI = UriBuilder.fromUri(iasHost) + .path("attestation/sgx/v2/report") + .build() + val httpRequest = HttpPost(reportURI) + httpRequest.entity = StringEntity(mapper.writeValueAsString(reportRequest), ContentType.APPLICATION_JSON) + client.execute(httpRequest).use { httpResponse -> + if (httpResponse.statusLine.statusCode != SC_OK) { + return httpResponse.toResponse("Error from Intel Attestation Service") + } + 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}") + return responseOf("HTTPS connection failed: ${e.message}", SC_FORBIDDEN) + } catch (e: IOException) { + log.error("HTTP client error", e) + return responseOf("HTTP client error: ${e.message}") + } + return Response.ok() + .entity(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 ByteArray.decodeBase64(): ByteArray = Base64.getDecoder().decode(this) + private fun String.decodeURL(): String = URLDecoder.decode(this, "UTF-8") +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/JacksonConfig.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/JacksonConfig.kt new file mode 100644 index 0000000000..67daefb9c1 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/JacksonConfig.kt @@ -0,0 +1,16 @@ +package net.corda.attestation + +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-server/src/main/kotlin/net/corda/attestation/RemoteAttestation.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/RemoteAttestation.kt new file mode 100644 index 0000000000..afa42ca24a --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/RemoteAttestation.kt @@ -0,0 +1,449 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.message.* +import net.corda.attestation.message.ias.ReportRequest +import net.corda.attestation.message.ias.ReportResponse +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.conn.ssl.SSLConnectionSocketFactory.getDefaultHostnameVerifier +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.bouncycastle.asn1.ASN1InputStream +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.DLSequence +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.* +import java.net.URI +import java.net.URLDecoder +import java.nio.ByteBuffer +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.* +import java.util.* +import javax.crypto.SecretKey +import javax.net.ssl.SSLException +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse.* +import javax.ws.rs.* +import javax.ws.rs.core.* + +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@Path("/attest") +class RemoteAttestation { + private companion object { + @JvmStatic + private val log: Logger = LoggerFactory.getLogger(RemoteAttestation::class.java) + @JvmStatic + private val SPID = "84D402C36BA9EF9B0A86EF1A9CC8CE4F".hexToBytes() + @JvmStatic + private val LINKABLE_QUOTE = byteArrayOf(0x01, 0x00) + + private const val intelAES = 0 + private const val maxNonceLength = 32 + private const val tlvHeaderSize = 8 + private const val AES_CMAC_FUNC = 1 + private const val secretKeyAttrName = "Secret-Key" + private const val transientKeyPairAttrName = "Transient-Key-Pair" + private const val smkAttrName = "SMK" + + private val mapper = ObjectMapper().registerModule(JavaTimeModule()) + private val crypto = Crypto() + private val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509") + private val keyFactory: KeyFactory = KeyFactory.getInstance("EC") + private val storePassword = (System.getProperty("javax.net.ssl.keyStorePassword") ?: "").toCharArray() + private val isvStore: KeyStore + private val iasStore: KeyStore + private val serviceKeyPair: KeyPair + private val pkixParameters: PKIXParameters + private val ecParameters: ECParameterSpec + + private val iasHost: URI = URI.create("https://${System.getProperty("ias.host", "localhost:8443")}") + private val isDummy = iasHost.host == "localhost" + private val isvKeyAlias = if (isDummy) "jetty" else "isv" + + 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) + + val keyStoreType = System.getProperty("javax.net.ssl.keyStoreType", "PKCS12") + isvStore = loadKeyStore("javax.net.ssl.keyStore", storePassword, keyStoreType) + serviceKeyPair = loadKeyStoreResource("isv-svc.pfx", storePassword).getKeyPair("isv-svc", storePassword) + + val iasResourceName = if (isDummy) "dummyIAStrust.pfx" else "ias.pfx" + iasStore = loadKeyStoreResource(iasResourceName, storePassword) + + val revocationListOptions = if (isDummy) EnumSet.of(SOFT_FAIL, PREFER_CRLS, NO_FALLBACK) else EnumSet.of(SOFT_FAIL) + pkixParameters = PKIXParameters(iasStore.trustAnchorsFor("ias")).apply { + val rlChecker = CertPathValidator.getInstance("PKIX").revocationChecker as PKIXRevocationChecker + addCertPathChecker(rlChecker.apply { options = revocationListOptions }) + } + } + + private fun loadKeyStore(propertyName: String, password: CharArray, type: String = "PKCS12"): KeyStore { + val fileName = System.getProperty(propertyName) ?: throw IllegalStateException("System property $propertyName not set") + return KeyStore.getInstance(type).apply { + FileInputStream(fileName).use { input -> this.load(input, password) } + } + } + + private fun loadKeyStoreResource(resourceName: String, password: CharArray, type: String = "PKCS12"): KeyStore { + return KeyStore.getInstance(type).apply { + RemoteAttestation::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() + } + + @field:Context + private lateinit var httpRequest: HttpServletRequest + + @GET + @Path("/provision") + fun provision(): Response { + log.info("Provisioning requested") + return Response.ok() + .entity(ChallengeResponse(createNonce(), (serviceKeyPair.public as ECPublicKey).toLittleEndian())) + .build() + } + + @POST + @Path("/msg0") + fun receiveMsg0(m: Message0?): Response { + val msg0 = m ?: return responseOf("Message is missing", SC_BAD_REQUEST) + if (intelAES != msg0.extendedGID) { + return responseOf("Unsupported extended GID value", SC_BAD_REQUEST) + } + log.info("Message0 processed") + return Response.ok().build() + } + + /** + * Receives Msg1 and returns Msg2. + */ + @POST + @Path("/msg1") + fun handleMsg1(m: Message1?): Response { + val msg1 = m ?: return responseOf("Message is missing", SC_BAD_REQUEST) + val session = httpRequest.session ?: return responseOf("No session in progress", SC_UNAUTHORIZED) + + log.info("HTTP Session: {}", session.id) + + val revocationList = try { + createHttpClient().use { client -> + val sigRlURI = UriBuilder.fromUri(iasHost) + .path("attestation/sgx/v2/sigrl/{gid}") + .build(msg1.platformGID) + val getSigRL = HttpGet(sigRlURI) + client.execute(getSigRL).use { response -> + if (response.statusLine.statusCode != SC_OK) { + return response.toResponse("Error from Intel Attestation Service (HTTP ${response.statusLine.statusCode})") + } + EntityUtils.toByteArray(response.entity).decodeBase64() + } + } + } catch (e: SSLException) { + log.error("HTTPS error: ${e.message}") + return responseOf("HTTPS connection failed: ${e.message}", SC_FORBIDDEN) + } catch (e: IOException) { + log.error("HTTP client error", e) + return responseOf("HTTP client error: ${e.message}") + } + + val transientKeyPair = crypto.generateKeyPair() + session.setAttribute(transientKeyPairAttrName, transientKeyPair) + + val peerPublicKey = try { + keyFactory.generatePublic(msg1.ga.toBigEndianKeySpec(ecParameters)) as ECPublicKey + } catch (e: IllegalArgumentException) { + return responseOf(e.message ?: "", SC_BAD_REQUEST) + } + session.setAttribute(secretKeyAttrName, crypto.generateSecretKey(transientKeyPair.private, peerPublicKey)) + + log.info("Message1 processed - returning Message2") + + val smk = crypto.generateSMK(transientKeyPair.private, peerPublicKey) + session.setAttribute(smkAttrName, smk) + + val publicKey = transientKeyPair.public as ECPublicKey + val signatureGbGa = signatureOf(publicKey, peerPublicKey) + val gb = publicKey.toLittleEndian() + val msg2 = Message2( + gb = gb, + spid = SPID.toHexString(), + keyDerivationFuncId = AES_CMAC_FUNC, + signatureGbGa = signatureGbGa, + aesCMAC = crypto.aesCMAC(smk, { aes -> + aes.update(gb) + aes.update(SPID) + aes.update(LINKABLE_QUOTE) + aes.update(AES_CMAC_FUNC.toShort().toLittleEndian()) + aes.update(signatureGbGa) + }), + revocationList = revocationList + ) + return Response.ok(msg2).build() + } + + /** + * Receives Msg3 and return Msg4. + */ + @POST + @Path("/msg3") + fun handleMsg3(m: Message3?): Response { + val msg3 = m ?: return responseOf("Message is missing", SC_BAD_REQUEST) + val session = httpRequest.session + ?: return responseOf("No session in progress", SC_UNAUTHORIZED) + log.info("HTTP Session: {}", session.id) + + val secretKey = session.getAttribute(secretKeyAttrName) as? SecretKey + ?: return responseOf("Secret key has not been calculated yet", SC_UNAUTHORIZED) + val smk = session.getAttribute(smkAttrName) as? ByteArray + ?: return responseOf("SMK value has not been calculated yet", SC_UNAUTHORIZED) + validateCMAC(msg3, smk) + + val transientKeyPair = session.getAttribute(transientKeyPairAttrName) as? KeyPair + ?: return responseOf("DH key unavailable", SC_UNAUTHORIZED) + val peerPublicKey = try { + keyFactory.generatePublic(msg3.ga.toBigEndianKeySpec(ecParameters)) + } catch (e: IllegalArgumentException) { + return responseOf(e.message ?: "", SC_BAD_REQUEST) + } + if (crypto.generateSecretKey(transientKeyPair.private, peerPublicKey) != secretKey) { + return responseOf("Keys do not match!", SC_FORBIDDEN) + } + validateNonce(msg3.nonce) + + log.debug("Quote: {}", msg3.quote.toHexArrayString()) + log.debug("Security manifest: {}", msg3.securityManifest?.toHexArrayString()) + log.debug("Nonce: {}", msg3.nonce) + + val report: ReportResponse = try { + createHttpClient().use { client -> + val reportURI = UriBuilder.fromUri(iasHost) + .path("attestation/sgx/v2/report") + .build() + val httpRequest = HttpPost(reportURI) + val reportRequest = ReportRequest( + isvEnclaveQuote = msg3.quote, + pseManifest = msg3.securityManifest?.ifNotZeros(), + nonce = msg3.nonce + ) + httpRequest.entity = StringEntity(mapper.writeValueAsString(reportRequest), ContentType.APPLICATION_JSON) + client.execute(httpRequest).use { httpResponse -> + if (httpResponse.statusLine.statusCode != SC_OK) { + return httpResponse.toResponse("Error from Intel Attestation Service (HTTP ${httpResponse.statusLine.statusCode})") + } + mapper.readValue(validate(httpResponse), ReportResponse::class.java) + } + } + } catch (e: SSLException) { + log.error("HTTPS error: ${e.message}") + return responseOf("HTTPS connection failed: ${e.message}", SC_FORBIDDEN) + } catch (e: IOException) { + log.error("HTTP client error", e) + return responseOf("HTTP client error: ${e.message}") + } + + log.info("Report ID: {}", report.id) + log.info("Quote Status: {}", report.isvEnclaveQuoteStatus) + log.info("Message3 processed - returning Message4") + + val secretIV = crypto.createIV() + val secretData = crypto.encrypt("And now for something completely different!".toByteArray(), secretKey, secretIV) + val platformInfo = report.platformInfoBlob?.removeHeader(tlvHeaderSize) ?: byteArrayOf() + val mk = crypto.generateMK(transientKeyPair.private, peerPublicKey) + val msg4 = Message4( + reportID = report.id, + quoteStatus = report.isvEnclaveQuoteStatus.toString(), + quoteBody = report.isvEnclaveQuoteBody, + aesCMAC = crypto.aesCMAC(mk, { aes -> + aes.update(platformInfo) + }), + securityManifestStatus = report.pseManifestStatus?.toString(), + securityManifestHash = report.pseManifestHash, + platformInfo = platformInfo, + epidPseudonym = report.epidPseudonym, + nonce = report.nonce, + secret = secretData.encryptedData(), + secretHash = secretData.authenticationTag(), + secretIV = secretIV, + timestamp = report.timestamp + ) + return Response.ok(msg4) + .build() + } + + private fun createNonce(): String = UUID.randomUUID().let { uuid -> + String.format("%016x%016x", uuid.mostSignificantBits, uuid.leastSignificantBits) + } + + 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 validateCMAC(msg3: Message3, smk: ByteArray) { + val cmac = crypto.aesCMAC(smk, { aes -> + aes.update(msg3.ga) + aes.update(msg3.securityManifest ?: byteArrayOf()) + aes.update(msg3.quote) + }) + if (!cmac.contentEquals(msg3.aesCMAC)) { + throw BadRequestException(responseOf("Incorrect CMAC value", SC_BAD_REQUEST)) + } + } + + private fun validate(response: HttpResponse): String { + return EntityUtils.toByteArray(response.entity).let { payload -> + val iasSignature = response.requireHeader("X-IASReport-Signature") + val iasSigningCertificate = response.requireHeader("X-IASReport-Signing-Certificate").decodeURL() + + val certificatePath = try { + parseCertificates(iasSigningCertificate) + } catch (e: CertificateException) { + log.error("Failed to parse certificate from HTTP header '{}': {}", iasSigningCertificate, e.message) + throw ForbiddenException(response.toResponse("Invalid X-IASReport HTTP headers", SC_FORBIDDEN)) + } + + try { + val certValidator = CertPathValidator.getInstance("PKIX") + certValidator.validate(certificatePath, pkixParameters) + } catch (e: GeneralSecurityException) { + log.error("Certificate '{}' is invalid: {}", certificatePath, e.message) + throw ForbiddenException(response.toResponse("Invalid IAS certificate", SC_FORBIDDEN)) + } + + val signature = try { + Signature.getInstance("SHA256withRSA").apply { + initVerify(certificatePath.certificates[0]) + } + } catch (e: GeneralSecurityException) { + log.error("Failed to initialise signature: {}", e.message) + throw ForbiddenException(response.toResponse("", SC_FORBIDDEN)) + } + + try { + signature.update(payload) + if (!signature.verify(iasSignature.toByteArray().decodeBase64())) { + throw ForbiddenException(response.toResponse("Report failed IAS signature check", SC_FORBIDDEN)) + } + } catch (e: SignatureException) { + log.error("Failed to parse signature from IAS: {}", e.message) + throw ForbiddenException(response.toResponse("Corrupt IAS signature data", SC_FORBIDDEN)) + } + + String(payload, UTF_8) + } + } + + 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 responseOf(message: String, statusCode: Int = SC_INTERNAL_SERVER_ERROR): Response = Response.status(statusCode) + .entity(AttestationError(message)) + .build() + + private fun HttpResponse.requireHeader(name: String): String { + return (this.getFirstHeader(name) ?: throw ForbiddenException(toResponse("Response header '$name' missing", SC_FORBIDDEN))).value + } + + 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 createHttpClient(): CloseableHttpClient { + val sslContext = SSLContextBuilder() + .loadKeyMaterial(isvStore, storePassword, { _, _ -> isvKeyAlias }) + .setSecureRandom(crypto.random) + .build() + val registry = RegistryBuilder.create() + .register("https", SSLConnectionSocketFactory(sslContext, getDefaultHostnameVerifier())) + .build() + return HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager(registry).apply { + socketConfig = httpSocketConfig + }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + private fun signatureOf(publicKey: ECPublicKey, peerKey: ECPublicKey): ByteArray { + val signature = Signature.getInstance("SHA256WithECDSA").let { signer -> + signer.initSign(serviceKeyPair.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 String.decodeURL(): String = URLDecoder.decode(this, "UTF-8") + private fun ByteArray.removeHeader(headerSize: Int) = copyOfRange(headerSize, size) + private fun ByteArray.ifNotZeros(): ByteArray? { + for (i in 0 until size) { + if (this[i] != 0.toByte()) return this + } + return null + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/SgxApplication.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/SgxApplication.kt new file mode 100644 index 0000000000..7cbd2fdbae --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/SgxApplication.kt @@ -0,0 +1,13 @@ +package net.corda.attestation + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.security.Security +import javax.ws.rs.ApplicationPath +import javax.ws.rs.core.Application + +@ApplicationPath("/") +class SgxApplication : Application() { + init { + Security.addProvider(BouncyCastleProvider()) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/AttestationError.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/AttestationError.kt new file mode 100644 index 0000000000..eedf3a29c2 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/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) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ChallengeResponse.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ChallengeResponse.kt new file mode 100644 index 0000000000..f6c6943452 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ChallengeResponse.kt @@ -0,0 +1,16 @@ +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("nonce", "serviceKey") +@JsonInclude(NON_NULL) +class ChallengeResponse( + @param:JsonProperty("nonce") + val nonce: String, + + @param:JsonProperty("serviceKey") + val serviceKey: ByteArray +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message0.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message0.kt new file mode 100644 index 0000000000..840f695e29 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message0.kt @@ -0,0 +1,13 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("extendedGID") +@JsonInclude(NON_NULL) +class Message0( + @param:JsonProperty("extendedGID") + val extendedGID: Int +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message1.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message1.kt new file mode 100644 index 0000000000..a5d58867cf --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message1.kt @@ -0,0 +1,15 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("ga", "platformGID") +class Message1( + // The client's 512 bit public DH key (Little Endian) + @param:JsonProperty("ga") + val ga: ByteArray, + + // Platform GID value from the SGX client + @param:JsonProperty("platformGID") + val platformGID: String +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message2.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message2.kt new file mode 100644 index 0000000000..d1f5e13cdb --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message2.kt @@ -0,0 +1,37 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder( + "gb", + "spid", + "linkableQuote", + "keyDerivationFuncId", + "signatureGbGa", + "aesCMAC", + "revocationList" +) +class Message2( + // The server's 512 bit public DH key (Little Endian) + @param:JsonProperty("gb") + val gb: ByteArray, + + @param:JsonProperty("spid") + val spid: String, + + @param:JsonProperty("linkableQuote") + val linkableQuote: Boolean = true, + + @param:JsonProperty("keyDerivationFuncId") + val keyDerivationFuncId: Int, + + @param:JsonProperty("signatureGbGa") + val signatureGbGa: ByteArray, // Not ASN.1 encoded + + @param:JsonProperty("aesCMAC") + val aesCMAC: ByteArray, // Not ASN.1 encoded + + @param:JsonProperty("revocationList") + val revocationList: ByteArray +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message3.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message3.kt new file mode 100644 index 0000000000..609472f0b2 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message3.kt @@ -0,0 +1,26 @@ +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("aesCMAC", "ga", "quote", "securityManifest", "nonce") +@JsonInclude(NON_NULL) +class Message3( + @param:JsonProperty("aesCMAC") + val aesCMAC: ByteArray, // Not ASN.1 encoded + + // The client's 512 bit public DH key (Little Endian) + @param:JsonProperty("ga") + val ga: ByteArray, + + @param:JsonProperty("quote") + val quote: ByteArray, + + @param:JsonProperty("securityManifest") + val securityManifest: ByteArray? = null, + + @param:JsonProperty("nonce") + val nonce: String? = null +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message4.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message4.kt new file mode 100644 index 0000000000..c17232b205 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message4.kt @@ -0,0 +1,67 @@ +package net.corda.attestation.message + +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 java.time.LocalDateTime + +@JsonPropertyOrder( + "reportID", + "quoteStatus", + "quoteBody", + "aesCMAC", + "securityManifestStatus", + "securityManifestHash", + "platformInfo", + "epidPseudonym", + "nonce", + "secret", + "secretHash", + "secretIV", + "timestamp" +) +@JsonInclude(NON_NULL) +class Message4( + @param:JsonProperty("reportID") + val reportID: String, + + @param:JsonProperty("quoteStatus") + val quoteStatus: String, + + @param:JsonProperty("quoteBody") + val quoteBody: ByteArray, + + @param:JsonProperty("aesCMAC") + val aesCMAC: ByteArray, + + @param:JsonProperty("securityManifestStatus") + val securityManifestStatus: String? = null, + + @param:JsonProperty("securityManifestHash") + val securityManifestHash: String? = null, + + @param:JsonProperty("platformInfo") + @get:JsonInclude(NON_EMPTY) + val platformInfo: ByteArray? = null, + + @param:JsonProperty("epidPseudonym") + val epidPseudonym: ByteArray? = null, + + @param:JsonProperty("nonce") + val nonce: String? = null, + + @param:JsonProperty("secret") + val secret: ByteArray, + + @param:JsonProperty("secretHash") + val secretHash: ByteArray, + + @param:JsonProperty("secretIV") + val secretIV: ByteArray, + + @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-server/src/main/kotlin/net/corda/attestation/message/ias/HexadecimalSerialisers.kt b/sgx-jvm/remote-attestation/attestation-server/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-server/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-server/src/main/kotlin/net/corda/attestation/message/ias/ManifestStatus.kt b/sgx-jvm/remote-attestation/attestation-server/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-server/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-server/src/main/kotlin/net/corda/attestation/message/ias/QuoteStatus.kt b/sgx-jvm/remote-attestation/attestation-server/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-server/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-server/src/main/kotlin/net/corda/attestation/message/ias/ReportProxyResponse.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportProxyResponse.kt new file mode 100644 index 0000000000..6ddac73ce3 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportProxyResponse.kt @@ -0,0 +1,19 @@ +package net.corda.attestation.message.ias + +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-server/src/main/kotlin/net/corda/attestation/message/ias/ReportRequest.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportRequest.kt new file mode 100644 index 0000000000..7bf5002795 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportRequest.kt @@ -0,0 +1,19 @@ +package net.corda.attestation.message.ias + +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-server/src/main/kotlin/net/corda/attestation/message/ias/ReportResponse.kt b/sgx-jvm/remote-attestation/attestation-server/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-server/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-server/src/main/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponse.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponse.kt new file mode 100644 index 0000000000..aa046d56f5 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponse.kt @@ -0,0 +1,16 @@ +package net.corda.attestation.message.ias + +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("spid", "revocationList") +@JsonInclude(NON_NULL) +class RevocationListProxyResponse ( + @param:JsonProperty("spid") + val spid: String, + + @param:JsonProperty("revocationList") + val revocationList: ByteArray +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/MockIAS.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/MockIAS.kt new file mode 100644 index 0000000000..c0ef8e0859 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/MockIAS.kt @@ -0,0 +1,59 @@ +package net.corda.mockias + +import net.corda.attestation.message.ias.ManifestStatus +import net.corda.attestation.message.ias.QuoteStatus +import net.corda.attestation.message.ias.ReportRequest +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() == "0000000b") "" else "AAIADgAAAAEAAAABAAAAAGSf/es1h/XiJeCg7bXmX0S/NUpJ2jmcEJglQUI8VT5sLGU7iMFu3/UTCv9uPADal3LhbrQvhBa6+/dWbj8hnsE=") + .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/attestation-server/src/main/kotlin/net/corda/mockias/ReportSigner.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/ReportSigner.kt new file mode 100644 index 0000000000..33f5fe24dc --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/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 \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/io/SignatureOutputStream.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/io/SignatureOutputStream.kt new file mode 100644 index 0000000000..c923a039af --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/io/SignatureOutputStream.kt @@ -0,0 +1,42 @@ +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() + } + + @Throws(SignatureException::class) + fun sign(): ByteArray = signature.sign() +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/resources/dummyIAS.pfx b/sgx-jvm/remote-attestation/attestation-server/src/main/resources/dummyIAS.pfx new file mode 100644 index 0000000000..d9aea1caeb Binary files /dev/null and b/sgx-jvm/remote-attestation/attestation-server/src/main/resources/dummyIAS.pfx differ diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/resources/dummyIAStrust.pfx b/sgx-jvm/remote-attestation/attestation-server/src/main/resources/dummyIAStrust.pfx new file mode 100644 index 0000000000..0e46286671 Binary files /dev/null and b/sgx-jvm/remote-attestation/attestation-server/src/main/resources/dummyIAStrust.pfx differ 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 new file mode 100644 index 0000000000..5711c1a9c8 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/resources/log4j2.xml @@ -0,0 +1,49 @@ + + + + + ${sys:attestation.home} + attestation-server + ${sys:log-path}/archive + info + debug + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/security.properties b/sgx-jvm/remote-attestation/attestation-server/src/main/security.properties new file mode 100644 index 0000000000..28b8893e70 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/security.properties @@ -0,0 +1 @@ +crypto.policy=unlimited diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/.gitignore b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/.gitignore new file mode 100644 index 0000000000..92409802c8 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/.gitignore @@ -0,0 +1 @@ +client.key diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/AttestationReportSigningCACert.pem b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/AttestationReportSigningCACert.pem new file mode 100644 index 0000000000..27332a1d1f --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/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/attestation-server/src/main/ssl/intel-ssl/client.crt b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/client.crt new file mode 100644 index 0000000000..0e1132974f --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/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/attestation-server/src/main/ssl/intel-ssl/generate-keystores.sh b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/generate-keystores.sh new file mode 100755 index 0000000000..c8c8f268fe --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/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 connecting with IAS. +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/attestation-server/src/main/webapp/WEB-INF/web.xml b/sgx-jvm/remote-attestation/attestation-server/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..5ef0bb1716 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,9 @@ + + + + 10 + + diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/ByteUtils.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/ByteUtils.kt new file mode 100644 index 0000000000..e20a9c953c --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/ByteUtils.kt @@ -0,0 +1,10 @@ +@file:JvmName("ByteUtils") +package net.corda.attestation + +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() + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/CryptoProvider.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/CryptoProvider.kt new file mode 100644 index 0000000000..d8482e0492 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/CryptoProvider.kt @@ -0,0 +1,16 @@ +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 + +class CryptoProvider : TestRule { + override fun apply(statement: Statement, description: Description?): Statement { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(BouncyCastleProvider()) + } + return statement + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/CryptoTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/CryptoTest.kt new file mode 100644 index 0000000000..30b2d06bac --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/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 = 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-server/src/test/kotlin/net/corda/attestation/DummyRandom.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/DummyRandom.kt new file mode 100644 index 0000000000..ad05129616 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/DummyRandom.kt @@ -0,0 +1,15 @@ +package net.corda.attestation + +import java.security.SecureRandom +import java.util.* + +class DummyRandom : SecureRandom(byteArrayOf()) { + override fun nextBoolean() = false + override fun nextInt() = 9 + override fun nextInt(bound: Int) = 0 + override fun nextBytes(bytes: ByteArray) = Arrays.fill(bytes, 9) + override fun nextDouble() = 9.0 + override fun nextFloat() = 9.0f + override fun nextLong() = 9L + override fun nextGaussian() = 0.0 +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/EndianUtilsTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/EndianUtilsTest.kt new file mode 100644 index 0000000000..2ea1634098 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/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-server/src/test/kotlin/net/corda/attestation/SampleTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/SampleTest.kt new file mode 100644 index 0000000000..513697d572 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/SampleTest.kt @@ -0,0 +1,84 @@ +package net.corda.attestation + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.security.* +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.spec.* + +class SampleTest { + private companion object { + private val privateKeyBytesBE = unsignedByteArrayOf( + 0x40, 0x8A, 0x06, 0x1A, 0xE6, 0x81, 0x3A, 0x7B, 0xAC, 0x4E, 0x34, 0xAA, 0x4D, 0x61, 0x3C, 0x86, + 0xF3, 0xDD, 0x9C, 0x48, 0x82, 0xA8, 0x68, 0x57, 0xFC, 0xB3, 0x9D, 0xF9, 0x81, 0xB6, 0x56, 0x2D + ) + private val publicKeyBytesBE = unsignedByteArrayOf( + 0x39, 0x16, 0xB0, 0x69, 0xB2, 0xBB, 0x0F, 0x92, 0xC4, 0x10, 0x27, 0x30, 0x9E, + 0x45, 0x61, 0x93, 0xB1, 0x67, 0x4F, 0xFB, 0x3E, 0xBC, 0x1F, 0xC5, 0xAE, 0x9F, 0x1A, 0x59, 0x45, 0x9F, 0x8C, 0xC0, + 0x4F, 0x96, 0x00, 0x30, 0x6E, 0x6C, 0x1F, 0x23, 0xF1, 0x5A, 0x2B, 0x1C, 0xC8, 0x32, 0xEB, 0xB8, 0xDC, 0x6A, 0x1A, + 0xA9, 0xE0, 0xCA, 0x35, 0x2A, 0x72, 0x46, 0x52, 0x2B, 0x24, 0x6B, 0x98, 0x5D + ) + + private val privateKeyBytesLE = unsignedByteArrayOf( + 0x90, 0xe7, 0x6c, 0xbb, 0x2d, 0x52, 0xa1, 0xce, 0x3b, 0x66, 0xde, 0x11, 0x43, 0x9c, 0x87, 0xec, + 0x1f, 0x86, 0x6a, 0x3b, 0x65, 0xb6, 0xae, 0xea, 0xad, 0x57, 0x34, 0x53, 0xd1, 0x03, 0x8c, 0x01 + ) + private val publicKeyBytesLE = unsignedByteArrayOf( + 0x72, 0x12, 0x8a, 0x7a, 0x17, 0x52, 0x6e, 0xbf, 0x85, 0xd0, 0x3a, 0x62, 0x37, 0x30, 0xae, 0xad, + 0x3e, 0x3d, 0xaa, 0xee, 0x9c, 0x60, 0x73, 0x1d, 0xb0, 0x5b, 0xe8, 0x62, 0x1c, 0x4b, 0xeb, 0x38, + 0xd4, 0x81, 0x40, 0xd9, 0x50, 0xe2, 0x57, 0x7b, 0x26, 0xee, 0xb7, 0x41, 0xe7, 0xc6, 0x14, 0xe2, + 0x24, 0xb7, 0xbd, 0xc9, 0x03, 0xf2, 0x9a, 0x28, 0xa8, 0x3c, 0xc8, 0x10, 0x11, 0x14, 0x5e, 0x06 + ) + } + + private lateinit var ecParameters: ECParameterSpec + private lateinit var keyFactory: KeyFactory + private lateinit var crypto: Crypto + + private lateinit var publicKey: ECPublicKey + private lateinit var privateKey: ECPrivateKey + + @Before + fun setup() { + crypto = Crypto(DummyRandom()) + keyFactory = KeyFactory.getInstance("EC") + ecParameters = (crypto.generateKeyPair().public as ECPublicKey).params + publicKey = keyFactory.generatePublic(publicKeyBytesLE.toBigEndianKeySpec(ecParameters)) as ECPublicKey + privateKey = keyFactory.generatePrivate(ECPrivateKeySpec(privateKeyBytesLE.reversedArray().toPositiveInteger(), ecParameters)) as ECPrivateKey + } + + @Test + fun checkKey() { + val myData = "And now for something completely different!" + val signature = Signature.getInstance("SHA256WithECDSA").let { signer -> + signer.initSign(privateKey, crypto.random) + signer.update(myData.toByteArray()) + signer.sign() + } + val verified = Signature.getInstance("SHA256WithECDSA").let { verifier -> + verifier.initVerify(publicKey) + verifier.update(myData.toByteArray()) + verifier.verify(signature) + } + assertTrue(verified) + } + + @Test + fun reversePublicKey() { + val transformedKey = keyFactory.generatePublic(publicKey.toLittleEndian().toBigEndianKeySpec(ecParameters)) as ECPublicKey + assertArrayEquals(publicKey.encoded, transformedKey.encoded) + } +} + +fun ByteArray.toKeySpec(ecParameters: ECParameterSpec): KeySpec { + if (size != KEY_SIZE) { + throw IllegalArgumentException("Public key has incorrect size ($size bytes)") + } + val ecPoint = ECPoint( + copyOf(size / 2).toPositiveInteger(), + copyOfRange(size / 2, size).toPositiveInteger() + ) + return ECPublicKeySpec(ecPoint, ecParameters) +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ChallengeResponseTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ChallengeResponseTest.kt new file mode 100644 index 0000000000..b3f634c610 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ChallengeResponseTest.kt @@ -0,0 +1,35 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class ChallengeResponseTest { + private companion object { + private val serviceKeyData = byteArrayOf(0x10, 0x00, 0x22, 0x00) + private val serviceKeyBase64 = serviceKeyData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialise() { + val challenge = ChallengeResponse("", serviceKeyData) + val str = mapper.writeValueAsString(challenge) + assertEquals("""{"nonce":"","serviceKey":"$serviceKeyBase64"}""", str) + } + + @Test + fun testDeserialise() { + val str = """{"nonce":"","serviceKey":"$serviceKeyBase64"}""" + val challenge = mapper.readValue(str, ChallengeResponse::class.java) + assertEquals("", challenge.nonce) + assertArrayEquals(serviceKeyData, challenge.serviceKey) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message0Test.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message0Test.kt new file mode 100644 index 0000000000..99e539868b --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message0Test.kt @@ -0,0 +1,29 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class Message0Test { + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialise() { + val msg0 = Message0(0x000F207F) + val str = mapper.writeValueAsString(msg0) + assertEquals("""{"extendedGID":991359}""", str) + } + + @Test + fun testDeserialise() { + val str = """{"extendedGID":991359}""" + val msg0 = mapper.readValue(str, Message0::class.java) + assertEquals(0x000F207F, msg0.extendedGID) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message1Test.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message1Test.kt new file mode 100644 index 0000000000..a0a4689bee --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message1Test.kt @@ -0,0 +1,38 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class Message1Test { + private companion object { + 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 msg1 = Message1( + ga = gaData, + platformGID = "2139062143" + ) + val str = mapper.writeValueAsString(msg1) + assertEquals("""{"ga":"$gaBase64","platformGID":"2139062143"}""", str) + } + + @Test + fun testDeserialise() { + val str = """{"ga":"$gaBase64","platformGID":"2139062143"}""" + val msg1 = mapper.readValue(str, Message1::class.java) + assertArrayEquals(gaData, msg1.ga) + assertEquals("2139062143", msg1.platformGID) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message2Test.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message2Test.kt new file mode 100644 index 0000000000..d7995cad23 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message2Test.kt @@ -0,0 +1,72 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class Message2Test { + private companion object { + private const val SPID = "8F42710C36029FA11744" + + private val gbData = byteArrayOf(0x10, 0x00, 0x22, 0x00) + private val gbBase64 = gbData.toBase64() + private val revocationListData = byteArrayOf(0x7F, 0x7F, 0x7F, 0x7F) + private val revocationListBase64 = revocationListData.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 msg2 = Message2( + gb = gbData, + spid = SPID, + linkableQuote = true, + keyDerivationFuncId = 1, + signatureGbGa = signatureData, + aesCMAC = aesCMACData, + revocationList = revocationListData + ) + val str = mapper.writeValueAsString(msg2) + assertEquals("{" + + "\"gb\":\"$gbBase64\"," + + "\"spid\":\"$SPID\"," + + "\"linkableQuote\":true," + + "\"keyDerivationFuncId\":1," + + "\"signatureGbGa\":\"$signatureBase64\"," + + "\"aesCMAC\":\"$aesCMACBase64\"," + + "\"revocationList\":\"$revocationListBase64\"" + + "}", str) + } + + @Test + fun testDeserialise() { + val str = """{ + "gb":"$gbBase64", + "spid":"$SPID", + "linkableQuote":true, + "keyDerivationFuncId":1, + "signatureGbGa":"$signatureBase64", + "aesCMAC":"$aesCMACBase64", + "revocationList":"$revocationListBase64" + }""" + val msg2 = mapper.readValue(str, Message2::class.java) + assertArrayEquals(gbData, msg2.gb) + assertEquals(SPID, msg2.spid) + assertTrue(msg2.linkableQuote) + assertEquals(1, msg2.keyDerivationFuncId) + assertArrayEquals(signatureData, msg2.signatureGbGa) + assertArrayEquals(aesCMACData, msg2.aesCMAC) + assertArrayEquals(revocationListData, msg2.revocationList) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message3Test.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message3Test.kt new file mode 100644 index 0000000000..7e61a12fe7 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message3Test.kt @@ -0,0 +1,90 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class Message3Test { + private companion object { + private val cmacData = byteArrayOf(0x50, 0x60, 0x70, 0x7F) + private val cmacBase64 = cmacData.toBase64() + private val gaData = byteArrayOf(0x10, 0x00, 0x22, 0x00) + private val gaBase64 = gaData.toBase64() + private val quoteData = byteArrayOf(0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F) + private val quoteBase64 = quoteData.toBase64() + private val manifestData = byteArrayOf(0x44, 0x44, 0x44, 0x44, 0x44, 0x44) + private val manifestBase64 = manifestData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testBasicSerialise() { + val msg3 = Message3( + aesCMAC = cmacData, + ga = gaData, + quote = quoteData + ) + val str = mapper.writeValueAsString(msg3) + assertEquals("{" + + "\"aesCMAC\":\"$cmacBase64\"," + + "\"ga\":\"$gaBase64\"," + + "\"quote\":\"$quoteBase64\"" + + "}", str) + } + + @Test + fun testFullSerialise() { + val msg3 = Message3( + aesCMAC = cmacData, + ga = gaData, + quote = quoteData, + securityManifest = manifestData, + nonce = "" + ) + val str = mapper.writeValueAsString(msg3) + assertEquals("{" + + "\"aesCMAC\":\"$cmacBase64\"," + + "\"ga\":\"$gaBase64\"," + + "\"quote\":\"$quoteBase64\"," + + "\"securityManifest\":\"$manifestBase64\"," + + "\"nonce\":\"\"" + + "}", str) + } + + @Test + fun testBasicDeserialise() { + val str = """{ + "aesCMAC":"$cmacBase64", + "ga":"$gaBase64", + "quote":"$quoteBase64" + }""" + val msg3 = mapper.readValue(str, Message3::class.java) + assertArrayEquals(cmacData, msg3.aesCMAC) + assertArrayEquals(gaData, msg3.ga) + assertArrayEquals(quoteData, msg3.quote) + } + + @Test + fun testFullDeserialise() { + val str = """{ + "aesCMAC":"$cmacBase64", + "ga":"$gaBase64", + "quote":"$quoteBase64", + "securityManifest":"$manifestBase64", + "nonce":"" + }""" + val msg3 = mapper.readValue(str, Message3::class.java) + assertArrayEquals(cmacData, msg3.aesCMAC) + assertArrayEquals(gaData, msg3.ga) + assertArrayEquals(quoteData, msg3.quote) + assertArrayEquals(manifestData, msg3.securityManifest) + assertEquals("", msg3.nonce) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message4Test.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message4Test.kt new file mode 100644 index 0000000000..ae054f7c2f --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message4Test.kt @@ -0,0 +1,156 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +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 Message4Test { + private companion object { + private val iso8601Time = "2017-11-09T15:03:46.345678" + private val testTimestamp = LocalDateTime.from(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(iso8601Time)) + private val quoteBodyData = byteArrayOf(0x7D, 0x7D, 0x7D, 0x7D, 0x7D, 0x7D, 0x7D, 0x7D) + private val quoteBodyBase64 = quoteBodyData.toBase64() + private val cmacData = byteArrayOf(0x17, 0x1F, 0x73, 0x66, 0x2E, 0x3F) + private val cmacBase64 = cmacData.toBase64() + private val platformInfoData = unsignedByteArrayOf(0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF) + private val platformInfoBase64 = platformInfoData.toBase64() + private val pseudonymData = byteArrayOf(0x31, 0x7E, 0x4A, 0x14) + private val pseudonymBase64 = pseudonymData.toBase64() + private val secretData = "Mystery data".toByteArray() + private val secretBase64 = secretData.toBase64() + private val secretHashData = byteArrayOf(0x02, 0x08, 0x25, 0x74) + private val secretHashBase64 = secretHashData.toBase64() + private val secretIVData = byteArrayOf(0x62, 0x72, 0x2A, 0x4F, 0x0E, -0x44) + private val secretIVBase64 = secretIVData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper().registerModule(JavaTimeModule()) + } + + @Test + fun testBasicSerialise() { + val msg4 = Message4( + reportID = "", + quoteStatus = "OK", + quoteBody = quoteBodyData, + aesCMAC = cmacData, + platformInfo = byteArrayOf(), + secret = secretData, + secretHash = secretHashData, + secretIV = secretIVData, + timestamp = testTimestamp + ) + val str = mapper.writeValueAsString(msg4) + assertEquals("{" + + "\"reportID\":\"\"," + + "\"quoteStatus\":\"OK\"," + + "\"quoteBody\":\"$quoteBodyBase64\"," + + "\"aesCMAC\":\"$cmacBase64\"," + + "\"secret\":\"$secretBase64\"," + + "\"secretHash\":\"$secretHashBase64\"," + + "\"secretIV\":\"$secretIVBase64\"," + + "\"timestamp\":\"$iso8601Time\"" + + "}", str) + } + + @Test + fun testFullSerialise() { + val msg4 = Message4( + reportID = "", + quoteStatus = "GROUP_OUT_OF_DATE", + quoteBody = quoteBodyData, + aesCMAC = cmacData, + securityManifestStatus = "INVALID", + securityManifestHash = "", + platformInfo = platformInfoData, + epidPseudonym = pseudonymData, + nonce = "", + secret = secretData, + secretHash = secretHashData, + secretIV = secretIVData, + timestamp = testTimestamp + ) + val str = mapper.writeValueAsString(msg4) + assertEquals("{" + + "\"reportID\":\"\"," + + "\"quoteStatus\":\"GROUP_OUT_OF_DATE\"," + + "\"quoteBody\":\"$quoteBodyBase64\"," + + "\"aesCMAC\":\"$cmacBase64\"," + + "\"securityManifestStatus\":\"INVALID\"," + + "\"securityManifestHash\":\"\"," + + "\"platformInfo\":\"$platformInfoBase64\"," + + "\"epidPseudonym\":\"$pseudonymBase64\"," + + "\"nonce\":\"\"," + + "\"secret\":\"$secretBase64\"," + + "\"secretHash\":\"$secretHashBase64\"," + + "\"secretIV\":\"$secretIVBase64\"," + + "\"timestamp\":\"$iso8601Time\"" + + "}", str) + } + + @Test + fun testBasicDeserialise() { + val str = """{ + "reportID":"", + "quoteStatus":"OK", + "quoteBody":"$quoteBodyBase64", + "aesCMAC":"$cmacBase64", + "secret":"$secretBase64", + "secretHash":"$secretHashBase64", + "secretIV":"$secretIVBase64", + "timestamp":"$iso8601Time" + }""" + val msg4 = mapper.readValue(str, Message4::class.java) + assertEquals("", msg4.reportID) + assertEquals("OK", msg4.quoteStatus) + assertArrayEquals(quoteBodyData, msg4.quoteBody) + assertArrayEquals(cmacData, msg4.aesCMAC) + assertNull(msg4.platformInfo) + assertArrayEquals(secretData, msg4.secret) + assertArrayEquals(secretHashData, msg4.secretHash) + assertArrayEquals(secretIVData, msg4.secretIV) + assertEquals(testTimestamp, msg4.timestamp) + } + + @Test + fun testFullDeserialise() { + val str = """{ + "reportID":"", + "quoteStatus":"GROUP_OUT_OF_DATE", + "quoteBody":"$quoteBodyBase64", + "aesCMAC":"$cmacBase64", + "securityManifestStatus":"INVALID", + "securityManifestHash":"", + "platformInfo":"$platformInfoBase64", + "epidPseudonym":"$pseudonymBase64", + "nonce":"", + "secret":"$secretBase64", + "secretHash":"$secretHashBase64", + "secretIV":"$secretIVBase64", + "timestamp":"$iso8601Time" + }""" + val msg4 = mapper.readValue(str, Message4::class.java) + assertEquals("", msg4.reportID) + assertEquals("GROUP_OUT_OF_DATE", msg4.quoteStatus) + assertArrayEquals(quoteBodyData, msg4.quoteBody) + assertArrayEquals(cmacData, msg4.aesCMAC) + assertEquals("INVALID", msg4.securityManifestStatus) + assertEquals("", msg4.securityManifestHash) + assertArrayEquals(platformInfoData, msg4.platformInfo) + assertArrayEquals(pseudonymData, msg4.epidPseudonym) + assertEquals("", msg4.nonce) + assertArrayEquals(secretData, msg4.secret) + assertArrayEquals(secretHashData, msg4.secretHash) + assertArrayEquals(secretIVData, msg4.secretIV) + assertEquals(testTimestamp, msg4.timestamp) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/MessageUtils.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/MessageUtils.kt new file mode 100644 index 0000000000..7603136b5c --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/MessageUtils.kt @@ -0,0 +1,6 @@ +@file:JvmName("MessageUtils") +package net.corda.attestation.message + +import java.util.* + +fun ByteArray.toBase64(): String = Base64.getEncoder().encodeToString(this) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportProxyResponseTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportProxyResponseTest.kt new file mode 100644 index 0000000000..0745cd196b --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportProxyResponseTest.kt @@ -0,0 +1,49 @@ +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.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) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportRequestTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportRequestTest.kt new file mode 100644 index 0000000000..db464fecbd --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportRequestTest.kt @@ -0,0 +1,62 @@ +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.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) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportResponseTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportResponseTest.kt new file mode 100644 index 0000000000..4a7856d2a9 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/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.toBase64 +import net.corda.attestation.message.ias.ManifestStatus.* +import net.corda.attestation.message.ias.QuoteStatus.* +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 = "", + isvEnclaveQuoteStatus = QuoteStatus.OK, + isvEnclaveQuoteBody = quoteBodyData, + timestamp = testTimestamp + ) + val str = mapper.writeValueAsString(response) + assertEquals("{" + + "\"id\":\"\"," + + "\"timestamp\":\"$iso8601Time\"," + + "\"isvEnclaveQuoteStatus\":\"OK\"," + + "\"isvEnclaveQuoteBody\":\"$quoteBodyBase64\"" + + "}", str) + } + + @Test + fun testSerialiseFull() { + val response = ReportResponse( + id = "", + 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\":\"\"," + + "\"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":"", + "isvEnclaveQuoteStatus":"OK", + "isvEnclaveQuoteBody":"$quoteBodyBase64", + "timestamp":"$iso8601Time" + }""" + val response = mapper.readValue(str, ReportResponse::class.java) + assertEquals("", 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":"", + "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("", 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-server/src/test/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponseTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponseTest.kt new file mode 100644 index 0000000000..70f4819dba --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponseTest.kt @@ -0,0 +1,46 @@ +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.toBase64 +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class RevocationListProxyResponseTest { + private companion object { + private const val SPID = "84D402C36BA9EF9B0A86EF1A9CC8CE4F" + private val revocationListData = byteArrayOf(0x51, 0x62, 0x43, 0x24, 0x75, 0x4D) + private val revocationListBase64 = revocationListData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialiseBasic() { + val response = RevocationListProxyResponse( + spid = SPID, + revocationList = revocationListData + ) + val str = mapper.writeValueAsString(response) + assertEquals("{" + + "\"spid\":\"$SPID\"," + + "\"revocationList\":\"$revocationListBase64\"" + + "}", str) + } + + @Test + fun testDeserialiseBasic() { + val str = """{ + "spid":"$SPID", + "revocationList":"$revocationListBase64" + }""" + val response = mapper.readValue(str, RevocationListProxyResponse::class.java) + assertEquals(SPID, response.spid) + assertArrayEquals(revocationListData, response.revocationList) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/mockias/io/SignatureOutputStreamTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/mockias/io/SignatureOutputStreamTest.kt new file mode 100644 index 0000000000..a7ce271d32 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/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()) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/build.gradle b/sgx-jvm/remote-attestation/build.gradle new file mode 100644 index 0000000000..5f77b998ad --- /dev/null +++ b/sgx-jvm/remote-attestation/build.gradle @@ -0,0 +1,63 @@ +buildscript { + // For sharing constants between builds + Properties constants = new Properties() + file("../../constants.properties").withInputStream { constants.load(it) } + + ext.kotlin_version = constants.getProperty("kotlinVersion") + ext.bouncycastle_version = constants.getProperty("bouncycastleVersion") + ext.resteasy_version = '3.1.4.Final' + ext.jackson_version = '2.9.2' + ext.slf4j_version = '1.7.25' + ext.log4j_version = '2.9.1' + ext.junit_version = '4.12' + + repositories { + mavenLocal() + mavenCentral() + jcenter() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" + } +} + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +allprojects { + tasks.withType(KotlinCompile).all { + kotlinOptions { + languageVersion = "1.1" + apiVersion = "1.1" + jvmTarget = "1.8" + javaParameters = true // Useful for reflection. + } + } + + tasks.withType(Test) { + // Prevent the project from creating temporary files outside of the build directory. + systemProperties['java.io.tmpdir'] = buildDir + } + + group 'com.r3.corda.enterprise' + version '1.0-SNAPSHOT' + + repositories { + mavenLocal() + mavenCentral() + jcenter() + } + + configurations { + compile { + // We want to use SLF4J's version of these bindings: jcl-over-slf4j + // Remove any transitive dependency on Apache's version. + exclude group: 'commons-logging', module: 'commons-logging' + } + } +} + +task wrapper(type: Wrapper) { + gradleVersion = "4.3.1" +} diff --git a/sgx-jvm/remote-attestation/gradle/wrapper/gradle-wrapper.jar b/sgx-jvm/remote-attestation/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..6b6ea3ab4f Binary files /dev/null and b/sgx-jvm/remote-attestation/gradle/wrapper/gradle-wrapper.jar differ diff --git a/sgx-jvm/remote-attestation/gradle/wrapper/gradle-wrapper.properties b/sgx-jvm/remote-attestation/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..0e680f3759 --- /dev/null +++ b/sgx-jvm/remote-attestation/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.3.1-bin.zip diff --git a/sgx-jvm/remote-attestation/gradlew b/sgx-jvm/remote-attestation/gradlew new file mode 100755 index 0000000000..cccdd3d517 --- /dev/null +++ b/sgx-jvm/remote-attestation/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/sgx-jvm/remote-attestation/gradlew.bat b/sgx-jvm/remote-attestation/gradlew.bat new file mode 100644 index 0000000000..e95643d6a2 --- /dev/null +++ b/sgx-jvm/remote-attestation/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sgx-jvm/remote-attestation/settings.gradle b/sgx-jvm/remote-attestation/settings.gradle new file mode 100644 index 0000000000..45be9e2c58 --- /dev/null +++ b/sgx-jvm/remote-attestation/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'remote-attestation' +include 'attestation-server'