ENT-1074 - Proof-of-concept ISV for SGX remote attestation (#161)

* Initial WIP.
* Configure IAS host via system properties.
* Create separate Gretty configurations for testing and for IAS.
* (WIP) Separate configuration values from WAR; Add msg3 -> msg4 handling.
* Check the IAS report's cryptographic signature.
* Accept CertPath from IAS instead of a Certificate.
* Validate the certificate chain for the IAS report.
* Refactor response handling, and add a secret to Message4.
* Append public DH keys to generated shared secret.
* Use DH secret to generate a 256 bit AES key.
* Fix runISV Gradle task so that it creates WAR file.
* Migrate MockIAS service into a separate package.
* Remove unused aesCMAC field from Message3.
* Configure HTTP sessions to expire after 10 idle minutes.
* Ensure we select the "isv" key for MTLS with Intel Attestation Service.
* Set key alias for Intel's public certificate.
* Implement GET /attest/provision endpoint.
* Use elliptic curves for Diffie-Hellman keys.
* Pass public keys as Little Endian byte arrays without ASN.1 encoding.
* Add AES-CMAC signature to Message2.
* Remove signature fields from QUOTE body for sending to IAS.
* Add a dummy AES-CMAC field to Message3 for later validation.
* Generate AEC-CMAC for Message 3, and refactor crypto functionality.
* Calculate AES-CMAC using AES/CBC/PKCS5Padding algorithm.
* Use BouncyCastle's AESCMAC algorithm for MAC calculation.
* Include standard crypto test vectors to the unit tests.
* Encrypt MSG3 secret using AES/GCM/NoPadding with 128 bit key.
* Hash shared key with Little Endian versions of public keys.
* Refactor so that hexToBytes() is a utility.
* Simplify signing of MocKIAS report.
* Separate AES/GCM authentication tag from the encrypted data.
* Create /ias/report endpoint for ISV which proxies IAS.
* Remove unnecessary @Throws from MockIAS handlers.
* Log HTTP error status from IAS.
* Replace runISV task with startISV and stopISV tasks.
* Refactor tests to use CryptoProvider @Rule instead of @Suite.
* Move Web server for integration tests to use non-production ports.
* Add proxy endpoint for IAS revocation list.
* Generate an ECDSA "service key" for signing (gb|ga).
* Generate a persistent key-pair for the ISV to sign with.
* Verify the (Gb|Ga) signature from Message2.
* Add debugging aids.
* Fix Gradle warning.
* Remove TLV header from Platform Info Body for MSG4.
* Small tidy-up.
* Use SPID "as-is" when calculating CMAC for MSG2.
* Add DEBUG messages for MSG2's KDK and SMK values (AES-CMAC).
* Add DEBUG logging for ECDH shared secret.
* More DEBUG logging.
* The ECDH shared secret *is* the x-coordinate: no need to subrange.
* Adjust MockIAS to return an empty revocationList for GID 0000000b.
* Fix ArrayOutOfBoundsException for "small" integer values.
* Test MSG1 with empty revocation list.
* Add extra logging for IAS report request.
* ReportResponse object cannot be null.
* Fix misreading of spec - don't remove quote's signature when requesting report from IAS.
* Log invalid contents of X-IAS-Report-Signing-Certificate HTTP header.
* Build CertPath for IAS from explicit list of Certificates.
* Rename quote fields on IAS ReportResponse to match Intel.
* Log report ID and quote status from IAS.
* Add a revocation list checker to the certificate path validator.
* Tweak revocation list options, depending on IAS vs MockIAS.
* Extract Intel's certificate specifically by alias for PKIX.
* Tune quote body returned by MockIAS.
* Add AES-CMAC field to Message4 for validation.
* Increase GCM authentication tag to 128 bits.
* Receive platformInfoBlob from IAS as hexadecimal string.
* Generate secret encryption key using KDK and SK values.
* Marshall platformInfoBlob between Base16 string and ByteArray.
* Interpret status results from IAS as enums.
* Use lateinit for HttpServletRequest field.
* Refactor ExceptionHandler out of messages package.
* Alias is for ISV, so rename it.
* Refactor classes into more correct packages.
* Use random 96 bit IV for GCM encryption.
* Parameterise HTTP/HTTPS ports via Gradle.
* Do not forward a securityManifest containing only zeros to IAS.
* Address review comments.
* Review comment: Use NativePRNGNonBlocking for SecureRandom.
* Rename isv.pfx to isv-svc.pfx
* Rename keystore to isv.pfx, for clarity.
* Update scripts so that they no longer require user input.
* Generate isv.pfx from the key and certificates.
* Remove private key from repository.
* Declare an empty PSE Manifest to be invalid.
* Generate keystores "on the fly".
* Rename integration tests to end in "IT" instead of "Test".
* Add README
* Turn remote-attestation into a separate Gradle project.
This commit is contained in:
Chris Rankin 2017-12-12 13:34:26 +00:00 committed by GitHub
parent 1fc200efa7
commit 2725f53ef5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 3999 additions and 1 deletions

View File

@ -56,4 +56,3 @@ project(':hsm-tool').with {
projectDir = file("$settingsDir/sgx-jvm/hsm-tool") projectDir = file("$settingsDir/sgx-jvm/hsm-tool")
} }
include 'perftestcordapp' include 'perftestcordapp'

View File

@ -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`.

View File

@ -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)

View File

@ -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
}
}

View File

@ -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())
}
}

View File

@ -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}")
}
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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())
}
}

View File

@ -0,0 +1,2 @@
crypto.policy=unlimited

View File

@ -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} <<EOF
yes
EOF
rm -f jetty.cert

View File

@ -0,0 +1,119 @@
package net.corda.attestation
import org.slf4j.LoggerFactory
import java.nio.charset.StandardCharsets.*
import java.security.*
import java.security.spec.ECGenParameterSpec
import javax.crypto.Cipher
import javax.crypto.Cipher.*
import javax.crypto.KeyAgreement
import javax.crypto.Mac
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
class Crypto(val random: SecureRandom = SecureRandom.getInstance("NativePRNGNonBlocking")) {
internal companion object {
private const val AES_ALGORITHM = "AES/GCM/NoPadding"
private const val macBlockSize = 16
private const val gcmIvLength = 12
const val gcmTagLength = 16
@JvmStatic
private val log = LoggerFactory.getLogger(Crypto::class.java)
@JvmStatic
private val smkValue = byteArrayOf(
0x01,
'S'.toAscii(),
'M'.toAscii(),
'K'.toAscii(),
0x00,
0x80.toByte(),
0x00
)
@JvmStatic
private val mkValue = byteArrayOf(
0x01,
'M'.toAscii(),
'K'.toAscii(),
0x00,
0x80.toByte(),
0x00
)
@JvmStatic
private val skValue = byteArrayOf(
0x01,
'S'.toAscii(),
'K'.toAscii(),
0x00,
0x80.toByte(),
0x00
)
private fun Char.toAscii() = toString().toByteArray(US_ASCII)[0]
}
private val keyPairGenerator: KeyPairGenerator = KeyPairGenerator.getInstance("EC")
init {
keyPairGenerator.initialize(ECGenParameterSpec("secp256r1"), random)
}
fun generateKeyPair(): KeyPair = keyPairGenerator.generateKeyPair()
fun aesCMAC(key: ByteArray = ByteArray(macBlockSize), value: ByteArray): ByteArray = aesCMAC(key, { aes -> 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) })

View File

@ -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()

View File

@ -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<WebApplicationException> {
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
}
}

View File

@ -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<ConnectionSocketFactory>()
.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")
}

View File

@ -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<ObjectMapper> {
private val mapper = ObjectMapper().registerModule(JavaTimeModule())
override fun getContext(type: Class<*>?): ObjectMapper = mapper
}

View File

@ -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<TrustAnchor>
= 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<Certificate>()
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<ConnectionSocketFactory>()
.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
}
}

View File

@ -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())
}
}

View File

@ -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)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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>(ByteArray::class.java) {
override fun serialize(value: ByteArray, gen: JsonGenerator, provider: SerializerProvider) = gen.writeString(value.toHexString())
}
class HexadecimalDeserialiser : StdDeserializer<ByteArray>(ByteArray::class.java) {
override fun deserialize(p: JsonParser, context: DeserializationContext) = p.valueAsString.hexToBytes()
}

View File

@ -0,0 +1,10 @@
package net.corda.attestation.message.ias
enum class ManifestStatus {
OK,
UNKNOWN,
INVALID,
OUT_OF_DATE,
REVOKED,
RL_VERSION_MISMATCH
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
}

View File

@ -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

View File

@ -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()
}

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info">
<Properties>
<Property name="log-path">${sys:attestation.home}</Property>
<Property name="log-name">attestation-server</Property>
<Property name="archive">${sys:log-path}/archive</Property>
<Property name="consoleLogLevel">info</Property>
<Property name="defaultLogLevel">debug</Property>
</Properties>
<ThresholdFilter level="trace"/>
<Appenders>
<!-- Will generate up to 10 log files for a given day. During every rollover it will delete
those that are older than 60 days, but keep the most recent 10 GB -->
<RollingFile name="RollingFile-Appender"
fileName="${sys:log-path}/${log-name}.log"
filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="%date{ISO8601}{UTC}Z [%-5level] %c - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<DefaultRolloverStrategy min="1" max="10">
<Delete basePath="${archive}" maxDepth="1">
<IfFileName glob="${log-name}*.log.gz"/>
<IfLastModified age="60d">
<IfAny>
<IfAccumulatedFileSize exceeds="10 GB"/>
</IfAny>
</IfLastModified>
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</Appenders>
<Loggers>
<Root level="${sys:defaultLogLevel}">
<AppenderRef ref="RollingFile-Appender"/>
</Root>
<Logger name="org.apache.http" level="warn"/>
<Logger name="org.jboss.resteasy" level="warn"/>
</Loggers>
</Configuration>

View File

@ -0,0 +1 @@
crypto.policy=unlimited

View File

@ -0,0 +1 @@
client.key

View File

@ -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-----

View File

@ -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-----

View File

@ -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} <<EOF
yes
EOF
fi

View File

@ -0,0 +1,23 @@
#!/bin/sh
STOREPASS=attestation
ALIAS=isv-svc
rm -f isv-svc.pfx
openssl ecparam -name secp256r1 -genkey -noout -out privateKey.pem
openssl req -new -key privateKey.pem -x509 -out server.crt -days 1000 <<EOF
.
.
.
localhost
EOF
openssl pkcs12 -export -out isv-svc.pfx -inkey privateKey.pem -in server.crt -passout pass:${STOREPASS}
keytool -keystore isv-svc.pfx -storetype pkcs12 -changealias -alias 1 -destalias ${ALIAS} -storepass ${STOREPASS}
rm -f *.pem *.crt

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<JarScanner>
<JarScanFilter defaultTldScan="false"/>
</JarScanner>
</Context>

View File

@ -0,0 +1,9 @@
<?xml version='1.0' encoding='UTF-8'?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<session-config>
<session-timeout>10</session-timeout>
</session-config>
</web-app>

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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("<nonsense>", serviceKeyData)
val str = mapper.writeValueAsString(challenge)
assertEquals("""{"nonce":"<nonsense>","serviceKey":"$serviceKeyBase64"}""", str)
}
@Test
fun testDeserialise() {
val str = """{"nonce":"<nonsense>","serviceKey":"$serviceKeyBase64"}"""
val challenge = mapper.readValue(str, ChallengeResponse::class.java)
assertEquals("<nonsense>", challenge.nonce)
assertArrayEquals(serviceKeyData, challenge.serviceKey)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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 = "<nonce-value>"
)
val str = mapper.writeValueAsString(msg3)
assertEquals("{"
+ "\"aesCMAC\":\"$cmacBase64\","
+ "\"ga\":\"$gaBase64\","
+ "\"quote\":\"$quoteBase64\","
+ "\"securityManifest\":\"$manifestBase64\","
+ "\"nonce\":\"<nonce-value>\""
+ "}", 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":"<nonce-value>"
}"""
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("<nonce-value>", msg3.nonce)
}
}

View File

@ -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 = "<report-id>",
quoteStatus = "OK",
quoteBody = quoteBodyData,
aesCMAC = cmacData,
platformInfo = byteArrayOf(),
secret = secretData,
secretHash = secretHashData,
secretIV = secretIVData,
timestamp = testTimestamp
)
val str = mapper.writeValueAsString(msg4)
assertEquals("{"
+ "\"reportID\":\"<report-id>\","
+ "\"quoteStatus\":\"OK\","
+ "\"quoteBody\":\"$quoteBodyBase64\","
+ "\"aesCMAC\":\"$cmacBase64\","
+ "\"secret\":\"$secretBase64\","
+ "\"secretHash\":\"$secretHashBase64\","
+ "\"secretIV\":\"$secretIVBase64\","
+ "\"timestamp\":\"$iso8601Time\""
+ "}", str)
}
@Test
fun testFullSerialise() {
val msg4 = Message4(
reportID = "<report-id>",
quoteStatus = "GROUP_OUT_OF_DATE",
quoteBody = quoteBodyData,
aesCMAC = cmacData,
securityManifestStatus = "INVALID",
securityManifestHash = "<hash-value>",
platformInfo = platformInfoData,
epidPseudonym = pseudonymData,
nonce = "<nonce-value>",
secret = secretData,
secretHash = secretHashData,
secretIV = secretIVData,
timestamp = testTimestamp
)
val str = mapper.writeValueAsString(msg4)
assertEquals("{"
+ "\"reportID\":\"<report-id>\","
+ "\"quoteStatus\":\"GROUP_OUT_OF_DATE\","
+ "\"quoteBody\":\"$quoteBodyBase64\","
+ "\"aesCMAC\":\"$cmacBase64\","
+ "\"securityManifestStatus\":\"INVALID\","
+ "\"securityManifestHash\":\"<hash-value>\","
+ "\"platformInfo\":\"$platformInfoBase64\","
+ "\"epidPseudonym\":\"$pseudonymBase64\","
+ "\"nonce\":\"<nonce-value>\","
+ "\"secret\":\"$secretBase64\","
+ "\"secretHash\":\"$secretHashBase64\","
+ "\"secretIV\":\"$secretIVBase64\","
+ "\"timestamp\":\"$iso8601Time\""
+ "}", str)
}
@Test
fun testBasicDeserialise() {
val str = """{
"reportID":"<report-id>",
"quoteStatus":"OK",
"quoteBody":"$quoteBodyBase64",
"aesCMAC":"$cmacBase64",
"secret":"$secretBase64",
"secretHash":"$secretHashBase64",
"secretIV":"$secretIVBase64",
"timestamp":"$iso8601Time"
}"""
val msg4 = mapper.readValue(str, Message4::class.java)
assertEquals("<report-id>", 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":"<report-id>",
"quoteStatus":"GROUP_OUT_OF_DATE",
"quoteBody":"$quoteBodyBase64",
"aesCMAC":"$cmacBase64",
"securityManifestStatus":"INVALID",
"securityManifestHash":"<hash-value>",
"platformInfo":"$platformInfoBase64",
"epidPseudonym":"$pseudonymBase64",
"nonce":"<nonce-value>",
"secret":"$secretBase64",
"secretHash":"$secretHashBase64",
"secretIV":"$secretIVBase64",
"timestamp":"$iso8601Time"
}"""
val msg4 = mapper.readValue(str, Message4::class.java)
assertEquals("<report-id>", msg4.reportID)
assertEquals("GROUP_OUT_OF_DATE", msg4.quoteStatus)
assertArrayEquals(quoteBodyData, msg4.quoteBody)
assertArrayEquals(cmacData, msg4.aesCMAC)
assertEquals("INVALID", msg4.securityManifestStatus)
assertEquals("<hash-value>", msg4.securityManifestHash)
assertArrayEquals(platformInfoData, msg4.platformInfo)
assertArrayEquals(pseudonymData, msg4.epidPseudonym)
assertEquals("<nonce-value>", msg4.nonce)
assertArrayEquals(secretData, msg4.secret)
assertArrayEquals(secretHashData, msg4.secretHash)
assertArrayEquals(secretIVData, msg4.secretIV)
assertEquals(testTimestamp, msg4.timestamp)
}
}

View File

@ -0,0 +1,6 @@
@file:JvmName("MessageUtils")
package net.corda.attestation.message
import java.util.*
fun ByteArray.toBase64(): String = Base64.getEncoder().encodeToString(this)

View File

@ -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 = "<signature-data>",
certificatePath = "<certificate-path>",
report = reportData
)
val str = mapper.writeValueAsString(response)
assertEquals("{"
+ "\"signature\":\"<signature-data>\","
+ "\"certificatePath\":\"<certificate-path>\","
+ "\"report\":\"$reportBase64\""
+ "}", str)
}
@Test
fun testDeserialiseBasic() {
val str = """{
"signature":"<signature-data>",
"certificatePath":"<certificate-path>",
"report":"$reportBase64"
}"""
val response = mapper.readValue(str, ReportProxyResponse::class.java)
assertEquals("<signature-data>", response.signature)
assertEquals("<certificate-path>", response.certificatePath)
assertArrayEquals(reportData, response.report)
}
}

View File

@ -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 = "<my-nonce>"
)
val str = mapper.writeValueAsString(request)
assertEquals("{\"isvEnclaveQuote\":\"$quoteBase64\",\"pseManifest\":\"$manifestBase64\",\"nonce\":\"<my-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":"<my-nonce>"}"""
val request = mapper.readValue(str, ReportRequest::class.java)
assertArrayEquals(quoteData, request.isvEnclaveQuote)
assertArrayEquals(manifestData, request.pseManifest)
assertEquals("<my-nonce>", 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)
}
}

View File

@ -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 = "<report-id>",
isvEnclaveQuoteStatus = QuoteStatus.OK,
isvEnclaveQuoteBody = quoteBodyData,
timestamp = testTimestamp
)
val str = mapper.writeValueAsString(response)
assertEquals("{"
+ "\"id\":\"<report-id>\","
+ "\"timestamp\":\"$iso8601Time\","
+ "\"isvEnclaveQuoteStatus\":\"OK\","
+ "\"isvEnclaveQuoteBody\":\"$quoteBodyBase64\""
+ "}", str)
}
@Test
fun testSerialiseFull() {
val response = ReportResponse(
id = "<report-id>",
isvEnclaveQuoteStatus = GROUP_OUT_OF_DATE,
isvEnclaveQuoteBody = quoteBodyData,
platformInfoBlob = platformInfoData,
revocationReason = 1,
pseManifestStatus = INVALID,
pseManifestHash = "<manifest-hash>",
nonce = "<nonce>",
epidPseudonym = pseudonymData,
timestamp = testTimestamp
)
val str = mapper.writeValueAsString(response)
assertEquals("{"
+ "\"nonce\":\"<nonce>\","
+ "\"id\":\"<report-id>\","
+ "\"timestamp\":\"$iso8601Time\","
+ "\"epidPseudonym\":\"$pseudonymBase64\","
+ "\"isvEnclaveQuoteStatus\":\"GROUP_OUT_OF_DATE\","
+ "\"isvEnclaveQuoteBody\":\"$quoteBodyBase64\","
+ "\"pseManifestStatus\":\"INVALID\","
+ "\"pseManifestHash\":\"<manifest-hash>\","
+ "\"platformInfoBlob\":\"123456789abcdef\","
+ "\"revocationReason\":1"
+ "}", str)
}
@Test
fun testDeserialiseBasic() {
val str = """{
"id":"<report-id>",
"isvEnclaveQuoteStatus":"OK",
"isvEnclaveQuoteBody":"$quoteBodyBase64",
"timestamp":"$iso8601Time"
}"""
val response = mapper.readValue(str, ReportResponse::class.java)
assertEquals("<report-id>", 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":"<report-id>",
"isvEnclaveQuoteStatus":"GROUP_OUT_OF_DATE",
"isvEnclaveQuoteBody":"$quoteBodyBase64",
"platformInfoBlob":"0123456789ABCDEF",
"revocationReason":1,
"pseManifestStatus":"OK",
"pseManifestHash":"<manifest-hash>",
"nonce":"<nonce>",
"epidPseudonym":"$pseudonymBase64",
"timestamp":"$iso8601Time"
}"""
val response = mapper.readValue(str, ReportResponse::class.java)
assertEquals("<report-id>", 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("<manifest-hash>", response.pseManifestHash)
assertEquals("<nonce>", response.nonce)
assertArrayEquals(pseudonymData, response.epidPseudonym)
assertEquals(testTimestamp, response.timestamp)
}
}

View File

@ -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)
}
}

View File

@ -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())
}
}

View File

@ -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"
}

Binary file not shown.

View File

@ -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

172
sgx-jvm/remote-attestation/gradlew vendored Executable file
View File

@ -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" "$@"

84
sgx-jvm/remote-attestation/gradlew.bat vendored Normal file
View File

@ -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

View File

@ -0,0 +1,2 @@
rootProject.name = 'remote-attestation'
include 'attestation-server'