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:
Chris Rankin 2017-12-22 14:42:42 +00:00 committed by GitHub
parent 83efd33fc7
commit c545a58c1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
143 changed files with 7521 additions and 35 deletions

View File

@ -67,7 +67,7 @@ ADD dependencies/proguard6.0beta1.tar.gz /usr/share/
# Expose ports for remote GDB and Java debugging, and test servers # Expose ports for remote GDB and Java debugging, and test servers
EXPOSE 2000 5005 8080 9080 EXPOSE 2000 5005 8080 8084 9080
# Environment # Environment

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

View File

@ -1,5 +1,7 @@
# Remote Attestation # Remote Attestation
![Flow between Challenger, Host, ISV and IAS](challenger-flow.png "Remote Attestation Flow")
## Project Organisation ## Project Organisation
* **Enclave** * **Enclave**

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
package net.corda.attestation.challenger
class ChallengerException(message: String?, cause: Throwable?) : Exception(message, cause) {
constructor(message: String) : this(message, null)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV
BAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV
BAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0
YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy
MzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL
U2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD
DCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G
CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR+tXc8u1EtJzLA10Feu1Wg+p7e
LmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh
rgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT
L/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe
NpEJUmg4ktal4qgIAxk+QHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ
byinkNndn+Bgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H
afuVeLHcDsRp6hol4P+ZFIhu8mmbI1u0hH3W/0C2BuYXB5PC+5izFFh/nP0lc2Lf
6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM
RoOaX4AS+909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX
MFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50
L0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW
BBR4Q3t2pn680K9+QjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9+Qjfr
NXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq
hkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir
IEqucRiJSSx+HjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi+ripMtPZ
sFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi
zLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra
Ud4APK0wZTGtfPXU7w+IBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA
152Sq049ESDz+1rRGc2NVEqh1KaGXmtXvqxXcTB+Ljy5Bw2ke0v8iGngFBPqCTVB
3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5+xmBc388v9Dm21HGfcC8O
DD+gT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R+mJTLwPXVMrv
DaVzWh5aiEx+idkSGMnX
-----END CERTIFICATE-----

View File

@ -0,0 +1,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

View File

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

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

View File

@ -0,0 +1,119 @@
package net.corda.attestation
import org.slf4j.LoggerFactory
import java.nio.charset.StandardCharsets.*
import java.security.*
import java.security.spec.ECGenParameterSpec
import javax.crypto.Cipher
import javax.crypto.Cipher.*
import javax.crypto.KeyAgreement
import javax.crypto.Mac
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
class Crypto(val random: SecureRandom = SecureRandom.getInstance("NativePRNGNonBlocking")) {
internal companion object {
private const val AES_ALGORITHM = "AES/GCM/NoPadding"
private const val macBlockSize = 16
private const val gcmIvLength = 12
const val gcmTagLength = 16
@JvmStatic
private val log = LoggerFactory.getLogger(Crypto::class.java)
@JvmStatic
private val smkValue = byteArrayOf(
0x01,
'S'.toAscii(),
'M'.toAscii(),
'K'.toAscii(),
0x00,
0x80.toByte(),
0x00
)
@JvmStatic
private val mkValue = byteArrayOf(
0x01,
'M'.toAscii(),
'K'.toAscii(),
0x00,
0x80.toByte(),
0x00
)
@JvmStatic
private val skValue = byteArrayOf(
0x01,
'S'.toAscii(),
'K'.toAscii(),
0x00,
0x80.toByte(),
0x00
)
private fun Char.toAscii() = toString().toByteArray(US_ASCII)[0]
}
private val keyPairGenerator: KeyPairGenerator = KeyPairGenerator.getInstance("EC")
init {
keyPairGenerator.initialize(ECGenParameterSpec("secp256r1"), random)
}
fun generateKeyPair(): KeyPair = keyPairGenerator.generateKeyPair()
fun aesCMAC(key: ByteArray = ByteArray(macBlockSize), value: ByteArray): ByteArray = aesCMAC(key, { aes -> aes.update(value) })
fun aesCMAC(key: ByteArray, update: (aes: Mac) -> Unit): ByteArray = Mac.getInstance("AESCMAC").let { aes ->
aes.init(SecretKeySpec(key, "AES"))
update(aes)
aes.doFinal()
}
private fun createGCMParameters(iv: ByteArray) = GCMParameterSpec(gcmTagLength * 8, iv)
fun createIV(): ByteArray = ByteArray(gcmIvLength).apply { random.nextBytes(this) }
fun encrypt(data: ByteArray, secretKey: SecretKey, secretIV: ByteArray): ByteArray = Cipher.getInstance(AES_ALGORITHM).let { cip ->
cip.init(ENCRYPT_MODE, secretKey, createGCMParameters(secretIV), random)
cip.doFinal(data)
}
@Suppress("UNUSED")
fun decrypt(data: ByteArray, secretKey: SecretKey, secretIV: ByteArray): ByteArray = Cipher.getInstance(AES_ALGORITHM).let { cip ->
cip.init(DECRYPT_MODE, secretKey, createGCMParameters(secretIV))
cip.doFinal(data)
}
fun generateSharedSecret(privateKey: PrivateKey, peerPublicKey: PublicKey): ByteArray {
return KeyAgreement.getInstance("ECDH").let { ka ->
ka.init(privateKey, random)
ka.doPhase(peerPublicKey, true)
ka.generateSecret()
}
}
private fun generateKDK(sharedSecret: ByteArray)
= aesCMAC(ByteArray(macBlockSize), sharedSecret.reversedArray()).apply { log.debug("KDK: {}", toHexArrayString()) }
private fun generateSMK(sharedSecret: ByteArray)
= aesCMAC(generateKDK(sharedSecret), smkValue).apply { log.debug("SMK: {}", toHexArrayString()) }
fun generateSMK(privateKey: PrivateKey, peerPublicKey: PublicKey): ByteArray
= generateSMK(generateSharedSecret(privateKey, peerPublicKey))
private fun generateMK(sharedSecret: ByteArray)
= aesCMAC(generateKDK(sharedSecret), mkValue).apply { log.debug("MK: {}", toHexArrayString()) }
fun generateMK(privateKey: PrivateKey, peerPublicKey: PublicKey): ByteArray
= generateMK(generateSharedSecret(privateKey, peerPublicKey))
private fun generateSK(sharedSecret: ByteArray)
= aesCMAC(generateKDK(sharedSecret), skValue).apply { log.debug("SK: {}", toHexArrayString()) }
fun generateSecretKey(privateKey: PrivateKey, peerPublicKey: PublicKey): SecretKey
= SecretKeySpec(generateSK(generateSharedSecret(privateKey, peerPublicKey)), "AES")
}
fun ByteArray.authenticationTag(): ByteArray = copyOfRange(size - Crypto.gcmTagLength, size)
fun ByteArray.encryptedData(): ByteArray = copyOf(size - Crypto.gcmTagLength)
fun ByteArray.toHexArrayString(): String = joinToString(prefix="[", separator=",", postfix="]", transform={ b -> String.format("0x%02x", b) })

View File

@ -0,0 +1,62 @@
@file:JvmName("EndianUtils")
package net.corda.attestation
import java.math.BigInteger
import java.nio.ByteBuffer
import java.nio.ByteOrder.LITTLE_ENDIAN
import java.security.interfaces.ECPublicKey
import java.security.spec.ECParameterSpec
import java.security.spec.ECPoint
import java.security.spec.ECPublicKeySpec
import java.security.spec.KeySpec
const val KEY_SIZE = 64
fun ByteArray.removeLeadingZeros(): ByteArray {
if (isEmpty() || this[0] != 0.toByte()) { return this }
for (i in 1 until size) {
if (this[i] != 0.toByte()) {
return copyOfRange(i, size)
}
}
return byteArrayOf()
}
fun ByteArray.toPositiveInteger() = BigInteger(1, this)
fun ByteArray.toHexString(): String = toPositiveInteger().toString(16)
fun BigInteger.toLittleEndian(size: Int = KEY_SIZE) = ByteArray(size).apply {
val leBytes = toByteArray().reversedArray()
System.arraycopy(leBytes, 0, this, 0, Math.min(size, leBytes.size))
}
fun BigInteger.toUnsignedBytes(): ByteArray = toByteArray().removeLeadingZeros()
fun String.hexToBytes(): ByteArray = BigInteger(this, 16).toUnsignedBytes()
fun ByteArray.toBigEndianKeySpec(ecParameters: ECParameterSpec): KeySpec {
if (size != KEY_SIZE) {
throw IllegalArgumentException("Public key has incorrect size ($size bytes)")
}
val ecPoint = ECPoint(
copyOf(size / 2).reversedArray().toPositiveInteger(),
copyOfRange(size / 2, size).reversedArray().toPositiveInteger()
)
return ECPublicKeySpec(ecPoint, ecParameters)
}
fun ECPublicKey.toLittleEndian(size: Int = KEY_SIZE): ByteArray {
val x = w.affineX.toByteArray().reversedArray()
val y = w.affineY.toByteArray().reversedArray()
return ByteArray(size).apply {
// Automatically discards any extra "most significant" last byte, which is assumed to be zero.
val half = size / 2
System.arraycopy(x, 0, this, 0, Math.min(half, x.size))
System.arraycopy(y, 0, this, half, Math.min(half, y.size))
}
}
fun Short.toLittleEndian(): ByteArray = ByteBuffer.allocate(2)
.order(LITTLE_ENDIAN)
.putShort(this)
.array()

View File

@ -0,0 +1,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)

View File

@ -0,0 +1,7 @@
package net.corda.attestation.message
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonPropertyOrder
@JsonPropertyOrder("message")
class AttestationError(@param:JsonProperty("message") val message: String)

View File

@ -0,0 +1,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
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
@file:JvmName("HexadecimalSerialisers")
package net.corda.attestation.message.ias
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import net.corda.attestation.hexToBytes
import net.corda.attestation.toHexString
class HexadecimalSerialiser : StdSerializer<ByteArray>(ByteArray::class.java) {
override fun serialize(value: ByteArray, gen: JsonGenerator, provider: SerializerProvider) = gen.writeString(value.toHexString())
}
class HexadecimalDeserialiser : StdDeserializer<ByteArray>(ByteArray::class.java) {
override fun deserialize(p: JsonParser, context: DeserializationContext) = p.valueAsString.hexToBytes()
}

View File

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

View File

@ -0,0 +1,11 @@
package net.corda.attestation.message.ias
enum class QuoteStatus {
OK,
SIGNATURE_INVALID,
GROUP_REVOKED,
SIGNATURE_REVOKED,
KEY_REVOKED,
SIGRL_VERSION_MISMATCH,
GROUP_OUT_OF_DATE
}

View File

@ -0,0 +1,58 @@
package net.corda.attestation.message.ias
import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonInclude.Include.*
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonPropertyOrder
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import java.time.LocalDateTime
@JsonPropertyOrder(
"nonce",
"id",
"timestamp",
"epidPseudonym",
"isvEnclaveQuoteStatus",
"isvEnclaveQuoteBody",
"pseManifestStatus",
"pseManifestHash",
"platformInfoBlob",
"revocationReason"
)
@JsonInclude(NON_NULL)
class ReportResponse(
@param:JsonProperty("id")
val id: String,
@param:JsonProperty("isvEnclaveQuoteStatus")
val isvEnclaveQuoteStatus: QuoteStatus,
@param:JsonProperty("isvEnclaveQuoteBody")
val isvEnclaveQuoteBody: ByteArray,
@param:JsonProperty("platformInfoBlob")
@get:JsonSerialize(using = HexadecimalSerialiser::class)
@get:JsonDeserialize(using = HexadecimalDeserialiser::class)
val platformInfoBlob: ByteArray? = null,
@param:JsonProperty("revocationReason")
val revocationReason: Int? = null,
@param:JsonProperty("pseManifestStatus")
val pseManifestStatus: ManifestStatus? = null,
@param:JsonProperty("pseManifestHash")
val pseManifestHash: String? = null,
@param:JsonProperty("nonce")
val nonce: String? = null,
@param:JsonProperty("epidPseudonym")
val epidPseudonym: ByteArray? = null,
@param:JsonProperty("timestamp")
@field:JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS", timezone = "UTC")
val timestamp: LocalDateTime
)

View File

@ -0,0 +1,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)

View File

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

View File

@ -0,0 +1,103 @@
package net.corda.attestation
import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.nio.charset.StandardCharsets.*
import java.security.KeyFactory
import java.security.interfaces.ECPublicKey
class CryptoTest {
private lateinit var keyFactory: KeyFactory
private lateinit var crypto: Crypto
@Rule
@JvmField
val cryptoProvider = CryptoProvider()
@Before
fun setup() {
keyFactory = KeyFactory.getInstance("EC")
crypto = 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)
}
}

View File

@ -0,0 +1,86 @@
package net.corda.attestation
import org.junit.Assert.*
import org.junit.Test
import sun.security.ec.ECPublicKeyImpl
import java.math.BigInteger
import java.security.KeyPairGenerator
import java.security.interfaces.ECPublicKey
import java.security.spec.ECGenParameterSpec
import java.security.spec.ECPoint
class EndianUtilsTest {
@Test
fun testBigIntegerToLittleEndian() {
val source = BigInteger("8877665544332211", 16)
assertArrayEquals(unsignedByteArrayOf(0x11), source.toLittleEndian(1))
assertArrayEquals(unsignedByteArrayOf(0x11, 0x22), source.toLittleEndian(2))
assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33), source.toLittleEndian(3))
assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44), source.toLittleEndian(4))
assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44, 0x55), source.toLittleEndian(5))
assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44, 0x55, 0x66), source.toLittleEndian(6))
assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77), source.toLittleEndian(7))
assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88), source.toLittleEndian(8))
}
@Test
fun testRemovingLeadingZeros() {
assertArrayEquals(byteArrayOf(), byteArrayOf().removeLeadingZeros())
assertArrayEquals(byteArrayOf(), byteArrayOf(0x00, 0x00, 0x00, 0x00).removeLeadingZeros())
assertArrayEquals(byteArrayOf(0x7F, 0x63), byteArrayOf(0x00, 0x00, 0x7F, 0x63).removeLeadingZeros())
assertArrayEquals(byteArrayOf(0x7F, 0x43), byteArrayOf(0x7F, 0x43).removeLeadingZeros())
}
@Test
fun testUnsignedBytes() {
val source = BigInteger("FFAA", 16)
assertArrayEquals(unsignedByteArrayOf(0xFF, 0xAA), source.toUnsignedBytes())
}
@Test
fun testToHexString() {
val source = unsignedByteArrayOf(0xFF, 0xAA, 0x00)
assertEquals("FFAA00", source.toHexString().toUpperCase())
}
@Test
fun testShortToLittleEndian() {
assertArrayEquals(unsignedByteArrayOf(0x0F, 0x00), 15.toShort().toLittleEndian())
assertArrayEquals(unsignedByteArrayOf(0xFF, 0xFF), 65535.toShort().toLittleEndian())
assertArrayEquals(unsignedByteArrayOf(0x00, 0x01), 256.toShort().toLittleEndian())
}
@Test
fun testLittleEndianPublicKey() {
val ecParameters = (KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1")) }.generateKeyPair().public as ECPublicKey).params
val publicKey = ECPublicKeyImpl(ECPoint(BigInteger.TEN, BigInteger.ONE), ecParameters)
val littleEndian2 = publicKey.toLittleEndian(2)
assertArrayEquals(byteArrayOf(0x0A, 0x01), littleEndian2)
val littleEndian4 = publicKey.toLittleEndian(4)
assertArrayEquals(byteArrayOf(0x0A, 0x00, 0x01, 0x00), littleEndian4)
val littleEndian8 = publicKey.toLittleEndian(8)
assertArrayEquals(byteArrayOf(0x0A, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00), littleEndian8)
}
@Test
fun testLittleEndianUnsigned() {
val veryBigNumber1 = BigInteger("FFFFFFFFEEEEEEEE", 16)
val veryBigNumber2 = BigInteger("AAAAAAAABBBBBBBB", 16)
val ecParameters = (KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1")) }.generateKeyPair().public as ECPublicKey).params
val publicKey = ECPublicKeyImpl(ECPoint(veryBigNumber1, veryBigNumber2), ecParameters)
val littleEndian16 = publicKey.toLittleEndian(16)
assertArrayEquals(byteArray(4, 0xEE)
.plus(byteArray(4, 0xFF)
.plus(byteArray(4, 0xBB))
.plus(byteArray(4, 0xAA))), littleEndian16)
}
@Test
fun testLittleEndianUnderflow() {
val number = BigInteger("007FEDCB", 16)
val littleEndian = number.toLittleEndian(4)
assertArrayEquals(unsignedByteArrayOf(0xcb, 0xed, 0x7f, 0x00), littleEndian)
}
}

View File

@ -0,0 +1,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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,129 @@
package net.corda.attestation.message.ias
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import net.corda.attestation.message.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)
}
}

View 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

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

View File

@ -0,0 +1 @@
wrapper.hpp

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

View File

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

View File

@ -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__ */

View 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__ */

View File

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

View File

@ -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__ */

View File

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

View File

@ -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__ */

View File

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

View File

@ -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__ */

View 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, &quote,
&quote_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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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