mirror of
https://github.com/corda/corda.git
synced 2025-01-13 16:30:25 +00:00
Remote Attestation Phase 2 (#235)
* Initial host server skeleton. * Create IASProxy project, and skeleton for attestation host. * Fix up tests * Extend attestation host skeleton, and make test ports configurable. * Enhance MockIAS to make pseManifestStatus optional. * Make IASProxy endpoints asynchronous. * Add sub-modules for challenger and for common code. * Create integration test for host's provisioning endpoint. * Flesh out attestation challenger WAR. * Package refactoring, to be more Java9 friendly. * Refactor more messages into attestation-common. * Remove our private key from the repository. * Declare an empty PSE Manifest to be invalid. * Fix basic integration test issues for challenger and host. * Integrate keystore scripts into the build properly. * Name keystore targets explicitly for Gradle. * Allow HTTP conversation between Challenger, Host and ISV using session ID. * Add MockHost for challenger's integration tests. * Reconcile HTTP port numbers between Phase1 and Phase2 components. * Remove elements that can be inherited from root project. * Add placeholder README. * Add convenient extension functions to ObjectMapper. * Extend integration test coverage for challenger/host/isv. * Catch IOException from HttpClient for challenger. * Integrate host sub-module with remote-attestation project. * Begin integrating host/enclave code from Phase I. * Rename challenger's HTTP endpoint. * Generate keystore for challenger "on the fly". * Add native JNI code for accessing the SGX enclave. * Point Gradle to the correct enclave object. * Fixes for generating a Quote for this enclave. * Return the IAS report to the challenger for verification. * Begin populating the challenger's AttestationResponse message. * Enable the challenger to pass encrypted secrets into the enclave. * Align challenger, host and isv ports. * Refactor challenger as a fat-jar application. * AttestationResponse is not shared, so refactor into challenger. * Move HttpClientContext objects into HttpClient blocks. * Remove unused Message2 and Message3 objects. * Add realistic dummy value for reportID from IAS. * Small tidy-up on attestation host. * First set of review comments. * Add missing exception message. * Update location of environment file. * Use empty mock revocation lists by default. * Improve logging and add "happy path" test for provisioning secrets. * Update Gradle files so that we can run attestation-host from IntelliJ. * The platformInfo field from IAS can be null, so allow this. Also protect other JNI pointer parameters from NPE. * Allow Gradle to build hardware enclave.
This commit is contained in:
parent
83efd33fc7
commit
c545a58c1d
@ -67,7 +67,7 @@ ADD dependencies/proguard6.0beta1.tar.gz /usr/share/
|
||||
|
||||
# Expose ports for remote GDB and Java debugging, and test servers
|
||||
|
||||
EXPOSE 2000 5005 8080 9080
|
||||
EXPOSE 2000 5005 8080 8084 9080
|
||||
|
||||
# Environment
|
||||
|
||||
|
107
sgx-jvm/remote-attestation/README-Phase2.md
Normal file
107
sgx-jvm/remote-attestation/README-Phase2.md
Normal file
@ -0,0 +1,107 @@
|
||||
Remote Attestation Phase 2
|
||||
==========================
|
||||
|
||||
Phase 2 contains the following components:
|
||||
- `ias-proxy`: This is the ISV, and is an authorised client of the Intel Attestation Service (IAS).
|
||||
- `attestation-host`: This is a WAR running on an SGX-capable host with an enclave, and a client of `ias-proxy`.
|
||||
- `attestation-challenger`: This is an executable JAR, and client of `attestation-host`.
|
||||
|
||||
Building Instructions
|
||||
---------------------
|
||||
|
||||
- Ensure that your user ID belongs to the `docker` group. This will enable you to run Docker as an unprivileged user.
|
||||
|
||||
- Source the `environment` file:
|
||||
```bash
|
||||
$ . sgx-jvm/environment
|
||||
```
|
||||
|
||||
- Build the Docker container:
|
||||
```bash
|
||||
$ cd sgx-jvm/containers/core
|
||||
$ make
|
||||
```
|
||||
|
||||
- Build the SGX SDK:
|
||||
```bash
|
||||
$ sx build linux-sgx clean all
|
||||
```
|
||||
|
||||
- Build the SGX enclave:
|
||||
```bash
|
||||
$ sx build [-hp] remote-attestation/enclave clean all
|
||||
```
|
||||
Add the `-hp` options to build in "pre-release" mode for SGX hardware.
|
||||
|
||||
- Build the Attestation Host WAR:
|
||||
```bash
|
||||
$ sx build remote-attestation/attestation-host
|
||||
```
|
||||
|
||||
- Build the JNI library for the Attestation Host:
|
||||
```bash
|
||||
$ sx build [-hp] remote-attestation/attestation-host/native clean all
|
||||
```
|
||||
Add the `-hp` options to build in "pre-release" mode for SGX hardware. This setting
|
||||
must match the setting used to build the SGX enclave, or they will be incompatible
|
||||
at runtime.
|
||||
|
||||
- Install our private key for Mutual-TLS with IAS:
|
||||
```bash
|
||||
$ cp client.key sgx-jvm/remote-attestation/ias-proxy/src/main/ssl/intel-ssl
|
||||
```
|
||||
|
||||
- Build the IAS Proxy:
|
||||
```bash
|
||||
$ cd sgx-jvm/remote-attestation
|
||||
$ gradlew ias-proxy:build
|
||||
```
|
||||
|
||||
- Build the Attestation Challenger:
|
||||
```bash
|
||||
$ cd sgx-jvm/remote-attestation
|
||||
$ gradlew attestation-challenger:installDist
|
||||
```
|
||||
|
||||
Execution Instructions
|
||||
----------------------
|
||||
|
||||
- To launch the Attestation Host:
|
||||
```bash
|
||||
$ sx exec
|
||||
$ cd sgx-jvm/remote-attestation/attestation-host
|
||||
$ nohup ../gradlew [-Phardware=true] startHost >& OUT &
|
||||
$ tail -f build/logs/attestation-host.log
|
||||
```
|
||||
This can be shutdown again using:
|
||||
```bash
|
||||
$ ../gradlew stopHost
|
||||
```
|
||||
|
||||
- To launch the IAS Proxy:
|
||||
```bash
|
||||
$ cd sgx-jvm/remote-attestation/ias-proxy
|
||||
$ nohup ../gradlew startISV >& OUT &
|
||||
$ tail -f build/logs/ias-proxy.log
|
||||
```
|
||||
This can be shutdown again using:
|
||||
```bash
|
||||
$ ../gradlew stopISV
|
||||
```
|
||||
|
||||
- To execute the Attestation Challenger:
|
||||
```bash
|
||||
$ cd sgx-jvm/remote-attestation/attestation-challenger/build/install/attestation-challenger
|
||||
$ bin/attestation-challenger
|
||||
```
|
||||
Use this executable's `--help` option for more information.
|
||||
|
||||
When all of the components are working correctly, you should expect the challenger
|
||||
to output something like:
|
||||
```bash
|
||||
$ bin/attestation-challenger
|
||||
Report ID: 197283916372863387388037565359257649452
|
||||
Quote Status: OK
|
||||
Timestamp: 2017-12-20T15:06:37.222956
|
||||
Secret provisioned successfully.
|
||||
```
|
@ -1,5 +1,7 @@
|
||||
# Remote Attestation
|
||||
|
||||
![Flow between Challenger, Host, ISV and IAS](challenger-flow.png "Remote Attestation Flow")
|
||||
|
||||
## Project Organisation
|
||||
|
||||
* **Enclave**
|
||||
|
@ -0,0 +1,89 @@
|
||||
buildscript {
|
||||
ext.keyStoreDir = "$buildDir/keystore"
|
||||
ext.cli_version = '1.4'
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
|
||||
description 'Proof-of-concept Remote Attestation Challenger'
|
||||
|
||||
configurations {
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.compileClasspath + test.compileClasspath
|
||||
runtimeClasspath += main.runtimeClasspath + test.runtimeClasspath
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainClassName = 'net.corda.attestation.challenger.Main'
|
||||
|
||||
dependencies {
|
||||
compile project(':attestation-common')
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
|
||||
testCompile "junit:junit:$junit_version"
|
||||
|
||||
compile "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version"
|
||||
compile "com.fasterxml.jackson.core:jackson-core:$jackson_version"
|
||||
compile "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
|
||||
compile "com.fasterxml.jackson.core:jackson-annotations:$jackson_version"
|
||||
compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version"
|
||||
compile "org.apache.httpcomponents:httpclient:$httpclient_version"
|
||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||
compile "org.apache.logging.log4j:log4j-core:$log4j_version"
|
||||
compile "org.slf4j:jcl-over-slf4j:$slf4j_version"
|
||||
compile "commons-cli:commons-cli:$cli_version"
|
||||
testCompile project(path: ':attestation-common', configuration: 'testArtifacts')
|
||||
}
|
||||
|
||||
task createChallengerKeyStores(type: Exec) {
|
||||
doFirst {
|
||||
mkdir keyStoreDir
|
||||
}
|
||||
|
||||
inputs.dir "$projectDir/src/main/ssl/challenger"
|
||||
outputs.files "$keyStoreDir/challenger.pfx"
|
||||
workingDir keyStoreDir
|
||||
commandLine "$projectDir/src/main/ssl/challenger/generate-keystore.sh"
|
||||
}
|
||||
|
||||
task createIntelKeyStores(type: Exec) {
|
||||
doFirst {
|
||||
mkdir keyStoreDir
|
||||
}
|
||||
|
||||
inputs.dir "$projectDir/src/main/ssl/intel"
|
||||
outputs.files "$keyStoreDir/ias.pfx"
|
||||
workingDir keyStoreDir
|
||||
commandLine "$projectDir/src/main/ssl/intel/generate-keystores.sh"
|
||||
}
|
||||
|
||||
processResources {
|
||||
dependsOn.addAll createChallengerKeyStores, createIntelKeyStores
|
||||
from keyStoreDir
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
// Enable "unlimited" encryption.
|
||||
systemProperties["java.security.properties"] = "$projectDir/src/main/security.properties"
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes(
|
||||
'Main-Class': mainClassName,
|
||||
'Class-Path': configurations.runtime.collect { it.getName() }.join(' '),
|
||||
'Automatic-Module-Name': 'net.corda.attestation.challenger'
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package net.corda.attestation.challenger
|
||||
|
||||
import net.corda.attestation.message.ias.QuoteStatus
|
||||
import java.security.interfaces.ECPublicKey
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class AttestationResult(
|
||||
val reportID: String,
|
||||
val quoteStatus: QuoteStatus,
|
||||
val peerPublicKey: ECPublicKey,
|
||||
val platformInfo: ByteArray?,
|
||||
val timestamp: LocalDateTime
|
||||
)
|
@ -0,0 +1,281 @@
|
||||
package net.corda.attestation.challenger
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import net.corda.attestation.*
|
||||
import net.corda.attestation.message.*
|
||||
import net.corda.attestation.message.ias.ReportResponse
|
||||
import org.apache.http.HttpStatus.*
|
||||
import org.apache.http.client.config.CookieSpecs.*
|
||||
import org.apache.http.client.config.RequestConfig
|
||||
import org.apache.http.client.methods.HttpPost
|
||||
import org.apache.http.client.protocol.HttpClientContext
|
||||
import org.apache.http.config.SocketConfig
|
||||
import org.apache.http.entity.ContentType.APPLICATION_JSON
|
||||
import org.apache.http.entity.StringEntity
|
||||
import org.apache.http.impl.client.BasicCookieStore
|
||||
import org.apache.http.impl.client.CloseableHttpClient
|
||||
import org.apache.http.impl.client.HttpClients
|
||||
import org.apache.http.impl.conn.BasicHttpClientConnectionManager
|
||||
import org.bouncycastle.asn1.ASN1InputStream
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.bouncycastle.asn1.DLSequence
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.*
|
||||
import java.security.cert.*
|
||||
import java.security.cert.Certificate
|
||||
import java.security.interfaces.ECPublicKey
|
||||
import java.security.spec.ECParameterSpec
|
||||
import java.util.*
|
||||
|
||||
class Challenger(
|
||||
private val keyPair: KeyPair,
|
||||
private val enclaveHost: URI,
|
||||
private val pkixParameters: PKIXParameters
|
||||
) {
|
||||
private companion object {
|
||||
@JvmStatic
|
||||
private val log: Logger = LoggerFactory.getLogger(Challenger::class.java)
|
||||
|
||||
private const val AES_CMAC_FUNC = 1.toShort()
|
||||
private const val tlvHeaderSize = 8
|
||||
|
||||
private val httpRequestConfig: RequestConfig = RequestConfig.custom()
|
||||
.setCookieSpec(STANDARD_STRICT)
|
||||
.setConnectTimeout(20_000)
|
||||
.setSocketTimeout(5_000)
|
||||
.build()
|
||||
private val httpSocketConfig: SocketConfig = SocketConfig.custom()
|
||||
.setSoReuseAddress(true)
|
||||
.setTcpNoDelay(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val mapper = ObjectMapper().registerModule(JavaTimeModule())
|
||||
private val keyFactory: KeyFactory = KeyFactory.getInstance("EC")
|
||||
private val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
|
||||
private val transientKeyPair: KeyPair
|
||||
private val ecParameters: ECParameterSpec
|
||||
private val crypto = Crypto()
|
||||
|
||||
private val cookies = BasicCookieStore()
|
||||
|
||||
init {
|
||||
ecParameters = (crypto.generateKeyPair().public as ECPublicKey).params
|
||||
log.info("Elliptic Curve Parameters: {}", ecParameters)
|
||||
|
||||
transientKeyPair = crypto.generateKeyPair()
|
||||
}
|
||||
|
||||
|
||||
fun attestToEnclave(): AttestationResult {
|
||||
createHttpClient().use { client ->
|
||||
val context = HttpClientContext.create().apply {
|
||||
cookieStore = cookies
|
||||
}
|
||||
|
||||
// Send our public key, and receive the host's transient DH public key.
|
||||
val challengeResponse: ChallengeResponse = try {
|
||||
val challengeURI = enclaveHost.toString() + "/challenge"
|
||||
log.info("Invoking host: {}", challengeURI)
|
||||
|
||||
val challengeRequest = ChallengeRequest(
|
||||
nonce = createNonce(),
|
||||
gc = (keyPair.public as ECPublicKey).toLittleEndian()
|
||||
)
|
||||
val httpRequest = HttpPost(challengeURI).apply {
|
||||
entity = StringEntity(mapper.writeValueAsString(challengeRequest), APPLICATION_JSON)
|
||||
}
|
||||
client.execute(httpRequest, context).use { httpResponse ->
|
||||
val statusCode = httpResponse.statusLine.statusCode
|
||||
if (statusCode != SC_OK) {
|
||||
throw ChallengerException("Challenge request to enclave failed (HTTP $statusCode)")
|
||||
}
|
||||
mapper.readValue(httpResponse.entity.content)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
log.error("HTTP client error", e)
|
||||
throw ChallengerException(e.message, e)
|
||||
}
|
||||
|
||||
val peerPublicKey = keyFactory.generatePublic(challengeResponse.ga.toBigEndianKeySpec(ecParameters)) as ECPublicKey
|
||||
val smk = crypto.generateSMK(transientKeyPair.private, peerPublicKey)
|
||||
|
||||
// Send our public key and signatures to the enclave.
|
||||
val reportBody: ReportProxyResponse = try {
|
||||
val attestURI = enclaveHost.toString() + "/attest"
|
||||
log.info("Invoking host: {}", attestURI)
|
||||
|
||||
val publicKey = transientKeyPair.public as ECPublicKey
|
||||
val signatureGbGa = signatureOf(publicKey, peerPublicKey)
|
||||
val gb = publicKey.toLittleEndian()
|
||||
val signatureRequest = AttestationRequest(
|
||||
gb = gb,
|
||||
signatureGbGa = signatureGbGa,
|
||||
aesCMAC = crypto.aesCMAC(smk, { aes ->
|
||||
aes.update(gb)
|
||||
aes.update(challengeResponse.spid.hexToBytes())
|
||||
aes.update(challengeResponse.quoteType.toLittleEndian())
|
||||
aes.update(AES_CMAC_FUNC.toLittleEndian())
|
||||
aes.update(signatureGbGa)
|
||||
})
|
||||
)
|
||||
val httpRequest = HttpPost(attestURI).apply {
|
||||
entity = StringEntity(mapper.writeValueAsString(signatureRequest), APPLICATION_JSON)
|
||||
}
|
||||
client.execute(httpRequest, context).use { httpResponse ->
|
||||
val statusCode = httpResponse.statusLine.statusCode
|
||||
if (statusCode != SC_OK) {
|
||||
throw ChallengerException("Failed sending signatures to enclave (HTTP $statusCode)")
|
||||
}
|
||||
mapper.readValue(httpResponse.entity.content)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
log.error("HTTP request error", e)
|
||||
throw ChallengerException(e.message, e)
|
||||
}
|
||||
|
||||
// Check that this message really came from Intel.
|
||||
validateSignature(reportBody)
|
||||
|
||||
val reportResponse: ReportResponse = mapper.readValue(reportBody.report.inputStream())
|
||||
val platformInfo = reportResponse.platformInfoBlob?.removeHeader(tlvHeaderSize)
|
||||
log.info("Attestation completed")
|
||||
|
||||
// Successful response
|
||||
return AttestationResult(
|
||||
reportID = reportResponse.id,
|
||||
quoteStatus = reportResponse.isvEnclaveQuoteStatus,
|
||||
peerPublicKey = peerPublicKey,
|
||||
platformInfo = platformInfo,
|
||||
timestamp = reportResponse.timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSecret(secretValue: String, attestation: AttestationResult) {
|
||||
val mk = crypto.generateMK(transientKeyPair.private, attestation.peerPublicKey)
|
||||
val secretKey = crypto.generateSecretKey(transientKeyPair.private, attestation.peerPublicKey)
|
||||
|
||||
try {
|
||||
createHttpClient().use { client ->
|
||||
val secretIV = crypto.createIV()
|
||||
val secretData = crypto.encrypt(secretValue.toByteArray(), secretKey, secretIV)
|
||||
val secretURI = enclaveHost.toString() + "/secret"
|
||||
log.info("Invoking host: {}", secretURI)
|
||||
|
||||
val context = HttpClientContext.create().apply {
|
||||
cookieStore = cookies
|
||||
}
|
||||
|
||||
val secretRequest = SecretRequest(
|
||||
data = secretData.encryptedData(),
|
||||
authTag = secretData.authenticationTag(),
|
||||
iv = secretIV,
|
||||
platformInfo = attestation.platformInfo,
|
||||
aesCMAC = crypto.aesCMAC(mk, { aes ->
|
||||
aes.update(attestation.platformInfo)
|
||||
})
|
||||
)
|
||||
val httpRequest = HttpPost(secretURI).apply {
|
||||
entity = StringEntity(mapper.writeValueAsString(secretRequest), APPLICATION_JSON)
|
||||
}
|
||||
client.execute(httpRequest, context).use { httpResponse ->
|
||||
val statusCode = httpResponse.statusLine.statusCode
|
||||
if (statusCode != SC_OK) {
|
||||
throw ChallengerException("Failed sending secret to enclave (HTTP $statusCode)")
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Successfully provisioned to enclave")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
log.error("HTTP client error", e)
|
||||
throw ChallengerException(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNonce(): String = UUID.randomUUID().let { uuid ->
|
||||
String.format("%016x%016x", uuid.mostSignificantBits, uuid.leastSignificantBits)
|
||||
}
|
||||
|
||||
private fun createHttpClient(): CloseableHttpClient {
|
||||
return HttpClients.custom()
|
||||
.setConnectionManager(BasicHttpClientConnectionManager().apply {
|
||||
socketConfig = httpSocketConfig
|
||||
})
|
||||
.setDefaultRequestConfig(httpRequestConfig)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun validateSignature(reportResponse: ReportProxyResponse) {
|
||||
val certificatePath = try {
|
||||
parseCertificates(reportResponse.certificatePath)
|
||||
} catch (e: CertificateException) {
|
||||
log.error("Failed to parse certificate from HTTP header '{}': {}", reportResponse.certificatePath, e.message)
|
||||
throw e
|
||||
}
|
||||
|
||||
try {
|
||||
val certValidator = CertPathValidator.getInstance("PKIX")
|
||||
certValidator.validate(certificatePath, pkixParameters)
|
||||
} catch (e: GeneralSecurityException) {
|
||||
log.error("Certificate '{}' is invalid: {}", certificatePath, e.message)
|
||||
throw e
|
||||
}
|
||||
|
||||
val signature = try {
|
||||
Signature.getInstance("SHA256withRSA").apply {
|
||||
initVerify(certificatePath.certificates[0])
|
||||
}
|
||||
} catch (e: GeneralSecurityException) {
|
||||
log.error("Failed to initialise signature: {}", e.message)
|
||||
throw e
|
||||
}
|
||||
|
||||
try {
|
||||
signature.update(reportResponse.report)
|
||||
if (!signature.verify(reportResponse.signature.toByteArray().decodeBase64())) {
|
||||
throw ChallengerException("Report failed IAS signature check")
|
||||
}
|
||||
} catch (e: SignatureException) {
|
||||
log.error("Failed to parse signature from IAS: {}", e.message)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseCertificates(iasCertificateHeader: String): CertPath {
|
||||
val certificates = mutableListOf<Certificate>()
|
||||
iasCertificateHeader.byteInputStream().use { input ->
|
||||
while (input.available() > 0) {
|
||||
certificates.add(certificateFactory.generateCertificate(input))
|
||||
}
|
||||
}
|
||||
return certificateFactory.generateCertPath(certificates)
|
||||
}
|
||||
|
||||
private fun signatureOf(publicKey: ECPublicKey, peerKey: ECPublicKey): ByteArray {
|
||||
val signature = Signature.getInstance("SHA256WithECDSA").let { signer ->
|
||||
signer.initSign(keyPair.private, crypto.random)
|
||||
signer.update(publicKey.toLittleEndian())
|
||||
signer.update(peerKey.toLittleEndian())
|
||||
signer.sign()
|
||||
}
|
||||
return ByteBuffer.allocate(KEY_SIZE).let { buf ->
|
||||
ASN1InputStream(signature).use { input ->
|
||||
for (number in input.readObject() as DLSequence) {
|
||||
val pos = (number as ASN1Integer).positiveValue.toLittleEndian(KEY_SIZE / 2)
|
||||
buf.put(pos)
|
||||
}
|
||||
buf.array()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.decodeBase64(): ByteArray = Base64.getDecoder().decode(this)
|
||||
private fun ByteArray.removeHeader(headerSize: Int) = copyOfRange(headerSize, size)
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package net.corda.attestation.challenger
|
||||
|
||||
class ChallengerException(message: String?, cause: Throwable?) : Exception(message, cause) {
|
||||
constructor(message: String) : this(message, null)
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
@file:JvmName("Main")
|
||||
package net.corda.attestation.challenger
|
||||
|
||||
import net.corda.attestation.message.ias.QuoteStatus
|
||||
import org.apache.commons.cli.DefaultParser
|
||||
import org.apache.commons.cli.HelpFormatter
|
||||
import org.apache.commons.cli.Options
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import java.net.URI
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.Security
|
||||
import java.security.cert.PKIXRevocationChecker.Option.*
|
||||
import java.security.cert.*
|
||||
import java.util.*
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val command = DefaultParser().parse(options, args)
|
||||
if (command.hasOption("h")) {
|
||||
HelpFormatter().printHelp("challenger", options)
|
||||
return
|
||||
}
|
||||
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
|
||||
val keyStorePassword = command.getOptionValue("t", DEFAULT_PASSWORD).toCharArray()
|
||||
val trustStorePassword = command.getOptionValue("k", DEFAULT_PASSWORD).toCharArray()
|
||||
val challengerKeyPair = loadKeyStoreResource("challenger.pfx", keyStorePassword).getKeyPair("challenge", keyStorePassword)
|
||||
|
||||
val iasStore = loadKeyStoreResource("ias.pfx", trustStorePassword)
|
||||
val pkixParameters = PKIXParameters(iasStore.trustAnchorsFor("ias")).apply {
|
||||
val rlChecker = CertPathValidator.getInstance("PKIX").revocationChecker as PKIXRevocationChecker
|
||||
addCertPathChecker(rlChecker.apply { options = EnumSet.of(SOFT_FAIL) })
|
||||
}
|
||||
|
||||
val hostname = command.getOptionValue("n", "localhost")
|
||||
val port = command.getOptionValue("p", "8080")
|
||||
val secretValue = command.getOptionValue("s", "And now for something very different indeed!")
|
||||
|
||||
Challenger(
|
||||
keyPair = challengerKeyPair,
|
||||
enclaveHost = URI.create("http://$hostname:$port/host"),
|
||||
pkixParameters = pkixParameters
|
||||
).apply {
|
||||
val attestation = attestToEnclave()
|
||||
println("Report ID: ${attestation.reportID}")
|
||||
println("Quote Status: ${attestation.quoteStatus}")
|
||||
println("Timestamp: ${attestation.timestamp}")
|
||||
|
||||
if (attestation.quoteStatus == QuoteStatus.OK) {
|
||||
setSecret(secretValue, attestation)
|
||||
println("Secret provisioned successfully.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_PASSWORD = "attestation"
|
||||
|
||||
private val options = Options().apply {
|
||||
addOption("t", "trustPassword", true, "Password for IAS trust store")
|
||||
addOption("k", "keyPassword", true, "Password for Challenger's key store")
|
||||
addOption("n", "hostname", true, "Hostname for Enclave/Host")
|
||||
addOption("p", "port", true, "Port number for Enclave/Host")
|
||||
addOption("s", "secret", true, "A secret string to be provisioned")
|
||||
addOption("h", "help", false, "Displays usage")
|
||||
}
|
||||
|
||||
private fun loadKeyStoreResource(resourceName: String, password: CharArray, type: String = "PKCS12"): KeyStore {
|
||||
return KeyStore.getInstance(type).apply {
|
||||
Challenger::class.java.classLoader.getResourceAsStream(resourceName)?.use { input ->
|
||||
load(input, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun KeyStore.getKeyPair(alias: String, password: CharArray): KeyPair {
|
||||
val privateKey = getKey(alias, password) as PrivateKey
|
||||
return KeyPair(getCertificate(alias).publicKey, privateKey)
|
||||
}
|
||||
|
||||
private fun KeyStore.trustAnchorsFor(vararg aliases: String): Set<TrustAnchor>
|
||||
= aliases.map { alias -> TrustAnchor(getCertificate(alias) as X509Certificate, null) }.toSet()
|
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info">
|
||||
|
||||
<Properties>
|
||||
<Property name="attestation.home">.</Property>
|
||||
<Property name="log-path">${sys:attestation.home}</Property>
|
||||
<Property name="log-name">attestation-challenger</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>
|
@ -0,0 +1 @@
|
||||
crypto.policy=unlimited
|
@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
STOREPASS=attestation
|
||||
ALIAS=challenge
|
||||
|
||||
openssl ecparam -name secp256r1 -genkey -noout -out privateKey.pem
|
||||
openssl req -new -key privateKey.pem -x509 -out server.crt -days 1000 <<EOF
|
||||
.
|
||||
.
|
||||
.
|
||||
.
|
||||
.
|
||||
localhost
|
||||
.
|
||||
EOF
|
||||
|
||||
openssl pkcs12 -export -out challenger.pfx -inkey privateKey.pem -in server.crt -passout pass:${STOREPASS}
|
||||
|
||||
keytool -keystore challenger.pfx -storetype pkcs12 -changealias -alias 1 -destalias ${ALIAS} -storepass ${STOREPASS}
|
||||
|
||||
rm -f *.pem *.crt
|
@ -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-----
|
@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
|
||||
rm -f ias.pfx
|
||||
|
||||
STOREPASS=attestation
|
||||
|
||||
DIRHOME=$(dirname $0)
|
||||
INTEL_CRTFILE=${DIRHOME}/AttestationReportSigningCACert.pem
|
||||
|
||||
## Generate trust store for Intel's certificate.
|
||||
if [ -r ${INTEL_CRTFILE} ]; then
|
||||
keytool -import -keystore ias.pfx -storetype pkcs12 -file ${INTEL_CRTFILE} -alias ias -storepass ${STOREPASS} <<EOF
|
||||
yes
|
||||
EOF
|
||||
fi
|
@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
set +o posix
|
||||
|
||||
ALIAS=ias
|
||||
KEYPASS=attestation
|
||||
STOREPASS=attestation
|
||||
|
||||
rm -f dummyIAS.pfx dummyIAS-trust.pfx
|
||||
|
||||
CNF=`cat <<EOF
|
||||
[ v3_ca ]
|
||||
keyUsage=digitalSignature,keyEncipherment
|
||||
subjectKeyIdentifier=hash
|
||||
authorityKeyIdentifier=keyid:always,issuer:always
|
||||
basicConstraints=CA:TRUE
|
||||
EOF
|
||||
`
|
||||
|
||||
# Generate keystore
|
||||
openssl genrsa -out client.key 2048
|
||||
openssl req -key client.key -new -out client.req <<EOF
|
||||
.
|
||||
.
|
||||
.
|
||||
.
|
||||
.
|
||||
localhost
|
||||
.
|
||||
.
|
||||
.
|
||||
EOF
|
||||
openssl x509 -req -days 1000 -in client.req -signkey client.key -out client.crt -extensions v3_ca -extfile <(echo $CNF)
|
||||
openssl pkcs12 -export -out dummyIAS.pfx -inkey client.key -in client.crt -passout pass:${STOREPASS}
|
||||
|
||||
keytool -keystore dummyIAS.pfx -storetype pkcs12 -changealias -alias 1 -destalias ${ALIAS} -storepass ${STOREPASS}
|
||||
|
||||
# Generate truststore
|
||||
keytool -importcert -file client.crt -keystore dummyIAS-trust.pfx -storetype pkcs12 -alias ${ALIAS} -storepass ${STOREPASS} <<EOF
|
||||
yes
|
||||
EOF
|
||||
|
||||
rm -f client.key client.crt client.req
|
30
sgx-jvm/remote-attestation/attestation-common/build.gradle
Normal file
30
sgx-jvm/remote-attestation/attestation-common/build.gradle
Normal file
@ -0,0 +1,30 @@
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
description 'Common code shared between remote attestation modules'
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
|
||||
testCompile "junit:junit:$junit_version"
|
||||
|
||||
compile "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.bouncycastle:bcpkix-jdk15on:$bouncycastle_version"
|
||||
compile "org.slf4j:slf4j-api:$slf4j_version"
|
||||
}
|
||||
|
||||
configurations {
|
||||
testArtifacts.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
task testJar(type: Jar) {
|
||||
classifier "tests"
|
||||
from sourceSets.test.output
|
||||
}
|
||||
|
||||
artifacts {
|
||||
testArtifacts testJar
|
||||
}
|
@ -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) })
|
@ -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()
|
@ -0,0 +1,8 @@
|
||||
@file:JvmName("JsonUtils")
|
||||
package net.corda.attestation
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import java.io.InputStream
|
||||
|
||||
inline fun <reified T : Any> ObjectMapper.readValue(input: InputStream): T = readValue(input, T::class.java)
|
||||
inline fun <reified T : Any> ObjectMapper.readValue(input: String): T = readValue(input, T::class.java)
|
@ -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)
|
@ -0,0 +1,17 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder
|
||||
|
||||
@JsonPropertyOrder("gb", "signatureGbGa", "aesCMAC")
|
||||
class AttestationRequest(
|
||||
// The challenger's public DH key (Little Endian)
|
||||
@param:JsonProperty("gb")
|
||||
val gb: ByteArray,
|
||||
|
||||
@param:JsonProperty("signatureGbGa")
|
||||
val signatureGbGa: ByteArray,
|
||||
|
||||
@param:JsonProperty("aesCMAC")
|
||||
val aesCMAC: ByteArray
|
||||
)
|
@ -0,0 +1,17 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder
|
||||
|
||||
@JsonPropertyOrder("gc", "nonce")
|
||||
@JsonInclude(NON_NULL)
|
||||
class ChallengeRequest(
|
||||
// The challenger's permanent public key (Little Endian).
|
||||
@param:JsonProperty("gc")
|
||||
val gc: ByteArray,
|
||||
|
||||
@param:JsonProperty("nonce")
|
||||
val nonce: String
|
||||
)
|
@ -0,0 +1,17 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder
|
||||
|
||||
@JsonPropertyOrder("ga", "spid", "quoteType")
|
||||
class ChallengeResponse(
|
||||
// The host's public DH key (Little Endian)
|
||||
@param:JsonProperty("ga")
|
||||
val ga: ByteArray,
|
||||
|
||||
@param:JsonProperty("spid")
|
||||
val spid: String,
|
||||
|
||||
@param:JsonProperty("quoteType")
|
||||
val quoteType: Short
|
||||
)
|
@ -0,0 +1,19 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include.*
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder
|
||||
|
||||
@JsonPropertyOrder("signature", "certificatePath", "report")
|
||||
@JsonInclude(NON_NULL)
|
||||
class ReportProxyResponse(
|
||||
@param:JsonProperty("signature")
|
||||
val signature: String,
|
||||
|
||||
@param:JsonProperty("certificatePath")
|
||||
val certificatePath: String,
|
||||
|
||||
@param:JsonProperty("report")
|
||||
val report: ByteArray
|
||||
)
|
@ -0,0 +1,19 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include.*
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder
|
||||
|
||||
@JsonPropertyOrder("isvEnclaveQuote", "pseManifest", "nonce")
|
||||
@JsonInclude(NON_NULL)
|
||||
class ReportRequest(
|
||||
@param:JsonProperty("isvEnclaveQuote")
|
||||
val isvEnclaveQuote: ByteArray,
|
||||
|
||||
@param:JsonProperty("pseManifest")
|
||||
val pseManifest: ByteArray? = null,
|
||||
|
||||
@param:JsonProperty("nonce")
|
||||
val nonce: String? = null
|
||||
)
|
@ -0,0 +1,25 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include.*
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder
|
||||
|
||||
@JsonPropertyOrder("data", "authTag", "iv", "platformInfo", "aesCMAC")
|
||||
@JsonInclude(NON_NULL)
|
||||
class SecretRequest(
|
||||
@param:JsonProperty("data")
|
||||
val data: ByteArray,
|
||||
|
||||
@param:JsonProperty("authTag")
|
||||
val authTag: ByteArray,
|
||||
|
||||
@param:JsonProperty("iv")
|
||||
val iv: ByteArray,
|
||||
|
||||
@param:JsonProperty("platformInfo")
|
||||
val platformInfo: ByteArray? = null,
|
||||
|
||||
@param:JsonProperty("aesCMAC")
|
||||
val aesCMAC: ByteArray
|
||||
)
|
@ -0,0 +1,13 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder
|
||||
|
||||
@JsonPropertyOrder("spid", "quoteType")
|
||||
class ServiceResponse(
|
||||
@param:JsonProperty("spid")
|
||||
val spid: String,
|
||||
|
||||
@param:JsonProperty("quoteType")
|
||||
val quoteType: Short
|
||||
)
|
@ -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()
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package net.corda.attestation.message.ias
|
||||
|
||||
enum class ManifestStatus {
|
||||
OK,
|
||||
UNKNOWN,
|
||||
INVALID,
|
||||
OUT_OF_DATE,
|
||||
REVOKED,
|
||||
RL_VERSION_MISMATCH
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
)
|
@ -0,0 +1,14 @@
|
||||
@file:JvmName("ByteUtils")
|
||||
package net.corda.attestation
|
||||
|
||||
import java.util.*
|
||||
|
||||
fun byteArray(size: Int, of: Int) = ByteArray(size, { of.toByte() })
|
||||
|
||||
fun unsignedByteArrayOf(vararg values: Int) = ByteArray(values.size).apply {
|
||||
for (i in 0 until values.size) {
|
||||
this[i] = values[i].toByte()
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.toBase64(): String = Base64.getEncoder().encodeToString(this)
|
@ -0,0 +1,19 @@
|
||||
package net.corda.attestation
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
import java.security.Security
|
||||
|
||||
@Suppress("UNUSED")
|
||||
class CryptoProvider : TestRule {
|
||||
override fun apply(statement: Statement, description: Description?): Statement {
|
||||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
return statement
|
||||
}
|
||||
|
||||
val crypto: Crypto by lazy { Crypto() }
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package net.corda.attestation
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.nio.charset.StandardCharsets.*
|
||||
import java.security.KeyFactory
|
||||
import java.security.interfaces.ECPublicKey
|
||||
|
||||
class CryptoTest {
|
||||
private lateinit var keyFactory: KeyFactory
|
||||
private lateinit var crypto: Crypto
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val cryptoProvider = CryptoProvider()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
keyFactory = KeyFactory.getInstance("EC")
|
||||
crypto = cryptoProvider.crypto
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testKeyConversions() {
|
||||
val keyPair = crypto.generateKeyPair()
|
||||
val ecPublicKey = keyPair.public as ECPublicKey
|
||||
val ecParameters = ecPublicKey.params
|
||||
val bytes = ecPublicKey.toLittleEndian()
|
||||
val resultKey = keyFactory.generatePublic(bytes.toBigEndianKeySpec(ecParameters)) as ECPublicKey
|
||||
assertEquals(ecPublicKey, resultKey)
|
||||
assertArrayEquals(ecPublicKey.encoded, resultKey.encoded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSharedSecret() {
|
||||
val keyPairA = crypto.generateKeyPair()
|
||||
val keyPairB = crypto.generateKeyPair()
|
||||
|
||||
val secretA = crypto.generateSharedSecret(keyPairA.private, keyPairB.public)
|
||||
val secretB = crypto.generateSharedSecret(keyPairB.private, keyPairA.public)
|
||||
assertArrayEquals(secretB, secretA)
|
||||
assertEquals(32, secretA.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEncryption() {
|
||||
val keyPairA = crypto.generateKeyPair()
|
||||
val keyPairB = crypto.generateKeyPair()
|
||||
val secretKeyA = crypto.generateSecretKey(keyPairA.private, keyPairB.public)
|
||||
val secretKeyB = crypto.generateSecretKey(keyPairB.private, keyPairA.public)
|
||||
assertEquals(secretKeyA, secretKeyB)
|
||||
assertArrayEquals(secretKeyA.encoded, secretKeyB.encoded)
|
||||
|
||||
val iv = crypto.createIV()
|
||||
val data = crypto.encrypt("Sooper secret string value!".toByteArray(), secretKeyA, iv)
|
||||
assertEquals("Sooper secret string value!", String(crypto.decrypt(data, secretKeyB, iv), UTF_8))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAesCMAC() {
|
||||
val messageBytes = "Hello World".toByteArray()
|
||||
val keyBytes = "0123456789012345".toByteArray()
|
||||
val cmac = crypto.aesCMAC(keyBytes, messageBytes)
|
||||
assertArrayEquals("3AFAFFFC4EB9274ABD6C9CC3D8B6984A".hexToBytes(), cmac)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToHexArrayString() {
|
||||
val bytes = unsignedByteArrayOf(0xf5, 0x04, 0x83, 0x71)
|
||||
assertEquals("[0xf5,0x04,0x83,0x71]", bytes.toHexArrayString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test vectors from RFC4493 and NIST 800-38B`() {
|
||||
// Key as defined on https://tools.ietf.org/html/rfc4493.html#appendix-A
|
||||
val testKey = "2b7e151628aed2a6abf7158809cf4f3c".hexToBytes()
|
||||
|
||||
// Example 1: len = 0
|
||||
val out0 = crypto.aesCMAC(testKey, ByteArray(0))
|
||||
assertArrayEquals("bb1d6929e95937287fa37d129b756746".hexToBytes(), out0)
|
||||
|
||||
// Example 2: len = 16
|
||||
val out16 = crypto.aesCMAC(testKey, "6bc1bee22e409f96e93d7e117393172a".hexToBytes())
|
||||
assertArrayEquals("070a16b46b4d4144f79bdd9dd04a287c".hexToBytes(), out16)
|
||||
|
||||
// Example 3: len = 40
|
||||
val messageBytes40 = ("6bc1bee22e409f96e93d7e117393172a" +
|
||||
"ae2d8a571e03ac9c9eb76fac45af8e51" +
|
||||
"30c81c46a35ce411").hexToBytes()
|
||||
val out40 = crypto.aesCMAC(testKey, messageBytes40)
|
||||
assertArrayEquals("dfa66747de9ae63030ca32611497c827".hexToBytes(), out40)
|
||||
|
||||
// Example 4: len = 64
|
||||
val messageBytes64 = ("6bc1bee22e409f96e93d7e117393172a" +
|
||||
"ae2d8a571e03ac9c9eb76fac45af8e51" +
|
||||
"30c81c46a35ce411e5fbc1191a0a52ef" +
|
||||
"f69f2445df4f9b17ad2b417be66c3710").hexToBytes()
|
||||
val out64 = crypto.aesCMAC(testKey, messageBytes64)
|
||||
assertArrayEquals("51f0bebf7e3b9d92fc49741779363cfe".hexToBytes(), out64)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package net.corda.attestation
|
||||
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.TrustAnchor
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
@Suppress("UNUSED")
|
||||
class KeyStoreProvider(private val storeName: String, private val storePassword: String) : TestRule {
|
||||
private lateinit var keyStore: KeyStore
|
||||
|
||||
private fun loadKeyStoreResource(resourceName: String, password: CharArray, type: String = "PKCS12"): KeyStore {
|
||||
return KeyStore.getInstance(type).apply {
|
||||
KeyStoreProvider::class.java.classLoader.getResourceAsStream(resourceName)?.use { input ->
|
||||
load(input, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun apply(statement: Statement, description: Description?): Statement {
|
||||
keyStore = loadKeyStoreResource(storeName, storePassword.toCharArray())
|
||||
return statement
|
||||
}
|
||||
|
||||
fun getKeyPair(alias: String, password: String): KeyPair {
|
||||
val privateKey = keyStore.getKey(alias, password.toCharArray()) as PrivateKey
|
||||
return KeyPair(keyStore.getCertificate(alias).publicKey, privateKey)
|
||||
}
|
||||
|
||||
fun trustAnchorsFor(vararg aliases: String): Set<TrustAnchor>
|
||||
= aliases.map { alias -> TrustAnchor(keyStore.getCertificate(alias) as X509Certificate, null) }.toSet()
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class AttestationErrorTest {
|
||||
private lateinit var mapper: ObjectMapper
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mapper = ObjectMapper()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSerialise() {
|
||||
val error = AttestationError("<error-message>")
|
||||
val str = mapper.writeValueAsString(error)
|
||||
assertEquals("""{"message":"<error-message>"}""", str)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSerialiseEmpty() {
|
||||
val request = AttestationError("")
|
||||
val str = mapper.writeValueAsString(request)
|
||||
assertEquals("""{"message":""}""", str)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeserialise() {
|
||||
val str = """{"message":"<error-message>"}"""
|
||||
val error = mapper.readValue(str, AttestationError::class.java)
|
||||
assertEquals("<error-message>", error.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeserialiseEmpty() {
|
||||
val str = """{"message":""}"""
|
||||
val error = mapper.readValue(str, AttestationError::class.java)
|
||||
assertEquals("", error.message)
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.attestation.toBase64
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class AttestationRequestTest {
|
||||
private companion object {
|
||||
private val publicKeyData = byteArrayOf(0x3F, 0x2B, 0x52, 0x31)
|
||||
private val publicKeyBase64 = publicKeyData.toBase64()
|
||||
private val signatureData = byteArrayOf(0x31, 0x35, 0x5D, 0x1A, 0x27, 0x44)
|
||||
private val signatureBase64 = signatureData.toBase64()
|
||||
private val aesCMACData = byteArrayOf(0x7C, 0x62, 0x50, 0x2B, 0x47, 0x0E)
|
||||
private val aesCMACBase64 = aesCMACData.toBase64()
|
||||
}
|
||||
|
||||
private lateinit var mapper: ObjectMapper
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mapper = ObjectMapper()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSerialise() {
|
||||
val attestation = AttestationRequest(
|
||||
gb = publicKeyData,
|
||||
signatureGbGa = signatureData,
|
||||
aesCMAC = aesCMACData
|
||||
)
|
||||
val str = mapper.writeValueAsString(attestation)
|
||||
assertEquals("""{"gb":"$publicKeyBase64","signatureGbGa":"$signatureBase64","aesCMAC":"$aesCMACBase64"}""", str)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeserialise() {
|
||||
val str = """{"gb":"$publicKeyBase64","signatureGbGa":"$signatureBase64","aesCMAC":"$aesCMACBase64"}"""
|
||||
val attestation = mapper.readValue(str, AttestationRequest::class.java)
|
||||
assertArrayEquals(publicKeyData, attestation.gb)
|
||||
assertArrayEquals(signatureData, attestation.signatureGbGa)
|
||||
assertArrayEquals(aesCMACData, attestation.aesCMAC)
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.attestation.toBase64
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ChallengeRequestTest {
|
||||
private companion object {
|
||||
private val publicKeyData = byteArrayOf(0x1F, 0x0A, 0x22, 0x37)
|
||||
private val publicKeyBase64 = publicKeyData.toBase64()
|
||||
}
|
||||
|
||||
private lateinit var mapper: ObjectMapper
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mapper = ObjectMapper()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSerialise() {
|
||||
val challenge = ChallengeRequest(publicKeyData, "<nonce-value>")
|
||||
val str = mapper.writeValueAsString(challenge)
|
||||
assertEquals("""{"gc":"$publicKeyBase64","nonce":"<nonce-value>"}""", str)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeserialise() {
|
||||
val str = """{"gc":"$publicKeyBase64","nonce":"<nonce-value>"}"""
|
||||
val challenge = mapper.readValue(str, ChallengeRequest::class.java)
|
||||
assertArrayEquals(publicKeyData, challenge.gc)
|
||||
assertEquals("<nonce-value>", challenge.nonce)
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.attestation.toBase64
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ChallengeResponseTest {
|
||||
private companion object {
|
||||
private const val SPID = "0123456789ABCDEF"
|
||||
private val gaData = byteArrayOf(0x10, 0x00, 0x22, 0x00)
|
||||
private val gaBase64 = gaData.toBase64()
|
||||
}
|
||||
|
||||
private lateinit var mapper: ObjectMapper
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mapper = ObjectMapper()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSerialise() {
|
||||
val challenge = ChallengeResponse(ga = gaData, spid = SPID, quoteType = 1)
|
||||
val str = mapper.writeValueAsString(challenge)
|
||||
assertEquals("""{"ga":"$gaBase64","spid":"$SPID","quoteType":1}""", str)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeserialise() {
|
||||
val str = """{"ga":"$gaBase64","spid":"$SPID","quoteType":1}"""
|
||||
val challenge = mapper.readValue(str, ChallengeResponse::class.java)
|
||||
assertArrayEquals(gaData, challenge.ga)
|
||||
assertEquals(SPID, challenge.spid)
|
||||
assertEquals(1.toShort(), challenge.quoteType)
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.attestation.toBase64
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ReportProxyResponseTest {
|
||||
private companion object {
|
||||
private val reportData = byteArrayOf(0x51, 0x62, 0x43, 0x24, 0x75, 0x4D)
|
||||
private val reportBase64 = reportData.toBase64()
|
||||
}
|
||||
|
||||
private lateinit var mapper: ObjectMapper
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mapper = ObjectMapper()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSerialiseBasic() {
|
||||
val response = ReportProxyResponse(
|
||||
signature = "<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)
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.attestation.toBase64
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ReportRequestTest {
|
||||
private companion object {
|
||||
private val quoteData = byteArrayOf(0x41, 0x42, 0x43, 0x44, 0x45, 0x46)
|
||||
private val quoteBase64 = quoteData.toBase64()
|
||||
|
||||
private val manifestData = byteArrayOf(0x55, 0x72, 0x19, 0x5B)
|
||||
private val manifestBase64 = manifestData.toBase64()
|
||||
}
|
||||
|
||||
private lateinit var mapper: ObjectMapper
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mapper = ObjectMapper()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSerialise() {
|
||||
val request = ReportRequest(
|
||||
isvEnclaveQuote = quoteData,
|
||||
pseManifest = manifestData,
|
||||
nonce = "<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)
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package net.corda.attestation.message
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.attestation.toBase64
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ServiceResponseTest {
|
||||
private companion object {
|
||||
private const val SPID = "0123456789ABCDEF"
|
||||
private val gaData = byteArrayOf(0x10, 0x00, 0x22, 0x00)
|
||||
private val gaBase64 = gaData.toBase64()
|
||||
}
|
||||
|
||||
private lateinit var mapper: ObjectMapper
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mapper = ObjectMapper()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSerialise() {
|
||||
val service = ServiceResponse(spid = SPID, quoteType = 1)
|
||||
val str = mapper.writeValueAsString(service)
|
||||
assertEquals("""{"spid":"$SPID","quoteType":1}""", str)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeserialise() {
|
||||
val str = """{"spid":"$SPID","quoteType":1}"""
|
||||
val service = mapper.readValue(str, ServiceResponse::class.java)
|
||||
assertEquals(SPID, service.spid)
|
||||
assertEquals(1.toShort(), service.quoteType)
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package net.corda.attestation.message.ias
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import net.corda.attestation.message.ias.ManifestStatus.*
|
||||
import net.corda.attestation.message.ias.QuoteStatus.*
|
||||
import net.corda.attestation.toBase64
|
||||
import net.corda.attestation.unsignedByteArrayOf
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class ReportResponseTest {
|
||||
private companion object {
|
||||
private val iso8601Time = "2017-11-08T18:19:27.123456"
|
||||
private val testTimestamp = LocalDateTime.from(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(iso8601Time))
|
||||
|
||||
private val quoteBodyData = byteArrayOf(0x61, 0x62, 0x63, 0x64, 0x65, 0x66)
|
||||
private val quoteBodyBase64 = quoteBodyData.toBase64()
|
||||
|
||||
private val platformInfoData = unsignedByteArrayOf(0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef)
|
||||
|
||||
private val pseudonymData = byteArrayOf(0x63, 0x18, 0x33, 0x72)
|
||||
private val pseudonymBase64 = pseudonymData.toBase64()
|
||||
}
|
||||
|
||||
private lateinit var mapper: ObjectMapper
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mapper = ObjectMapper().registerModule(JavaTimeModule())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSerialiseBasic() {
|
||||
val response = ReportResponse(
|
||||
id = "197283916372863387388037565359257649452",
|
||||
isvEnclaveQuoteStatus = QuoteStatus.OK,
|
||||
isvEnclaveQuoteBody = quoteBodyData,
|
||||
timestamp = testTimestamp
|
||||
)
|
||||
val str = mapper.writeValueAsString(response)
|
||||
assertEquals("{"
|
||||
+ "\"id\":\"197283916372863387388037565359257649452\","
|
||||
+ "\"timestamp\":\"$iso8601Time\","
|
||||
+ "\"isvEnclaveQuoteStatus\":\"OK\","
|
||||
+ "\"isvEnclaveQuoteBody\":\"$quoteBodyBase64\""
|
||||
+ "}", str)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSerialiseFull() {
|
||||
val response = ReportResponse(
|
||||
id = "197283916372863387388037565359257649452",
|
||||
isvEnclaveQuoteStatus = GROUP_OUT_OF_DATE,
|
||||
isvEnclaveQuoteBody = quoteBodyData,
|
||||
platformInfoBlob = platformInfoData,
|
||||
revocationReason = 1,
|
||||
pseManifestStatus = INVALID,
|
||||
pseManifestHash = "<manifest-hash>",
|
||||
nonce = "<nonce>",
|
||||
epidPseudonym = pseudonymData,
|
||||
timestamp = testTimestamp
|
||||
)
|
||||
val str = mapper.writeValueAsString(response)
|
||||
assertEquals("{"
|
||||
+ "\"nonce\":\"<nonce>\","
|
||||
+ "\"id\":\"197283916372863387388037565359257649452\","
|
||||
+ "\"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":"197283916372863387388037565359257649452",
|
||||
"isvEnclaveQuoteStatus":"OK",
|
||||
"isvEnclaveQuoteBody":"$quoteBodyBase64",
|
||||
"timestamp":"$iso8601Time"
|
||||
}"""
|
||||
val response = mapper.readValue(str, ReportResponse::class.java)
|
||||
assertEquals("197283916372863387388037565359257649452", response.id)
|
||||
assertEquals(QuoteStatus.OK, response.isvEnclaveQuoteStatus)
|
||||
assertArrayEquals(quoteBodyData, response.isvEnclaveQuoteBody)
|
||||
assertNull(response.platformInfoBlob)
|
||||
assertNull(response.revocationReason)
|
||||
assertNull(response.pseManifestStatus)
|
||||
assertNull(response.pseManifestHash)
|
||||
assertNull(response.nonce)
|
||||
assertNull(response.epidPseudonym)
|
||||
assertEquals(testTimestamp, response.timestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeserialiseFull() {
|
||||
val str = """{
|
||||
"id":"197283916372863387388037565359257649452",
|
||||
"isvEnclaveQuoteStatus":"GROUP_OUT_OF_DATE",
|
||||
"isvEnclaveQuoteBody":"$quoteBodyBase64",
|
||||
"platformInfoBlob":"0123456789ABCDEF",
|
||||
"revocationReason":1,
|
||||
"pseManifestStatus":"OK",
|
||||
"pseManifestHash":"<manifest-hash>",
|
||||
"nonce":"<nonce>",
|
||||
"epidPseudonym":"$pseudonymBase64",
|
||||
"timestamp":"$iso8601Time"
|
||||
}"""
|
||||
val response = mapper.readValue(str, ReportResponse::class.java)
|
||||
assertEquals("197283916372863387388037565359257649452", response.id)
|
||||
assertEquals(QuoteStatus.GROUP_OUT_OF_DATE, response.isvEnclaveQuoteStatus)
|
||||
assertArrayEquals(quoteBodyData, response.isvEnclaveQuoteBody)
|
||||
assertArrayEquals(platformInfoData, response.platformInfoBlob)
|
||||
assertEquals(1, response.revocationReason)
|
||||
assertEquals(ManifestStatus.OK, response.pseManifestStatus)
|
||||
assertEquals("<manifest-hash>", response.pseManifestHash)
|
||||
assertEquals("<nonce>", response.nonce)
|
||||
assertArrayEquals(pseudonymData, response.epidPseudonym)
|
||||
assertEquals(testTimestamp, response.timestamp)
|
||||
}
|
||||
}
|
29
sgx-jvm/remote-attestation/attestation-host/Makefile
Normal file
29
sgx-jvm/remote-attestation/attestation-host/Makefile
Normal file
@ -0,0 +1,29 @@
|
||||
.PHONY: host host-native docs clean \
|
||||
unit-tests integration-tests
|
||||
|
||||
# === GENERAL PARAMETERS ==========================================================================
|
||||
|
||||
SHELL = /bin/bash
|
||||
MAKEFILE_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
GRADLE_HOME ?= $(MAKEFILE_DIR)/../.gradle/
|
||||
GRADLE ?= $(MAKEFILE_DIR)/../gradlew -g $(GRADLE_HOME)
|
||||
|
||||
# === PSEUDO TARGETS ==============================================================================
|
||||
|
||||
host:
|
||||
$(GRADLE) compileKotlin
|
||||
|
||||
host-native:
|
||||
make -C native all
|
||||
|
||||
docs:
|
||||
$(GRADLE) cleanDokka dokka
|
||||
|
||||
unit-tests:
|
||||
$(GRADLE) --rerun-tasks -q -Pdebug=$(DEBUG) test
|
||||
|
||||
integration-tests:
|
||||
$(GRADLE) --rerun-tasks -q -Pdebug=$(DEBUG) integrationTest
|
||||
|
||||
clean:
|
||||
$(GRADLE) clean
|
185
sgx-jvm/remote-attestation/attestation-host/build.gradle
Normal file
185
sgx-jvm/remote-attestation/attestation-host/build.gradle
Normal file
@ -0,0 +1,185 @@
|
||||
buildscript {
|
||||
ext.keyStoreDir = "$buildDir/keystore"
|
||||
ext.nativeBuildDir = "$projectDir/native/build"
|
||||
ext.enclaveBuildDir = "$projectDir/../enclave/build"
|
||||
|
||||
ext.hardware = project.hasProperty("hardware") && (ext.hardware == "1" || ext.hardware == "yes" || ext.hardware == "true")
|
||||
ext.debug = project.hasProperty("debug") && (ext.debug == "1" || ext.debug == "yes" || ext.debug == "true")
|
||||
|
||||
if (!project.hasProperty("debugPort")) {
|
||||
ext.debugPort = 5005
|
||||
} else {
|
||||
ext.debugPort = Integer.parseInt(ext.debugPort.toString())
|
||||
}
|
||||
|
||||
if (ext.debug) {
|
||||
ext.debugArgs = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,timeout=10000,address=$debugPort"
|
||||
} else {
|
||||
ext.debugArgs = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort"
|
||||
}
|
||||
}
|
||||
|
||||
apply from: 'utilities.gradle'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'war'
|
||||
apply plugin: 'org.akhikhl.gretty'
|
||||
|
||||
description 'Proof-of-Concept Remote Attestation Host'
|
||||
|
||||
import org.akhikhl.gretty.AppStartTask
|
||||
import org.akhikhl.gretty.AppStopTask
|
||||
|
||||
configurations {
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.compileClasspath + test.compileClasspath
|
||||
runtimeClasspath += main.runtimeClasspath + test.runtimeClasspath
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':attestation-common')
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
|
||||
testCompile "junit:junit:$junit_version"
|
||||
|
||||
compile "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version"
|
||||
compile "org.jboss.resteasy:resteasy-jaxrs:$resteasy_version"
|
||||
compile "org.jboss.resteasy:resteasy-jackson2-provider:$resteasy_version"
|
||||
compile "org.jboss.resteasy:resteasy-servlet-initializer:$resteasy_version"
|
||||
compile "com.fasterxml.jackson.core:jackson-core:$jackson_version"
|
||||
compile "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
|
||||
compile "com.fasterxml.jackson.core:jackson-annotations:$jackson_version"
|
||||
compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version"
|
||||
compile "org.apache.httpcomponents:httpclient:$httpclient_version"
|
||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||
compile "org.apache.logging.log4j:log4j-core:$log4j_version"
|
||||
runtime "org.apache.logging.log4j:log4j-web:$log4j_version"
|
||||
compile "org.slf4j:jcl-over-slf4j:$slf4j_version"
|
||||
|
||||
testCompile project(path: ':attestation-common', configuration: 'testArtifacts')
|
||||
}
|
||||
|
||||
task createMockKeyStores(type: Exec) {
|
||||
doFirst {
|
||||
mkdir keyStoreDir
|
||||
}
|
||||
|
||||
inputs.dir "$projectDir/src/main/ssl/mockisv"
|
||||
outputs.files "$keyStoreDir/dummyIAS.pfx", "$keyStoreDir/dummyIAS-trust.pfx"
|
||||
workingDir keyStoreDir
|
||||
commandLine "$projectDir/src/main/ssl/mockisv/generate-keystores.sh"
|
||||
}
|
||||
|
||||
processResources {
|
||||
dependsOn createMockKeyStores
|
||||
from keyStoreDir
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
// Enable "unlimited" encryption.
|
||||
systemProperties["java.security.properties"] = "$projectDir/src/main/security.properties"
|
||||
|
||||
// Location of JNI object.
|
||||
systemProperties["java.library.path"] = nativeBuildDir
|
||||
|
||||
// Location of enclave object.
|
||||
systemProperties["corda.sgx.enclave.path"] = enclaveBuildDir
|
||||
|
||||
// Allow us to connect to JVM within enclave
|
||||
jvmArgs debugArgs
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
systemProperties["test.httpPort"] = testHttpPort
|
||||
}
|
||||
|
||||
gretty {
|
||||
httpPort = testHttpPort
|
||||
contextPath = "/"
|
||||
servletContainer = 'tomcat8'
|
||||
logDir = "$buildDir/logs"
|
||||
logFileName = "gretty-test"
|
||||
integrationTestTask = 'integrationTest'
|
||||
jvmArgs = [
|
||||
"-Dorg.jboss.logging.provider=slf4j",
|
||||
"-Djava.security.properties=$projectDir/src/main/security.properties",
|
||||
"-Dattestation.home=$buildDir/logs",
|
||||
"-Disv.host=localhost:$testHttpPort/mockisv",
|
||||
"-Djava.library.path=$nativeBuildDir",
|
||||
"-Dcorda.sgx.enclave.path=$enclaveBuildDir",
|
||||
]
|
||||
}
|
||||
|
||||
task('startHost', type: AppStartTask, dependsOn: war) {
|
||||
prepareServerConfig {
|
||||
httpPort = hostHttpPort
|
||||
servletContainer = 'tomcat8'
|
||||
logDir = "$buildDir/logs"
|
||||
logFileName = "gretty-host"
|
||||
jvmArgs = [
|
||||
"-Dorg.jboss.logging.provider=slf4j",
|
||||
"-Djava.security.properties=$projectDir/src/main/security.properties",
|
||||
"-Dattestation.home=$buildDir/logs",
|
||||
"-Disv.host=localhost:$isvHttpPort",
|
||||
"-Djava.library.path=$nativeBuildDir",
|
||||
"-Dcorda.sgx.enclave.path=$enclaveBuildDir",
|
||||
]
|
||||
}
|
||||
|
||||
prepareWebAppConfig {
|
||||
contextPath = "/"
|
||||
inplace = false
|
||||
}
|
||||
|
||||
interactive = false
|
||||
}
|
||||
|
||||
task("stopHost", type: AppStopTask)
|
||||
|
||||
task cleanEnclave(type: Exec) {
|
||||
commandLine containerArgs("enclave", "clean")
|
||||
}
|
||||
|
||||
task cleanJniLibrary(type: Exec) {
|
||||
commandLine containerArgs("attestation-host/native", "clean")
|
||||
}
|
||||
|
||||
clean.dependsOn.addAll cleanEnclave, cleanJniLibrary
|
||||
|
||||
task buildEnclave(type: Exec) {
|
||||
commandLine containerArgs("enclave", "all")
|
||||
}
|
||||
|
||||
task buildJniLibrary(type: Exec, dependsOn: [buildEnclave, classes]) {
|
||||
commandLine containerArgs("attestation-host/native", "all")
|
||||
}
|
||||
|
||||
build.dependsOn.addAll buildJniLibrary
|
||||
|
||||
task runUnitTestsInContainer(type: Task, dependsOn: buildJniLibrary) {
|
||||
doLast { containerDebugWait(projectDir, "attestation-host", "unit-tests") }
|
||||
}
|
||||
|
||||
task runIntegrationTestsInContainer(type: Task, dependsOn: buildJniLibrary) {
|
||||
doLast { containerDebugWait(projectDir, "attestation-host", "integration-tests") }
|
||||
}
|
||||
|
||||
task debugUnitTestsInContainer(type: Task, dependsOn: buildJniLibrary) {
|
||||
doLast { containerDebugWait(projectDir, "attestation-host", "DEBUG=1", "unit-tests") }
|
||||
}
|
||||
|
||||
task debugIntegrationTestsInContainer(type: Task, dependsOn: buildJniLibrary) {
|
||||
doLast { containerDebugWait(projectDir, "attestation-host", "DEBUG=1", "integration-tests") }
|
||||
}
|
1
sgx-jvm/remote-attestation/attestation-host/native/.gitignore
vendored
Normal file
1
sgx-jvm/remote-attestation/attestation-host/native/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
wrapper.hpp
|
115
sgx-jvm/remote-attestation/attestation-host/native/Makefile
Normal file
115
sgx-jvm/remote-attestation/attestation-host/native/Makefile
Normal file
@ -0,0 +1,115 @@
|
||||
.PHONY: all clean
|
||||
|
||||
# === GENERAL PARAMETERS ==========================================================================
|
||||
|
||||
SHELL = /bin/bash
|
||||
MAKEFILE_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
MODE ?= DEBUG # or RELEASE
|
||||
|
||||
GRADLE_FILE = $(MAKEFILE_DIR)/../build.gradle
|
||||
NAME = corda_sgx_ra
|
||||
VERSION := $(shell sed -n "s/^version = '\([^']*\)'.*/\\1/p" $(GRADLE_FILE))
|
||||
PLATFORM := $(shell uname -s | tr [:upper:] [:lower:])
|
||||
|
||||
OUT_DIR = $(MAKEFILE_DIR)/build
|
||||
OBJ_DIR = $(MAKEFILE_DIR)/obj
|
||||
|
||||
HOST_LIBRARY = $(OUT_DIR)/lib$(NAME).so
|
||||
|
||||
# === BUILD PARAMETERS ============================================================================
|
||||
|
||||
ifeq ($(PLATFORM),linux)
|
||||
JDK_HOME ?= $(dir $(word 1,$(wildcard /usr/lib/jvm/*/)))
|
||||
JDK_INC_DIRS = -I$(JDK_HOME)include -I$(JDK_HOME)include/linux
|
||||
endif
|
||||
|
||||
LOGGING_DEFS =
|
||||
ifeq ($(LOGGING),TRUE)
|
||||
LOGGING_DEFS = -DLOGGING
|
||||
endif
|
||||
|
||||
CPP = g++
|
||||
|
||||
CPPFLAGS_BASE = $(JDK_INC_DIRS) -Wall -fPIC \
|
||||
$(SGX_DEFS) $(LOGGING_DEFS)
|
||||
CPPFLAGS_DEBUG = $(CPPFLAGS_BASE) -g -O0 -DDEBUG
|
||||
CPPFLAGS_RELEASE = $(CPPFLAGS_BASE) -s -DNDEBUG
|
||||
|
||||
LDFLAGS_BASE = \
|
||||
-shared \
|
||||
-Wl,-soname,lib$(NAME).so \
|
||||
-Wl,-rpath,$(SGX_LIB_DIR):../../linux-sgx/build/linux \
|
||||
-Wl,-z,defs \
|
||||
-Wl,--no-as-needed \
|
||||
-L$(SGX_LIB_DIR) \
|
||||
-l$(URTS_LIB) \
|
||||
-l$(CAPABLE_LIB) \
|
||||
-l$(UAE_SERVICE_LIB) \
|
||||
-lpthread
|
||||
LDFLAGS_DEBUG = $(LDFLAGS_BASE)
|
||||
LDFLAGS_RELEASE = $(LDFLAGS_BASE) -s
|
||||
|
||||
# === SGX-SPECIFIC BUILD PARAMETERS ===============================================================
|
||||
|
||||
SGX_SDK := $(MAKEFILE_DIR)/../../../linux-sgx
|
||||
include $(MAKEFILE_DIR)/../../enclave/sgx.mk
|
||||
|
||||
# === MODE-SPECIFIC BUILD PARAMETERS ==============================================================
|
||||
|
||||
ifeq ($(subst release,RELEASE,$(MODE)),RELEASE)
|
||||
CPPFLAGS = $(CPPFLAGS_RELEASE) $(SGX_CPPFLAGS_RELEASE)
|
||||
LDFLAGS = $(LDFLAGS_RELEASE)
|
||||
else
|
||||
CPPFLAGS = $(CPPFLAGS_DEBUG) $(SGX_CPPFLAGS_DEBUG)
|
||||
LDFLAGS = $(LDFLAGS_DEBUG)
|
||||
endif
|
||||
|
||||
# === PSEUDO TARGETS ==============================================================================
|
||||
|
||||
all: $(HOST_LIBRARY)
|
||||
|
||||
clean:
|
||||
@rm -f $(JNI_HEADERS)
|
||||
@$(RM) -rf $(OUT_DIR)
|
||||
@$(RM) -rf $(OBJ_DIR)
|
||||
|
||||
# === HOST ========================================================================================
|
||||
|
||||
HOST_SOURCES = $(wildcard *.cpp)
|
||||
HOST_OBJECTS = $(addprefix $(OBJ_DIR)/, $(HOST_SOURCES:.cpp=.o))
|
||||
|
||||
ENCLAVE_PROJECT = $(MAKEFILE_DIR)/../../enclave
|
||||
ENCLAVE_OBJECTS = $(ENCLAVE_PROJECT)/obj/enclave_u.o
|
||||
ENCLAVE_INC_DIR = $(MAKEFILE_DIR)/../../enclave/rpc
|
||||
|
||||
JNI_HEADERS = wrapper.hpp
|
||||
JNI_SOURCES = $(JNI_HEADERS:.hpp=.cpp)
|
||||
JNI_OBJECTS = $(addprefix $(OBJ_DIR)/, $(JNI_SOURCES:.cpp=.o))
|
||||
CLASSPATH = $(MAKEFILE_DIR)/../build/classes/kotlin/main
|
||||
|
||||
$(HOST_LIBRARY): $(HOST_OBJECTS) $(ENCLAVE_OBJECTS) $(JNI_OBJECTS) | $(OUT_DIR)
|
||||
$(CPP) $(LDFLAGS) -o $@ $^ \
|
||||
$(SGX_LIB_DIR)/lib$(UKEY_EXCHNG).a
|
||||
|
||||
$(HOST_OBJECTS): $(HOST_SOURCES) | $(OBJ_DIR)
|
||||
|
||||
$(ENCLAVE_OBJECTS):
|
||||
make -C $(ENCLAVE_PROJECT) obj-untrusted
|
||||
|
||||
$(JNI_OBJECTS): $(JNI_SOURCES) | $(JNI_HEADERS)
|
||||
|
||||
$(JNI_HEADERS): | $(JNI_SOURCES)
|
||||
javah -o $@ -cp $(CLASSPATH) net.corda.attestation.host.sgx.bridge.wrapper.NativeWrapper
|
||||
|
||||
$(OBJ_DIR)/%.o: %.cpp
|
||||
@mkdir -p $(@D)
|
||||
$(CPP) $(CPPFLAGS) -I$(ENCLAVE_INC_DIR) -o $@ -c $<
|
||||
|
||||
# === BUILD DIRECTORIES ===========================================================================
|
||||
|
||||
$(OUT_DIR):
|
||||
@mkdir -p $(OUT_DIR)
|
||||
|
||||
$(OBJ_DIR):
|
||||
@mkdir -p $(OBJ_DIR)
|
@ -0,0 +1,95 @@
|
||||
#include <cstddef>
|
||||
|
||||
#include <sgx.h>
|
||||
#include <sgx_key_exchange.h>
|
||||
#include <sgx_uae_service.h>
|
||||
|
||||
#include "enclave-manager.hpp"
|
||||
#include "logging.hpp"
|
||||
|
||||
// Instantiate a new enclave from a signed enclave binary, and return the
|
||||
// identifier of the instance.
|
||||
sgx_enclave_id_t create_enclave(
|
||||
const char *path,
|
||||
bool use_platform_services,
|
||||
sgx_status_t *result,
|
||||
sgx_launch_token_t *token
|
||||
) {
|
||||
int updated = 0; // Indication of whether the launch token was updated.
|
||||
sgx_enclave_id_t enclave_id = 0; // The identifier of the created enclave.
|
||||
|
||||
// If the launch token is empty, then create a new enclave. Otherwise, try
|
||||
// to re-activate the existing enclave. `SGX_DEBUG_FLAG` is automatically
|
||||
// set to 1 in debug mode, and 0 in release mode.
|
||||
sgx_status_t status = sgx_create_enclave(
|
||||
path, SGX_DEBUG_FLAG, token, &updated, &enclave_id, NULL
|
||||
);
|
||||
|
||||
LOG(enclave_id, status, 0, "sgx_create_enclave()");
|
||||
|
||||
// Store the return value of the operation.
|
||||
if (NULL != result) {
|
||||
*result = status;
|
||||
}
|
||||
|
||||
// Return the identifier of the created enclave. Remember that if `status`
|
||||
// is `SGX_ERROR_ENCLAVE_LOST`, the enclave should be destroyed and then
|
||||
// re-created.
|
||||
return (SGX_SUCCESS == status) ? enclave_id : 0;
|
||||
}
|
||||
|
||||
// Destroy enclave if currently loaded.
|
||||
bool destroy_enclave(sgx_enclave_id_t enclave_id) {
|
||||
if (enclave_id != 0){
|
||||
// Attempt to destroy the enclave if we are provided with a valid
|
||||
// enclave identifier.
|
||||
sgx_status_t status = sgx_destroy_enclave(enclave_id);
|
||||
|
||||
LOG(enclave_id, status, 0, "sgx_destroy_enclave()");
|
||||
|
||||
return SGX_SUCCESS == status;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the status of the SGX device on the current machine.
|
||||
sgx_device_status_t get_device_status(void) {
|
||||
#if SGX_SIM == 1
|
||||
#pragma message "get_device_status() is being simulated"
|
||||
// If in simulation mode, simulate device capabilities.
|
||||
return SGX_ENABLED;
|
||||
#endif
|
||||
|
||||
// Try to retrieve the current status of the SGX device.
|
||||
sgx_device_status_t status;
|
||||
sgx_status_t ret = sgx_cap_enable_device(&status);
|
||||
|
||||
LOG(0, ret, 0, "sgx_cap_enable_device() = { status = %x }", status);
|
||||
|
||||
if (SGX_SUCCESS != ret) {
|
||||
return SGX_DISABLED;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
// Report which extended Intel EPID Group the client uses by default.
|
||||
uint32_t get_extended_group_id(sgx_status_t *result) {
|
||||
uint32_t egid;
|
||||
|
||||
// The extended EPID group identifier is indicative of which attestation
|
||||
// service the client is supposed to be communicating with. Currently, only
|
||||
// a value of zero is supported, which is referring to Intel. The user
|
||||
// should verify the retrieved extended group identifier, as any other
|
||||
// value than zero will be disregarded by the service provider.
|
||||
sgx_status_t status = sgx_get_extended_epid_group_id(&egid);
|
||||
|
||||
LOG(0, status, 0, "sgx_get_extended_epid_group_id() = %u", egid);
|
||||
|
||||
// Store the return value of the operation.
|
||||
if (NULL != result) {
|
||||
*result = status;
|
||||
}
|
||||
|
||||
return egid;
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
#ifndef __ENCLAVE_MANAGER_H__
|
||||
#define __ENCLAVE_MANAGER_H__
|
||||
|
||||
#include <sgx_capable.h>
|
||||
#include <sgx_urts.h>
|
||||
|
||||
/**
|
||||
* Instantiate a new enclave from a signed enclave binary, and return the
|
||||
* identifier of the instance.
|
||||
*
|
||||
* @param path The file name of the signed enclave binary to load.
|
||||
* @param use_platform_services If true, Intel's platform services are used to
|
||||
* add extra protection against replay attacks during nonce generation and to
|
||||
* provide a trustworthy monotonic counter.
|
||||
* @param result Variable receiving the result of the operation, if not NULL.
|
||||
* @param token Pointer to launch token; cannot be NULL.
|
||||
*
|
||||
* @return The identifier of the created enclave.
|
||||
*/
|
||||
sgx_enclave_id_t create_enclave(
|
||||
const char *path,
|
||||
bool use_platform_services,
|
||||
sgx_status_t *result,
|
||||
sgx_launch_token_t *token
|
||||
);
|
||||
|
||||
/**
|
||||
* Destroy enclave if currently loaded.
|
||||
*
|
||||
* @param enclave_id The identifier of the enclave to destroy.
|
||||
*
|
||||
* @return True if the enclave was active and got destroyed. False otherwise.
|
||||
*/
|
||||
bool destroy_enclave(
|
||||
sgx_enclave_id_t enclave_id
|
||||
);
|
||||
|
||||
/**
|
||||
* Check the status of the SGX device on the current machine.
|
||||
*/
|
||||
sgx_device_status_t get_device_status(void);
|
||||
|
||||
/**
|
||||
* Report which extended Intel EPID Group the client uses by default. The key
|
||||
* used to sign a Quote will be a member of the extended EPID Group reported in
|
||||
* this API. The application will typically use this value to tell the ISV
|
||||
* Service Provider which group to use during remote attestation.
|
||||
*
|
||||
* @param result Variable receiving the result of the operation, if not NULL.
|
||||
*
|
||||
* @return The extended EPID group identifier.
|
||||
*/
|
||||
uint32_t get_extended_group_id(sgx_status_t *result);
|
||||
|
||||
#endif /* __ENCLAVE_MANAGER_H__ */
|
13
sgx-jvm/remote-attestation/attestation-host/native/jni.hpp
Normal file
13
sgx-jvm/remote-attestation/attestation-host/native/jni.hpp
Normal file
@ -0,0 +1,13 @@
|
||||
#ifndef __JNI_HPP__
|
||||
#define __JNI_HPP__
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#define NATIVE_WRAPPER(return_type, method) \
|
||||
JNIEXPORT return_type JNICALL \
|
||||
Java_net_corda_attestation_host_sgx_bridge_wrapper_NativeWrapper_##method
|
||||
|
||||
#define KLASS(name) \
|
||||
("net/corda/attestation/host/sgx/bridge/wrapper/" name)
|
||||
|
||||
#endif /* __JNI_HPP__ */
|
@ -0,0 +1,32 @@
|
||||
#include <cstdio>
|
||||
|
||||
#include "logging.hpp"
|
||||
|
||||
void log(
|
||||
sgx_enclave_id_t enclave_id,
|
||||
sgx_status_t status,
|
||||
sgx_ra_context_t context,
|
||||
const char *message,
|
||||
...
|
||||
) {
|
||||
char mode[4] = { 0 };
|
||||
mode[0] = (SGX_SIM == 0) ? 'H' : 'S';
|
||||
mode[1] = (SGX_DEBUG == 0) ? 'R' : 'D';
|
||||
mode[2] = (SGX_PRERELEASE == 0) ? 'x' : 'P';
|
||||
mode[3] = 0;
|
||||
|
||||
char buffer[1024];
|
||||
va_list args;
|
||||
va_start(args, message);
|
||||
vsnprintf(buffer, sizeof(buffer), message, args);
|
||||
va_end(args);
|
||||
|
||||
printf(
|
||||
"SGX(id=%lx,status=%x,ctx=%u,mode=%s): %s\n",
|
||||
(uint64_t)enclave_id,
|
||||
(uint32_t)status,
|
||||
(uint32_t)context,
|
||||
mode,
|
||||
buffer
|
||||
);
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
#ifndef __LOGGING_HPP__
|
||||
#define __LOGGING_HPP__
|
||||
|
||||
#include <cstdarg>
|
||||
|
||||
#include <sgx_key_exchange.h>
|
||||
#include <sgx_urts.h>
|
||||
|
||||
#ifdef LOGGING
|
||||
#define LOG(enclave_id, status, context, message, ...) \
|
||||
log(enclave_id, (sgx_status_t)(status), context, message, ##__VA_ARGS__)
|
||||
#else
|
||||
#define LOG(enclave_id, status, context, message, ...) ;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Log message to standard output.
|
||||
*
|
||||
* @param enclave_id The enclave identifier.
|
||||
* @param status The outcome of the last SGX operation.
|
||||
* @param context The remote attestation context.
|
||||
* @param message The message.
|
||||
*/
|
||||
void log(
|
||||
sgx_enclave_id_t enclave_id,
|
||||
sgx_status_t status,
|
||||
sgx_ra_context_t context,
|
||||
const char *message,
|
||||
...
|
||||
);
|
||||
|
||||
#endif /* __LOGGING_HPP__ */
|
@ -0,0 +1,285 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sgx_uae_service.h>
|
||||
|
||||
#include "enclave_u.h"
|
||||
#include "logging.hpp"
|
||||
#include "remote-attestation.hpp"
|
||||
|
||||
// Initialize the remote attestation.
|
||||
sgx_status_t initialize_remote_attestation(
|
||||
// Inputs
|
||||
sgx_enclave_id_t enclave_id,
|
||||
bool use_platform_services,
|
||||
sgx_ec256_public_t *key_challenger,
|
||||
|
||||
// Outputs
|
||||
sgx_ra_context_t *context
|
||||
) {
|
||||
sgx_status_t ret;
|
||||
|
||||
// Perform ECALL into the application enclave to initialize the remote
|
||||
// attestation. The resulting attestation context will be stored in the
|
||||
// variable referenced by the `context` parameter.
|
||||
sgx_status_t _ret = initializeRemoteAttestation(
|
||||
enclave_id, &ret, use_platform_services, key_challenger, context
|
||||
);
|
||||
|
||||
LOG(enclave_id, _ret | ret, *context, "initialize_remote_attestation()");
|
||||
|
||||
// If the ECALL itself failed, report why. Otherwise, return the status of
|
||||
// the underlying function call.
|
||||
return (SGX_SUCCESS != _ret) ? _ret : ret;
|
||||
}
|
||||
|
||||
// Clean up and finalize the remote attestation process.
|
||||
sgx_status_t finalize_remote_attestation(
|
||||
sgx_enclave_id_t enclave_id,
|
||||
sgx_ra_context_t context
|
||||
) {
|
||||
sgx_status_t ret;
|
||||
|
||||
// Perform ECALL into the application enclave to close the current
|
||||
// attestation context and tidy up.
|
||||
sgx_status_t _ret = finalizeRemoteAttestation(enclave_id, &ret, context);
|
||||
|
||||
LOG(enclave_id, _ret | ret, context, "finalize_remote_attestation()");
|
||||
|
||||
// If the ECALL itself failed, report why. Otherwise, return the status of
|
||||
// the underlying function call.
|
||||
return (SGX_SUCCESS != _ret) ? _ret : ret;
|
||||
}
|
||||
|
||||
// Retrieve the application enclave's public key and the platform's group
|
||||
// identifier.
|
||||
sgx_status_t get_public_key_and_group_identifier(
|
||||
// Inputs
|
||||
sgx_enclave_id_t enclave_id,
|
||||
sgx_ra_context_t context,
|
||||
|
||||
// Outputs
|
||||
sgx_ec256_public_t *public_key,
|
||||
sgx_epid_group_id_t *group_id,
|
||||
|
||||
// Retry logic
|
||||
int max_retry_count,
|
||||
unsigned int retry_wait_in_secs
|
||||
) {
|
||||
sgx_status_t ret;
|
||||
sgx_ra_msg1_t message;
|
||||
|
||||
// It is generally recommended that the caller should wait (typically
|
||||
// several seconds to tens of seconds) and retry `sgx_ra_get_msg1()` if
|
||||
// `SGX_ERROR_BUSY` is returned.
|
||||
int retry_count = max_retry_count;
|
||||
|
||||
while (retry_count-- >= 0) {
|
||||
// Using an ECALL proxy to `sgx_ra_get_ga()` in the `sgx_tkey_exchange`
|
||||
// library to retrieve the public key of the application enclave.
|
||||
ret = sgx_ra_get_msg1(context, enclave_id, sgx_ra_get_ga, &message);
|
||||
|
||||
LOG(enclave_id, ret, context, "sgx_ra_get_msg1()");
|
||||
|
||||
if (SGX_ERROR_BUSY == ret) {
|
||||
// Wait before retrying...
|
||||
sleep(retry_wait_in_secs);
|
||||
} else if (SGX_SUCCESS != ret) {
|
||||
return ret;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Store the public key; components X and Y, each 256 bits long.
|
||||
if (NULL != public_key) {
|
||||
memcpy(public_key, &message.g_a, sizeof(sgx_ec256_public_t));
|
||||
}
|
||||
|
||||
// Store the EPID group identifier. Note, this is not the same as the
|
||||
// extended group identifier.
|
||||
if (NULL != group_id) {
|
||||
memcpy(group_id, &message.gid, sizeof(sgx_epid_group_id_t));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Process details received from challenger via the service provider, and
|
||||
// generate quote.
|
||||
sgx_status_t process_challenger_details_and_generate_quote(
|
||||
// Inputs
|
||||
sgx_enclave_id_t enclave_id,
|
||||
sgx_ra_context_t context,
|
||||
sgx_ec256_public_t *challenger_public_key,
|
||||
sgx_spid_t *service_provider_id,
|
||||
uint16_t quote_type,
|
||||
uint16_t key_derivation_function,
|
||||
sgx_ec256_signature_t *signature,
|
||||
sgx_mac_t *challenger_mac,
|
||||
uint32_t revocation_list_size,
|
||||
uint8_t *revocation_list,
|
||||
|
||||
// Outputs
|
||||
sgx_mac_t *enclave_mac,
|
||||
sgx_ec256_public_t *enclave_public_key,
|
||||
sgx_ps_sec_prop_desc_t *security_properties,
|
||||
uint8_t **quote,
|
||||
size_t *quote_size,
|
||||
|
||||
// Retry logic
|
||||
int max_retry_count,
|
||||
unsigned int retry_wait_in_secs
|
||||
) {
|
||||
sgx_status_t ret = SGX_SUCCESS;
|
||||
size_t msg_in_size = sizeof(sgx_ra_msg2_t) + revocation_list_size;
|
||||
sgx_ra_msg2_t *msg_in = (sgx_ra_msg2_t*)malloc(msg_in_size);
|
||||
sgx_ra_msg3_t *msg_out = NULL;
|
||||
uint32_t msg_out_size;
|
||||
|
||||
if (NULL == msg_in) {
|
||||
return SGX_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
// Populate input message (message 2 in the Intel attestation flow).
|
||||
memcpy(&msg_in->g_b, challenger_public_key, sizeof(sgx_ec256_public_t));
|
||||
memcpy(&msg_in->spid, service_provider_id, sizeof(sgx_spid_t));
|
||||
msg_in->quote_type = quote_type;
|
||||
msg_in->kdf_id = key_derivation_function;
|
||||
memcpy(&msg_in->sign_gb_ga, signature, sizeof(sgx_ec256_signature_t));
|
||||
memcpy(&msg_in->mac, challenger_mac, sizeof(sgx_mac_t));
|
||||
msg_in->sig_rl_size = revocation_list_size;
|
||||
if (revocation_list_size > 0) {
|
||||
memcpy(&msg_in->sig_rl, revocation_list, revocation_list_size);
|
||||
}
|
||||
|
||||
// Nullify outputs.
|
||||
*quote = NULL;
|
||||
|
||||
// It is generally recommended that the caller should wait (typically
|
||||
// several seconds to tens of seconds) and retry `sgx_ra_proc_msg2()` if
|
||||
// `SGX_ERROR_BUSY` is returned.
|
||||
int retry_count = max_retry_count;
|
||||
|
||||
while (retry_count-- >= 0) {
|
||||
// Using an ECALL proxy to `sgx_ra_proc_msg2_trusted()` in the
|
||||
// `sgx_tkey_exchange` library to process the incoming details from the
|
||||
// challenger, and `sgx_ra_get_msg3_trusted()` in the same library to
|
||||
// generate the quote.
|
||||
ret = sgx_ra_proc_msg2(
|
||||
context,
|
||||
enclave_id,
|
||||
sgx_ra_proc_msg2_trusted,
|
||||
sgx_ra_get_msg3_trusted,
|
||||
msg_in,
|
||||
sizeof(sgx_ra_msg2_t) + revocation_list_size,
|
||||
&msg_out,
|
||||
&msg_out_size
|
||||
);
|
||||
|
||||
LOG(enclave_id, ret, context, "sgx_ra_proc_msg2()");
|
||||
|
||||
if (SGX_ERROR_BUSY == ret) {
|
||||
// Wait before retrying...
|
||||
sleep(retry_wait_in_secs);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate outputs from the returned message structure.
|
||||
if (NULL != msg_out) {
|
||||
memcpy(enclave_mac, &msg_out->mac, sizeof(sgx_mac_t));
|
||||
memcpy(enclave_public_key, &msg_out->g_a, sizeof(sgx_ec256_public_t));
|
||||
|
||||
size_t z_sec_prop = sizeof(sgx_ps_sec_prop_desc_t);
|
||||
memcpy(security_properties, &msg_out->ps_sec_prop, z_sec_prop);
|
||||
}
|
||||
|
||||
// Populate the quote structure.
|
||||
if (NULL != msg_out) {
|
||||
*quote_size = msg_out_size - offsetof(sgx_ra_msg3_t, quote);
|
||||
*quote = (uint8_t*)malloc(*quote_size);
|
||||
if (NULL != quote) {
|
||||
memcpy(*quote, &msg_out->quote, *quote_size);
|
||||
}
|
||||
} else {
|
||||
*quote = NULL;
|
||||
}
|
||||
|
||||
// The output message is generated by the library and thus has to be freed
|
||||
// upon completion.
|
||||
free(msg_out);
|
||||
|
||||
// Allocated due to the variable size revocation list. Free up the
|
||||
// temporary structure.
|
||||
free(msg_in);
|
||||
|
||||
// Check if the malloc() call for the output quote failed above; if it did,
|
||||
// it was due to an out-of-memory condition.
|
||||
if (NULL == quote && SGX_SUCCESS == ret) {
|
||||
return SGX_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
sgx_status_t verify_attestation_response(
|
||||
// Inputs
|
||||
sgx_enclave_id_t enclave_id,
|
||||
sgx_ra_context_t context,
|
||||
uint8_t *message,
|
||||
size_t message_size,
|
||||
uint8_t *cmac,
|
||||
size_t cmac_size,
|
||||
uint8_t *secret,
|
||||
size_t secret_size,
|
||||
uint8_t *gcm_iv,
|
||||
uint8_t *gcm_mac,
|
||||
size_t gcm_mac_size,
|
||||
|
||||
// Outputs
|
||||
uint8_t *sealed_secret,
|
||||
size_t *sealed_secret_size,
|
||||
sgx_status_t *cmac_status
|
||||
) {
|
||||
// Check the generated CMAC from the service provider.
|
||||
sgx_status_t ret = SGX_SUCCESS;
|
||||
sgx_status_t _ret = verifyCMAC(
|
||||
enclave_id, &ret, context, message, message_size, cmac, cmac_size
|
||||
);
|
||||
|
||||
*cmac_status = ret;
|
||||
LOG(enclave_id, _ret, context, "verify_cmac() = %x", (uint32_t)ret);
|
||||
|
||||
// Abort if call failed. Otherwise, forward the outcome to the caller.
|
||||
if (SGX_SUCCESS != _ret) {
|
||||
return _ret;
|
||||
}
|
||||
|
||||
// Try to decrypt and verify the attestation response.
|
||||
_ret = verifyAttestationResponse(
|
||||
enclave_id, &ret, context, secret, secret_size,
|
||||
gcm_iv, gcm_mac, gcm_mac_size,
|
||||
sealed_secret, sizeof(sgx_sealed_data_t) + secret_size
|
||||
);
|
||||
|
||||
LOG(
|
||||
enclave_id, _ret, context,
|
||||
"verify_attestation_response() = %x",
|
||||
(uint32_t)ret
|
||||
);
|
||||
|
||||
// Abort if unable to verify attestation response.
|
||||
if (SGX_SUCCESS != (_ret | ret)) {
|
||||
return (SGX_SUCCESS != _ret) ? _ret : ret;
|
||||
}
|
||||
|
||||
// Return sealed secret if requested. The buffer is populated by the ECALL
|
||||
// above, if sealed_secret is non-null.
|
||||
if (NULL != sealed_secret_size) {
|
||||
*sealed_secret_size = sizeof(sgx_sealed_data_t) + secret_size;
|
||||
}
|
||||
|
||||
return SGX_SUCCESS;
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
#ifndef __REMOTE_ATTESTATION_H__
|
||||
#define __REMOTE_ATTESTATION_H__
|
||||
|
||||
#include <sgx_urts.h>
|
||||
#include <sgx_key_exchange.h>
|
||||
#include <sgx_ukey_exchange.h>
|
||||
|
||||
/**
|
||||
* Initialize the remote attestation.
|
||||
*
|
||||
* @param enclave_id The identifier of the enclave facilitating the remote
|
||||
* attestation.
|
||||
* @param use_platform_services If true, the enclave establishes a session with
|
||||
* the PSE before initializing the attestation context. This provides
|
||||
* additional nonce replay protection and a reliable monotonic counter.
|
||||
* @param key_challenger ECDSA public key of the challenger with the 8 magic
|
||||
* bytes removed, and X and Y components changed to little-endian.
|
||||
* @param context The variable receiving the context constructed during
|
||||
* initialization.
|
||||
*
|
||||
* @return Status code indicative of the outcome of the operation.
|
||||
*/
|
||||
sgx_status_t initialize_remote_attestation(
|
||||
// Inputs
|
||||
sgx_enclave_id_t enclave_id,
|
||||
bool use_platform_services,
|
||||
sgx_ec256_public_t *key_challenger,
|
||||
|
||||
// Outputs
|
||||
sgx_ra_context_t* context
|
||||
);
|
||||
|
||||
/**
|
||||
* Clean up and finalize the remote attestation process.
|
||||
*
|
||||
* @param enclave_id The identifier of the enclave facilitating the remote
|
||||
* attestation.
|
||||
* @param context The context constructed during initialization.
|
||||
*
|
||||
* @return SGX_SUCCESS if successful, or SGX_ERROR_INVALID_PARAMETER if
|
||||
* an invalid context is provided.
|
||||
*/
|
||||
sgx_status_t finalize_remote_attestation(
|
||||
sgx_enclave_id_t enclave_id,
|
||||
sgx_ra_context_t context
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the public key of the application enclave, and the identifier of the
|
||||
* EPID group the platform belongs to.
|
||||
*
|
||||
* @param enclave_id The identifier of the application enclave.
|
||||
* @param context The context constructed during initialization.
|
||||
* @param public_key Variable receiving the elliptic curve public key of the
|
||||
* application enclave, based on the NIST P-256 elliptic curve..
|
||||
* @param group_id Variable receiving the identifier of the platform's EPID
|
||||
* group.
|
||||
* @param max_retry_count The maximum number of times to retry the operation.
|
||||
* @param retry_wait_in_secs The number of seconds to wait between each retry.
|
||||
*
|
||||
* @return SGX_SUCCESS if successful, otherwise an error code indicative of
|
||||
* what went wrong.
|
||||
*/
|
||||
sgx_status_t get_public_key_and_group_identifier(
|
||||
// Inputs
|
||||
sgx_enclave_id_t enclave_id,
|
||||
sgx_ra_context_t context,
|
||||
|
||||
// Outputs
|
||||
sgx_ec256_public_t *public_key,
|
||||
sgx_epid_group_id_t *group_id,
|
||||
|
||||
// Retry logic
|
||||
int max_retry_count,
|
||||
unsigned int retry_wait_in_secs
|
||||
);
|
||||
|
||||
/**
|
||||
* Process details received from the challenger via the service provider, and
|
||||
* generate quote. If the service provider accepts the quote, negotiated
|
||||
* session keys between the application enclave and the challenger are ready
|
||||
* for use. However, if it fails, the application should notify the service
|
||||
* provider of the error or the service provider needs a time-out mechanism to
|
||||
* terminate the remote attestation transaction when it does not receive the
|
||||
* message.
|
||||
*
|
||||
* @param enclave_id The identifier of the application enclave.
|
||||
* @param context The context constructed during initialization.
|
||||
* @param challenger_public_key The public key of the challenger.
|
||||
* @param service_provider_id The identifier of the service provider.
|
||||
* @param quote_type Indicates the quote type, linkable (1) or unlinkable (0).
|
||||
* @param key_derivation_function The ID of the key derivation function used.
|
||||
* @param signature An ECDSA signature over the challenger and application
|
||||
* enclave's public keys.
|
||||
* @param mac A 128-bit AES-CMAC generated by the service provider.
|
||||
* @param revocation_list_size The size of revocation_list, in bytes.
|
||||
* @param revocation_list The signature revocation list certificate of the
|
||||
* Intel EPID group identified by the group identifier in message 1.
|
||||
* @param enclave_mac AES-CMAC generated by the enclave.
|
||||
* @param enclave_public_key Variable receiving the public key of the
|
||||
* application enclave.
|
||||
* @param security_properties Variable receiving the security properties of the
|
||||
* Intel SGX platform service. If the security property information is not
|
||||
* required in the remote attestation and key exchange process, this field will
|
||||
* be all zeros.
|
||||
* @param quote Variable receiving a pointer to the quote returned from
|
||||
* sgx_get_quote. This should be freed after use.
|
||||
* @param quote_size Variable receiving the size of the quote.
|
||||
* @param max_retry_count The maximum number of times to retry the operation.
|
||||
* @param retry_wait_in_secs The number of seconds to wait between each retry.
|
||||
*
|
||||
* @return SGX_SUCCESS if successful, otherwise an error code indicative of
|
||||
* what went wrong.
|
||||
*/
|
||||
sgx_status_t process_challenger_details_and_generate_quote(
|
||||
// Inputs
|
||||
sgx_enclave_id_t enclave_id,
|
||||
sgx_ra_context_t context,
|
||||
sgx_ec256_public_t *challenger_public_key,
|
||||
sgx_spid_t *service_provider_id,
|
||||
uint16_t quote_type,
|
||||
uint16_t key_derivation_function,
|
||||
sgx_ec256_signature_t *signature,
|
||||
sgx_mac_t *challenger_mac,
|
||||
uint32_t revocation_list_size,
|
||||
uint8_t *revocation_list,
|
||||
|
||||
// Outputs
|
||||
sgx_mac_t *enclave_mac,
|
||||
sgx_ec256_public_t *enclave_public_key,
|
||||
sgx_ps_sec_prop_desc_t *security_properties,
|
||||
uint8_t **quote,
|
||||
size_t *quote_size,
|
||||
|
||||
// Retry logic
|
||||
int max_retry_count,
|
||||
unsigned int retry_wait_in_secs
|
||||
);
|
||||
|
||||
/**
|
||||
* Verify attestation response from service provider.
|
||||
*
|
||||
* @param enclave_id The identifier of the application enclave.
|
||||
* @param context The context constructed during initialization.
|
||||
* @param message The received attestation result message.
|
||||
* @param message_size The size of the attestation result message.
|
||||
* @param cmac The CMAC computed over the attestation result message.
|
||||
* @param cmac_size The size of the CMAC.
|
||||
* @param secret The encrypted secret provisioned by the challenger.
|
||||
* @param secret_size The size of the encrypted secret.
|
||||
* @param gcm_iv The 12-byte initialization vector used in the decryption.
|
||||
* @param gcm_mac The GCM-MAC generated over the secret.
|
||||
* @param gcm_mac_size The size of the GCM-MAC.
|
||||
* @param sealed_secret Pre-allocated buffer receiving the sealed secret.
|
||||
* @param sealed_secret_size The size of the sealed secret.
|
||||
* @param cmac_status The variable receiving the outcome of the CMAC check.
|
||||
*
|
||||
* @return SGX_SUCCESS if successful, otherwise an error code indicative of
|
||||
* what went wrong.
|
||||
*/
|
||||
sgx_status_t verify_attestation_response(
|
||||
// Inputs
|
||||
sgx_enclave_id_t enclave_id,
|
||||
sgx_ra_context_t context,
|
||||
uint8_t *message,
|
||||
size_t message_size,
|
||||
uint8_t *cmac,
|
||||
size_t cmac_size,
|
||||
uint8_t *secret,
|
||||
size_t secret_size,
|
||||
uint8_t *gcm_iv,
|
||||
uint8_t *gcm_mac,
|
||||
size_t gcm_mac_size,
|
||||
|
||||
// Outputs
|
||||
uint8_t *sealed_secret,
|
||||
size_t *sealed_secret_size,
|
||||
sgx_status_t *cmac_status
|
||||
);
|
||||
|
||||
#endif /* __REMOTE_ATTESTATION_H__ */
|
@ -0,0 +1,15 @@
|
||||
#include "enclave_u.h"
|
||||
#include "sealing.hpp"
|
||||
|
||||
// Check whether the application enclave is able to unseal a secret.
|
||||
sgx_status_t unseal_secret(
|
||||
sgx_enclave_id_t enclave_id,
|
||||
uint8_t *sealed_secret,
|
||||
size_t sealed_secret_size
|
||||
) {
|
||||
sgx_status_t status = SGX_SUCCESS;
|
||||
sgx_status_t ret = unsealSecret(
|
||||
enclave_id, &status, sealed_secret, sealed_secret_size
|
||||
);
|
||||
return SGX_SUCCESS != ret ? ret : status;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
#ifndef __SEALING_H__
|
||||
#define __SEALING_H__
|
||||
|
||||
#include <cstdlib>
|
||||
#include <sgx_urts.h>
|
||||
|
||||
/**
|
||||
* Check whether the application enclave is able to unseal a persisted, sealed
|
||||
* secret.
|
||||
*
|
||||
* @param enclave_id The identifier of the application enclave.
|
||||
* @param sealed_secret The pre-existing, sealed secret.
|
||||
* @param sealed_secret_size The size of the sealed secret.
|
||||
*
|
||||
* @return An indication of whether or not the enclave was able to unseal the
|
||||
* secret.
|
||||
*/
|
||||
sgx_status_t unseal_secret(
|
||||
sgx_enclave_id_t enclave_id,
|
||||
uint8_t *sealed_secret,
|
||||
size_t sealed_secret_size
|
||||
);
|
||||
|
||||
#endif /* __SEALING_H__ */
|
394
sgx-jvm/remote-attestation/attestation-host/native/wrapper.cpp
Normal file
394
sgx-jvm/remote-attestation/attestation-host/native/wrapper.cpp
Normal file
@ -0,0 +1,394 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <sgx_tcrypto.h>
|
||||
#include <sgx_tseal.h>
|
||||
|
||||
#include "wrapper.hpp"
|
||||
#include "jni.hpp"
|
||||
|
||||
#include "logging.hpp"
|
||||
#include "enclave-manager.hpp"
|
||||
#include "remote-attestation.hpp"
|
||||
#include "sealing.hpp"
|
||||
|
||||
NATIVE_WRAPPER(jint, getDeviceStatus)
|
||||
(JNIEnv *, jobject)
|
||||
{
|
||||
// Get the status of the SGX device on the local machine.
|
||||
return get_device_status();
|
||||
}
|
||||
|
||||
NATIVE_WRAPPER(jobject, getExtendedGroupIdentifier)
|
||||
(JNIEnv *env, jobject)
|
||||
{
|
||||
// FindClass/GetMethodID both throw an exception upon failure, so we don't
|
||||
// need to perform any further NULL-checks.
|
||||
jclass klass = env->FindClass(KLASS("ExtendedGroupIdentifierResult"));
|
||||
jmethodID cid = env->GetMethodID(klass, "<init>", "(IJ)V");
|
||||
if (cid == NULL) { return NULL; }
|
||||
|
||||
// Get the extended EPID group identifier from SGX.
|
||||
sgx_status_t status = SGX_ERROR_UNEXPECTED;
|
||||
uint32_t extended_group_id = get_extended_group_id(&status);
|
||||
|
||||
// Construct and return ExtendedGroupIdentifierResult(identifier, status).
|
||||
return env->NewObject(klass, cid, extended_group_id, status);
|
||||
}
|
||||
|
||||
NATIVE_WRAPPER(jobject, createEnclave)
|
||||
(JNIEnv *env, jobject, jstring path, jboolean use_platform_services,
|
||||
jbyteArray in_launch_token)
|
||||
{
|
||||
// FindClass/GetMethodID both throw an exception upon failure, so we don't
|
||||
// need to perform any further NULL-checks.
|
||||
jclass klass = env->FindClass(KLASS("EnclaveResult"));
|
||||
jmethodID cid = env->GetMethodID(klass, "<init>", "(J[BJ)V");
|
||||
if (cid == NULL) { return NULL; }
|
||||
|
||||
// Marshall inputs.
|
||||
const char *n_path = env->GetStringUTFChars(path, NULL);
|
||||
sgx_status_t status;
|
||||
|
||||
// Initialize launch token.
|
||||
sgx_launch_token_t launch_token = { 0 };
|
||||
env->GetByteArrayRegion(
|
||||
in_launch_token, 0, sizeof(sgx_launch_token_t), (jbyte*)&launch_token
|
||||
);
|
||||
|
||||
// Create the enclave.
|
||||
sgx_enclave_id_t enclave_id = create_enclave(
|
||||
n_path, (bool)use_platform_services, &status,
|
||||
&launch_token
|
||||
);
|
||||
|
||||
// Construct resulting launch token (could be the same as the input).
|
||||
jbyteArray _launch_token = env->NewByteArray(sizeof(sgx_launch_token_t));
|
||||
env->SetByteArrayRegion(
|
||||
_launch_token, 0, sizeof(sgx_launch_token_t), (jbyte*)&launch_token
|
||||
);
|
||||
|
||||
// Free up memory.
|
||||
env->ReleaseStringUTFChars(path, n_path);
|
||||
|
||||
// Construct and return EnclaveResult(identifier, launch_token, status).
|
||||
return env->NewObject(klass, cid, enclave_id, _launch_token, status);
|
||||
}
|
||||
|
||||
NATIVE_WRAPPER(jboolean, destroyEnclave)
|
||||
(JNIEnv *, jobject, jlong enclave_id)
|
||||
{
|
||||
// Destroy the enclave if a valid identifier has been passed in.
|
||||
if (enclave_id != 0) {
|
||||
return destroy_enclave((sgx_enclave_id_t)enclave_id);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
NATIVE_WRAPPER(jobject, initializeRemoteAttestation)
|
||||
(JNIEnv *env, jobject, jlong enclave_id, jboolean use_platform_services,
|
||||
jbyteArray in_key_challenger)
|
||||
{
|
||||
// FindClass/GetMethodID both throw an exception upon failure, so we don't
|
||||
// need to perform any further NULL-checks.
|
||||
jclass klass = env->FindClass(KLASS("InitializationResult"));
|
||||
jmethodID cid = env->GetMethodID(klass, "<init>", "(IJ)V");
|
||||
if (cid == NULL) { return NULL; }
|
||||
|
||||
// Marshall the public key passed in from the JVM.
|
||||
sgx_ec256_public_t key_challenger = { 0 };
|
||||
env->GetByteArrayRegion(
|
||||
in_key_challenger, 0, sizeof(sgx_ec256_public_t),
|
||||
(jbyte*)&key_challenger
|
||||
);
|
||||
|
||||
// Initialize the remote attestation context.
|
||||
sgx_ra_context_t context;
|
||||
sgx_status_t status = initialize_remote_attestation(
|
||||
enclave_id, use_platform_services, &key_challenger, &context
|
||||
);
|
||||
|
||||
// Construct and return InitializationResult(context, status).
|
||||
return env->NewObject(klass, cid, context, status);
|
||||
}
|
||||
|
||||
NATIVE_WRAPPER(jlong, finalizeRemoteAttestation)
|
||||
(JNIEnv *, jobject, jlong enclave_id, jint context)
|
||||
{
|
||||
// Finalize the remote attestation
|
||||
return finalize_remote_attestation(enclave_id, context);
|
||||
}
|
||||
|
||||
NATIVE_WRAPPER(jobject, getPublicKeyAndGroupIdentifier)
|
||||
(JNIEnv *env, jobject, jlong enclave_id, jint context, jint max_retry_count,
|
||||
jint retry_wait_in_secs)
|
||||
{
|
||||
// FindClass/GetMethodID both throw an exception upon failure, so we don't
|
||||
// need to perform any further NULL-checks.
|
||||
jclass klass = env->FindClass(KLASS("PublicKeyAndGroupIdentifier"));
|
||||
jmethodID cid = env->GetMethodID(klass, "<init>", "([BIJ)V");
|
||||
if (cid == NULL) { return NULL; }
|
||||
|
||||
// Get the public key of the application enclave, and the group identifier
|
||||
// of the platform.
|
||||
sgx_ec256_public_t public_key;
|
||||
sgx_epid_group_id_t group_id;
|
||||
sgx_status_t status = get_public_key_and_group_identifier(
|
||||
enclave_id, context, &public_key, &group_id, max_retry_count,
|
||||
retry_wait_in_secs
|
||||
);
|
||||
|
||||
// Cast group identifier into an unsigned integer.
|
||||
uint32_t gid = *((uint32_t*)group_id);
|
||||
|
||||
// Create managed array used to return the enclave's public key.
|
||||
jbyteArray _public_key = env->NewByteArray(sizeof(sgx_ec256_public_t));
|
||||
if (NULL == _public_key) {
|
||||
// Out of memory - abort
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Copy public key bytes over to managed array.
|
||||
env->SetByteArrayRegion(
|
||||
_public_key, 0, sizeof(sgx_ec256_public_t), (jbyte*)&public_key
|
||||
);
|
||||
|
||||
// Return PublicKeyAndGroupIdentifier(publicKey, groupIdentifier, status).
|
||||
return env->NewObject(klass, cid, _public_key, gid, status);
|
||||
}
|
||||
|
||||
NATIVE_WRAPPER(jobject, processServiceProviderDetailsAndGenerateQuote)
|
||||
(JNIEnv *env, jobject, jlong enclave_id, jint context,
|
||||
jbyteArray in_challenger_public_key, jbyteArray in_service_provider_id,
|
||||
jshort quote_type, jshort key_derivation_function, jbyteArray in_signature,
|
||||
jbyteArray in_mac, jint revocation_list_size,
|
||||
jbyteArray in_revocation_list, jint max_retry_count,
|
||||
jint retry_wait_in_secs)
|
||||
{
|
||||
// FindClass/GetMethodID both throw an exception upon failure, so we don't
|
||||
// need to perform any further NULL-checks.
|
||||
jclass klass = env->FindClass(KLASS("QuoteResult"));
|
||||
jmethodID cid = env->GetMethodID(klass, "<init>", "([B[B[B[BJ)V");
|
||||
if (cid == NULL) { return NULL; }
|
||||
|
||||
// Marshal inputs.
|
||||
sgx_ec256_public_t challenger_public_key;
|
||||
sgx_spid_t service_provider_id;
|
||||
sgx_ec256_signature_t signature;
|
||||
sgx_mac_t mac;
|
||||
uint8_t *revocation_list = (uint8_t*)malloc(revocation_list_size);
|
||||
|
||||
// Check if there's enough free memory to allocate a buffer for the
|
||||
// revocation list.
|
||||
if (NULL == revocation_list) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
env->GetByteArrayRegion(
|
||||
in_challenger_public_key, 0, sizeof(sgx_ec256_public_t),
|
||||
(jbyte*)&challenger_public_key
|
||||
);
|
||||
env->GetByteArrayRegion(
|
||||
in_service_provider_id, 0, sizeof(sgx_spid_t),
|
||||
(jbyte*)&service_provider_id
|
||||
);
|
||||
env->GetByteArrayRegion(
|
||||
in_signature, 0, sizeof(sgx_ec256_signature_t),
|
||||
(jbyte*)&signature
|
||||
);
|
||||
env->GetByteArrayRegion(
|
||||
in_mac, 0, sizeof(sgx_mac_t),
|
||||
(jbyte*)&mac
|
||||
);
|
||||
env->GetByteArrayRegion(
|
||||
in_revocation_list, 0, revocation_list_size, (jbyte*)revocation_list
|
||||
);
|
||||
|
||||
// Output variables.
|
||||
sgx_mac_t enclave_mac = { 0 };
|
||||
sgx_ec256_public_t enclave_public_key = { 0 };
|
||||
sgx_ps_sec_prop_desc_t security_properties = { 0 };
|
||||
uint8_t *quote = NULL;
|
||||
size_t quote_size = 0;
|
||||
|
||||
// Process details received from challenger via the service provider, and
|
||||
// generate quote.
|
||||
sgx_status_t status = process_challenger_details_and_generate_quote(
|
||||
// Inputs
|
||||
enclave_id, context, &challenger_public_key, &service_provider_id,
|
||||
quote_type, key_derivation_function, &signature, &mac,
|
||||
revocation_list_size, revocation_list,
|
||||
// Outputs
|
||||
&enclave_mac, &enclave_public_key, &security_properties, "e,
|
||||
"e_size,
|
||||
// Retry logic
|
||||
max_retry_count, retry_wait_in_secs
|
||||
);
|
||||
|
||||
LOG(
|
||||
enclave_id, status, context,
|
||||
"process_challenger_details_and_generate_quote() = quote(size=%u)",
|
||||
quote_size
|
||||
);
|
||||
|
||||
// Create output objects.
|
||||
jbyteArray _enclave_mac = env->NewByteArray(sizeof(sgx_mac_t));
|
||||
env->SetByteArrayRegion(
|
||||
_enclave_mac, 0, sizeof(sgx_mac_t), (jbyte*)&enclave_mac
|
||||
);
|
||||
jbyteArray _enclave_public_key = env->NewByteArray(
|
||||
sizeof(sgx_ec256_public_t)
|
||||
);
|
||||
env->SetByteArrayRegion(
|
||||
_enclave_public_key, 0, sizeof(sgx_ec256_public_t),
|
||||
(jbyte*)&enclave_public_key
|
||||
);
|
||||
jbyteArray _security_properties = env->NewByteArray(
|
||||
sizeof(sgx_ps_sec_prop_desc_t)
|
||||
);
|
||||
env->SetByteArrayRegion(
|
||||
_security_properties, 0, sizeof(sgx_ps_sec_prop_desc_t),
|
||||
(jbyte*)&security_properties
|
||||
);
|
||||
|
||||
jbyteArray _quote = NULL;
|
||||
|
||||
// Free up memory.
|
||||
if (NULL != quote) {
|
||||
_quote = env->NewByteArray(quote_size);
|
||||
env->SetByteArrayRegion(_quote, 0, quote_size, (jbyte*)quote);
|
||||
free(quote);
|
||||
} else {
|
||||
_quote = env->NewByteArray(0);
|
||||
}
|
||||
free(revocation_list);
|
||||
|
||||
// Return QuoteResult(mac, publicKey, securityProperties, quote, status).
|
||||
return env->NewObject(
|
||||
klass, cid,
|
||||
_enclave_mac, _enclave_public_key, _security_properties, _quote, status
|
||||
);
|
||||
}
|
||||
|
||||
NATIVE_WRAPPER(jobject, verifyAttestationResponse)
|
||||
(JNIEnv *env, jobject, jlong enclave_id, jint context,
|
||||
jbyteArray message, jbyteArray cmac, jbyteArray secret,
|
||||
jbyteArray gcm_iv, jbyteArray gcm_mac)
|
||||
{
|
||||
// FindClass/GetMethodID both throw an exception upon failure, so we don't
|
||||
// need to perform any further NULL-checks.
|
||||
jclass klass = env->FindClass(KLASS("VerificationResult"));
|
||||
jmethodID cid = env->GetMethodID(klass, "<init>", "([BJJ)V");
|
||||
if (cid == NULL) { return NULL; }
|
||||
|
||||
// Get buffer sizes.
|
||||
size_t message_size = (message == NULL) ? 0 : env->GetArrayLength(message);
|
||||
size_t cmac_size = (cmac == NULL) ? 0 : env->GetArrayLength(cmac);
|
||||
size_t secret_size = (secret == NULL) ? 0 : env->GetArrayLength(secret);
|
||||
size_t gcm_mac_size = (gcm_mac == NULL) ? 0 : env->GetArrayLength(gcm_mac);
|
||||
|
||||
// Allocate buffers.
|
||||
uint8_t *_message = (uint8_t*)malloc(message_size);
|
||||
uint8_t *_cmac = (uint8_t*)malloc(cmac_size);
|
||||
uint8_t *_secret = (uint8_t*)malloc(secret_size);
|
||||
uint8_t *_gcm_iv = (uint8_t*)calloc(1, SGX_AESGCM_IV_SIZE);
|
||||
uint8_t *_gcm_mac = (uint8_t*)malloc(gcm_mac_size);
|
||||
|
||||
// Length of secret is preserved during encryption, but prepend header.
|
||||
size_t _sealed_secret_size = sizeof(sgx_sealed_data_t) + secret_size;
|
||||
uint8_t *_sealed_secret = (uint8_t*)malloc(_sealed_secret_size);
|
||||
|
||||
// Check if we ran out of memory.
|
||||
if (NULL == _message || NULL == _cmac || NULL == _secret ||
|
||||
NULL == _gcm_iv || NULL == _gcm_mac || NULL == _sealed_secret) {
|
||||
free(_message);
|
||||
free(_cmac);
|
||||
free(_secret);
|
||||
free(_gcm_iv);
|
||||
free(_gcm_mac);
|
||||
free(_sealed_secret);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Marshal inputs.
|
||||
if (message != NULL) {
|
||||
env->GetByteArrayRegion(message, 0, message_size, (jbyte*)_message);
|
||||
}
|
||||
if (cmac != NULL) {
|
||||
env->GetByteArrayRegion(cmac, 0, cmac_size, (jbyte*)_cmac);
|
||||
}
|
||||
if (secret != NULL) {
|
||||
env->GetByteArrayRegion(secret, 0, secret_size, (jbyte*)_secret);
|
||||
}
|
||||
if (gcm_iv != NULL) {
|
||||
env->GetByteArrayRegion(gcm_iv, 0, SGX_AESGCM_IV_SIZE, (jbyte*)_gcm_iv);
|
||||
}
|
||||
if (gcm_mac != NULL) {
|
||||
env->GetByteArrayRegion(gcm_mac, 0, gcm_mac_size, (jbyte*)_gcm_mac);
|
||||
}
|
||||
|
||||
// Verify the attestation response received from the service provider.
|
||||
sgx_status_t cmac_status = SGX_SUCCESS;
|
||||
sgx_status_t status = verify_attestation_response(
|
||||
enclave_id, context, _message, message_size, _cmac, cmac_size,
|
||||
_secret, secret_size, _gcm_iv, _gcm_mac, gcm_mac_size, _sealed_secret,
|
||||
&_sealed_secret_size, &cmac_status
|
||||
);
|
||||
|
||||
// Free temporary allocations.
|
||||
free(_message);
|
||||
free(_cmac);
|
||||
free(_secret);
|
||||
free(_gcm_iv);
|
||||
free(_gcm_mac);
|
||||
|
||||
// Marshal outputs.
|
||||
jbyteArray sealed_secret;
|
||||
if (NULL != _sealed_secret) {
|
||||
sealed_secret = env->NewByteArray(_sealed_secret_size);
|
||||
env->SetByteArrayRegion(
|
||||
sealed_secret, 0, _sealed_secret_size, (jbyte*)_sealed_secret
|
||||
);
|
||||
free(_sealed_secret);
|
||||
} else {
|
||||
sealed_secret = env->NewByteArray(0);
|
||||
}
|
||||
|
||||
// Return VerificationResult(sealedSecret, cmacValidationStatus, status).
|
||||
return env->NewObject(klass, cid, sealed_secret, cmac_status, status);
|
||||
}
|
||||
|
||||
NATIVE_WRAPPER(jlong, unsealSecret)
|
||||
(JNIEnv *env, jobject, jlong enclave_id, jbyteArray sealed_secret)
|
||||
{
|
||||
// Check if we've actually got a sealed secret to unseal.
|
||||
uint8_t *_sealed_secret = NULL;
|
||||
size_t sealed_secret_size = env->GetArrayLength(sealed_secret);
|
||||
if (0 == sealed_secret_size) {
|
||||
return SGX_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
// Allocate buffer.
|
||||
_sealed_secret = (uint8_t*)malloc(sealed_secret_size);
|
||||
|
||||
// Check if we ran out of memory.
|
||||
if (NULL == _sealed_secret) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Marshal inputs.
|
||||
env->GetByteArrayRegion(
|
||||
sealed_secret, 0, sealed_secret_size, (jbyte*)_sealed_secret
|
||||
);
|
||||
|
||||
// Try to unseal the secret.
|
||||
sgx_status_t result = unseal_secret(
|
||||
enclave_id, _sealed_secret, sealed_secret_size
|
||||
);
|
||||
|
||||
// Free temporary allocations.
|
||||
free(_sealed_secret);
|
||||
|
||||
return result;
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package net.corda.attestation.host
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import net.corda.attestation.Crypto
|
||||
import net.corda.attestation.CryptoProvider
|
||||
import net.corda.attestation.message.AttestationError
|
||||
import net.corda.attestation.message.AttestationRequest
|
||||
import net.corda.attestation.readValue
|
||||
import net.corda.attestation.toLittleEndian
|
||||
import org.apache.http.HttpHeaders.CONTENT_TYPE
|
||||
import org.apache.http.HttpStatus.*
|
||||
import org.apache.http.client.CookieStore
|
||||
import org.apache.http.client.config.CookieSpecs.STANDARD_STRICT
|
||||
import org.apache.http.client.config.RequestConfig
|
||||
import org.apache.http.client.methods.HttpPost
|
||||
import org.apache.http.client.protocol.HttpClientContext
|
||||
import org.apache.http.config.SocketConfig
|
||||
import org.apache.http.entity.ContentType.APPLICATION_JSON
|
||||
import org.apache.http.entity.StringEntity
|
||||
import org.apache.http.impl.client.BasicCookieStore
|
||||
import org.apache.http.impl.client.CloseableHttpClient
|
||||
import org.apache.http.impl.client.HttpClients
|
||||
import org.apache.http.impl.conn.BasicHttpClientConnectionManager
|
||||
import org.apache.http.message.BasicHeader
|
||||
import org.apache.http.util.EntityUtils
|
||||
import org.junit.*
|
||||
import org.junit.Assert.*
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.security.KeyPair
|
||||
import java.security.interfaces.ECPublicKey
|
||||
|
||||
class AttestOnlyIT {
|
||||
private companion object {
|
||||
private val httpPort = Integer.getInteger("test.httpPort")
|
||||
}
|
||||
private lateinit var httpClient: CloseableHttpClient
|
||||
private lateinit var cookies: CookieStore
|
||||
private lateinit var keyPair: KeyPair
|
||||
private lateinit var mapper: ObjectMapper
|
||||
private lateinit var crypto: Crypto
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val cryptoProvider = CryptoProvider()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
crypto = cryptoProvider.crypto
|
||||
mapper = ObjectMapper().registerModule(JavaTimeModule())
|
||||
cookies = BasicCookieStore()
|
||||
keyPair = crypto.generateKeyPair()
|
||||
|
||||
val httpRequestConfig: RequestConfig = RequestConfig.custom()
|
||||
.setCookieSpec(STANDARD_STRICT)
|
||||
.setConnectTimeout(20_000)
|
||||
.setSocketTimeout(5_000)
|
||||
.build()
|
||||
val httpSocketConfig: SocketConfig = SocketConfig.custom()
|
||||
.setSoReuseAddress(true)
|
||||
.setTcpNoDelay(true)
|
||||
.build()
|
||||
httpClient = HttpClients.custom()
|
||||
.setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig })
|
||||
.setDefaultRequestConfig(httpRequestConfig)
|
||||
.build()
|
||||
}
|
||||
|
||||
@After
|
||||
fun done() {
|
||||
httpClient.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `attest without payload`() {
|
||||
val context = HttpClientContext.create().apply {
|
||||
cookieStore = cookies
|
||||
}
|
||||
|
||||
val httpRequest = HttpPost("http://localhost:$httpPort/host/attest").apply {
|
||||
addHeader(BasicHeader(CONTENT_TYPE, APPLICATION_JSON.mimeType))
|
||||
}
|
||||
val responseBody = httpClient.execute(httpRequest, context).use { response ->
|
||||
val output = EntityUtils.toString(response.entity, UTF_8)
|
||||
assertEquals(output, SC_BAD_REQUEST, response.statusLine.statusCode)
|
||||
output
|
||||
}
|
||||
|
||||
val error: AttestationError = mapper.readValue(responseBody)
|
||||
assertEquals("Message is missing", error.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test attest requires challenge first`() {
|
||||
val context = HttpClientContext.create().apply {
|
||||
cookieStore = cookies
|
||||
}
|
||||
|
||||
val attestRequest = AttestationRequest(
|
||||
gb = (keyPair.public as ECPublicKey).toLittleEndian(),
|
||||
signatureGbGa = byteArrayOf(),
|
||||
aesCMAC = byteArrayOf()
|
||||
)
|
||||
val httpRequest = HttpPost("http://localhost:$httpPort/host/attest").apply {
|
||||
entity = StringEntity(mapper.writeValueAsString(attestRequest), APPLICATION_JSON)
|
||||
}
|
||||
val responseBody = httpClient.execute(httpRequest, context).use { response ->
|
||||
val output = EntityUtils.toString(response.entity, UTF_8)
|
||||
assertEquals(output, SC_UNAUTHORIZED, response.statusLine.statusCode)
|
||||
output
|
||||
}
|
||||
|
||||
val error: AttestationError = mapper.readValue(responseBody)
|
||||
assertEquals("No response from our challenge", error.message)
|
||||
}
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
package net.corda.attestation.host
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import net.corda.attestation.*
|
||||
import net.corda.attestation.message.*
|
||||
import org.apache.http.HttpStatus.*
|
||||
import org.apache.http.client.CookieStore
|
||||
import org.apache.http.client.config.CookieSpecs.STANDARD_STRICT
|
||||
import org.apache.http.client.config.RequestConfig
|
||||
import org.apache.http.client.methods.HttpPost
|
||||
import org.apache.http.client.protocol.HttpClientContext
|
||||
import org.apache.http.config.SocketConfig
|
||||
import org.apache.http.entity.ContentType.APPLICATION_JSON
|
||||
import org.apache.http.entity.StringEntity
|
||||
import org.apache.http.impl.client.BasicCookieStore
|
||||
import org.apache.http.impl.client.CloseableHttpClient
|
||||
import org.apache.http.impl.client.HttpClients
|
||||
import org.apache.http.impl.conn.BasicHttpClientConnectionManager
|
||||
import org.apache.http.util.EntityUtils
|
||||
import org.junit.*
|
||||
import org.junit.Assert.*
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.security.*
|
||||
import java.security.cert.*
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.PKIXRevocationChecker.Option.*
|
||||
import java.security.interfaces.ECPublicKey
|
||||
import java.security.spec.ECParameterSpec
|
||||
import java.util.*
|
||||
|
||||
class AttestationIT {
|
||||
private companion object {
|
||||
private val httpPort = Integer.getInteger("test.httpPort")
|
||||
private val revocationListOptions = EnumSet.of(SOFT_FAIL, PREFER_CRLS, NO_FALLBACK)
|
||||
private const val AES_CMAC_FUNC = 1.toShort()
|
||||
}
|
||||
private lateinit var certificateFactory: CertificateFactory
|
||||
private lateinit var httpClient: CloseableHttpClient
|
||||
private lateinit var cookies: CookieStore
|
||||
private lateinit var challengerKeyPair: KeyPair
|
||||
private lateinit var transientKeyPair: KeyPair
|
||||
private lateinit var ecParameters: ECParameterSpec
|
||||
private lateinit var pkixParameters: PKIXParameters
|
||||
private lateinit var challengeResponse: ChallengeResponse
|
||||
private lateinit var peerPublicKey: ECPublicKey
|
||||
private lateinit var smk: ByteArray
|
||||
private lateinit var mapper: ObjectMapper
|
||||
private lateinit var crypto: Crypto
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val cryptoProvider = CryptoProvider()
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val signatureProvider = SignatureProvider(cryptoProvider)
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val keyStoreProvider = KeyStoreProvider("dummyIAS-trust.pfx", "attestation")
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
cookies = BasicCookieStore()
|
||||
crypto = cryptoProvider.crypto
|
||||
certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
challengerKeyPair = crypto.generateKeyPair()
|
||||
transientKeyPair = crypto.generateKeyPair()
|
||||
ecParameters = (transientKeyPair.public as ECPublicKey).params
|
||||
mapper = ObjectMapper().registerModule(JavaTimeModule())
|
||||
|
||||
pkixParameters = PKIXParameters(keyStoreProvider.trustAnchorsFor("ias")).apply {
|
||||
val rlChecker = CertPathValidator.getInstance("PKIX").revocationChecker as PKIXRevocationChecker
|
||||
addCertPathChecker(rlChecker.apply { options = revocationListOptions })
|
||||
}
|
||||
|
||||
val httpRequestConfig: RequestConfig = RequestConfig.custom()
|
||||
.setCookieSpec(STANDARD_STRICT)
|
||||
.setConnectTimeout(20_000)
|
||||
.setSocketTimeout(5_000)
|
||||
.build()
|
||||
val httpSocketConfig: SocketConfig = SocketConfig.custom()
|
||||
.setSoReuseAddress(true)
|
||||
.setTcpNoDelay(true)
|
||||
.build()
|
||||
httpClient = HttpClients.custom()
|
||||
.setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig })
|
||||
.setDefaultRequestConfig(httpRequestConfig)
|
||||
.build()
|
||||
|
||||
val context = HttpClientContext.create().apply {
|
||||
cookieStore = cookies
|
||||
}
|
||||
|
||||
challengerKeyPair = crypto.generateKeyPair()
|
||||
val challengeRequest = ChallengeRequest((challengerKeyPair.public as ECPublicKey).toLittleEndian(), "nonce-value")
|
||||
val request = HttpPost("http://localhost:$httpPort/host/challenge").apply {
|
||||
entity = StringEntity(mapper.writeValueAsString(challengeRequest), APPLICATION_JSON)
|
||||
}
|
||||
val response = httpClient.execute(request, context).use { response ->
|
||||
val output = EntityUtils.toString(response.entity, UTF_8)
|
||||
assertEquals(output, SC_OK, response.statusLine.statusCode)
|
||||
output
|
||||
}
|
||||
challengeResponse = mapper.readValue(response)
|
||||
|
||||
val keyFactory = KeyFactory.getInstance("EC")
|
||||
peerPublicKey = keyFactory.generatePublic(challengeResponse.ga.toBigEndianKeySpec(ecParameters)) as ECPublicKey
|
||||
smk = crypto.generateSMK(transientKeyPair.private, peerPublicKey)
|
||||
}
|
||||
|
||||
@After
|
||||
fun done() {
|
||||
httpClient.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun postAttestation() {
|
||||
val context = HttpClientContext.create().apply {
|
||||
cookieStore = cookies
|
||||
}
|
||||
|
||||
val gb = (transientKeyPair.public as ECPublicKey).toLittleEndian()
|
||||
val signatureGbGa = signatureProvider.signatureOf(challengerKeyPair.private, transientKeyPair.public as ECPublicKey, peerPublicKey)
|
||||
val attestRequest = AttestationRequest(
|
||||
gb = gb,
|
||||
signatureGbGa = signatureGbGa,
|
||||
aesCMAC = crypto.aesCMAC(smk, { aes ->
|
||||
aes.update(gb)
|
||||
aes.update(challengeResponse.spid.hexToBytes())
|
||||
aes.update(challengeResponse.quoteType.toLittleEndian())
|
||||
aes.update(AES_CMAC_FUNC.toLittleEndian())
|
||||
aes.update(signatureGbGa)
|
||||
})
|
||||
)
|
||||
val httpRequest = HttpPost("http://localhost:$httpPort/host/attest").apply {
|
||||
entity = StringEntity(mapper.writeValueAsString(attestRequest), APPLICATION_JSON)
|
||||
}
|
||||
val responseBody = httpClient.execute(httpRequest, context).use { response ->
|
||||
val output = EntityUtils.toString(response.entity, UTF_8)
|
||||
assertEquals(output, SC_OK, response.statusLine.statusCode)
|
||||
output
|
||||
}
|
||||
validateSignature(mapper.readValue(responseBody))
|
||||
}
|
||||
|
||||
private fun validateSignature(reportResponse: ReportProxyResponse) {
|
||||
val certificatePath = parseCertificates(reportResponse.certificatePath)
|
||||
CertPathValidator.getInstance("PKIX").apply {
|
||||
validate(certificatePath, pkixParameters)
|
||||
}
|
||||
|
||||
Signature.getInstance("SHA256withRSA").apply {
|
||||
initVerify(certificatePath.certificates[0])
|
||||
update(reportResponse.report)
|
||||
if (!verify(reportResponse.signature.toByteArray().decodeBase64())) {
|
||||
throw IllegalArgumentException("Incorrect response signature")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseCertificates(iasCertificateHeader: String): CertPath {
|
||||
val certificates = mutableListOf<Certificate>()
|
||||
iasCertificateHeader.byteInputStream().use { input ->
|
||||
while (input.available() > 0) {
|
||||
certificates.add(certificateFactory.generateCertificate(input))
|
||||
}
|
||||
}
|
||||
return certificateFactory.generateCertPath(certificates)
|
||||
}
|
||||
|
||||
private fun ByteArray.decodeBase64(): ByteArray = Base64.getDecoder().decode(this)
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package net.corda.attestation.host
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.attestation.*
|
||||
import net.corda.attestation.message.AttestationError
|
||||
import net.corda.attestation.message.ChallengeRequest
|
||||
import net.corda.attestation.message.ChallengeResponse
|
||||
import org.apache.http.HttpStatus.*
|
||||
import org.apache.http.client.config.RequestConfig
|
||||
import org.apache.http.client.methods.HttpPost
|
||||
import org.apache.http.config.SocketConfig
|
||||
import org.apache.http.entity.ContentType.APPLICATION_JSON
|
||||
import org.apache.http.entity.StringEntity
|
||||
import org.apache.http.impl.client.CloseableHttpClient
|
||||
import org.apache.http.impl.client.HttpClients
|
||||
import org.apache.http.impl.conn.BasicHttpClientConnectionManager
|
||||
import org.apache.http.util.EntityUtils
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.security.KeyFactory
|
||||
import java.security.interfaces.ECPublicKey
|
||||
import java.security.spec.ECParameterSpec
|
||||
|
||||
class ChallengeIT {
|
||||
private companion object {
|
||||
private val httpPort = Integer.getInteger("test.httpPort")
|
||||
}
|
||||
private lateinit var httpClient: CloseableHttpClient
|
||||
private lateinit var ecParameters: ECParameterSpec
|
||||
private lateinit var keyFactory: KeyFactory
|
||||
private lateinit var mapper: ObjectMapper
|
||||
private lateinit var crypto: Crypto
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val cryptoProvider = CryptoProvider()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mapper = ObjectMapper()
|
||||
crypto = cryptoProvider.crypto
|
||||
keyFactory = KeyFactory.getInstance("EC")
|
||||
ecParameters = (crypto.generateKeyPair().public as ECPublicKey).params
|
||||
val httpRequestConfig: RequestConfig = RequestConfig.custom()
|
||||
.setConnectTimeout(20_000)
|
||||
.setSocketTimeout(5_000)
|
||||
.build()
|
||||
val httpSocketConfig: SocketConfig = SocketConfig.custom()
|
||||
.setSoReuseAddress(true)
|
||||
.setTcpNoDelay(true)
|
||||
.build()
|
||||
httpClient = HttpClients.custom()
|
||||
.setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig })
|
||||
.setDefaultRequestConfig(httpRequestConfig)
|
||||
.build()
|
||||
}
|
||||
|
||||
@After
|
||||
fun done() {
|
||||
httpClient.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun challenge() {
|
||||
val keyPair = crypto.generateKeyPair()
|
||||
val challengeRequest = ChallengeRequest((keyPair.public as ECPublicKey).toLittleEndian(), "nonce-value")
|
||||
val request = HttpPost("http://localhost:$httpPort/host/challenge").apply {
|
||||
entity = StringEntity(mapper.writeValueAsString(challengeRequest), APPLICATION_JSON)
|
||||
}
|
||||
val response = httpClient.execute(request).use { response ->
|
||||
val output = EntityUtils.toString(response.entity)
|
||||
assertEquals(output, SC_OK, response.statusLine.statusCode)
|
||||
output
|
||||
}
|
||||
|
||||
val challengeResponse: ChallengeResponse = mapper.readValue(response)
|
||||
keyFactory.generatePublic(challengeResponse.ga.toBigEndianKeySpec(ecParameters))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testHugeNonce() {
|
||||
val keyPair = crypto.generateKeyPair()
|
||||
val challengeRequest = ChallengeRequest(
|
||||
gc = (keyPair.public as ECPublicKey).toLittleEndian(),
|
||||
nonce = "1234567890123456789012345678901234"
|
||||
)
|
||||
val httpRequest = HttpPost("http://localhost:$httpPort/host/challenge").apply {
|
||||
entity = StringEntity(mapper.writeValueAsString(challengeRequest), APPLICATION_JSON)
|
||||
}
|
||||
val responseBody = httpClient.execute(httpRequest).use { response ->
|
||||
val output = EntityUtils.toString(response.entity, UTF_8)
|
||||
assertEquals(output, SC_BAD_REQUEST, response.statusLine.statusCode)
|
||||
output
|
||||
}
|
||||
|
||||
val error: AttestationError = mapper.readValue(responseBody)
|
||||
assertEquals("Nonce is too large: maximum 32 digits", error.message)
|
||||
}
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
package net.corda.attestation.host
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.attestation.*
|
||||
import net.corda.attestation.message.AttestationRequest
|
||||
import net.corda.attestation.message.ChallengeRequest
|
||||
import net.corda.attestation.message.ChallengeResponse
|
||||
import net.corda.attestation.message.SecretRequest
|
||||
import org.apache.http.HttpStatus.SC_OK
|
||||
import org.apache.http.client.CookieStore
|
||||
import org.apache.http.client.config.RequestConfig
|
||||
import org.apache.http.client.methods.HttpPost
|
||||
import org.apache.http.client.protocol.HttpClientContext
|
||||
import org.apache.http.config.SocketConfig
|
||||
import org.apache.http.entity.ContentType.APPLICATION_JSON
|
||||
import org.apache.http.entity.StringEntity
|
||||
import org.apache.http.impl.client.BasicCookieStore
|
||||
import org.apache.http.impl.client.CloseableHttpClient
|
||||
import org.apache.http.impl.client.HttpClients
|
||||
import org.apache.http.impl.conn.BasicHttpClientConnectionManager
|
||||
import org.apache.http.util.EntityUtils
|
||||
import org.junit.*
|
||||
import org.junit.Assert.*
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPair
|
||||
import java.security.interfaces.ECPublicKey
|
||||
import java.security.spec.ECParameterSpec
|
||||
|
||||
class SecretIT {
|
||||
private companion object {
|
||||
private val httpPort = Integer.getInteger("test.httpPort")
|
||||
private const val AES_CMAC_FUNC = 1.toShort()
|
||||
}
|
||||
private lateinit var httpClient: CloseableHttpClient
|
||||
private lateinit var ecParameters: ECParameterSpec
|
||||
private lateinit var keyFactory: KeyFactory
|
||||
private lateinit var mapper: ObjectMapper
|
||||
private lateinit var crypto: Crypto
|
||||
private lateinit var cookies: CookieStore
|
||||
private lateinit var transientKeyPair: KeyPair
|
||||
private lateinit var challengerKeyPair: KeyPair
|
||||
private lateinit var peerPublicKey: ECPublicKey
|
||||
private lateinit var smk: ByteArray
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val cryptoProvider = CryptoProvider()
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val signatureProvider = SignatureProvider(cryptoProvider)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mapper = ObjectMapper()
|
||||
cookies = BasicCookieStore()
|
||||
crypto = cryptoProvider.crypto
|
||||
keyFactory = KeyFactory.getInstance("EC")
|
||||
transientKeyPair = crypto.generateKeyPair()
|
||||
ecParameters = (transientKeyPair.public as ECPublicKey).params
|
||||
challengerKeyPair = crypto.generateKeyPair()
|
||||
|
||||
val httpRequestConfig: RequestConfig = RequestConfig.custom()
|
||||
.setConnectTimeout(20_000)
|
||||
.setSocketTimeout(5_000)
|
||||
.build()
|
||||
val httpSocketConfig: SocketConfig = SocketConfig.custom()
|
||||
.setSoReuseAddress(true)
|
||||
.setTcpNoDelay(true)
|
||||
.build()
|
||||
httpClient = HttpClients.custom()
|
||||
.setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig })
|
||||
.setDefaultRequestConfig(httpRequestConfig)
|
||||
.build()
|
||||
|
||||
val context = HttpClientContext.create().apply {
|
||||
cookieStore = cookies
|
||||
}
|
||||
|
||||
val challengeRequest = ChallengeRequest((challengerKeyPair.public as ECPublicKey).toLittleEndian(), "nonce-value")
|
||||
var httpRequest = HttpPost("http://localhost:$httpPort/host/challenge").apply {
|
||||
entity = StringEntity(mapper.writeValueAsString(challengeRequest), APPLICATION_JSON)
|
||||
}
|
||||
val response = httpClient.execute(httpRequest, context).use { response ->
|
||||
val output = EntityUtils.toString(response.entity, UTF_8)
|
||||
assertEquals(output, SC_OK, response.statusLine.statusCode)
|
||||
output
|
||||
}
|
||||
val challengeResponse = mapper.readValue<ChallengeResponse>(response)
|
||||
|
||||
val keyFactory = KeyFactory.getInstance("EC")
|
||||
peerPublicKey = keyFactory.generatePublic(challengeResponse.ga.toBigEndianKeySpec(ecParameters)) as ECPublicKey
|
||||
smk = crypto.generateSMK(transientKeyPair.private, peerPublicKey)
|
||||
|
||||
val gb = (transientKeyPair.public as ECPublicKey).toLittleEndian()
|
||||
val signatureGbGa = signatureProvider.signatureOf(challengerKeyPair.private, transientKeyPair.public as ECPublicKey, peerPublicKey)
|
||||
val attestRequest = AttestationRequest(
|
||||
gb = gb,
|
||||
signatureGbGa = signatureGbGa,
|
||||
aesCMAC = crypto.aesCMAC(smk, { aes ->
|
||||
aes.update(gb)
|
||||
aes.update(challengeResponse.spid.hexToBytes())
|
||||
aes.update(challengeResponse.quoteType.toLittleEndian())
|
||||
aes.update(AES_CMAC_FUNC.toLittleEndian())
|
||||
aes.update(signatureGbGa)
|
||||
})
|
||||
)
|
||||
httpRequest = HttpPost("http://localhost:$httpPort/host/attest").apply {
|
||||
entity = StringEntity(mapper.writeValueAsString(attestRequest), APPLICATION_JSON)
|
||||
}
|
||||
httpClient.execute(httpRequest, context).use { httpResponse ->
|
||||
val output = EntityUtils.toString(httpResponse.entity, UTF_8)
|
||||
assertEquals(output, SC_OK, httpResponse.statusLine.statusCode)
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun done() {
|
||||
httpClient.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun postSecret() {
|
||||
val secretKey = crypto.generateSecretKey(transientKeyPair.private, peerPublicKey)
|
||||
val secretIV = crypto.createIV()
|
||||
val secretData = crypto.encrypt("And now for something completely different!".toByteArray(), secretKey, secretIV)
|
||||
|
||||
val platformInfo: ByteArray? = null
|
||||
val mk = crypto.generateMK(transientKeyPair.private, peerPublicKey)
|
||||
val secretRequest = SecretRequest(
|
||||
platformInfo = platformInfo,
|
||||
aesCMAC = crypto.aesCMAC(mk, { aes ->
|
||||
aes.update(platformInfo)
|
||||
}),
|
||||
data = secretData.encryptedData(),
|
||||
authTag = secretData.authenticationTag(),
|
||||
iv = secretIV
|
||||
)
|
||||
val request = HttpPost("http://localhost:$httpPort/host/secret").apply {
|
||||
entity = StringEntity(mapper.writeValueAsString(secretRequest), APPLICATION_JSON)
|
||||
}
|
||||
val context = HttpClientContext.create().apply {
|
||||
cookieStore = cookies
|
||||
}
|
||||
httpClient.execute(request, context).use { response ->
|
||||
val output = EntityUtils.toString(response.entity)
|
||||
assertEquals(output, SC_OK, response.statusLine.statusCode)
|
||||
output
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package net.corda.attestation.host
|
||||
|
||||
import net.corda.attestation.CryptoProvider
|
||||
import net.corda.attestation.KEY_SIZE
|
||||
import net.corda.attestation.toLittleEndian
|
||||
import org.bouncycastle.asn1.ASN1InputStream
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.bouncycastle.asn1.DLSequence
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.PrivateKey
|
||||
import java.security.Signature
|
||||
import java.security.interfaces.ECPublicKey
|
||||
|
||||
class SignatureProvider(private val cryptoProvider: CryptoProvider) : TestRule {
|
||||
override fun apply(stmt: Statement, description: Description): Statement {
|
||||
return stmt
|
||||
}
|
||||
|
||||
fun signatureOf(signingKey: PrivateKey, localKey: ECPublicKey, remoteKey: ECPublicKey): ByteArray {
|
||||
val signature = Signature.getInstance("SHA256WithECDSA").let { signer ->
|
||||
signer.initSign(signingKey, cryptoProvider.crypto.random)
|
||||
signer.update(localKey.toLittleEndian())
|
||||
signer.update(remoteKey.toLittleEndian())
|
||||
signer.sign()
|
||||
}
|
||||
return ByteBuffer.allocate(KEY_SIZE).let { buf ->
|
||||
ASN1InputStream(signature).use { input ->
|
||||
for (number in input.readObject() as DLSequence) {
|
||||
val pos = (number as ASN1Integer).positiveValue.toLittleEndian(KEY_SIZE / 2)
|
||||
buf.put(pos)
|
||||
}
|
||||
buf.array()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package net.corda.attestation.host
|
||||
|
||||
open class HostException(message: String, cause: Throwable?) : Exception(message, cause) {
|
||||
constructor(message: String) : this(message, null)
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package net.corda.attestation.host.sgx
|
||||
|
||||
import net.corda.attestation.host.sgx.enclave.ECKey
|
||||
import net.corda.attestation.host.sgx.enclave.Enclave
|
||||
import net.corda.attestation.host.sgx.enclave.SgxStatus
|
||||
import net.corda.attestation.host.sgx.entities.AttestationResult
|
||||
import net.corda.attestation.host.sgx.entities.Quote
|
||||
import net.corda.attestation.host.sgx.sealing.SealedSecret
|
||||
import net.corda.attestation.host.sgx.system.GroupIdentifier
|
||||
|
||||
typealias AttestationContext = Int
|
||||
|
||||
/**
|
||||
* Enclave used in remote attestation.
|
||||
*/
|
||||
interface AttestationEnclave : Enclave {
|
||||
|
||||
/**
|
||||
* The platform services offer an architectural enclave which provides a
|
||||
* trusted time source and a monotonic counter, which in turn can be used
|
||||
* for replay protection during nonce generation and for securely
|
||||
* calculating the length of time for which a secret shall be valid.
|
||||
*/
|
||||
val usePlatformServices: Boolean
|
||||
|
||||
/**
|
||||
* Create a context for the remote attestation and key exchange process.
|
||||
*
|
||||
* @param challengerKey The elliptic curve public key of the challenger
|
||||
* (NIST P-256 elliptic curve).
|
||||
*
|
||||
* @throws SgxException If unable to create context.
|
||||
*/
|
||||
fun initializeKeyExchange(challengerKey: ECKey)
|
||||
|
||||
/**
|
||||
* Finalize the remote attestation and key exchange process.
|
||||
*/
|
||||
fun finalizeKeyExchange()
|
||||
|
||||
/**
|
||||
* Get the public key of the application enclave, based on NIST P-256
|
||||
* elliptic curve, and the identifier of the EPID group to which the
|
||||
* platform belongs.
|
||||
*/
|
||||
fun getPublicKeyAndGroupIdentifier(): Pair<ECKey, GroupIdentifier>
|
||||
|
||||
/**
|
||||
* Process the response from the challenger and generate a quote for the
|
||||
* final step of the attestation process.
|
||||
*
|
||||
* @param challengerDetails Details received from the challenger.
|
||||
*/
|
||||
fun processChallengerDetailsAndGenerateQuote(challengerDetails: ChallengerDetails): Quote
|
||||
|
||||
/**
|
||||
* Verify the attestation response received from the service provider.
|
||||
*
|
||||
* @param attestationResult The received attestation response.
|
||||
*
|
||||
* @return A pair of (1) the outcome of the validation of the CMAC over
|
||||
* the security manifest, and (2) the sealed secret, if successful.
|
||||
*
|
||||
* @throws SgxException If unable to verify the response and seal the
|
||||
* secret.
|
||||
*/
|
||||
fun verifyAttestationResponse(
|
||||
attestationResult: AttestationResult
|
||||
): Pair<SgxStatus, SealedSecret>
|
||||
|
||||
/**
|
||||
* Attempt to unseal a secret inside the enclave and report the outcome of
|
||||
* the operation.
|
||||
*
|
||||
* @param sealedSecret The sealed secret provisioned by the challenger.
|
||||
*
|
||||
* @return A status code indicative of the outcome of the operation.
|
||||
*/
|
||||
fun unseal(sealedSecret: SealedSecret): SgxStatus
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package net.corda.attestation.host.sgx
|
||||
|
||||
import net.corda.attestation.host.sgx.enclave.ECKey
|
||||
import net.corda.attestation.host.sgx.entities.QuoteType
|
||||
|
||||
/**
|
||||
* Information retrieved from message 2 in the Intel remote attestation flow.
|
||||
*/
|
||||
class ChallengerDetails(
|
||||
/**
|
||||
* The public elliptic curve key of the challenger, based on the NIST
|
||||
* P-256 elliptic curve.
|
||||
*/
|
||||
val publicKey: ECKey,
|
||||
|
||||
/**
|
||||
* The identifier of the service provider, also known as SPID.
|
||||
*/
|
||||
val serviceProviderIdentifier: ByteArray,
|
||||
|
||||
/**
|
||||
* Indicates the quote type, i.e., whether it is linkable or
|
||||
* un-linkable.
|
||||
*/
|
||||
val quoteType: QuoteType,
|
||||
|
||||
/**
|
||||
* The identifier of the key derivation function.
|
||||
*/
|
||||
val keyDerivationFunctionIdentifier: Short,
|
||||
|
||||
/**
|
||||
* An ECDSA signature of (g_b||g_a), using the challenger's ECDSA
|
||||
* private key corresponding to the public key specified in the
|
||||
* sgx_ra_init function, where g_b is the public elliptic curve key of
|
||||
* the challenger and g_a is the public key of application enclave,
|
||||
* provided by the application enclave, in the remote attestation and
|
||||
* key exchange message (message 1).
|
||||
*/
|
||||
val signature: ByteArray,
|
||||
|
||||
/**
|
||||
* The 128-bit AES-CMAC generated by the service provider. See
|
||||
* [sgx_ra_msg2_t](https://software.intel.com/en-us/node/709237)
|
||||
* for more details on its derivation.
|
||||
*/
|
||||
val messageAuthenticationCode: ByteArray,
|
||||
|
||||
/**
|
||||
* The Intel EPID signature revocation list certificate of the Intel
|
||||
* EPID group identified by the group identifier in the remote
|
||||
* attestation and key exchange message (message 1).
|
||||
*/
|
||||
val signatureRevocationList: ByteArray
|
||||
)
|
@ -0,0 +1,204 @@
|
||||
package net.corda.attestation.host.sgx.bridge
|
||||
|
||||
import net.corda.attestation.host.sgx.AttestationContext
|
||||
import net.corda.attestation.host.sgx.AttestationEnclave
|
||||
import net.corda.attestation.host.sgx.ChallengerDetails
|
||||
import net.corda.attestation.host.sgx.bridge.enclave.NativeEnclave
|
||||
import net.corda.attestation.host.sgx.bridge.wrapper.NativeWrapper
|
||||
import net.corda.attestation.host.sgx.enclave.ECKey
|
||||
import net.corda.attestation.host.sgx.enclave.SgxException
|
||||
import net.corda.attestation.host.sgx.enclave.SgxStatus
|
||||
import net.corda.attestation.host.sgx.entities.AttestationException
|
||||
import net.corda.attestation.host.sgx.entities.AttestationResult
|
||||
import net.corda.attestation.host.sgx.entities.Quote
|
||||
import net.corda.attestation.host.sgx.sealing.SealedSecret
|
||||
import net.corda.attestation.host.sgx.system.GroupIdentifier
|
||||
import net.corda.attestation.host.sgx.system.SgxSystem
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Path
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
/**
|
||||
* Enclave used in remote attestation.
|
||||
*/
|
||||
class NativeAttestationEnclave @JvmOverloads constructor(
|
||||
enclavePath: Path,
|
||||
override val usePlatformServices: Boolean = false
|
||||
) : NativeEnclave(enclavePath, usePlatformServices), AttestationEnclave {
|
||||
|
||||
private companion object {
|
||||
|
||||
@JvmStatic
|
||||
private val log: Logger = LoggerFactory.getLogger(NativeAttestationEnclave::class.java)
|
||||
|
||||
}
|
||||
|
||||
private val maxRetryCount: Int = 5
|
||||
|
||||
private val retryDelayInSeconds: Int = 5
|
||||
|
||||
private var context: AttestationContext? = null
|
||||
|
||||
/**
|
||||
* Create a context for the remote attestation and key exchange process.
|
||||
*
|
||||
* @param challengerKey The elliptic curve public key of the challenger
|
||||
* (NIST P-256 elliptic curve).
|
||||
*
|
||||
* @throws SgxException If unable to create context.
|
||||
*/
|
||||
override fun initializeKeyExchange(challengerKey: ECKey) {
|
||||
lock.withLock {
|
||||
val result = NativeWrapper.initializeRemoteAttestation(
|
||||
identifier,
|
||||
usePlatformServices,
|
||||
challengerKey.bytes
|
||||
)
|
||||
val status = SgxSystem.statusFromCode(result.result)
|
||||
context = result.context
|
||||
if (status != SgxStatus.SUCCESS) {
|
||||
throw SgxException(status, identifier, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up and finalize the remote attestation process.
|
||||
*/
|
||||
override fun finalizeKeyExchange() {
|
||||
lock.withLock {
|
||||
val oldContext = context
|
||||
if (oldContext != null) {
|
||||
val code = NativeWrapper.finalizeRemoteAttestation(
|
||||
identifier,
|
||||
oldContext
|
||||
)
|
||||
context = null
|
||||
val status = SgxSystem.statusFromCode(code)
|
||||
if (status != SgxStatus.SUCCESS) {
|
||||
throw SgxException(status, identifier, oldContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public key of the application enclave, based on NIST P-256
|
||||
* elliptic curve, and the identifier of the EPID group the platform
|
||||
* belongs to.
|
||||
*/
|
||||
override fun getPublicKeyAndGroupIdentifier(): Pair<ECKey, GroupIdentifier> {
|
||||
lock.withLock {
|
||||
val context = context ?: throw AttestationException("Not initialized.")
|
||||
val result = NativeWrapper.getPublicKeyAndGroupIdentifier(
|
||||
identifier,
|
||||
context,
|
||||
maxRetryCount,
|
||||
retryDelayInSeconds
|
||||
)
|
||||
val status = SgxSystem.statusFromCode(result.result)
|
||||
if (status != SgxStatus.SUCCESS) {
|
||||
throw SgxException(status, identifier, context)
|
||||
}
|
||||
|
||||
return Pair(ECKey.fromBytes(result.publicKey), result.groupIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the response from the challenger and generate a quote for the
|
||||
* final step of the attestation process.
|
||||
*
|
||||
* @param challengerDetails Details from the challenger.
|
||||
*/
|
||||
override fun processChallengerDetailsAndGenerateQuote(
|
||||
challengerDetails: ChallengerDetails
|
||||
): Quote {
|
||||
lock.withLock {
|
||||
val context = context ?: throw AttestationException("Not initialized.")
|
||||
val result = NativeWrapper.processServiceProviderDetailsAndGenerateQuote(
|
||||
identifier,
|
||||
context,
|
||||
challengerDetails.publicKey.bytes,
|
||||
challengerDetails.serviceProviderIdentifier,
|
||||
challengerDetails.quoteType.value,
|
||||
challengerDetails.keyDerivationFunctionIdentifier,
|
||||
challengerDetails.signature,
|
||||
challengerDetails.messageAuthenticationCode,
|
||||
challengerDetails.signatureRevocationList.size,
|
||||
challengerDetails.signatureRevocationList,
|
||||
maxRetryCount,
|
||||
retryDelayInSeconds
|
||||
)
|
||||
val status = SgxSystem.statusFromCode(result.result)
|
||||
if (status != SgxStatus.SUCCESS) {
|
||||
throw SgxException(status, identifier, context)
|
||||
}
|
||||
return Quote(
|
||||
result.messageAuthenticationCode,
|
||||
ECKey.fromBytes(result.publicKey),
|
||||
result.securityProperties,
|
||||
result.payload
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the attestation response received from the service provider.
|
||||
*
|
||||
* @param attestationResult The received attestation response.
|
||||
*
|
||||
* @return A pair of (1) the outcome of the validation of the CMAC over
|
||||
* the security manifest, and (2) the sealed secret, if successful.
|
||||
*
|
||||
* @throws SgxException If unable to verify the response or seal the
|
||||
* secret.
|
||||
*/
|
||||
override fun verifyAttestationResponse(
|
||||
attestationResult: AttestationResult
|
||||
): Pair<SgxStatus, SealedSecret> {
|
||||
lock.withLock {
|
||||
val context = context ?: throw AttestationException("Not initialized.")
|
||||
val result = NativeWrapper.verifyAttestationResponse(
|
||||
identifier,
|
||||
context,
|
||||
attestationResult.attestationResultMessage,
|
||||
attestationResult.aesCMAC,
|
||||
attestationResult.secret,
|
||||
attestationResult.secretIV,
|
||||
attestationResult.secretHash
|
||||
)
|
||||
val cmacValidationStatus = SgxSystem.statusFromCode(
|
||||
result.cmacValidationStatus
|
||||
)
|
||||
|
||||
if (cmacValidationStatus != SgxStatus.SUCCESS) {
|
||||
if (attestationResult.aesCMAC.isEmpty()) {
|
||||
log.warn("No CMAC available")
|
||||
} else {
|
||||
log.warn("Failed to validate AES-CMAC ({}).", cmacValidationStatus.name)
|
||||
}
|
||||
}
|
||||
|
||||
val status = SgxSystem.statusFromCode(result.result)
|
||||
if (status != SgxStatus.SUCCESS) {
|
||||
throw SgxException(status, identifier, context)
|
||||
}
|
||||
|
||||
return Pair(cmacValidationStatus, result.secret)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to unseal a secret inside the enclave and report the outcome of
|
||||
* the operation.
|
||||
*/
|
||||
override fun unseal(sealedSecret: SealedSecret): SgxStatus {
|
||||
lock.withLock {
|
||||
val result = NativeWrapper.unsealSecret(identifier, sealedSecret)
|
||||
return SgxSystem.statusFromCode(result)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package net.corda.attestation.host.sgx.bridge.enclave
|
||||
|
||||
import net.corda.attestation.host.sgx.bridge.system.NativeSgxSystem
|
||||
import net.corda.attestation.host.sgx.bridge.wrapper.LaunchToken
|
||||
import net.corda.attestation.host.sgx.bridge.wrapper.NativeWrapper
|
||||
import net.corda.attestation.host.sgx.bridge.wrapper.newLaunchToken
|
||||
import net.corda.attestation.host.sgx.enclave.Enclave
|
||||
import net.corda.attestation.host.sgx.enclave.EnclaveIdentifier
|
||||
import net.corda.attestation.host.sgx.enclave.SgxStatus
|
||||
import net.corda.attestation.host.sgx.system.SgxSystem
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.locks.Lock
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
/**
|
||||
* Representation of an enclave on an SGX-enabled system.
|
||||
*
|
||||
* @param enclavePath The path to the signed enclave binary.
|
||||
* @param usePlatformServices Whether or not to leverage Intel's platform
|
||||
* services (for replay protection in nonce generation, etc.).
|
||||
*/
|
||||
open class NativeEnclave @JvmOverloads constructor(
|
||||
private val enclavePath: Path,
|
||||
private val usePlatformServices: Boolean = false
|
||||
) : Enclave {
|
||||
|
||||
/**
|
||||
* Lock for ensuring single entry of enclave calls.
|
||||
*/
|
||||
protected val lock: Lock = ReentrantLock()
|
||||
|
||||
private var enclaveId: EnclaveIdentifier = 0
|
||||
|
||||
private var launchToken: LaunchToken = newLaunchToken()
|
||||
|
||||
/**
|
||||
* The SGX-enabled system on which this enclave is running.
|
||||
*/
|
||||
override val system: SgxSystem
|
||||
get() = NativeSgxSystem()
|
||||
|
||||
/**
|
||||
* The enclave identifier.
|
||||
*/
|
||||
override val identifier: EnclaveIdentifier
|
||||
get() = enclaveId
|
||||
|
||||
/**
|
||||
* Create enclave used for remote attestation, and consequently for secret
|
||||
* sealing and unsealing.
|
||||
*/
|
||||
override fun create(): SgxStatus {
|
||||
lock.withLock {
|
||||
val result = NativeWrapper.createEnclave(
|
||||
enclavePath.toString(), // The path to the signed enclave binary
|
||||
usePlatformServices, // Whether to use Intel's services
|
||||
launchToken // New or pre-existing launch token.
|
||||
)
|
||||
val status = SgxSystem.statusFromCode(result.result)
|
||||
if (status == SgxStatus.ERROR_ENCLAVE_LOST) {
|
||||
// If the enclave was lost, we need to destroy it. Not doing so
|
||||
// will result in EPC memory leakage that could prevent
|
||||
// subsequent enclaves from loading.
|
||||
destroy()
|
||||
}
|
||||
enclaveId = result.identifier
|
||||
launchToken = result.token
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy enclave if running.
|
||||
*/
|
||||
override fun destroy(): Boolean {
|
||||
lock.withLock {
|
||||
if (enclaveId != 0L) {
|
||||
// Only attempt to destroy enclave if one has been created
|
||||
val result = NativeWrapper.destroyEnclave(enclaveId)
|
||||
enclaveId = 0L
|
||||
launchToken = newLaunchToken()
|
||||
return result
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the enclave has been run before or not.
|
||||
*/
|
||||
override fun isFresh(): Boolean {
|
||||
lock.withLock {
|
||||
val nullByte = 0.toByte()
|
||||
return identifier == 0L &&
|
||||
(0 until launchToken.size).all { launchToken[it] == nullByte }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package net.corda.attestation.host.sgx.bridge.system
|
||||
|
||||
import net.corda.attestation.host.sgx.bridge.wrapper.NativeWrapper
|
||||
import net.corda.attestation.host.sgx.enclave.SgxException
|
||||
import net.corda.attestation.host.sgx.enclave.SgxStatus
|
||||
import net.corda.attestation.host.sgx.entities.AttestationException
|
||||
import net.corda.attestation.host.sgx.system.ExtendedGroupIdentifier
|
||||
import net.corda.attestation.host.sgx.system.SgxDeviceStatus
|
||||
import net.corda.attestation.host.sgx.system.SgxSystem
|
||||
|
||||
/**
|
||||
* Query system properties of an SGX-enabled environment.
|
||||
*/
|
||||
class NativeSgxSystem : SgxSystem {
|
||||
|
||||
/**
|
||||
* Check if the client platform is enabled for Intel SGX. The application
|
||||
* must be run with administrator privileges to get the status
|
||||
* successfully.
|
||||
*
|
||||
* @return The current status of the SGX device.
|
||||
*/
|
||||
override fun getDeviceStatus(): SgxDeviceStatus {
|
||||
return SgxSystem.deviceStatusFromCode(NativeWrapper.getDeviceStatus())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the extended Intel EPID Group the client uses by default. The key
|
||||
* used to sign a quote will be a member of the this group.
|
||||
*/
|
||||
override fun getExtendedGroupIdentifier(): ExtendedGroupIdentifier {
|
||||
val result = NativeWrapper.getExtendedGroupIdentifier()
|
||||
val status = SgxSystem.statusFromCode(result.result)
|
||||
if (status != SgxStatus.SUCCESS) {
|
||||
throw SgxException(status)
|
||||
}
|
||||
return SgxSystem.extendedGroupIdentifier(result.extendedGroupIdentifier)
|
||||
?: throw AttestationException("Invalid extended EPID group")
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package net.corda.attestation.host.sgx.bridge.wrapper
|
||||
|
||||
import net.corda.attestation.host.sgx.enclave.EnclaveIdentifier
|
||||
|
||||
/**
|
||||
* The result of a call to [NativeWrapper.createEnclave].
|
||||
*/
|
||||
class EnclaveResult(
|
||||
/**
|
||||
* The identifier of the created enclave, if any.
|
||||
*/
|
||||
val identifier: EnclaveIdentifier,
|
||||
|
||||
/**
|
||||
* The launch token of the enclave.
|
||||
*/
|
||||
val token: LaunchToken,
|
||||
|
||||
/**
|
||||
* The output status code of the enclave creation (of type [SgxStatus]
|
||||
* downstream).
|
||||
*/
|
||||
val result: Long
|
||||
)
|
@ -0,0 +1,17 @@
|
||||
package net.corda.attestation.host.sgx.bridge.wrapper
|
||||
|
||||
/**
|
||||
* The result of a call to [NativeWrapper.getExtendedGroupIdentifier].
|
||||
*/
|
||||
data class ExtendedGroupIdentifierResult(
|
||||
/**
|
||||
* The extended Intel EPID group identifier (of type
|
||||
* [ExtendedGroupIdentifier] downstream).
|
||||
*/
|
||||
val extendedGroupIdentifier: Int,
|
||||
|
||||
/**
|
||||
* The result of the operation (of type [SgxStatus] downstream).
|
||||
*/
|
||||
val result: Long
|
||||
)
|
@ -0,0 +1,18 @@
|
||||
package net.corda.attestation.host.sgx.bridge.wrapper
|
||||
|
||||
import net.corda.attestation.host.sgx.AttestationContext
|
||||
|
||||
/**
|
||||
* The result of a call to [NativeWrapper.initializeRemoteAttestation].
|
||||
*/
|
||||
data class InitializationResult(
|
||||
/**
|
||||
* The context returned from the call to sgx_ra_init().
|
||||
*/
|
||||
val context: AttestationContext,
|
||||
|
||||
/**
|
||||
* The result of the operation (of type [SgxStatus] downstream).
|
||||
*/
|
||||
val result: Long
|
||||
)
|
@ -0,0 +1,13 @@
|
||||
package net.corda.attestation.host.sgx.bridge.wrapper
|
||||
|
||||
/**
|
||||
* An opaque type used to hold enclave launch information. Used by
|
||||
* sgx_create_enclave to initialize an enclave. The license is generated by the
|
||||
* launch enclave.
|
||||
*/
|
||||
typealias LaunchToken = ByteArray
|
||||
|
||||
/**
|
||||
* Create a new launch token.
|
||||
*/
|
||||
fun newLaunchToken(): LaunchToken = ByteArray(1024)
|
@ -0,0 +1,99 @@
|
||||
package net.corda.attestation.host.sgx.bridge.wrapper
|
||||
|
||||
import net.corda.attestation.host.sgx.AttestationContext
|
||||
import net.corda.attestation.host.sgx.enclave.EnclaveIdentifier
|
||||
import net.corda.attestation.host.sgx.sealing.ProvisionedSecret
|
||||
import net.corda.attestation.host.sgx.sealing.SealedSecret
|
||||
|
||||
@Suppress("KDocMissingDocumentation")
|
||||
internal object NativeWrapper {
|
||||
|
||||
init {
|
||||
System.loadLibrary("corda_sgx_ra")
|
||||
}
|
||||
|
||||
// enclave-management.cpp
|
||||
|
||||
@Throws(Exception::class)
|
||||
external fun getDeviceStatus(): Int
|
||||
|
||||
@Throws(Exception::class)
|
||||
external fun getExtendedGroupIdentifier(): ExtendedGroupIdentifierResult
|
||||
|
||||
@Throws(Exception::class)
|
||||
external fun createEnclave(
|
||||
path: String,
|
||||
usePlatformServices: Boolean,
|
||||
token: LaunchToken
|
||||
): EnclaveResult
|
||||
|
||||
@Throws(Exception::class)
|
||||
external fun destroyEnclave(
|
||||
id: Long
|
||||
): Boolean
|
||||
|
||||
// remote-attestation.cpp
|
||||
|
||||
@Throws(Exception::class)
|
||||
external fun initializeRemoteAttestation(
|
||||
enclaveIdentifier: EnclaveIdentifier,
|
||||
usePlatformServices: Boolean,
|
||||
challengerKey: ByteArray
|
||||
): InitializationResult
|
||||
|
||||
@Throws(Exception::class)
|
||||
external fun finalizeRemoteAttestation(
|
||||
enclaveIdentifier: EnclaveIdentifier,
|
||||
context: AttestationContext
|
||||
): Long
|
||||
|
||||
@Throws(Exception::class)
|
||||
external fun getPublicKeyAndGroupIdentifier(
|
||||
enclaveIdentifier: EnclaveIdentifier,
|
||||
context: AttestationContext,
|
||||
maxRetryCount: Int,
|
||||
retryWaitInSeconds: Int
|
||||
): PublicKeyAndGroupIdentifier
|
||||
|
||||
@Throws(Exception::class)
|
||||
external fun processServiceProviderDetailsAndGenerateQuote(
|
||||
enclaveIdentifier: EnclaveIdentifier,
|
||||
context: AttestationContext,
|
||||
publicKey: ByteArray,
|
||||
serviceProviderIdentifier: ByteArray,
|
||||
quoteType: Short,
|
||||
keyDerivationFunctionIdentifier: Short,
|
||||
signature: ByteArray,
|
||||
messageAuthenticationCode: ByteArray,
|
||||
signatureRevocationSize: Int,
|
||||
signatureRevocationList:ByteArray,
|
||||
maxRetryCount: Int,
|
||||
retryWaitInSeconds: Int
|
||||
): QuoteResult
|
||||
|
||||
@Throws(Exception::class)
|
||||
external fun verifyAttestationResponse(
|
||||
enclaveIdentifier: EnclaveIdentifier,
|
||||
context: AttestationContext,
|
||||
attestationResultMessage: ByteArray?,
|
||||
aesCmac: ByteArray,
|
||||
secret: ByteArray,
|
||||
gcmIV: ByteArray,
|
||||
gcmMac: ByteArray
|
||||
): VerificationResult
|
||||
|
||||
// sealing.cpp
|
||||
|
||||
@Throws(Exception::class)
|
||||
external fun sealSecret(
|
||||
enclaveIdentifier: EnclaveIdentifier,
|
||||
provisionedSecret: ProvisionedSecret
|
||||
): SealingOperationResult
|
||||
|
||||
@Throws(Exception::class)
|
||||
external fun unsealSecret(
|
||||
enclaveIdentifier: EnclaveIdentifier,
|
||||
sealedSecret: SealedSecret
|
||||
): Long
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package net.corda.attestation.host.sgx.bridge.wrapper
|
||||
|
||||
import net.corda.attestation.host.sgx.system.GroupIdentifier
|
||||
|
||||
/**
|
||||
* The result of a call to [NativeWrapper.getPublicKeyAndGroupIdentifier].
|
||||
*/
|
||||
class PublicKeyAndGroupIdentifier(
|
||||
/**
|
||||
* The public elliptic curve key of the application enclave (of type
|
||||
* [ECKey] downstream).
|
||||
*/
|
||||
val publicKey: ByteArray,
|
||||
|
||||
/**
|
||||
* The identifier of the EPID group to which the enclave belongs.
|
||||
*/
|
||||
val groupIdentifier: GroupIdentifier,
|
||||
|
||||
/**
|
||||
* The result of the operation (of type [SgxStatus] downstream).
|
||||
*/
|
||||
val result: Long
|
||||
)
|
@ -0,0 +1,40 @@
|
||||
package net.corda.attestation.host.sgx.bridge.wrapper
|
||||
|
||||
/**
|
||||
* The result of a call to
|
||||
* [NativeWrapper.processServiceProviderDetailsAndGenerateQuote].
|
||||
*/
|
||||
class QuoteResult(
|
||||
/**
|
||||
* The 128-bit AES-CMAC generated by the application enclave. See
|
||||
* [sgx_ra_msg3_t](https://software.intel.com/en-us/node/709238) for
|
||||
* more details on its derivation.
|
||||
*/
|
||||
val messageAuthenticationCode: ByteArray,
|
||||
|
||||
/**
|
||||
* The public elliptic curve key of the application enclave (of type
|
||||
* [ECKey] downstream).
|
||||
*/
|
||||
val publicKey: ByteArray,
|
||||
|
||||
/**
|
||||
* Security property of the Intel SGX Platform Service. If the Intel
|
||||
* SGX Platform Service security property information is not required
|
||||
* in the remote attestation and key exchange process, this field will
|
||||
* be all zeros. The buffer is 256 bytes long.
|
||||
*/
|
||||
val securityProperties: ByteArray,
|
||||
|
||||
/**
|
||||
* Quote returned from sgx_get_quote. More details about how the quote
|
||||
* is derived can be found in Intel's documentation:
|
||||
* [sgx_ra_msg3_t](https://software.intel.com/en-us/node/709238)
|
||||
*/
|
||||
val payload: ByteArray,
|
||||
|
||||
/**
|
||||
* The result of the operation (of type [SgxStatus] downstream).
|
||||
*/
|
||||
val result: Long
|
||||
)
|
@ -0,0 +1,19 @@
|
||||
package net.corda.attestation.host.sgx.bridge.wrapper
|
||||
|
||||
import net.corda.attestation.host.sgx.sealing.SealedSecret
|
||||
|
||||
/**
|
||||
* The result of a call to [NativeWrapper.sealSecret].
|
||||
*/
|
||||
class SealingOperationResult(
|
||||
/**
|
||||
* The sealed secret, if any.
|
||||
*/
|
||||
val sealedSecret: SealedSecret,
|
||||
|
||||
/**
|
||||
* The output result of the operation (of type [SealingResult]
|
||||
* downstream).
|
||||
*/
|
||||
val result: Long
|
||||
)
|
@ -0,0 +1,25 @@
|
||||
package net.corda.attestation.host.sgx.bridge.wrapper
|
||||
|
||||
import net.corda.attestation.host.sgx.sealing.SealedSecret
|
||||
|
||||
/**
|
||||
* The result of a call to [NativeWrapper.verifyAttestationResponse].
|
||||
*/
|
||||
class VerificationResult(
|
||||
/**
|
||||
* The sealed secret returned if the attestation result was
|
||||
* successfully verified.
|
||||
*/
|
||||
val secret: SealedSecret,
|
||||
|
||||
/**
|
||||
* The outcome of the validation of the CMAC over the attestation
|
||||
* result message (of type [SgxStatus] downstream).
|
||||
*/
|
||||
val cmacValidationStatus: Long,
|
||||
|
||||
/**
|
||||
* The result of the operation (of type [SgxStatus] downstream).
|
||||
*/
|
||||
val result: Long
|
||||
)
|
@ -0,0 +1,43 @@
|
||||
package net.corda.attestation.host.sgx.enclave
|
||||
|
||||
/**
|
||||
* Public key based on NIST P-256 elliptic curve
|
||||
*
|
||||
* @param componentX The 256-bit X component of the key.
|
||||
* @param componentY The 256-bit Y component of the key.
|
||||
*/
|
||||
class ECKey(
|
||||
componentX: ByteArray,
|
||||
componentY: ByteArray
|
||||
) {
|
||||
|
||||
/**
|
||||
* The bytes constituting the elliptic curve public key.
|
||||
*/
|
||||
val bytes: ByteArray = componentX.plus(componentY)
|
||||
|
||||
companion object {
|
||||
private const val KEY_SIZE = 64
|
||||
|
||||
/**
|
||||
* Create a public key from a byte array.
|
||||
*
|
||||
* @param bytes The 64 bytes forming the NIST P-256 elliptic curve.
|
||||
*/
|
||||
fun fromBytes(bytes: ByteArray): ECKey {
|
||||
if (bytes.size != KEY_SIZE) {
|
||||
throw Exception("Expected $KEY_SIZE bytes, but got ${bytes.size}")
|
||||
}
|
||||
val componentX = bytes.copyOfRange(0, KEY_SIZE / 2)
|
||||
val componentY = bytes.copyOfRange(KEY_SIZE / 2, KEY_SIZE)
|
||||
return ECKey(componentX, componentY)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Short-hand generator for supplying a constant X or Y component.
|
||||
*/
|
||||
fun ecKeyComponent(vararg bytes: Int) = bytes.map { it.toByte() }.toByteArray()
|
@ -0,0 +1,87 @@
|
||||
package net.corda.attestation.host.sgx.enclave
|
||||
|
||||
import net.corda.attestation.host.sgx.system.SgxSystem
|
||||
|
||||
/**
|
||||
* The identifier of an enclave.
|
||||
*/
|
||||
typealias EnclaveIdentifier = Long
|
||||
|
||||
/**
|
||||
* Representation an enclave.
|
||||
*/
|
||||
interface Enclave {
|
||||
/**
|
||||
* The SGX-enabled system on which this enclave is running.
|
||||
*/
|
||||
val system: SgxSystem
|
||||
|
||||
/**
|
||||
* The enclave identifier.
|
||||
*/
|
||||
val identifier: EnclaveIdentifier
|
||||
|
||||
/**
|
||||
* Create enclave used for remote attestation, and consequently for secret
|
||||
* sealing and unsealing.
|
||||
*/
|
||||
fun create(): SgxStatus
|
||||
|
||||
/**
|
||||
* Destroy enclave if running.
|
||||
*/
|
||||
fun destroy(): Boolean
|
||||
|
||||
/**
|
||||
* Destroy and re-create the enclave. This is normally done if the enclave
|
||||
* is lost due to a power transition or similar events.
|
||||
*/
|
||||
fun recreate(): SgxStatus {
|
||||
destroy()
|
||||
return create()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the enclave has been run before or not.
|
||||
*/
|
||||
fun isFresh(): Boolean
|
||||
|
||||
/**
|
||||
* Check whether an enclave has already been created and initialized.
|
||||
* Otherwise, try to create required enclave or re-create one in the cases
|
||||
* where an older one has been lost due to a power transition or similar.
|
||||
*
|
||||
* @throws SgxException If unable to create enclave.
|
||||
* @throws SgxUnavailableException If SGX is unavailable or for some reason
|
||||
* disabled.
|
||||
*/
|
||||
fun activate() {
|
||||
// First, make sure SGX is available and that it is enabled. Under some
|
||||
// circumstances, a reboot may be required to enable SGX. In either
|
||||
// case, as long as the extensions aren't enabled, an
|
||||
// [SgxUnavailableException] will be thrown.
|
||||
system.ensureAvailable()
|
||||
|
||||
// If the enclave has already been created and is active, we are good
|
||||
// to proceed.
|
||||
var status = create()
|
||||
if (status == SgxStatus.SUCCESS) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if an attestation enclave was previously created. If it was
|
||||
// and it is no longer available, recreate one to the same
|
||||
// specification. Note: Losing an enclave is normally the result of a
|
||||
// power transition.
|
||||
if (status == SgxStatus.ERROR_ENCLAVE_LOST) {
|
||||
status = recreate()
|
||||
if (status != SgxStatus.SUCCESS) {
|
||||
throw SgxException(status, identifier)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Some other error occurred, let's abort
|
||||
throw SgxException(status, identifier)
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package net.corda.attestation.host.sgx.enclave
|
||||
|
||||
import net.corda.attestation.host.HostException
|
||||
import net.corda.attestation.host.sgx.AttestationContext
|
||||
|
||||
/**
|
||||
* Exception raised whenever there's a problem creating, destroying or
|
||||
* interacting with an enclave or SGX.
|
||||
*
|
||||
* @property status The status or outcome of an SGX operation.
|
||||
* @property enclaveIdentifier The identifier of the enclave, if available.
|
||||
* @property context The established remote attestation context, if available.
|
||||
*/
|
||||
class SgxException @JvmOverloads constructor(
|
||||
val status: SgxStatus,
|
||||
private val enclaveIdentifier: EnclaveIdentifier? = null,
|
||||
private val context: AttestationContext? = null
|
||||
) : HostException(status.message) {
|
||||
/**
|
||||
* Human readable representation of the exception.
|
||||
*/
|
||||
override fun toString(): String {
|
||||
val message = super.toString()
|
||||
val identifierString = if (enclaveIdentifier != null) {
|
||||
"0x${java.lang.Long.toHexString(enclaveIdentifier)}"
|
||||
} else {
|
||||
"null"
|
||||
}
|
||||
val contextString = if (context != null) {
|
||||
"0x${java.lang.Integer.toHexString(context)}"
|
||||
} else {
|
||||
"null"
|
||||
}
|
||||
return "$message (enclave=$identifierString, " +
|
||||
"context=$contextString, status=${status.name})"
|
||||
}
|
||||
}
|
@ -0,0 +1,289 @@
|
||||
package net.corda.attestation.host.sgx.enclave
|
||||
|
||||
/**
|
||||
* The status of an SGX operation
|
||||
*
|
||||
* @property code The native status code returned from the SGX API.
|
||||
* @property message A human readable representation of the state.
|
||||
*/
|
||||
enum class SgxStatus(val code: Long, val message: String) {
|
||||
/**
|
||||
* Success.
|
||||
*/
|
||||
SUCCESS(0x0000, "Success"),
|
||||
|
||||
/**
|
||||
* Unexpected error.
|
||||
*/
|
||||
ERROR_UNEXPECTED(0x0001, "Unexpected error"),
|
||||
|
||||
/**
|
||||
* The parameter is incorrect.
|
||||
*/
|
||||
ERROR_INVALID_PARAMETER(0x0002, "The parameter is incorrect"),
|
||||
|
||||
/**
|
||||
* Not enough memory is available to complete this operation.
|
||||
*/
|
||||
ERROR_OUT_OF_MEMORY(0x0003, "Not enough memory is available to complete this operation"),
|
||||
|
||||
/**
|
||||
* Enclave lost after power transition or used in child process created by linux:fork().
|
||||
*/
|
||||
ERROR_ENCLAVE_LOST(0x0004, "Enclave lost after power transition or used in child process created by linux:fork()"),
|
||||
|
||||
/**
|
||||
* SGX API is invoked in incorrect order or state.
|
||||
*/
|
||||
ERROR_INVALID_STATE(0x0005, "SGX API is invoked in incorrect order or state"),
|
||||
|
||||
/**
|
||||
* The ECALL/OCALL index is invalid.
|
||||
*/
|
||||
ERROR_INVALID_FUNCTION(0x1001, "The ecall/ocall index is invalid"),
|
||||
|
||||
/**
|
||||
* The enclave is out of TCS.
|
||||
*/
|
||||
ERROR_OUT_OF_TCS(0x1003, "The enclave is out of TCS"),
|
||||
|
||||
/**
|
||||
* The enclave is crashed.
|
||||
*/
|
||||
ERROR_ENCLAVE_CRASHED(0x1006, "The enclave is crashed"),
|
||||
|
||||
/**
|
||||
* The ECALL is not allowed at this time, e.g. ECALL is blocked by the dynamic entry table, or nested ECALL is not allowed during initialization.
|
||||
*/
|
||||
ERROR_ECALL_NOT_ALLOWED(0x1007, "The ECALL is not allowed at this time, e.g. ECALL is blocked by the dynamic entry table, or nested ECALL is not allowed during initialization"),
|
||||
|
||||
/**
|
||||
* The OCALL is not allowed at this time, e.g. OCALL is not allowed during exception handling.
|
||||
*/
|
||||
ERROR_OCALL_NOT_ALLOWED(0x1008, "The OCALL is not allowed at this time, e.g. OCALL is not allowed during exception handling"),
|
||||
|
||||
/**
|
||||
* The enclave is running out of stack.
|
||||
*/
|
||||
ERROR_STACK_OVERRUN(0x1009, "The enclave is running out of stack"),
|
||||
|
||||
/**
|
||||
* The enclave image has one or more undefined symbols.
|
||||
*/
|
||||
ERROR_UNDEFINED_SYMBOL(0x2000, "The enclave image has one or more undefined symbols"),
|
||||
|
||||
/**
|
||||
* The enclave image is not correct.
|
||||
*/
|
||||
ERROR_INVALID_ENCLAVE(0x2001, "The enclave image is not correct."),
|
||||
|
||||
/**
|
||||
* The enclave identifier is invalid.
|
||||
*/
|
||||
ERROR_INVALID_ENCLAVE_ID(0x2002, "The enclave identifier is invalid."),
|
||||
|
||||
/**
|
||||
* The signature is invalid.
|
||||
*/
|
||||
ERROR_INVALID_SIGNATURE(0x2003, "The signature is invalid"),
|
||||
|
||||
/**
|
||||
* The enclave is signed as product enclave, and can not be created as debuggable enclave.
|
||||
*/
|
||||
ERROR_NDEBUG_ENCLAVE(0x2004, "The enclave is signed as product enclave, and can not be created as debuggable enclave"),
|
||||
|
||||
/**
|
||||
* Not enough EPC is available to load the enclave.
|
||||
*/
|
||||
ERROR_OUT_OF_EPC(0x2005, "Not enough EPC is available to load the enclave"),
|
||||
|
||||
/**
|
||||
* Cannot open SGX device.
|
||||
*/
|
||||
ERROR_NO_DEVICE(0x2006, "Cannot open SGX device"),
|
||||
|
||||
/**
|
||||
* Page mapping failed in driver.
|
||||
*/
|
||||
ERROR_MEMORY_MAP_CONFLICT(0x2007, "Page mapping failed in driver"),
|
||||
|
||||
/**
|
||||
* The metadata is incorrect.
|
||||
*/
|
||||
ERROR_INVALID_METADATA(0x2009, "The metadata is incorrect"),
|
||||
|
||||
/**
|
||||
* Device is busy, mostly EINIT failed.
|
||||
*/
|
||||
ERROR_DEVICE_BUSY(0x200c, "Device is busy, mostly EINIT failed"),
|
||||
|
||||
/**
|
||||
* Metadata version is inconsistent between uRTS and sgx_sign, or uRTS is incompatible with current platform.
|
||||
*/
|
||||
ERROR_INVALID_VERSION(0x200d, "Metadata version is inconsistent between uRTS and sgx_sign, or uRTS is incompatible with current platform"),
|
||||
|
||||
/**
|
||||
* The target enclave 32/64 bit mode or SIM/HW mode is incompatible with the mode of current uRTS.
|
||||
*/
|
||||
ERROR_MODE_INCOMPATIBLE(0x200e, "The target enclave 32/64 bit mode or SIM/HW mode is incompatible with the mode of current uRTS"),
|
||||
|
||||
/**
|
||||
* Cannot open enclave file.
|
||||
*/
|
||||
ERROR_ENCLAVE_FILE_ACCESS(0x200f, "Cannot open enclave file"),
|
||||
|
||||
/**
|
||||
* The MiscSelct/MiscMask settings are not correct.
|
||||
*/
|
||||
ERROR_INVALID_MISC(0x2010, "The MiscSelct/MiscMask settings are not correct"),
|
||||
|
||||
/**
|
||||
* Indicates verification error for reports, sealed data, etc.
|
||||
*/
|
||||
ERROR_MAC_MISMATCH(0x3001, "Indicates verification error for reports, sealed data, etc"),
|
||||
|
||||
/**
|
||||
* The enclave is not authorized.
|
||||
*/
|
||||
ERROR_INVALID_ATTRIBUTE(0x3002, "The enclave is not authorized"),
|
||||
|
||||
/**
|
||||
* The CPU SVN is beyond platform's CPU SVN value.
|
||||
*/
|
||||
ERROR_INVALID_CPUSVN(0x3003, "The CPU SVN is beyond platform's CPU SVN value"),
|
||||
|
||||
/**
|
||||
* The ISV SVN is greater than the enclave's ISV SVN.
|
||||
*/
|
||||
ERROR_INVALID_ISVSVN(0x3004, "The ISV SVN is greater than the enclave's ISV SVN"),
|
||||
|
||||
/**
|
||||
* The key name is an unsupported value.
|
||||
*/
|
||||
ERROR_INVALID_KEYNAME(0x3005, "The key name is an unsupported value"),
|
||||
|
||||
/**
|
||||
* Indicates AESM didn't respond or the requested service is not supported.
|
||||
*/
|
||||
ERROR_SERVICE_UNAVAILABLE(0x4001, "Indicates AESM didn't respond or the requested service is not supported"),
|
||||
|
||||
/**
|
||||
* The request to AESM timed out.
|
||||
*/
|
||||
ERROR_SERVICE_TIMEOUT(0x4002, "The request to AESM timed out"),
|
||||
|
||||
/**
|
||||
* Indicates EPID blob verification error.
|
||||
*/
|
||||
ERROR_AE_INVALID_EPIDBLOB(0x4003, "Indicates EPID blob verification error"),
|
||||
|
||||
/**
|
||||
* Enclave has no privilege to get launch token.
|
||||
*/
|
||||
ERROR_SERVICE_INVALID_PRIVILEGE(0x4004, "Enclave has no privilege to get launch token"),
|
||||
|
||||
/**
|
||||
* The EPID group membership is revoked.
|
||||
*/
|
||||
ERROR_EPID_MEMBER_REVOKED(0x4005, "The EPID group membership is revoked"),
|
||||
|
||||
/**
|
||||
* SGX needs to be updated.
|
||||
*/
|
||||
ERROR_UPDATE_NEEDED(0x4006, "SGX needs to be updated"),
|
||||
|
||||
/**
|
||||
* Network connection or proxy settings issue is encountered.
|
||||
*/
|
||||
ERROR_NETWORK_FAILURE(0x4007, "Network connection or proxy settings issue is encountered"),
|
||||
|
||||
/**
|
||||
* Session is invalid or ended by server.
|
||||
*/
|
||||
ERROR_AE_SESSION_INVALID(0x4008, "Session is invalid or ended by server"),
|
||||
|
||||
/**
|
||||
* Requested service is temporarily not available.
|
||||
*/
|
||||
ERROR_BUSY(0x400a, "Requested service is temporarily not available"),
|
||||
|
||||
/**
|
||||
* Monotonic Counter doesn't exist or has been invalidated.
|
||||
*/
|
||||
ERROR_MC_NOT_FOUND(0x400c, "Monotonic Counter doesn't exist or has been invalidated"),
|
||||
|
||||
/**
|
||||
* Caller doesn't have access to specified VMC.
|
||||
*/
|
||||
ERROR_MC_NO_ACCESS_RIGHT(0x400d, "Caller doesn't have access to specified VMC"),
|
||||
|
||||
/**
|
||||
* Monotonic counters are used up.
|
||||
*/
|
||||
ERROR_MC_USED_UP(0x400e, "Monotonic counters are used up"),
|
||||
|
||||
/**
|
||||
* Monotonic counters exceeds quota limitation.
|
||||
*/
|
||||
ERROR_MC_OVER_QUOTA(0x400f, "Monotonic counters exceeds quota limitation"),
|
||||
|
||||
/**
|
||||
* Key derivation function doesn't match during key exchange.
|
||||
*/
|
||||
ERROR_KDF_MISMATCH(0x4011, "Key derivation function doesn't match during key exchange"),
|
||||
|
||||
/**
|
||||
* EPID provisioning failed due to platform not being recognized by backend server.
|
||||
*/
|
||||
ERROR_UNRECOGNIZED_PLATFORM(0x4012, "EPID provisioning failed due to platform not being recognized by backend server"),
|
||||
|
||||
/**
|
||||
* Not privileged to perform this operation.
|
||||
*/
|
||||
ERROR_NO_PRIVILEGE(0x5002, "Not privileged to perform this operation"),
|
||||
|
||||
/**
|
||||
* The file is in a bad state.
|
||||
*/
|
||||
ERROR_FILE_BAD_STATUS(0x7001, "The file is in a bad state, run sgx_clearerr to try and fix it"),
|
||||
|
||||
/**
|
||||
* The KeyID field is all zeros, cannot re-generate the encryption key.
|
||||
*/
|
||||
ERROR_FILE_NO_KEY_ID(0x7002, "The KeyID field is all zeros, cannot re-generate the encryption key"),
|
||||
|
||||
/**
|
||||
* The current file name is different then the original file name (not allowed due to potential substitution attack).
|
||||
*/
|
||||
ERROR_FILE_NAME_MISMATCH(0x7003, "The current file name is different then the original file name (not allowed due to potential substitution attack)"),
|
||||
|
||||
/**
|
||||
* The file is not an SGX file.
|
||||
*/
|
||||
ERROR_FILE_NOT_SGX_FILE(0x7004, "The file is not an SGX file"),
|
||||
|
||||
/**
|
||||
* A recovery file cannot be opened, so flush operation cannot continue (only used when no EXXX is returned).
|
||||
*/
|
||||
ERROR_FILE_CANT_OPEN_RECOVERY_FILE(0x7005, "A recovery file cannot be opened, so flush operation cannot continue (only used when no EXXX is returned)"),
|
||||
|
||||
/**
|
||||
* A recovery file cannot be written, so flush operation cannot continue (only used when no EXXX is returned).
|
||||
*/
|
||||
ERROR_FILE_CANT_WRITE_RECOVERY_FILE(0x7006, "A recovery file cannot be written, so flush operation cannot continue (only used when no EXXX is returned)"),
|
||||
|
||||
/**
|
||||
* When opening the file, recovery is needed, but the recovery process failed.
|
||||
*/
|
||||
ERROR_FILE_RECOVERY_NEEDED(0x7007, "When opening the file, recovery is needed, but the recovery process failed"),
|
||||
|
||||
/**
|
||||
* fflush operation (to disk) failed (only used when no EXXX is returned).
|
||||
*/
|
||||
ERROR_FILE_FLUSH_FAILED(0x7008, "fflush operation (to disk) failed (only used when no EXXX is returned)"),
|
||||
|
||||
/**
|
||||
* fclose operation (to disk) failed (only used when no EXXX is returned).
|
||||
*/
|
||||
ERROR_FILE_CLOSE_FAILED(0x7009, "fclose operation (to disk) failed (only used when no EXXX is returned)"),
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package net.corda.attestation.host.sgx.entities
|
||||
|
||||
import net.corda.attestation.host.HostException
|
||||
|
||||
/**
|
||||
* Exception thrown during remote attestation.
|
||||
*/
|
||||
class AttestationException(message: String) : HostException(message)
|
@ -0,0 +1,33 @@
|
||||
package net.corda.attestation.host.sgx.entities
|
||||
|
||||
import net.corda.attestation.host.sgx.sealing.ProvisionedSecret
|
||||
|
||||
/**
|
||||
* The outcome of a remote attestation process.
|
||||
*/
|
||||
class AttestationResult(
|
||||
/**
|
||||
* The received attestation result message.
|
||||
*/
|
||||
val attestationResultMessage: ByteArray?,
|
||||
|
||||
/**
|
||||
* The CMAC over the attestation result message.
|
||||
*/
|
||||
val aesCMAC: ByteArray,
|
||||
|
||||
/**
|
||||
* Provisioned, encrypted secret if the attestation was successful.
|
||||
*/
|
||||
val secret: ProvisionedSecret,
|
||||
|
||||
/**
|
||||
* The initialization vector used as part of the decryption.
|
||||
*/
|
||||
val secretIV: ByteArray,
|
||||
|
||||
/**
|
||||
* The GCM MAC returned as part of the attestation response.
|
||||
*/
|
||||
val secretHash: ByteArray
|
||||
)
|
@ -0,0 +1,20 @@
|
||||
package net.corda.attestation.host.sgx.entities
|
||||
|
||||
/**
|
||||
* The status of the remote attestation process.
|
||||
*
|
||||
* @property message A human readable representation of the state.
|
||||
*/
|
||||
enum class AttestationStatus(val message: String) {
|
||||
|
||||
/**
|
||||
* The remote attestation was successful.
|
||||
*/
|
||||
SUCCESS("Remote attestation succeeded."),
|
||||
|
||||
/**
|
||||
* The remote attestation failed.
|
||||
*/
|
||||
FAIL("Remote attestation failed."),
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package net.corda.attestation.host.sgx.entities
|
||||
|
||||
import net.corda.attestation.host.sgx.enclave.ECKey
|
||||
|
||||
/**
|
||||
* Signed report from the application enclave. The corresponds to message 3 in
|
||||
* the Intel remote attestation flow.
|
||||
*/
|
||||
class Quote(
|
||||
/**
|
||||
* The 128-bit AES-CMAC generated by the application enclave. See
|
||||
* [sgx_ra_msg3_t](https://software.intel.com/en-us/node/709238) for
|
||||
* more details on its derivation.
|
||||
*/
|
||||
val messageAuthenticationCode: ByteArray,
|
||||
|
||||
/**
|
||||
* The public elliptic curve key of the application enclave.
|
||||
*/
|
||||
val publicKey: ECKey,
|
||||
|
||||
/**
|
||||
* Security property of the Intel SGX Platform Service. If the Intel
|
||||
* SGX Platform Service security property information is not required
|
||||
* in the remote attestation and key exchange process, this field will
|
||||
* be all zeros. The buffer is 256 bytes long.
|
||||
*/
|
||||
val securityProperties: ByteArray,
|
||||
|
||||
/**
|
||||
* Quote returned from sgx_get_quote. More details about how the quote
|
||||
* is derived can be found in Intel's documentation:
|
||||
* [sgx_ra_msg3_t](https://software.intel.com/en-us/node/709238)
|
||||
*/
|
||||
val payload: ByteArray
|
||||
)
|
@ -0,0 +1,71 @@
|
||||
package net.corda.attestation.host.sgx.entities
|
||||
|
||||
/**
|
||||
* The status of an enclave's quote as it has been processed by the Intel
|
||||
* Attestation service.
|
||||
*
|
||||
* @property description A human-readable description of the status code.
|
||||
*/
|
||||
enum class QuoteStatus(val description: String) {
|
||||
|
||||
/**
|
||||
* EPID signature of the ISV enclave quote was verified correctly and the
|
||||
* TCB level of the SGX platform is up-to- date.
|
||||
*/
|
||||
OK("EPID signature of the ISV enclave quote was verified correctly and " +
|
||||
"the TCB level of the SGX platform is up-to-date."),
|
||||
|
||||
/**
|
||||
* EPID signature of the ISV enclave quote was invalid. The content of the
|
||||
* quote is not trustworthy.
|
||||
*/
|
||||
SIGNATURE_INVALID("EPID signature of the ISV enclave quote was invalid."),
|
||||
|
||||
/**
|
||||
* The EPID group has been revoked. When this value is returned, the
|
||||
* revocation reason field of the Attestation Verification Report will
|
||||
* contain a revocation reason code for this EPID group as reported in the
|
||||
* EPID Group CRL. The content of the quote is not trustworthy.
|
||||
*/
|
||||
GROUP_REVOKED("The EPID group has been revoked."),
|
||||
|
||||
/**
|
||||
* The EPID private key used to sign the quote has been revoked by
|
||||
* signature. The content of the quote is not trustworthy.
|
||||
*/
|
||||
SIGNATURE_REVOKED("The EPID private key used to sign the quote has been " +
|
||||
"revoked by signature."),
|
||||
|
||||
/**
|
||||
* The EPID private key used to sign the quote has been directly revoked
|
||||
* (not by signature). The content of the quote is not trustworthy.
|
||||
*/
|
||||
KEY_REVOKED("The EPID private key used to sign the quote has been " +
|
||||
"directly revoked (not by signature)."),
|
||||
|
||||
/**
|
||||
* SigRL version in ISV enclave quote does not match the most recent
|
||||
* version of the SigRL. In rare situations, after SP retrieved the SigRL
|
||||
* from IAS and provided it to the platform, a newer version of the SigRL
|
||||
* is made available. As a result, the Attestation Verification Report will
|
||||
* indicate SIGRL_VERSION_MISMATCH. SP can retrieve the most recent version
|
||||
* of SigRL from the IAS and request the platform to perform remote
|
||||
* attestation again with the most recent version of SigRL. If the platform
|
||||
* keeps failing to provide a valid quote matching with the most recent
|
||||
* version of the SigRL, the content of the quote is not trustworthy.
|
||||
*/
|
||||
SIGRL_VERSION_MISMATCH("SigRL version in ISV enclave quote does not " +
|
||||
"match the most recent version of the SigRL."),
|
||||
|
||||
/**
|
||||
* The EPID signature of the ISV enclave quote has been verified correctly,
|
||||
* but the TCB level of SGX platform is outdated. The platform has not been
|
||||
* identified as compromised and thus it is not revoked. It is up to the
|
||||
* Service Provider to decide whether or not to trust the content of the
|
||||
* quote.
|
||||
*/
|
||||
GROUP_OUT_OF_DATE("The EPID signature of the ISV enclave quote has " +
|
||||
"been verified correctly, but the TCB level of SGX platform " +
|
||||
"is outdated.")
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package net.corda.attestation.host.sgx.entities
|
||||
|
||||
/**
|
||||
* The type of quote used in the attestation.
|
||||
*
|
||||
* @property value The native value of the quote type.
|
||||
*/
|
||||
enum class QuoteType(val value: Short) {
|
||||
/**
|
||||
* Unlinkable is random value-based, meaning that having two quotes you
|
||||
* cannot identify whether they are from the same source or not.
|
||||
*/
|
||||
UNLINKABLE(0),
|
||||
|
||||
/**
|
||||
* Linkable is name-based, meaning that having two quotes you can identify
|
||||
* if they come from the same enclave or not. Note that you can not
|
||||
* determine which enclave it is though.
|
||||
*/
|
||||
LINKABLE(1);
|
||||
|
||||
companion object {
|
||||
fun forValue(value: Short): QuoteType = when(value) {
|
||||
0.toShort() -> UNLINKABLE
|
||||
1.toShort() -> LINKABLE
|
||||
else -> throw IllegalArgumentException("Unknown QuoteType '$value'")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package net.corda.attestation.host.sgx.sealing
|
||||
|
||||
/**
|
||||
* Exception raised whenever there is a problem with a sealing operation.
|
||||
*
|
||||
* @property status The status or outcome of the operation.
|
||||
*/
|
||||
class SealingException(
|
||||
val status: SealingResult
|
||||
) : Exception(status.message)
|
@ -0,0 +1,21 @@
|
||||
package net.corda.attestation.host.sgx.sealing
|
||||
|
||||
/**
|
||||
* The outcome of a performed sealing operation.
|
||||
*
|
||||
* @property code The underlying status code.
|
||||
* @property message A human readable representation of the state.
|
||||
*/
|
||||
enum class SealingResult(val code: Long, val message: String) {
|
||||
|
||||
/**
|
||||
* Sealing was successful.
|
||||
*/
|
||||
SUCCESS(0, "Sealing was successful."),
|
||||
|
||||
/**
|
||||
* Sealing was unsuccessful.
|
||||
*/
|
||||
FAIL(1, "Failed to seal secret."),
|
||||
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package net.corda.attestation.host.sgx.sealing
|
||||
|
||||
import net.corda.attestation.host.sgx.AttestationEnclave
|
||||
import net.corda.attestation.host.sgx.enclave.SgxStatus
|
||||
|
||||
/**
|
||||
* Representation of a provisioned secret.
|
||||
*/
|
||||
typealias ProvisionedSecret = ByteArray
|
||||
|
||||
/**
|
||||
* Representation of a sealed secret.
|
||||
*/
|
||||
typealias SealedSecret = ByteArray
|
||||
|
||||
/**
|
||||
* Manager for storing and managing secrets.
|
||||
*
|
||||
* @property enclave The facilitating attestation enclave.
|
||||
*/
|
||||
open class SecretManager(
|
||||
private val enclave: AttestationEnclave
|
||||
) {
|
||||
|
||||
/**
|
||||
* Check that an existing secret (if available) is valid and hasn't
|
||||
* expired.
|
||||
*/
|
||||
fun isValid(): Boolean {
|
||||
// First off, check whether we actually have access to a secret
|
||||
if (!hasSecret()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Then, ensure that we can unseal the secret, that the lease has not
|
||||
// expired, etc.
|
||||
val result = unsealSecret()
|
||||
return result == SgxStatus.SUCCESS
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve sealed secret, or null if not available.
|
||||
*/
|
||||
open fun getSealedSecret(): SealedSecret? = null
|
||||
|
||||
/**
|
||||
* Persist the sealed secret to disk or similar, for future use.
|
||||
*
|
||||
* @param sealedSecret The secret sealed to the enclave's context.
|
||||
*/
|
||||
open fun persistSealedSecret(sealedSecret: SealedSecret) { }
|
||||
|
||||
/**
|
||||
* Check whether we have a secret persisted already.
|
||||
*/
|
||||
private fun hasSecret(): Boolean {
|
||||
return getSealedSecret() != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can unseal an existing secret.
|
||||
*/
|
||||
private fun unsealSecret(): SgxStatus {
|
||||
val sealedSecret = getSealedSecret()
|
||||
?: return SgxStatus.ERROR_INVALID_PARAMETER
|
||||
return enclave.unseal(sealedSecret)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package net.corda.attestation.host.sgx.system
|
||||
|
||||
enum class ExtendedGroupIdentifier(val value: Int) {
|
||||
|
||||
/**
|
||||
* Indicates that we are using Intel's Attestation Service.
|
||||
*/
|
||||
INTEL(0),
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package net.corda.attestation.host.sgx.system
|
||||
|
||||
/**
|
||||
* The Intel EPID group identifier.
|
||||
*/
|
||||
typealias GroupIdentifier = Int
|
||||
|
||||
/**
|
||||
* Get the string representation of the group identifier.
|
||||
*/
|
||||
fun GroupIdentifier.value(): String = Integer
|
||||
.toHexString(this)
|
||||
.padStart(8, '0')
|
@ -0,0 +1,60 @@
|
||||
package net.corda.attestation.host.sgx.system
|
||||
|
||||
/**
|
||||
* The status of the SGX device on the current machine.
|
||||
*
|
||||
* @property code The native status code returned from the SGX API.
|
||||
* @property message A human readable representation of the state.
|
||||
*/
|
||||
enum class SgxDeviceStatus(val code: Int, val message: String) {
|
||||
/**
|
||||
* The platform is enabled for Intel SGX.
|
||||
*/
|
||||
ENABLED(0, "SGX device is available and enabled"),
|
||||
|
||||
/**
|
||||
* This platform is disabled for Intel SGX. It is configured to be enabled
|
||||
* after the next reboot.
|
||||
*/
|
||||
DISABLED_REBOOT_REQUIRED(1, "Rebooted required"),
|
||||
|
||||
/**
|
||||
* The operating system does not support UEFI enabling of the Intel SGX
|
||||
* device. If UEFI is supported by the operating system in general, but
|
||||
* support for enabling the Intel SGX device does not exist, this function
|
||||
* returns the more general [DISABLED].
|
||||
*/
|
||||
DISABLED_LEGACY_OS(2, "Operating system with EFI support required"),
|
||||
|
||||
/**
|
||||
* This platform is disabled for Intel SGX. More details about the ability
|
||||
* to enable Intel SGX are unavailable. There may be cases when Intel SGX
|
||||
* can be enabled manually in the BIOS.
|
||||
*/
|
||||
DISABLED(3, "SGX device is not available"),
|
||||
|
||||
/**
|
||||
* The platform is disabled for Intel SGX but can be enabled using the
|
||||
* Software Control Interface.
|
||||
*/
|
||||
DISABLED_SCI_AVAILABLE(4, "Needs enabling using the SCI"),
|
||||
|
||||
/**
|
||||
* The platform is disabled for Intel SGX but can be enabled manually
|
||||
* through the BIOS menu. The Software Control Interface is not available
|
||||
* to enable Intel SGX on this platform.
|
||||
*/
|
||||
DISABLED_MANUAL_ENABLE(5, "Needs enabling through the BIOS menu"),
|
||||
|
||||
/**
|
||||
* The detected version of Windows 10 is incompatible with Hyper-V. Intel
|
||||
* SGX cannot be enabled on the target machine unless Hyper-V is disabled.
|
||||
*/
|
||||
DISABLED_HYPERV_ENABLED(6, "Hyper-V must be disabled"),
|
||||
|
||||
|
||||
/**
|
||||
* Intel SGX is not supported by this processor.
|
||||
*/
|
||||
DISABLED_UNSUPPORTED_CPU(7, "SGX not supported by processor"),
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package net.corda.attestation.host.sgx.system
|
||||
|
||||
import net.corda.attestation.host.sgx.enclave.SgxStatus
|
||||
import net.corda.attestation.host.sgx.entities.AttestationException
|
||||
import net.corda.attestation.host.sgx.entities.QuoteStatus
|
||||
|
||||
/**
|
||||
* Query system properties of an SGX-enabled environment.
|
||||
*/
|
||||
interface SgxSystem {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Get [SgxDeviceStatus] from numeric code.
|
||||
*/
|
||||
fun deviceStatusFromCode(code: Int): SgxDeviceStatus =
|
||||
enumValues<SgxDeviceStatus>().first { it.code == code }
|
||||
|
||||
/**
|
||||
* Get [SgxStatus] from numeric code.
|
||||
*/
|
||||
fun statusFromCode(code: Long): SgxStatus =
|
||||
enumValues<SgxStatus>().first { it.code == code }
|
||||
|
||||
/**
|
||||
* Get [ExtendedGroupIdentifier] from a numeric identifier.
|
||||
*/
|
||||
fun extendedGroupIdentifier(id: Int): ExtendedGroupIdentifier? =
|
||||
enumValues<ExtendedGroupIdentifier>().
|
||||
firstOrNull { it.value == id }
|
||||
|
||||
/**
|
||||
* Get [QuoteStatus] from string.
|
||||
*/
|
||||
fun quoteStatusFromString(
|
||||
code: String
|
||||
): QuoteStatus {
|
||||
return enumValues<QuoteStatus>()
|
||||
.firstOrNull { it.name == code }
|
||||
?: throw AttestationException(
|
||||
"Invalid quote status code '$code'")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the client platform is enabled for Intel SGX. The application
|
||||
* must be run with administrator privileges to get the status
|
||||
* successfully.
|
||||
*
|
||||
* @return The current status of the SGX device.
|
||||
*/
|
||||
fun getDeviceStatus(): SgxDeviceStatus
|
||||
|
||||
/**
|
||||
* Get the extended Intel EPID Group the client uses by default. The key
|
||||
* used to sign a quote will be a member of the this group.
|
||||
*/
|
||||
fun getExtendedGroupIdentifier(): ExtendedGroupIdentifier
|
||||
|
||||
/**
|
||||
* Check if SGX is available and enabled in the current runtime
|
||||
* environment.
|
||||
*
|
||||
* @throws SgxUnavailableException If SGX is unavailable or for some reason
|
||||
* disabled.
|
||||
*/
|
||||
fun ensureAvailable() {
|
||||
val status = getDeviceStatus()
|
||||
if (status != SgxDeviceStatus.ENABLED) {
|
||||
throw SgxUnavailableException(status)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package net.corda.attestation.host.sgx.system
|
||||
|
||||
|
||||
/**
|
||||
* Exception raised if SGX for some reason is unavailable on the system.
|
||||
*
|
||||
* @property status The status of the SGX device.
|
||||
*/
|
||||
class SgxUnavailableException(
|
||||
val status: SgxDeviceStatus
|
||||
) : Exception(status.message)
|
@ -0,0 +1,352 @@
|
||||
package net.corda.attestation.host.web
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import net.corda.attestation.*
|
||||
import net.corda.attestation.host.sgx.AttestationEnclave
|
||||
import net.corda.attestation.host.sgx.ChallengerDetails
|
||||
import net.corda.attestation.host.sgx.bridge.NativeAttestationEnclave
|
||||
import net.corda.attestation.host.sgx.enclave.ECKey
|
||||
import net.corda.attestation.host.sgx.enclave.SgxException
|
||||
import net.corda.attestation.host.sgx.enclave.SgxStatus
|
||||
import net.corda.attestation.host.sgx.entities.AttestationResult
|
||||
import net.corda.attestation.host.sgx.entities.QuoteType
|
||||
import net.corda.attestation.host.sgx.system.value
|
||||
import net.corda.attestation.message.*
|
||||
import org.apache.http.HttpResponse
|
||||
import org.apache.http.client.CookieStore
|
||||
import org.apache.http.client.config.RequestConfig
|
||||
import org.apache.http.client.methods.HttpGet
|
||||
import org.apache.http.client.methods.HttpPost
|
||||
import org.apache.http.client.protocol.HttpClientContext
|
||||
import org.apache.http.config.SocketConfig
|
||||
import org.apache.http.entity.ContentType.APPLICATION_JSON
|
||||
import org.apache.http.entity.StringEntity
|
||||
import org.apache.http.impl.client.BasicCookieStore
|
||||
import org.apache.http.impl.client.CloseableHttpClient
|
||||
import org.apache.http.impl.client.HttpClients
|
||||
import org.apache.http.impl.conn.BasicHttpClientConnectionManager
|
||||
import org.apache.http.util.EntityUtils
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import java.nio.file.Paths
|
||||
import java.security.*
|
||||
import java.security.interfaces.ECPublicKey
|
||||
import java.security.spec.ECParameterSpec
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutorService
|
||||
import javax.servlet.ServletContext
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse.*
|
||||
import javax.servlet.http.HttpSession
|
||||
import javax.ws.rs.*
|
||||
import javax.ws.rs.container.AsyncResponse
|
||||
import javax.ws.rs.container.Suspended
|
||||
import javax.ws.rs.core.Context
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
import javax.ws.rs.core.UriBuilder
|
||||
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/host")
|
||||
class AttestationHost {
|
||||
private companion object {
|
||||
@JvmStatic
|
||||
private val log: Logger = LoggerFactory.getLogger(AttestationHost::class.java)
|
||||
|
||||
private const val AES_CMAC_FUNC = 1.toShort()
|
||||
private const val maxNonceLength = 32
|
||||
private const val enclaveAttr = "Enclave"
|
||||
private const val challengerKeyAttr = "Challenger-Key"
|
||||
private const val challengerNonceAttr = "Challenger-Nonce"
|
||||
private const val challengeResponseAttr = "Challenge-Response"
|
||||
private const val platformGIDAttr = "Platform-GroupID"
|
||||
private const val conversationAttr = "Conversation-Cookies"
|
||||
private val isvHost: URI = URI.create("http://${System.getProperty("isv.host", "localhost:8080")}")
|
||||
private val enclavePath = Paths.get(System.getProperty("corda.sgx.enclave.path", "."))
|
||||
.resolve("corda_sgx_ra_enclave.so")
|
||||
|
||||
private val mapper = ObjectMapper().registerModule(JavaTimeModule())
|
||||
private val keyFactory: KeyFactory = KeyFactory.getInstance("EC")
|
||||
private val ecParameters: ECParameterSpec
|
||||
private val crypto = Crypto()
|
||||
|
||||
private val httpRequestConfig: RequestConfig = RequestConfig.custom()
|
||||
.setConnectTimeout(20_000)
|
||||
.setSocketTimeout(5_000)
|
||||
.build()
|
||||
private val httpSocketConfig: SocketConfig = SocketConfig.custom()
|
||||
.setSoReuseAddress(true)
|
||||
.setTcpNoDelay(true)
|
||||
.build()
|
||||
|
||||
init {
|
||||
ecParameters = (crypto.generateKeyPair().public as ECPublicKey).params
|
||||
log.info("Elliptic Curve Parameters: {}", ecParameters)
|
||||
}
|
||||
}
|
||||
|
||||
@field:Context
|
||||
private lateinit var httpRequest: HttpServletRequest
|
||||
|
||||
@field:Context
|
||||
private lateinit var servletContext: ServletContext
|
||||
|
||||
private val executor: ExecutorService by lazy {
|
||||
servletContext.getAttribute(ThreadPoolListener.threadPoolAttr) as ExecutorService
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/challenge")
|
||||
fun provision(challenge: ChallengeRequest?, @Suspended async: AsyncResponse) {
|
||||
if (challenge == null) {
|
||||
throw BadRequestException(responseOf("Message is missing", SC_BAD_REQUEST))
|
||||
}
|
||||
val session = httpRequest.session
|
||||
log.info("Challenge - HTTP Session: {}", session.id)
|
||||
|
||||
validateNonce(challenge.nonce)
|
||||
|
||||
val challengerPublicKey = try {
|
||||
keyFactory.generatePublic(challenge.gc.toBigEndianKeySpec(ecParameters)) as ECPublicKey
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw BadRequestException(responseOf(e.message ?: "", SC_BAD_REQUEST))
|
||||
}
|
||||
session.setAttribute(challengerKeyAttr, challengerPublicKey)
|
||||
session.setAttribute(challengerNonceAttr, challenge.nonce)
|
||||
|
||||
val enclave = NativeAttestationEnclave(enclavePath).apply {
|
||||
activate()
|
||||
initializeKeyExchange(ECKey.fromBytes(challenge.gc))
|
||||
}
|
||||
session.setAttribute(enclaveAttr, enclave)
|
||||
|
||||
// Remember the HTTP session ID so that we can maintain a conversation
|
||||
// across multiple requests between three HTTP servers.
|
||||
val cookies = BasicCookieStore()
|
||||
session.setAttribute(conversationAttr, cookies)
|
||||
|
||||
executor.submit {
|
||||
val context = HttpClientContext.create().apply {
|
||||
cookieStore = cookies
|
||||
}
|
||||
|
||||
// Request basic service information from ISV. This data doesn't change.
|
||||
val serviceResponse: ServiceResponse = try {
|
||||
createHttpClient().use { client ->
|
||||
val serviceURI = UriBuilder.fromUri(isvHost)
|
||||
.path("/isv/service")
|
||||
.build()
|
||||
log.info("Invoking ISV: {}", serviceURI)
|
||||
|
||||
val httpRequest = HttpGet(serviceURI)
|
||||
client.execute(httpRequest, context).use { httpResponse ->
|
||||
val statusCode = httpResponse.statusLine.statusCode
|
||||
if (statusCode != SC_OK) {
|
||||
async.resume(httpResponse.toResponse("Error from ISV Host (HTTP $statusCode)"))
|
||||
return@submit
|
||||
}
|
||||
mapper.readValue(httpResponse.entity.content)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
log.error("HTTP client error", e)
|
||||
async.resume(responseOf("HTTP connection failed: ${e.message}", SC_FORBIDDEN))
|
||||
return@submit
|
||||
}
|
||||
|
||||
val (enclaveKey, platformGID) = enclave.getPublicKeyAndGroupIdentifier()
|
||||
session.setAttribute(platformGIDAttr, platformGID)
|
||||
|
||||
val challengeResponse = ChallengeResponse(
|
||||
ga = enclaveKey.bytes,
|
||||
spid = serviceResponse.spid,
|
||||
quoteType = serviceResponse.quoteType
|
||||
)
|
||||
session.setAttribute(challengeResponseAttr, challengeResponse)
|
||||
async.resume(Response.ok(challengeResponse).build())
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/attest")
|
||||
fun attestation(attestation: AttestationRequest?, @Suspended async: AsyncResponse) {
|
||||
if (attestation == null) {
|
||||
throw BadRequestException(responseOf("Message is missing", SC_BAD_REQUEST))
|
||||
}
|
||||
val session = httpRequest.session
|
||||
log.info("Attestation - HTTP Session: {}", session.id)
|
||||
|
||||
val challengeResponse: ChallengeResponse = session.requireAttribute(challengeResponseAttr, "No response from our challenge")
|
||||
val challengeNonce: String = session.requireAttribute(challengerNonceAttr, "Challenger's nonce unavailable")
|
||||
val platformGID = session.requireAttribute<Int>(platformGIDAttr, "Platform GID unavailable").value()
|
||||
val cookies: CookieStore = session.requireAttribute(conversationAttr, "No existing HTTP session with ISV")
|
||||
val enclave: AttestationEnclave = session.requireAttribute(enclaveAttr, "Enclave unavailable")
|
||||
|
||||
executor.submit {
|
||||
val context = HttpClientContext.create().apply {
|
||||
cookieStore = cookies
|
||||
}
|
||||
|
||||
log.info("Platform GID: '{}'", platformGID)
|
||||
|
||||
val iasReportBody: ByteArray = createHttpClient().use { client ->
|
||||
/*
|
||||
* First fetch the signature revocation list from the IAS Proxy.
|
||||
*/
|
||||
val revocationList = try {
|
||||
val sigRlURI = UriBuilder.fromUri(isvHost)
|
||||
.path("/isv/sigrl/{gid}")
|
||||
.build(platformGID)
|
||||
log.info("Invoking ISV: {}", sigRlURI)
|
||||
|
||||
val getSigRL = HttpGet(sigRlURI)
|
||||
client.execute(getSigRL, context).use { response ->
|
||||
val statusCode = response.statusLine.statusCode
|
||||
if (statusCode != SC_OK) {
|
||||
async.resume(response.toResponse("Error from ISV Host (HTTP $statusCode)"))
|
||||
return@submit
|
||||
}
|
||||
EntityUtils.toByteArray(response.entity).decodeBase64()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
log.error("HTTP client error", e)
|
||||
async.resume(responseOf("HTTP client error: ${e.message}"))
|
||||
return@submit
|
||||
}
|
||||
|
||||
log.info("Fetched revocation list from IAS")
|
||||
|
||||
/*
|
||||
* Now tell the enclave to generate the quote.
|
||||
*/
|
||||
val quote = try {
|
||||
val challengerDetails = ChallengerDetails(
|
||||
publicKey = ECKey.fromBytes(attestation.gb),
|
||||
serviceProviderIdentifier = challengeResponse.spid.hexToBytes(),
|
||||
quoteType = QuoteType.forValue(challengeResponse.quoteType),
|
||||
keyDerivationFunctionIdentifier = AES_CMAC_FUNC,
|
||||
signature = attestation.signatureGbGa,
|
||||
messageAuthenticationCode = attestation.aesCMAC,
|
||||
signatureRevocationList = revocationList
|
||||
)
|
||||
enclave.processChallengerDetailsAndGenerateQuote(challengerDetails)
|
||||
} catch (e: Exception) {
|
||||
log.error("Attestation error", e)
|
||||
async.resume(responseOf("Attestation error: ${e.message}"))
|
||||
return@submit
|
||||
}
|
||||
|
||||
/*
|
||||
* Now pass the quote to the IAS Proxy for validation.
|
||||
*/
|
||||
val reportBody = try {
|
||||
val reportURI = UriBuilder.fromUri(isvHost)
|
||||
.path("/isv/report")
|
||||
.build()
|
||||
log.info("Invoking ISV: {}", reportURI)
|
||||
|
||||
val reportRequest = ReportRequest(
|
||||
isvEnclaveQuote = quote.payload,
|
||||
pseManifest = quote.securityProperties,
|
||||
nonce = challengeNonce
|
||||
)
|
||||
val httpRequest = HttpPost(reportURI).apply {
|
||||
entity = StringEntity(mapper.writeValueAsString(reportRequest), APPLICATION_JSON)
|
||||
}
|
||||
client.execute(httpRequest, context).use { httpResponse ->
|
||||
val statusCode = httpResponse.statusLine.statusCode
|
||||
if (statusCode != SC_OK) {
|
||||
async.resume(httpResponse.toResponse("Error from ISV Host (HTTP $statusCode)"))
|
||||
return@submit
|
||||
}
|
||||
EntityUtils.toByteArray(httpResponse.entity)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
log.error("HTTP client error", e)
|
||||
async.resume(responseOf("HTTP client error: ${e.message}"))
|
||||
return@submit
|
||||
}
|
||||
|
||||
// Return this to the challenger for validation.
|
||||
reportBody
|
||||
}
|
||||
|
||||
log.info("Received report from IAS")
|
||||
|
||||
// Successful response.
|
||||
async.resume(Response.ok(iasReportBody).build())
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/secret")
|
||||
fun secret(secret: SecretRequest?): Response {
|
||||
if (secret == null) {
|
||||
throw BadRequestException(responseOf("Message is missing", SC_BAD_REQUEST))
|
||||
}
|
||||
|
||||
val session = httpRequest.session
|
||||
log.info("Secret - HTTP Session: {}", session.id)
|
||||
log.info("platformInfo: {}", secret.platformInfo)
|
||||
log.info("aesCMAC: {}", secret.aesCMAC)
|
||||
log.info("secret: {}", secret.data)
|
||||
|
||||
val enclave: AttestationEnclave = session.requireAttribute(enclaveAttr, "Enclave unavailable")
|
||||
val attestationResult = AttestationResult(
|
||||
attestationResultMessage = secret.platformInfo,
|
||||
aesCMAC = secret.aesCMAC,
|
||||
secret = secret.data,
|
||||
secretHash = secret.authTag,
|
||||
secretIV = secret.iv
|
||||
)
|
||||
val (cmacStatus, sealedSecret) = try {
|
||||
enclave.verifyAttestationResponse(attestationResult)
|
||||
} catch (e: SgxException) {
|
||||
log.error("SGX enclave error", e)
|
||||
return responseOf("Failed to validate request", SC_BAD_REQUEST)
|
||||
}
|
||||
|
||||
if (cmacStatus != SgxStatus.SUCCESS) {
|
||||
log.error("CMAC validation failed: $cmacStatus")
|
||||
return responseOf("Invalid CMAC status '$cmacStatus'", SC_BAD_REQUEST)
|
||||
}
|
||||
|
||||
// Successful response.
|
||||
log.info("Sealed secret size: ${sealedSecret.size}")
|
||||
return Response.ok().build()
|
||||
}
|
||||
|
||||
private fun validateNonce(n: String?) {
|
||||
val nonce = n ?: return
|
||||
if (nonce.length > maxNonceLength) {
|
||||
throw BadRequestException(responseOf("Nonce is too large: maximum $maxNonceLength digits", SC_BAD_REQUEST))
|
||||
}
|
||||
}
|
||||
|
||||
private fun responseOf(message: String, statusCode: Int = SC_INTERNAL_SERVER_ERROR): Response = Response.status(statusCode)
|
||||
.entity(AttestationError(message))
|
||||
.build()
|
||||
|
||||
private fun HttpResponse.toResponse(message: String, statusCode: Int = statusLine.statusCode): Response {
|
||||
return Response.status(statusCode)
|
||||
.entity(AttestationError(message))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createHttpClient(): CloseableHttpClient {
|
||||
return HttpClients.custom()
|
||||
.setConnectionManager(BasicHttpClientConnectionManager().apply {
|
||||
socketConfig = httpSocketConfig
|
||||
})
|
||||
.setDefaultRequestConfig(httpRequestConfig)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun ByteArray.decodeBase64(): ByteArray = Base64.getDecoder().decode(this)
|
||||
private inline fun <reified T : Any> HttpSession.requireAttribute(attrName: String, errorMessage: String): T
|
||||
= getAttribute(attrName) as? T ?: throw NotAuthorizedException(responseOf(errorMessage, SC_UNAUTHORIZED))
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package net.corda.attestation.host.web
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import java.security.Security
|
||||
import javax.ws.rs.ApplicationPath
|
||||
import javax.ws.rs.core.Application
|
||||
|
||||
@ApplicationPath("/")
|
||||
class AttestationHostApplication : Application() {
|
||||
init {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package net.corda.attestation.host.web
|
||||
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.ws.rs.WebApplicationException
|
||||
import javax.ws.rs.core.Response
|
||||
import javax.ws.rs.ext.ExceptionMapper
|
||||
import javax.ws.rs.ext.Provider
|
||||
|
||||
@Provider
|
||||
class ExceptionHandler : ExceptionMapper<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
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package net.corda.attestation.host.web
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import javax.ws.rs.Consumes
|
||||
import javax.ws.rs.Produces
|
||||
import javax.ws.rs.ext.ContextResolver
|
||||
import javax.ws.rs.ext.Provider
|
||||
|
||||
@Consumes("application/*+json", "text/json")
|
||||
@Produces("application/*+json", "text/json")
|
||||
@Provider
|
||||
class JacksonConfig : ContextResolver<ObjectMapper> {
|
||||
private val mapper = ObjectMapper().registerModule(JavaTimeModule())
|
||||
override fun getContext(type: Class<*>?): ObjectMapper = mapper
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package net.corda.attestation.host.web
|
||||
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit.*
|
||||
import javax.servlet.ServletContextEvent
|
||||
import javax.servlet.ServletContextListener
|
||||
import javax.servlet.annotation.WebListener
|
||||
|
||||
/**
|
||||
* Creates an @ApplicationScoped resource without having to use CDI.
|
||||
*/
|
||||
@WebListener
|
||||
class ThreadPoolListener : ServletContextListener {
|
||||
companion object {
|
||||
const val threadPoolAttr = "Thread-Pool"
|
||||
private val log: Logger = LoggerFactory.getLogger(ThreadPoolListener::class.java)
|
||||
}
|
||||
|
||||
private lateinit var pool: ExecutorService
|
||||
|
||||
override fun contextInitialized(evt: ServletContextEvent) {
|
||||
log.info("Creating thread pool")
|
||||
pool = Executors.newCachedThreadPool()
|
||||
evt.servletContext.setAttribute(threadPoolAttr, pool)
|
||||
}
|
||||
|
||||
override fun contextDestroyed(evt: ServletContextEvent) {
|
||||
log.info("Destroying thread pool")
|
||||
pool.shutdown()
|
||||
try {
|
||||
evt.servletContext.removeAttribute(threadPoolAttr)
|
||||
if (!pool.awaitTermination(30, SECONDS)) {
|
||||
pool.shutdownNow()
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
log.error("Thread pool timed out on shutdown")
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user