From 2725f53ef5c13f086303a53c8d336afbe8744817 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 12 Dec 2017 13:34:26 +0000 Subject: [PATCH 1/2] ENT-1074 - Proof-of-concept ISV for SGX remote attestation (#161) * Initial WIP. * Configure IAS host via system properties. * Create separate Gretty configurations for testing and for IAS. * (WIP) Separate configuration values from WAR; Add msg3 -> msg4 handling. * Check the IAS report's cryptographic signature. * Accept CertPath from IAS instead of a Certificate. * Validate the certificate chain for the IAS report. * Refactor response handling, and add a secret to Message4. * Append public DH keys to generated shared secret. * Use DH secret to generate a 256 bit AES key. * Fix runISV Gradle task so that it creates WAR file. * Migrate MockIAS service into a separate package. * Remove unused aesCMAC field from Message3. * Configure HTTP sessions to expire after 10 idle minutes. * Ensure we select the "isv" key for MTLS with Intel Attestation Service. * Set key alias for Intel's public certificate. * Implement GET /attest/provision endpoint. * Use elliptic curves for Diffie-Hellman keys. * Pass public keys as Little Endian byte arrays without ASN.1 encoding. * Add AES-CMAC signature to Message2. * Remove signature fields from QUOTE body for sending to IAS. * Add a dummy AES-CMAC field to Message3 for later validation. * Generate AEC-CMAC for Message 3, and refactor crypto functionality. * Calculate AES-CMAC using AES/CBC/PKCS5Padding algorithm. * Use BouncyCastle's AESCMAC algorithm for MAC calculation. * Include standard crypto test vectors to the unit tests. * Encrypt MSG3 secret using AES/GCM/NoPadding with 128 bit key. * Hash shared key with Little Endian versions of public keys. * Refactor so that hexToBytes() is a utility. * Simplify signing of MocKIAS report. * Separate AES/GCM authentication tag from the encrypted data. * Create /ias/report endpoint for ISV which proxies IAS. * Remove unnecessary @Throws from MockIAS handlers. * Log HTTP error status from IAS. * Replace runISV task with startISV and stopISV tasks. * Refactor tests to use CryptoProvider @Rule instead of @Suite. * Move Web server for integration tests to use non-production ports. * Add proxy endpoint for IAS revocation list. * Generate an ECDSA "service key" for signing (gb|ga). * Generate a persistent key-pair for the ISV to sign with. * Verify the (Gb|Ga) signature from Message2. * Add debugging aids. * Fix Gradle warning. * Remove TLV header from Platform Info Body for MSG4. * Small tidy-up. * Use SPID "as-is" when calculating CMAC for MSG2. * Add DEBUG messages for MSG2's KDK and SMK values (AES-CMAC). * Add DEBUG logging for ECDH shared secret. * More DEBUG logging. * The ECDH shared secret *is* the x-coordinate: no need to subrange. * Adjust MockIAS to return an empty revocationList for GID 0000000b. * Fix ArrayOutOfBoundsException for "small" integer values. * Test MSG1 with empty revocation list. * Add extra logging for IAS report request. * ReportResponse object cannot be null. * Fix misreading of spec - don't remove quote's signature when requesting report from IAS. * Log invalid contents of X-IAS-Report-Signing-Certificate HTTP header. * Build CertPath for IAS from explicit list of Certificates. * Rename quote fields on IAS ReportResponse to match Intel. * Log report ID and quote status from IAS. * Add a revocation list checker to the certificate path validator. * Tweak revocation list options, depending on IAS vs MockIAS. * Extract Intel's certificate specifically by alias for PKIX. * Tune quote body returned by MockIAS. * Add AES-CMAC field to Message4 for validation. * Increase GCM authentication tag to 128 bits. * Receive platformInfoBlob from IAS as hexadecimal string. * Generate secret encryption key using KDK and SK values. * Marshall platformInfoBlob between Base16 string and ByteArray. * Interpret status results from IAS as enums. * Use lateinit for HttpServletRequest field. * Refactor ExceptionHandler out of messages package. * Alias is for ISV, so rename it. * Refactor classes into more correct packages. * Use random 96 bit IV for GCM encryption. * Parameterise HTTP/HTTPS ports via Gradle. * Do not forward a securityManifest containing only zeros to IAS. * Address review comments. * Review comment: Use NativePRNGNonBlocking for SecureRandom. * Rename isv.pfx to isv-svc.pfx * Rename keystore to isv.pfx, for clarity. * Update scripts so that they no longer require user input. * Generate isv.pfx from the key and certificates. * Remove private key from repository. * Declare an empty PSE Manifest to be invalid. * Generate keystores "on the fly". * Rename integration tests to end in "IT" instead of "Test". * Add README * Turn remote-attestation into a separate Gradle project. --- settings.gradle | 1 - .../attestation-server/README.md | 42 ++ .../attestation-server/build.gradle | 173 +++++++ .../net/corda/attestation/CryptoProvider.kt | 16 + .../corda/attestation/GetProvisioningIT.kt | 62 +++ .../kotlin/net/corda/attestation/IASIT.kt | 78 +++ .../net/corda/attestation/KeyStoreProvider.kt | 30 ++ .../net/corda/attestation/PostMessage0IT.kt | 62 +++ .../net/corda/attestation/PostMessage1IT.kt | 130 +++++ .../net/corda/attestation/PostMessage3IT.kt | 165 +++++++ .../corda/attestation/PostMessage3OnlyIT.kt | 112 +++++ .../net/corda/attestation/ReportProxyIT.kt | 104 ++++ .../attestation/RevocationListProxyIT.kt | 59 +++ .../src/integration-test/security.properties | 2 + .../src/integration-test/ssl/generate-ssl.sh | 18 + .../kotlin/net/corda/attestation/Crypto.kt | 119 +++++ .../net/corda/attestation/EndianUtils.kt | 62 +++ .../net/corda/attestation/ExceptionHandler.kt | 21 + .../kotlin/net/corda/attestation/IASProxy.kt | 180 +++++++ .../net/corda/attestation/JacksonConfig.kt | 16 + .../corda/attestation/RemoteAttestation.kt | 449 ++++++++++++++++++ .../net/corda/attestation/SgxApplication.kt | 13 + .../attestation/message/AttestationError.kt | 7 + .../attestation/message/ChallengeResponse.kt | 16 + .../net/corda/attestation/message/Message0.kt | 13 + .../net/corda/attestation/message/Message1.kt | 15 + .../net/corda/attestation/message/Message2.kt | 37 ++ .../net/corda/attestation/message/Message3.kt | 26 + .../net/corda/attestation/message/Message4.kt | 67 +++ .../message/ias/HexadecimalSerialisers.kt | 19 + .../attestation/message/ias/ManifestStatus.kt | 10 + .../attestation/message/ias/QuoteStatus.kt | 11 + .../message/ias/ReportProxyResponse.kt | 19 + .../attestation/message/ias/ReportRequest.kt | 19 + .../attestation/message/ias/ReportResponse.kt | 58 +++ .../ias/RevocationListProxyResponse.kt | 16 + .../main/kotlin/net/corda/mockias/MockIAS.kt | 59 +++ .../kotlin/net/corda/mockias/ReportSigner.kt | 72 +++ .../corda/mockias/io/SignatureOutputStream.kt | 42 ++ .../src/main/resources/dummyIAS.pfx | Bin 0 -> 2401 bytes .../src/main/resources/dummyIAStrust.pfx | Bin 0 -> 1002 bytes .../src/main/resources/log4j2.xml | 49 ++ .../src/main/security.properties | 1 + .../src/main/ssl/intel-ssl/.gitignore | 1 + .../AttestationReportSigningCACert.pem | 31 ++ .../src/main/ssl/intel-ssl/client.crt | 21 + .../main/ssl/intel-ssl/generate-keystores.sh | 34 ++ .../main/ssl/service-key/generate-keystore.sh | 23 + .../src/main/webapp/META-INF/context.xml | 6 + .../src/main/webapp/WEB-INF/web.xml | 9 + .../kotlin/net/corda/attestation/ByteUtils.kt | 10 + .../net/corda/attestation/CryptoProvider.kt | 16 + .../net/corda/attestation/CryptoTest.kt | 103 ++++ .../net/corda/attestation/DummyRandom.kt | 15 + .../net/corda/attestation/EndianUtilsTest.kt | 86 ++++ .../net/corda/attestation/SampleTest.kt | 84 ++++ .../message/ChallengeResponseTest.kt | 35 ++ .../corda/attestation/message/Message0Test.kt | 29 ++ .../corda/attestation/message/Message1Test.kt | 38 ++ .../corda/attestation/message/Message2Test.kt | 72 +++ .../corda/attestation/message/Message3Test.kt | 90 ++++ .../corda/attestation/message/Message4Test.kt | 156 ++++++ .../corda/attestation/message/MessageUtils.kt | 6 + .../message/ias/ReportProxyResponseTest.kt | 49 ++ .../message/ias/ReportRequestTest.kt | 62 +++ .../message/ias/ReportResponseTest.kt | 129 +++++ .../ias/RevocationListProxyResponseTest.kt | 46 ++ .../mockias/io/SignatureOutputStreamTest.kt | 53 +++ sgx-jvm/remote-attestation/build.gradle | 63 +++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54731 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + sgx-jvm/remote-attestation/gradlew | 172 +++++++ sgx-jvm/remote-attestation/gradlew.bat | 84 ++++ sgx-jvm/remote-attestation/settings.gradle | 2 + 74 files changed, 3999 insertions(+), 1 deletion(-) create mode 100644 sgx-jvm/remote-attestation/attestation-server/README.md create mode 100644 sgx-jvm/remote-attestation/attestation-server/build.gradle create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/CryptoProvider.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/GetProvisioningIT.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/IASIT.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/KeyStoreProvider.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage0IT.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage1IT.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3IT.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3OnlyIT.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/ReportProxyIT.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/RevocationListProxyIT.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/integration-test/security.properties create mode 100755 sgx-jvm/remote-attestation/attestation-server/src/integration-test/ssl/generate-ssl.sh create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/Crypto.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/EndianUtils.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/ExceptionHandler.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/IASProxy.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/JacksonConfig.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/RemoteAttestation.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/SgxApplication.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/AttestationError.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ChallengeResponse.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message0.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message1.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message2.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message3.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message4.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/HexadecimalSerialisers.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ManifestStatus.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/QuoteStatus.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportProxyResponse.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportRequest.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportResponse.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponse.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/MockIAS.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/ReportSigner.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/io/SignatureOutputStream.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/resources/dummyIAS.pfx create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/resources/dummyIAStrust.pfx create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/resources/log4j2.xml create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/security.properties create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/.gitignore create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/AttestationReportSigningCACert.pem create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/client.crt create mode 100755 sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/generate-keystores.sh create mode 100755 sgx-jvm/remote-attestation/attestation-server/src/main/ssl/service-key/generate-keystore.sh create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/webapp/META-INF/context.xml create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/main/webapp/WEB-INF/web.xml create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/ByteUtils.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/CryptoProvider.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/CryptoTest.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/DummyRandom.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/EndianUtilsTest.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/SampleTest.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ChallengeResponseTest.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message0Test.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message1Test.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message2Test.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message3Test.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message4Test.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/MessageUtils.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportProxyResponseTest.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportRequestTest.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportResponseTest.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponseTest.kt create mode 100644 sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/mockias/io/SignatureOutputStreamTest.kt create mode 100644 sgx-jvm/remote-attestation/build.gradle create mode 100644 sgx-jvm/remote-attestation/gradle/wrapper/gradle-wrapper.jar create mode 100644 sgx-jvm/remote-attestation/gradle/wrapper/gradle-wrapper.properties create mode 100755 sgx-jvm/remote-attestation/gradlew create mode 100644 sgx-jvm/remote-attestation/gradlew.bat create mode 100644 sgx-jvm/remote-attestation/settings.gradle diff --git a/settings.gradle b/settings.gradle index 2e20226e98..43cc37c09d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -56,4 +56,3 @@ project(':hsm-tool').with { projectDir = file("$settingsDir/sgx-jvm/hsm-tool") } include 'perftestcordapp' - diff --git a/sgx-jvm/remote-attestation/attestation-server/README.md b/sgx-jvm/remote-attestation/attestation-server/README.md new file mode 100644 index 0000000000..024d4471ce --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/README.md @@ -0,0 +1,42 @@ +Remote Attestation ISV: Proof-Of-Concept +---------------------------------------- + +This initial version of the ISV expects to communicate with the [Attestation Host](../host), which should run on hardware +with a SGX enclave. The ISV also communicates with the Intel Attestation Service (IAS) over HTTPS with mutual TLS, which +requires it to contain our development private key. (We have already shared this key's public key with Intel, and IAS +expects to use it to authenticate us.) + +Please install this private key in PEM formt as `src/main/ssl/intel-ssl/client.key`. + +This ISV runs as a WAR file within Tomcat8, and implements the message flow as described in Intel's [end-to-end example](https://software.intel.com/en-us/articles/intel-software-guard-extensions-remote-attestation-end-to-end-example) +using JSON and HTTP. The use of HTTP here is mere convenience for our proof-of-concept; we anticipate using +something completely different when we integrate with Corda. + +Gradle/Tomcat integration is achieved using the [Gretty plugin](https://github.com/akhikhl/gretty). + +You will need OpenSSL installed so that Gradle can generate the keystores and truststores required by HTTPS and Mutual TLS. + +## Building the ISV + +From this project directory, execute the command: + +```bash +$ ../gradlew build integrationTest +``` + +## Running the ISV + +To launch the ISV as a daemon process listening on TCP/8080, execute: + +```bash +$ nohup ../gradlew startISV & +``` + +The ISV can then be shutdown using: + +```bash +$ ../gradlew stopISV +``` + +It will log messages to `build/logs/attestation-server.log`. + diff --git a/sgx-jvm/remote-attestation/attestation-server/build.gradle b/sgx-jvm/remote-attestation/attestation-server/build.gradle new file mode 100644 index 0000000000..76d95e5184 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/build.gradle @@ -0,0 +1,173 @@ +buildscript { + repositories { + mavenLocal() + mavenCentral() + jcenter() + } + + dependencies { + classpath 'org.akhikhl.gretty:gretty:2.0.0' + } + + ext.keyStoreDir = "$buildDir/keystore" + ext.httpsKeyStoreDir = "$buildDir/https-keystore" + + // Port numbers to launch the different components on. + ext.isvHttpPort = 8080 + ext.isvTestHttpPort = 9080 + ext.iasTestHttpsPort = 9443 +} + +apply plugin: 'kotlin' +apply plugin: 'war' +apply plugin: 'org.akhikhl.gretty' + +description 'Server side of SGX remote attestation process' + +import org.akhikhl.gretty.AppStartTask +import org.akhikhl.gretty.AppStopTask + +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +sourceSets { + integrationTest { + kotlin { + compileClasspath += main.compileClasspath + test.compileClasspath + runtimeClasspath += main.runtimeClasspath + test.runtimeClasspath + //noinspection GroovyAssignabilityCheck + srcDir file('src/integration-test/kotlin') + } + } +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testCompile "junit:junit:$junit_version" + + compile "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version" + compile "org.jboss.resteasy:resteasy-jaxrs:$resteasy_version" + compile "org.jboss.resteasy:resteasy-jackson2-provider:$resteasy_version" + compile "org.jboss.resteasy:resteasy-servlet-initializer:$resteasy_version" + compile "com.fasterxml.jackson.core:jackson-core:$jackson_version" + compile "com.fasterxml.jackson.core:jackson-databind:$jackson_version" + compile "com.fasterxml.jackson.core:jackson-annotations:$jackson_version" + compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" + compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + compile "org.apache.logging.log4j:log4j-core:$log4j_version" + runtime "org.apache.logging.log4j:log4j-web:$log4j_version" + compile "org.slf4j:jcl-over-slf4j:$slf4j_version" +} + +tasks.withType(Test) { + // Enable "unlimited" encryption. + systemProperties["java.security.properties"] = "$projectDir/src/integration-test/security.properties" + + // Set logging directory for all tests. + systemProperties["attestation.home"] = "$buildDir/logs" +} + +task intelKeyStores(type: Exec) { + doFirst { + mkdir keyStoreDir + } + inputs.dir "$projectDir/src/main/ssl/intel-ssl" + outputs.dir keyStoreDir + workingDir keyStoreDir + commandLine "$projectDir/src/main/ssl/intel-ssl/generate-keystores.sh" +} + +task serviceKeyStore(type: Exec) { + doFirst { + mkdir keyStoreDir + } + inputs.dir "$projectDir/src/main/ssl/service-key" + outputs.dir keyStoreDir + workingDir keyStoreDir + commandLine "$projectDir/src/main/ssl/service-key/generate-keystore.sh" +} + +processResources { + dependsOn = [ intelKeyStores, serviceKeyStore ] + from keyStoreDir +} + +task integrationTest(type: Test) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath + systemProperties["javax.net.ssl.keyStore"] = "$httpsKeyStoreDir/keystore" + systemProperties["javax.net.ssl.keyStorePassword"] = "attestation" + systemProperties["test.isv.httpPort"] = isvTestHttpPort +} + +task httpsKeyStores(type: Exec) { + doFirst { + mkdir httpsKeyStoreDir + } + inputs.dir "$projectDir/src/integration-test/ssl" + outputs.dir httpsKeyStoreDir + workingDir httpsKeyStoreDir + commandLine "$projectDir/src/integration-test/ssl/generate-ssl.sh" +} + +project.afterEvaluate { + appBeforeIntegrationTest.dependsOn httpsKeyStores +} + +gretty { + httpPort = isvTestHttpPort + contextPath = "/" + servletContainer = 'tomcat8' + logDir = "$buildDir/logs" + logFileName = "gretty-test" + integrationTestTask = 'integrationTest' + jvmArgs = [ + "-Dorg.jboss.logging.provider=slf4j", + "-Djava.security.properties=$projectDir/src/integration-test/security.properties", + "-Djavax.net.ssl.keyStore=$httpsKeyStoreDir/keystore", + "-Djavax.net.ssl.keyStorePassword=attestation", + "-Djavax.net.ssl.trustStore=$httpsKeyStoreDir/truststore", + "-Djavax.net.ssl.trustStorePassword=attestation", + "-Dattestation.home=$buildDir/logs", + "-Dias.host=localhost:$iasTestHttpsPort", + ] + + httpsPort = iasTestHttpsPort + httpsEnabled = true + sslNeedClientAuth = true + sslKeyStorePath = "$httpsKeyStoreDir/keystore" + sslKeyStorePassword = 'attestation' + sslKeyManagerPassword = 'attestation' +} + +task('startISV', type: AppStartTask, dependsOn: war) { + prepareServerConfig { + httpPort = isvHttpPort + servletContainer = 'tomcat8' + logDir = "$buildDir/logs" + logFileName = "gretty-isv" + jvmArgs = [ + "-Dorg.jboss.logging.provider=slf4j", + "-Djava.security.properties=$projectDir/src/main/security.properties", + "-Djavax.net.ssl.keyStore=$keyStoreDir/isv.pfx", + "-Djavax.net.ssl.keyStorePassword=attestation", + "-Dias.host=test-as.sgx.trustedservices.intel.com", + "-Dattestation.home=$buildDir/logs", + ] + + httpsEnabled = false + } + + prepareWebAppConfig { + contextPath = "/" + inplace = false + } + + interactive = false +} + +task("stopISV", type: AppStopTask) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/CryptoProvider.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/CryptoProvider.kt new file mode 100644 index 0000000000..f0e13724c4 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/CryptoProvider.kt @@ -0,0 +1,16 @@ +package net.corda.attestation + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.security.Security + +class CryptoProvider : TestRule { + override fun apply(statement: Statement, description: Description?): Statement { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(BouncyCastleProvider()) + } + return statement + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/GetProvisioningIT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/GetProvisioningIT.kt new file mode 100644 index 0000000000..92fb37d443 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/GetProvisioningIT.kt @@ -0,0 +1,62 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.ChallengeResponse +import org.apache.http.HttpStatus.* +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpGet +import org.apache.http.config.SocketConfig +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.util.EntityUtils +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class GetProvisioningIT { + private val httpPort = Integer.getInteger("test.isv.httpPort") + private lateinit var httpClient: CloseableHttpClient + private lateinit var mapper: ObjectMapper + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Before + fun setup() { + mapper = ObjectMapper() + val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + httpClient = HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun getProvisioning() { + val request = HttpGet("http://localhost:$httpPort/attest/provision") + val response = httpClient.execute(request).use { response -> + val output = EntityUtils.toString(response.entity) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + + val challenge = mapper.readValue(response, ChallengeResponse::class.java) + assertTrue(challenge.nonce.isNotEmpty()) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/IASIT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/IASIT.kt new file mode 100644 index 0000000000..8091ac9c21 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/IASIT.kt @@ -0,0 +1,78 @@ +package net.corda.attestation + +import org.apache.http.HttpStatus.SC_OK +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpGet +import org.apache.http.config.SocketConfig +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.ssl.SSLContextBuilder +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import java.net.URI +import java.security.KeyStore +import java.security.SecureRandom +import javax.ws.rs.core.UriBuilder + +@Ignore("This class exists only to probe IAS, and is not really a test at all.") +class IASIT { + private val iasHost: URI = URI.create("https://test-as.sgx.trustedservices.intel.com") + private val storePassword = (System.getProperty("javax.net.ssl.keyStorePassword") ?: "").toCharArray() + private val random = SecureRandom() + + private lateinit var keyStore: KeyStore + + private val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + private val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + + private fun createHttpClient(): CloseableHttpClient { + val sslContext = SSLContextBuilder() + .loadKeyMaterial(keyStore, storePassword, { _, _ -> "isv" }) + .setSecureRandom(random) + .build() + return HttpClients.custom() + .setDefaultRequestConfig(httpRequestConfig) + .setDefaultSocketConfig(httpSocketConfig) + .setSSLContext(sslContext) + .build() + } + + private fun loadKeyStoreResource(resourceName: String, password: CharArray, type: String = "PKCS12"): KeyStore { + return KeyStore.getInstance(type).apply { + RemoteAttestation::class.java.classLoader.getResourceAsStream(resourceName)?.use { input -> + load(input, password) + } + } + } + + @Before + fun setup() { + keyStore = loadKeyStoreResource("isv.pfx", storePassword) + } + + @Test + fun huntGID() { + createHttpClient().use { httpClient -> + for (i in 1000..1999) { + val requestURI = UriBuilder.fromUri(iasHost) + .path("attestation/sgx/v2/sigrl/{gid}") + .build(String.format("%16x", i)) + val request = HttpGet(requestURI) + httpClient.execute(request).use { response -> + if (response.statusLine.statusCode == SC_OK) { + println("FOUND: $i") + } else { + println("NO: $i -> ${response.statusLine.statusCode}") + } + } + } + } + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/KeyStoreProvider.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/KeyStoreProvider.kt new file mode 100644 index 0000000000..74eaabc3f7 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/KeyStoreProvider.kt @@ -0,0 +1,30 @@ +package net.corda.attestation + +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.security.KeyPair +import java.security.KeyStore +import java.security.PrivateKey + +class KeyStoreProvider(private val storeName: String, private val storePassword: String) : TestRule { + private lateinit var keyStore: KeyStore + + private fun loadKeyStoreResource(resourceName: String, password: CharArray, type: String = "PKCS12"): KeyStore { + return KeyStore.getInstance(type).apply { + KeyStoreProvider::class.java.classLoader.getResourceAsStream(resourceName)?.use { input -> + load(input, password) + } + } + } + + override fun apply(statement: Statement, description: Description?): Statement { + keyStore = loadKeyStoreResource(storeName, storePassword.toCharArray()) + return statement + } + + fun getKeyPair(alias: String, password: String): KeyPair { + val privateKey = keyStore.getKey(alias, password.toCharArray()) as PrivateKey + return KeyPair(keyStore.getCertificate(alias).publicKey, privateKey) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage0IT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage0IT.kt new file mode 100644 index 0000000000..b2e4cb2571 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage0IT.kt @@ -0,0 +1,62 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.Message0 +import org.apache.http.HttpStatus.SC_OK +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpPost +import org.apache.http.config.SocketConfig +import org.apache.http.entity.ContentType.* +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.util.EntityUtils +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class PostMessage0IT { + private val httpPort = Integer.getInteger("test.isv.httpPort") + private lateinit var httpClient: CloseableHttpClient + private lateinit var mapper: ObjectMapper + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Before + fun setup() { + mapper = ObjectMapper() + val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + httpClient = HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun postMsg0() { + val request = HttpPost("http://localhost:$httpPort/attest/msg0") + val msg0 = Message0(extendedGID = 0) + request.entity = StringEntity(mapper.writeValueAsString(msg0), APPLICATION_JSON) + httpClient.execute(request).use { response -> + val output = EntityUtils.toString(response.entity) + assertEquals(output, SC_OK, response.statusLine.statusCode) + } + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage1IT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage1IT.kt new file mode 100644 index 0000000000..03a7d330ce --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage1IT.kt @@ -0,0 +1,130 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.Message1 +import net.corda.attestation.message.Message2 +import org.apache.http.HttpStatus.* +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpPost +import org.apache.http.config.SocketConfig +import org.apache.http.entity.ContentType.* +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.util.EntityUtils +import org.bouncycastle.asn1.ASN1EncodableVector +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.ASN1OutputStream +import org.bouncycastle.asn1.DLSequence +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.KeyFactory +import java.security.KeyPair +import java.security.Signature +import java.security.interfaces.ECPublicKey + +class PostMessage1IT { + private companion object { + private const val KEY_PASSWORD = "attestation" + private const val SERVICE_KEYSTORE = "isv-svc.pfx" + private val httpPort = Integer.getInteger("test.isv.httpPort") + } + private lateinit var httpClient: CloseableHttpClient + private lateinit var serviceKeyPair: KeyPair + private lateinit var keyPair: KeyPair + private lateinit var ecPublicKey: ECPublicKey + private lateinit var mapper: ObjectMapper + private lateinit var crypto: Crypto + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Rule + @JvmField + val keyStoreProvider = KeyStoreProvider(SERVICE_KEYSTORE, KEY_PASSWORD) + + @Before + fun setup() { + serviceKeyPair = keyStoreProvider.getKeyPair("isv-svc", KEY_PASSWORD) + mapper = ObjectMapper() + crypto = Crypto() + keyPair = crypto.generateKeyPair() + ecPublicKey = keyPair.public as ECPublicKey + val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + httpClient = HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun postMsg1() { + val ecParameters = ecPublicKey.params + val request = HttpPost("http://localhost:$httpPort/attest/msg1") + val msg1 = Message1(ga = ecPublicKey.toLittleEndian(), platformGID = "00000000") + request.entity = StringEntity(mapper.writeValueAsString(msg1), APPLICATION_JSON) + val response = httpClient.execute(request).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + + val msg2 = mapper.readValue(response, Message2::class.java) + assertEquals("84D402C36BA9EF9B0A86EF1A9CC8CE4F", msg2.spid.toUpperCase()) + assertEquals(KEY_SIZE, msg2.signatureGbGa.size) + assertEquals(80, msg2.revocationList.size) + + KeyFactory.getInstance("EC").generatePublic(msg2.gb.toBigEndianKeySpec(ecParameters)) + + val asn1Signature = ByteArrayOutputStream().let { baos -> + ASN1OutputStream(baos).apply { + writeObject(DLSequence(ASN1EncodableVector().apply { + add(ASN1Integer(msg2.signatureGbGa.copyOf(KEY_SIZE / 2).reversedArray().toPositiveInteger())) + add(ASN1Integer(msg2.signatureGbGa.copyOfRange(KEY_SIZE / 2, KEY_SIZE).reversedArray().toPositiveInteger())) + })) + } + baos.toByteArray() + } + val verified = Signature.getInstance("SHA256WithECDSA").let { verifier -> + verifier.initVerify(serviceKeyPair.public) + verifier.update(msg2.gb) + verifier.update(msg1.ga) + verifier.verify(asn1Signature) + } + assertTrue(verified) + } + + @Test + fun testEmptyRevocationList() { + val request = HttpPost("http://localhost:$httpPort/attest/msg1") + val msg1 = Message1(ga = ecPublicKey.toLittleEndian(), platformGID = "0000000b") + request.entity = StringEntity(mapper.writeValueAsString(msg1), APPLICATION_JSON) + val response = httpClient.execute(request).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + + val msg2 = mapper.readValue(response, Message2::class.java) + assertEquals(0, msg2.revocationList.size) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3IT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3IT.kt new file mode 100644 index 0000000000..984629a663 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3IT.kt @@ -0,0 +1,165 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.message.* +import org.apache.http.HttpStatus.* +import org.apache.http.client.CookieStore +import org.apache.http.client.config.CookieSpecs.STANDARD_STRICT +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpPost +import org.apache.http.client.protocol.HttpClientContext +import org.apache.http.config.SocketConfig +import org.apache.http.entity.ContentType.* +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.BasicCookieStore +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.util.EntityUtils +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.nio.charset.StandardCharsets.* +import java.security.* +import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec + +class PostMessage3IT { + private val httpPort = Integer.getInteger("test.isv.httpPort") + private lateinit var httpClient: CloseableHttpClient + private lateinit var cookies: CookieStore + private lateinit var keyPair: KeyPair + private lateinit var ecParameters: ECParameterSpec + private lateinit var msg2: Message2 + private lateinit var peerPublicKey: ECPublicKey + private lateinit var smk: ByteArray + private lateinit var mapper: ObjectMapper + private lateinit var crypto: Crypto + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Before + fun setup() { + crypto = Crypto() + cookies = BasicCookieStore() + keyPair = crypto.generateKeyPair() + ecParameters = (keyPair.public as ECPublicKey).params + mapper = ObjectMapper().registerModule(JavaTimeModule()) + + val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setCookieSpec(STANDARD_STRICT) + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + httpClient = HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val request = HttpPost("http://localhost:$httpPort/attest/msg1") + val msg1 = Message1(ga = (keyPair.public as ECPublicKey).toLittleEndian(), platformGID = "00000000") + request.entity = StringEntity(mapper.writeValueAsString(msg1), APPLICATION_JSON) + val response = httpClient.execute(request, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + msg2 = mapper.readValue(response, Message2::class.java) + + val keyFactory = KeyFactory.getInstance("EC") + peerPublicKey = keyFactory.generatePublic(msg2.gb.toBigEndianKeySpec(ecParameters)) as ECPublicKey + smk = crypto.generateSMK(keyPair.private, peerPublicKey) + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun postMsg3() { + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val request3 = HttpPost("http://localhost:$httpPort/attest/msg3") + val ga = (keyPair.public as ECPublicKey).toLittleEndian() + val quote = byteArrayOf(0x02, 0x04, 0x08, 0x10) + val msg3 = Message3( + aesCMAC = crypto.aesCMAC(smk, { aes -> + aes.update(ga) + aes.update(quote) + }), + ga = ga, + quote = quote + ) + request3.entity = StringEntity(mapper.writeValueAsString(msg3), APPLICATION_JSON) + val response3 = httpClient.execute(request3, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + val msg4 = mapper.readValue(response3, Message4::class.java) + assertEquals("OK", msg4.quoteStatus) + assertArrayEquals(quote, msg4.quoteBody) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22), msg4.platformInfo) + assertNull(msg4.securityManifestStatus) + + val cmac = crypto.aesCMAC(crypto.generateMK(keyPair.private, peerPublicKey), { aes -> + aes.update(msg4.platformInfo ?: byteArrayOf()) + }) + assertArrayEquals(cmac, msg4.aesCMAC) + + val secretKey = crypto.generateSecretKey(keyPair.private, peerPublicKey) + val secret = String(crypto.decrypt(msg4.secret.plus(msg4.secretHash), secretKey, msg4.secretIV), UTF_8) + assertEquals("And now for something completely different!", secret) + } + + @Test + fun testHugeNonce() { + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val request3 = HttpPost("http://localhost:$httpPort/attest/msg3") + val ga = (keyPair.public as ECPublicKey).toLittleEndian() + val quote = byteArrayOf(0x02, 0x04, 0x08, 0x10) + val msg3 = Message3( + aesCMAC = crypto.aesCMAC(smk, { aes -> + aes.update(ga) + aes.update(quote) + }), + ga = ga, + quote = quote, + nonce = "1234567890123456789012345678901234" + ) + request3.entity = StringEntity(mapper.writeValueAsString(msg3), APPLICATION_JSON) + val response3 = httpClient.execute(request3, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_BAD_REQUEST, response.statusLine.statusCode) + output + } + + val error = mapper.readValue(response3, AttestationError::class.java) + assertEquals("Nonce is too large: maximum 32 digits", error.message) + } + + private fun unsignedByteArrayOf(vararg values: Int) = ByteArray(values.size).apply { + for (i in 0 until values.size) { + this[i] = values[i].toByte() + } + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3OnlyIT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3OnlyIT.kt new file mode 100644 index 0000000000..5eee5f8f05 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/PostMessage3OnlyIT.kt @@ -0,0 +1,112 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.message.AttestationError +import net.corda.attestation.message.Message3 +import org.apache.http.HttpHeaders.CONTENT_TYPE +import org.apache.http.HttpStatus.* +import org.apache.http.client.CookieStore +import org.apache.http.client.config.CookieSpecs.STANDARD_STRICT +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpPost +import org.apache.http.client.protocol.HttpClientContext +import org.apache.http.config.SocketConfig +import org.apache.http.entity.ContentType.* +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.BasicCookieStore +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.message.BasicHeader +import org.apache.http.util.EntityUtils +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.KeyPair +import java.security.interfaces.ECPublicKey + +class PostMessage3OnlyIT { + private val httpPort = Integer.getInteger("test.isv.httpPort") + private lateinit var httpClient: CloseableHttpClient + private lateinit var cookies: CookieStore + private lateinit var keyPair: KeyPair + private lateinit var mapper: ObjectMapper + private lateinit var crypto: Crypto + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Before + fun setup() { + mapper = ObjectMapper().registerModule(JavaTimeModule()) + crypto = Crypto() + cookies = BasicCookieStore() + keyPair = crypto.generateKeyPair() + + val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setCookieSpec(STANDARD_STRICT) + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + httpClient = HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun `test msg3 without payload`() { + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val request3 = HttpPost("http://localhost:$httpPort/attest/msg3") + request3.addHeader(BasicHeader(CONTENT_TYPE, APPLICATION_JSON.mimeType)) + val response3 = httpClient.execute(request3, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_BAD_REQUEST, response.statusLine.statusCode) + output + } + + val error = mapper.readValue(response3, AttestationError::class.java) + assertEquals("Message is missing", error.message) + } + + @Test + fun `test Msg3 requires Msg1 first`() { + val context = HttpClientContext.create().apply { + cookieStore = cookies + } + + val request3 = HttpPost("http://localhost:$httpPort/attest/msg3") + val msg3 = Message3( + aesCMAC = byteArrayOf(), + ga = (keyPair.public as ECPublicKey).toLittleEndian(), + securityManifest = byteArrayOf(), + quote = byteArrayOf() + ) + request3.entity = StringEntity(mapper.writeValueAsString(msg3), APPLICATION_JSON) + val response3 = httpClient.execute(request3, context).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_UNAUTHORIZED, response.statusLine.statusCode) + output + } + + val error = mapper.readValue(response3, AttestationError::class.java) + assertEquals("Secret key has not been calculated yet", error.message) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/ReportProxyIT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/ReportProxyIT.kt new file mode 100644 index 0000000000..8048694958 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/ReportProxyIT.kt @@ -0,0 +1,104 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.message.ias.* +import org.apache.http.HttpStatus.* +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpPost +import org.apache.http.config.SocketConfig +import org.apache.http.entity.ContentType +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.util.EntityUtils +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.nio.charset.StandardCharsets.* + +class ReportProxyIT { + private val httpPort = Integer.getInteger("test.isv.httpPort") + private lateinit var httpClient: CloseableHttpClient + private lateinit var mapper: ObjectMapper + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Before + fun setup() { + mapper = ObjectMapper().registerModule(JavaTimeModule()) + val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + httpClient = HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun testIASReportWithManifest() { + val request = HttpPost("http://localhost:$httpPort/ias/report") + val quote = byteArrayOf(0x02, 0x04, 0x08, 0x10) + val requestMessage = ReportRequest( + isvEnclaveQuote = quote, + pseManifest = byteArrayOf(0x73, 0x42), + nonce = "0000000000000000" + ) + request.entity = StringEntity(mapper.writeValueAsString(requestMessage), ContentType.APPLICATION_JSON) + val response = httpClient.execute(request).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + + val responseMessage = mapper.readValue(response, ReportProxyResponse::class.java) + assertTrue(responseMessage.signature.isNotEmpty()) + assertTrue(responseMessage.certificatePath.isNotEmpty()) + + val iasReport = mapper.readValue(responseMessage.report.inputStream(), ReportResponse::class.java) + assertEquals(QuoteStatus.OK, iasReport.isvEnclaveQuoteStatus) + assertEquals("0000000000000000", iasReport.nonce) + assertEquals(ManifestStatus.OK, iasReport.pseManifestStatus) + } + + @Test + fun testIASReportWithoutManifest() { + val request = HttpPost("http://localhost:$httpPort/ias/report") + val quote = byteArrayOf(0x02, 0x04, 0x08, 0x10) + val requestMessage = ReportRequest( + isvEnclaveQuote = quote, + nonce = "0000000000000000" + ) + request.entity = StringEntity(mapper.writeValueAsString(requestMessage), ContentType.APPLICATION_JSON) + val response = httpClient.execute(request).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + + val responseMessage = mapper.readValue(response, ReportProxyResponse::class.java) + assertTrue(responseMessage.signature.isNotEmpty()) + assertTrue(responseMessage.certificatePath.isNotEmpty()) + + val iasReport = mapper.readValue(responseMessage.report.inputStream(), ReportResponse::class.java) + assertEquals(QuoteStatus.OK, iasReport.isvEnclaveQuoteStatus) + assertEquals("0000000000000000", iasReport.nonce) + assertNull(iasReport.pseManifestStatus) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/RevocationListProxyIT.kt b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/RevocationListProxyIT.kt new file mode 100644 index 0000000000..792f9c5891 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/kotlin/net/corda/attestation/RevocationListProxyIT.kt @@ -0,0 +1,59 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.ias.RevocationListProxyResponse +import org.apache.http.HttpStatus.* +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpGet +import org.apache.http.config.SocketConfig +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.util.EntityUtils +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.nio.charset.StandardCharsets.* + +class RevocationListProxyIT { + private val httpPort = Integer.getInteger("test.isv.httpPort") + private lateinit var httpClient: CloseableHttpClient + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + httpClient = HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager().apply { socketConfig = httpSocketConfig }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + @After + fun done() { + httpClient.close() + } + + @Test + fun testIASRevocationList() { + val request = HttpGet("http://localhost:$httpPort/ias/sigrl/0000000000000000") + val response = httpClient.execute(request).use { response -> + val output = EntityUtils.toString(response.entity, UTF_8) + assertEquals(output, SC_OK, response.statusLine.statusCode) + output + } + + val responseMessage = mapper.readValue(response, RevocationListProxyResponse::class.java) + assertEquals("84D402C36BA9EF9B0A86EF1A9CC8CE4F", responseMessage.spid) + assertTrue(responseMessage.revocationList.isNotEmpty()) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/security.properties b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/security.properties new file mode 100644 index 0000000000..314cf7d792 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/security.properties @@ -0,0 +1,2 @@ +crypto.policy=unlimited + diff --git a/sgx-jvm/remote-attestation/attestation-server/src/integration-test/ssl/generate-ssl.sh b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/ssl/generate-ssl.sh new file mode 100755 index 0000000000..538c5990ae --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/integration-test/ssl/generate-ssl.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +ALIAS=jetty +KEYPASS=attestation +STOREPASS=attestation + +rm -f keystore truststore + +# Generate the keystore and truststore that will allow us to enable HTTPS. +# Both keystore and truststore are expected to use password "attestation". + +keytool -keystore keystore -storetype pkcs12 -genkey -alias ${ALIAS} -dname CN=localhost -keyalg RSA -keypass ${KEYPASS} -storepass ${STOREPASS} +keytool -keystore keystore -storetype pkcs12 -export -alias ${ALIAS} -keyalg RSA -file jetty.cert -keypass ${KEYPASS} -storepass ${STOREPASS} +keytool -keystore truststore -storetype pkcs12 -import -alias ${ALIAS} -file jetty.cert -keypass ${KEYPASS} -storepass ${STOREPASS} < aes.update(value) }) + + fun aesCMAC(key: ByteArray, update: (aes: Mac) -> Unit): ByteArray = Mac.getInstance("AESCMAC").let { aes -> + aes.init(SecretKeySpec(key, "AES")) + update(aes) + aes.doFinal() + } + + private fun createGCMParameters(iv: ByteArray) = GCMParameterSpec(gcmTagLength * 8, iv) + + fun createIV(): ByteArray = ByteArray(gcmIvLength).apply { random.nextBytes(this) } + + fun encrypt(data: ByteArray, secretKey: SecretKey, secretIV: ByteArray): ByteArray = Cipher.getInstance(AES_ALGORITHM).let { cip -> + cip.init(ENCRYPT_MODE, secretKey, createGCMParameters(secretIV), random) + cip.doFinal(data) + } + + @Suppress("UNUSED") + fun decrypt(data: ByteArray, secretKey: SecretKey, secretIV: ByteArray): ByteArray = Cipher.getInstance(AES_ALGORITHM).let { cip -> + cip.init(DECRYPT_MODE, secretKey, createGCMParameters(secretIV)) + cip.doFinal(data) + } + + fun generateSharedSecret(privateKey: PrivateKey, peerPublicKey: PublicKey): ByteArray { + return KeyAgreement.getInstance("ECDH").let { ka -> + ka.init(privateKey, random) + ka.doPhase(peerPublicKey, true) + ka.generateSecret() + } + } + + private fun generateKDK(sharedSecret: ByteArray) + = aesCMAC(ByteArray(macBlockSize), sharedSecret.reversedArray()).apply { log.debug("KDK: {}", toHexArrayString()) } + + private fun generateSMK(sharedSecret: ByteArray) + = aesCMAC(generateKDK(sharedSecret), smkValue).apply { log.debug("SMK: {}", toHexArrayString()) } + + fun generateSMK(privateKey: PrivateKey, peerPublicKey: PublicKey): ByteArray + = generateSMK(generateSharedSecret(privateKey, peerPublicKey)) + + private fun generateMK(sharedSecret: ByteArray) + = aesCMAC(generateKDK(sharedSecret), mkValue).apply { log.debug("MK: {}", toHexArrayString()) } + + fun generateMK(privateKey: PrivateKey, peerPublicKey: PublicKey): ByteArray + = generateMK(generateSharedSecret(privateKey, peerPublicKey)) + + private fun generateSK(sharedSecret: ByteArray) + = aesCMAC(generateKDK(sharedSecret), skValue).apply { log.debug("SK: {}", toHexArrayString()) } + + fun generateSecretKey(privateKey: PrivateKey, peerPublicKey: PublicKey): SecretKey + = SecretKeySpec(generateSK(generateSharedSecret(privateKey, peerPublicKey)), "AES") +} + +fun ByteArray.authenticationTag(): ByteArray = copyOfRange(size - Crypto.gcmTagLength, size) +fun ByteArray.encryptedData(): ByteArray = copyOf(size - Crypto.gcmTagLength) +fun ByteArray.toHexArrayString(): String = joinToString(prefix="[", separator=",", postfix="]", transform={ b -> String.format("0x%02x", b) }) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/EndianUtils.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/EndianUtils.kt new file mode 100644 index 0000000000..bdd29f3862 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/EndianUtils.kt @@ -0,0 +1,62 @@ +@file:JvmName("EndianUtils") +package net.corda.attestation + +import java.math.BigInteger +import java.nio.ByteBuffer +import java.nio.ByteOrder.LITTLE_ENDIAN +import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec +import java.security.spec.ECPoint +import java.security.spec.ECPublicKeySpec +import java.security.spec.KeySpec + +const val KEY_SIZE = 64 + +fun ByteArray.removeLeadingZeros(): ByteArray { + if (isEmpty() || this[0] != 0.toByte()) { return this } + for (i in 1 until size) { + if (this[i] != 0.toByte()) { + return copyOfRange(i, size) + } + } + return byteArrayOf() +} + +fun ByteArray.toPositiveInteger() = BigInteger(1, this) +fun ByteArray.toHexString(): String = toPositiveInteger().toString(16) + +fun BigInteger.toLittleEndian(size: Int = KEY_SIZE) = ByteArray(size).apply { + val leBytes = toByteArray().reversedArray() + System.arraycopy(leBytes, 0, this, 0, Math.min(size, leBytes.size)) +} + +fun BigInteger.toUnsignedBytes(): ByteArray = toByteArray().removeLeadingZeros() + +fun String.hexToBytes(): ByteArray = BigInteger(this, 16).toUnsignedBytes() + +fun ByteArray.toBigEndianKeySpec(ecParameters: ECParameterSpec): KeySpec { + if (size != KEY_SIZE) { + throw IllegalArgumentException("Public key has incorrect size ($size bytes)") + } + val ecPoint = ECPoint( + copyOf(size / 2).reversedArray().toPositiveInteger(), + copyOfRange(size / 2, size).reversedArray().toPositiveInteger() + ) + return ECPublicKeySpec(ecPoint, ecParameters) +} + +fun ECPublicKey.toLittleEndian(size: Int = KEY_SIZE): ByteArray { + val x = w.affineX.toByteArray().reversedArray() + val y = w.affineY.toByteArray().reversedArray() + return ByteArray(size).apply { + // Automatically discards any extra "most significant" last byte, which is assumed to be zero. + val half = size / 2 + System.arraycopy(x, 0, this, 0, Math.min(half, x.size)) + System.arraycopy(y, 0, this, half, Math.min(half, y.size)) + } +} + +fun Short.toLittleEndian(): ByteArray = ByteBuffer.allocate(2) + .order(LITTLE_ENDIAN) + .putShort(this) + .array() diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/ExceptionHandler.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/ExceptionHandler.kt new file mode 100644 index 0000000000..8866b94d36 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/ExceptionHandler.kt @@ -0,0 +1,21 @@ +package net.corda.attestation + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import javax.ws.rs.WebApplicationException +import javax.ws.rs.core.Response +import javax.ws.rs.ext.ExceptionMapper +import javax.ws.rs.ext.Provider + +@Provider +class ExceptionHandler : ExceptionMapper { + private companion object { + @JvmStatic + private val log: Logger = LoggerFactory.getLogger(ExceptionHandler::class.java) + } + + override fun toResponse(e: WebApplicationException): Response { + log.error("HTTP Status: {}: {}", e.response.status, e.message) + return e.response + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/IASProxy.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/IASProxy.kt new file mode 100644 index 0000000000..fa531bbb71 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/IASProxy.kt @@ -0,0 +1,180 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.message.AttestationError +import net.corda.attestation.message.ias.ReportProxyResponse +import net.corda.attestation.message.ias.ReportRequest +import net.corda.attestation.message.ias.RevocationListProxyResponse +import org.apache.http.HttpResponse +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpGet +import org.apache.http.client.methods.HttpPost +import org.apache.http.config.RegistryBuilder +import org.apache.http.config.SocketConfig +import org.apache.http.conn.socket.ConnectionSocketFactory +import org.apache.http.conn.ssl.SSLConnectionSocketFactory +import org.apache.http.entity.ContentType +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.ssl.SSLContextBuilder +import org.apache.http.util.EntityUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.FileInputStream +import java.io.IOException +import java.net.URI +import java.net.URLDecoder +import java.security.KeyStore +import java.security.SecureRandom +import java.util.* +import javax.net.ssl.SSLException +import javax.servlet.http.HttpServletResponse.* +import javax.ws.rs.* +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import javax.ws.rs.core.UriBuilder + +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@Path("/ias") +class IASProxy { + private companion object { + @JvmStatic + private val log: Logger = LoggerFactory.getLogger(IASProxy::class.java) + @JvmStatic + private val SPID = "84D402C36BA9EF9B0A86EF1A9CC8CE4F" + + private val mapper = ObjectMapper().registerModule(JavaTimeModule()) + private val random = SecureRandom() + + private val storePassword = (System.getProperty("javax.net.ssl.keyStorePassword") ?: "").toCharArray() + private val keyStore: KeyStore + private val iasHost: URI = URI.create("https://${System.getProperty("ias.host", "localhost:8443")}") + private val isDummy = iasHost.host == "localhost" + private val isvKeyAlias = if (isDummy) "jetty" else "isv" + + private val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + private val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + + init { + val keyStoreType = System.getProperty("javax.net.ssl.keyStoreType", "PKCS12") + keyStore = loadKeyStore("javax.net.ssl.keyStore", storePassword, keyStoreType) + } + + private fun loadKeyStore(propertyName: String, password: CharArray, type: String = "PKCS12"): KeyStore { + val fileName = System.getProperty(propertyName) ?: throw IllegalStateException("System property $propertyName not set") + return KeyStore.getInstance(type).apply { + FileInputStream(fileName).use { input -> this.load(input, password) } + } + } + } + + @GET + @Path("/sigrl/{gid}") + fun proxyRevocationList(@PathParam("gid") platformGID: String): Response { + val revocationList = try { + createHttpClient().use { client -> + val sigRlURI = UriBuilder.fromUri(iasHost) + .path("attestation/sgx/v2/sigrl/{gid}") + .build(platformGID) + val getSigRL = HttpGet(sigRlURI) + client.execute(getSigRL).use { response -> + if (response.statusLine.statusCode != SC_OK) { + return response.toResponse("Error from Intel Attestation Service (HTTP ${response.statusLine.statusCode})") + } + EntityUtils.toByteArray(response.entity).decodeBase64() + } + } + } catch (e: SSLException) { + log.error("HTTPS error: ${e.message}") + return responseOf("HTTPS connection failed: ${e.message}", SC_FORBIDDEN) + } catch (e: IOException) { + log.error("HTTP client error", e) + return responseOf("HTTP client error: ${e.message}") + } + + return Response.ok() + .entity(RevocationListProxyResponse(SPID, revocationList)) + .build() + } + + @POST + @Path("/report") + fun proxyReport(prq: ReportRequest?): Response { + val reportRequest = prq ?: return responseOf("Message is missing", SC_BAD_REQUEST) + + val reportResponse = try { + createHttpClient().use { client -> + val reportURI = UriBuilder.fromUri(iasHost) + .path("attestation/sgx/v2/report") + .build() + val httpRequest = HttpPost(reportURI) + httpRequest.entity = StringEntity(mapper.writeValueAsString(reportRequest), ContentType.APPLICATION_JSON) + client.execute(httpRequest).use { httpResponse -> + if (httpResponse.statusLine.statusCode != SC_OK) { + return httpResponse.toResponse("Error from Intel Attestation Service") + } + ReportProxyResponse( + signature = httpResponse.requireHeader("X-IASReport-Signature"), + certificatePath = httpResponse.requireHeader("X-IASReport-Signing-Certificate").decodeURL(), + report = EntityUtils.toByteArray(httpResponse.entity) + ) + } + } + } catch (e: SSLException) { + log.error("HTTPS error: ${e.message}") + return responseOf("HTTPS connection failed: ${e.message}", SC_FORBIDDEN) + } catch (e: IOException) { + log.error("HTTP client error", e) + return responseOf("HTTP client error: ${e.message}") + } + return Response.ok() + .entity(reportResponse) + .build() + } + + private fun responseOf(message: String, statusCode: Int = SC_INTERNAL_SERVER_ERROR): Response = Response.status(statusCode) + .entity(AttestationError(message)) + .build() + + private fun HttpResponse.toResponse(message: String, statusCode: Int = statusLine.statusCode): Response { + return Response.status(statusCode) + .entity(AttestationError(message)) + .apply { + val requestIdHeader = getFirstHeader("Request-ID") ?: return@apply + this.header(requestIdHeader.name, requestIdHeader.value) + } + .build() + } + + private fun HttpResponse.requireHeader(name: String): String + = (this.getFirstHeader(name) ?: throw ForbiddenException(toResponse("Response header '$name' missing", SC_FORBIDDEN))).value + + private fun createHttpClient(): CloseableHttpClient { + val sslContext = SSLContextBuilder() + .loadKeyMaterial(keyStore, storePassword, { _, _ -> isvKeyAlias }) + .setSecureRandom(random) + .build() + val registry = RegistryBuilder.create() + .register("https", SSLConnectionSocketFactory(sslContext, SSLConnectionSocketFactory.getDefaultHostnameVerifier())) + .build() + return HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager(registry).apply { + socketConfig = httpSocketConfig + }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + private fun ByteArray.decodeBase64(): ByteArray = Base64.getDecoder().decode(this) + private fun String.decodeURL(): String = URLDecoder.decode(this, "UTF-8") +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/JacksonConfig.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/JacksonConfig.kt new file mode 100644 index 0000000000..67daefb9c1 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/JacksonConfig.kt @@ -0,0 +1,16 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import javax.ws.rs.Consumes +import javax.ws.rs.Produces +import javax.ws.rs.ext.ContextResolver +import javax.ws.rs.ext.Provider + +@Consumes("application/*+json", "text/json") +@Produces("application/*+json", "text/json") +@Provider +class JacksonConfig : ContextResolver { + private val mapper = ObjectMapper().registerModule(JavaTimeModule()) + override fun getContext(type: Class<*>?): ObjectMapper = mapper +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/RemoteAttestation.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/RemoteAttestation.kt new file mode 100644 index 0000000000..afa42ca24a --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/RemoteAttestation.kt @@ -0,0 +1,449 @@ +package net.corda.attestation + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.message.* +import net.corda.attestation.message.ias.ReportRequest +import net.corda.attestation.message.ias.ReportResponse +import org.apache.http.HttpResponse +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpGet +import org.apache.http.client.methods.HttpPost +import org.apache.http.config.RegistryBuilder +import org.apache.http.config.SocketConfig +import org.apache.http.conn.socket.ConnectionSocketFactory +import org.apache.http.conn.ssl.SSLConnectionSocketFactory +import org.apache.http.conn.ssl.SSLConnectionSocketFactory.getDefaultHostnameVerifier +import org.apache.http.entity.ContentType +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.ssl.SSLContextBuilder +import org.apache.http.util.EntityUtils +import org.bouncycastle.asn1.ASN1InputStream +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.DLSequence +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.* +import java.net.URI +import java.net.URLDecoder +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.* +import java.security.cert.* +import java.security.cert.Certificate +import java.security.cert.PKIXRevocationChecker.Option.* +import java.security.interfaces.ECPublicKey +import java.security.spec.* +import java.util.* +import javax.crypto.SecretKey +import javax.net.ssl.SSLException +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse.* +import javax.ws.rs.* +import javax.ws.rs.core.* + +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@Path("/attest") +class RemoteAttestation { + private companion object { + @JvmStatic + private val log: Logger = LoggerFactory.getLogger(RemoteAttestation::class.java) + @JvmStatic + private val SPID = "84D402C36BA9EF9B0A86EF1A9CC8CE4F".hexToBytes() + @JvmStatic + private val LINKABLE_QUOTE = byteArrayOf(0x01, 0x00) + + private const val intelAES = 0 + private const val maxNonceLength = 32 + private const val tlvHeaderSize = 8 + private const val AES_CMAC_FUNC = 1 + private const val secretKeyAttrName = "Secret-Key" + private const val transientKeyPairAttrName = "Transient-Key-Pair" + private const val smkAttrName = "SMK" + + private val mapper = ObjectMapper().registerModule(JavaTimeModule()) + private val crypto = Crypto() + private val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509") + private val keyFactory: KeyFactory = KeyFactory.getInstance("EC") + private val storePassword = (System.getProperty("javax.net.ssl.keyStorePassword") ?: "").toCharArray() + private val isvStore: KeyStore + private val iasStore: KeyStore + private val serviceKeyPair: KeyPair + private val pkixParameters: PKIXParameters + private val ecParameters: ECParameterSpec + + private val iasHost: URI = URI.create("https://${System.getProperty("ias.host", "localhost:8443")}") + private val isDummy = iasHost.host == "localhost" + private val isvKeyAlias = if (isDummy) "jetty" else "isv" + + private val httpRequestConfig: RequestConfig = RequestConfig.custom() + .setConnectTimeout(20_000) + .setSocketTimeout(5_000) + .build() + private val httpSocketConfig: SocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + + init { + ecParameters = (crypto.generateKeyPair().public as ECPublicKey).params + log.info("Elliptic Curve Parameters: {}", ecParameters) + + val keyStoreType = System.getProperty("javax.net.ssl.keyStoreType", "PKCS12") + isvStore = loadKeyStore("javax.net.ssl.keyStore", storePassword, keyStoreType) + serviceKeyPair = loadKeyStoreResource("isv-svc.pfx", storePassword).getKeyPair("isv-svc", storePassword) + + val iasResourceName = if (isDummy) "dummyIAStrust.pfx" else "ias.pfx" + iasStore = loadKeyStoreResource(iasResourceName, storePassword) + + val revocationListOptions = if (isDummy) EnumSet.of(SOFT_FAIL, PREFER_CRLS, NO_FALLBACK) else EnumSet.of(SOFT_FAIL) + pkixParameters = PKIXParameters(iasStore.trustAnchorsFor("ias")).apply { + val rlChecker = CertPathValidator.getInstance("PKIX").revocationChecker as PKIXRevocationChecker + addCertPathChecker(rlChecker.apply { options = revocationListOptions }) + } + } + + private fun loadKeyStore(propertyName: String, password: CharArray, type: String = "PKCS12"): KeyStore { + val fileName = System.getProperty(propertyName) ?: throw IllegalStateException("System property $propertyName not set") + return KeyStore.getInstance(type).apply { + FileInputStream(fileName).use { input -> this.load(input, password) } + } + } + + private fun loadKeyStoreResource(resourceName: String, password: CharArray, type: String = "PKCS12"): KeyStore { + return KeyStore.getInstance(type).apply { + RemoteAttestation::class.java.classLoader.getResourceAsStream(resourceName)?.use { input -> + load(input, password) + } + } + } + + private fun KeyStore.getKeyPair(alias: String, password: CharArray): KeyPair { + val privateKey = getKey(alias, password) as PrivateKey + return KeyPair(getCertificate(alias).publicKey, privateKey) + } + + private fun KeyStore.trustAnchorsFor(vararg aliases: String): Set + = aliases.map { alias -> TrustAnchor(getCertificate(alias) as X509Certificate, null) }.toSet() + } + + @field:Context + private lateinit var httpRequest: HttpServletRequest + + @GET + @Path("/provision") + fun provision(): Response { + log.info("Provisioning requested") + return Response.ok() + .entity(ChallengeResponse(createNonce(), (serviceKeyPair.public as ECPublicKey).toLittleEndian())) + .build() + } + + @POST + @Path("/msg0") + fun receiveMsg0(m: Message0?): Response { + val msg0 = m ?: return responseOf("Message is missing", SC_BAD_REQUEST) + if (intelAES != msg0.extendedGID) { + return responseOf("Unsupported extended GID value", SC_BAD_REQUEST) + } + log.info("Message0 processed") + return Response.ok().build() + } + + /** + * Receives Msg1 and returns Msg2. + */ + @POST + @Path("/msg1") + fun handleMsg1(m: Message1?): Response { + val msg1 = m ?: return responseOf("Message is missing", SC_BAD_REQUEST) + val session = httpRequest.session ?: return responseOf("No session in progress", SC_UNAUTHORIZED) + + log.info("HTTP Session: {}", session.id) + + val revocationList = try { + createHttpClient().use { client -> + val sigRlURI = UriBuilder.fromUri(iasHost) + .path("attestation/sgx/v2/sigrl/{gid}") + .build(msg1.platformGID) + val getSigRL = HttpGet(sigRlURI) + client.execute(getSigRL).use { response -> + if (response.statusLine.statusCode != SC_OK) { + return response.toResponse("Error from Intel Attestation Service (HTTP ${response.statusLine.statusCode})") + } + EntityUtils.toByteArray(response.entity).decodeBase64() + } + } + } catch (e: SSLException) { + log.error("HTTPS error: ${e.message}") + return responseOf("HTTPS connection failed: ${e.message}", SC_FORBIDDEN) + } catch (e: IOException) { + log.error("HTTP client error", e) + return responseOf("HTTP client error: ${e.message}") + } + + val transientKeyPair = crypto.generateKeyPair() + session.setAttribute(transientKeyPairAttrName, transientKeyPair) + + val peerPublicKey = try { + keyFactory.generatePublic(msg1.ga.toBigEndianKeySpec(ecParameters)) as ECPublicKey + } catch (e: IllegalArgumentException) { + return responseOf(e.message ?: "", SC_BAD_REQUEST) + } + session.setAttribute(secretKeyAttrName, crypto.generateSecretKey(transientKeyPair.private, peerPublicKey)) + + log.info("Message1 processed - returning Message2") + + val smk = crypto.generateSMK(transientKeyPair.private, peerPublicKey) + session.setAttribute(smkAttrName, smk) + + val publicKey = transientKeyPair.public as ECPublicKey + val signatureGbGa = signatureOf(publicKey, peerPublicKey) + val gb = publicKey.toLittleEndian() + val msg2 = Message2( + gb = gb, + spid = SPID.toHexString(), + keyDerivationFuncId = AES_CMAC_FUNC, + signatureGbGa = signatureGbGa, + aesCMAC = crypto.aesCMAC(smk, { aes -> + aes.update(gb) + aes.update(SPID) + aes.update(LINKABLE_QUOTE) + aes.update(AES_CMAC_FUNC.toShort().toLittleEndian()) + aes.update(signatureGbGa) + }), + revocationList = revocationList + ) + return Response.ok(msg2).build() + } + + /** + * Receives Msg3 and return Msg4. + */ + @POST + @Path("/msg3") + fun handleMsg3(m: Message3?): Response { + val msg3 = m ?: return responseOf("Message is missing", SC_BAD_REQUEST) + val session = httpRequest.session + ?: return responseOf("No session in progress", SC_UNAUTHORIZED) + log.info("HTTP Session: {}", session.id) + + val secretKey = session.getAttribute(secretKeyAttrName) as? SecretKey + ?: return responseOf("Secret key has not been calculated yet", SC_UNAUTHORIZED) + val smk = session.getAttribute(smkAttrName) as? ByteArray + ?: return responseOf("SMK value has not been calculated yet", SC_UNAUTHORIZED) + validateCMAC(msg3, smk) + + val transientKeyPair = session.getAttribute(transientKeyPairAttrName) as? KeyPair + ?: return responseOf("DH key unavailable", SC_UNAUTHORIZED) + val peerPublicKey = try { + keyFactory.generatePublic(msg3.ga.toBigEndianKeySpec(ecParameters)) + } catch (e: IllegalArgumentException) { + return responseOf(e.message ?: "", SC_BAD_REQUEST) + } + if (crypto.generateSecretKey(transientKeyPair.private, peerPublicKey) != secretKey) { + return responseOf("Keys do not match!", SC_FORBIDDEN) + } + validateNonce(msg3.nonce) + + log.debug("Quote: {}", msg3.quote.toHexArrayString()) + log.debug("Security manifest: {}", msg3.securityManifest?.toHexArrayString()) + log.debug("Nonce: {}", msg3.nonce) + + val report: ReportResponse = try { + createHttpClient().use { client -> + val reportURI = UriBuilder.fromUri(iasHost) + .path("attestation/sgx/v2/report") + .build() + val httpRequest = HttpPost(reportURI) + val reportRequest = ReportRequest( + isvEnclaveQuote = msg3.quote, + pseManifest = msg3.securityManifest?.ifNotZeros(), + nonce = msg3.nonce + ) + httpRequest.entity = StringEntity(mapper.writeValueAsString(reportRequest), ContentType.APPLICATION_JSON) + client.execute(httpRequest).use { httpResponse -> + if (httpResponse.statusLine.statusCode != SC_OK) { + return httpResponse.toResponse("Error from Intel Attestation Service (HTTP ${httpResponse.statusLine.statusCode})") + } + mapper.readValue(validate(httpResponse), ReportResponse::class.java) + } + } + } catch (e: SSLException) { + log.error("HTTPS error: ${e.message}") + return responseOf("HTTPS connection failed: ${e.message}", SC_FORBIDDEN) + } catch (e: IOException) { + log.error("HTTP client error", e) + return responseOf("HTTP client error: ${e.message}") + } + + log.info("Report ID: {}", report.id) + log.info("Quote Status: {}", report.isvEnclaveQuoteStatus) + log.info("Message3 processed - returning Message4") + + val secretIV = crypto.createIV() + val secretData = crypto.encrypt("And now for something completely different!".toByteArray(), secretKey, secretIV) + val platformInfo = report.platformInfoBlob?.removeHeader(tlvHeaderSize) ?: byteArrayOf() + val mk = crypto.generateMK(transientKeyPair.private, peerPublicKey) + val msg4 = Message4( + reportID = report.id, + quoteStatus = report.isvEnclaveQuoteStatus.toString(), + quoteBody = report.isvEnclaveQuoteBody, + aesCMAC = crypto.aesCMAC(mk, { aes -> + aes.update(platformInfo) + }), + securityManifestStatus = report.pseManifestStatus?.toString(), + securityManifestHash = report.pseManifestHash, + platformInfo = platformInfo, + epidPseudonym = report.epidPseudonym, + nonce = report.nonce, + secret = secretData.encryptedData(), + secretHash = secretData.authenticationTag(), + secretIV = secretIV, + timestamp = report.timestamp + ) + return Response.ok(msg4) + .build() + } + + private fun createNonce(): String = UUID.randomUUID().let { uuid -> + String.format("%016x%016x", uuid.mostSignificantBits, uuid.leastSignificantBits) + } + + private fun validateNonce(n: String?) { + val nonce = n ?: return + if (nonce.length > maxNonceLength) { + throw BadRequestException(responseOf("Nonce is too large: maximum $maxNonceLength digits", SC_BAD_REQUEST)) + } + } + + private fun validateCMAC(msg3: Message3, smk: ByteArray) { + val cmac = crypto.aesCMAC(smk, { aes -> + aes.update(msg3.ga) + aes.update(msg3.securityManifest ?: byteArrayOf()) + aes.update(msg3.quote) + }) + if (!cmac.contentEquals(msg3.aesCMAC)) { + throw BadRequestException(responseOf("Incorrect CMAC value", SC_BAD_REQUEST)) + } + } + + private fun validate(response: HttpResponse): String { + return EntityUtils.toByteArray(response.entity).let { payload -> + val iasSignature = response.requireHeader("X-IASReport-Signature") + val iasSigningCertificate = response.requireHeader("X-IASReport-Signing-Certificate").decodeURL() + + val certificatePath = try { + parseCertificates(iasSigningCertificate) + } catch (e: CertificateException) { + log.error("Failed to parse certificate from HTTP header '{}': {}", iasSigningCertificate, e.message) + throw ForbiddenException(response.toResponse("Invalid X-IASReport HTTP headers", SC_FORBIDDEN)) + } + + try { + val certValidator = CertPathValidator.getInstance("PKIX") + certValidator.validate(certificatePath, pkixParameters) + } catch (e: GeneralSecurityException) { + log.error("Certificate '{}' is invalid: {}", certificatePath, e.message) + throw ForbiddenException(response.toResponse("Invalid IAS certificate", SC_FORBIDDEN)) + } + + val signature = try { + Signature.getInstance("SHA256withRSA").apply { + initVerify(certificatePath.certificates[0]) + } + } catch (e: GeneralSecurityException) { + log.error("Failed to initialise signature: {}", e.message) + throw ForbiddenException(response.toResponse("", SC_FORBIDDEN)) + } + + try { + signature.update(payload) + if (!signature.verify(iasSignature.toByteArray().decodeBase64())) { + throw ForbiddenException(response.toResponse("Report failed IAS signature check", SC_FORBIDDEN)) + } + } catch (e: SignatureException) { + log.error("Failed to parse signature from IAS: {}", e.message) + throw ForbiddenException(response.toResponse("Corrupt IAS signature data", SC_FORBIDDEN)) + } + + String(payload, UTF_8) + } + } + + private fun parseCertificates(iasCertificateHeader: String): CertPath { + val certificates = mutableListOf() + iasCertificateHeader.byteInputStream().use { input -> + while (input.available() > 0) { + certificates.add(certificateFactory.generateCertificate(input)) + } + } + return certificateFactory.generateCertPath(certificates) + } + + private fun responseOf(message: String, statusCode: Int = SC_INTERNAL_SERVER_ERROR): Response = Response.status(statusCode) + .entity(AttestationError(message)) + .build() + + private fun HttpResponse.requireHeader(name: String): String { + return (this.getFirstHeader(name) ?: throw ForbiddenException(toResponse("Response header '$name' missing", SC_FORBIDDEN))).value + } + + private fun HttpResponse.toResponse(message: String, statusCode: Int = statusLine.statusCode): Response { + return Response.status(statusCode) + .entity(AttestationError(message)) + .apply { + val requestIdHeader = getFirstHeader("Request-ID") ?: return@apply + this.header(requestIdHeader.name, requestIdHeader.value) + } + .build() + } + + private fun createHttpClient(): CloseableHttpClient { + val sslContext = SSLContextBuilder() + .loadKeyMaterial(isvStore, storePassword, { _, _ -> isvKeyAlias }) + .setSecureRandom(crypto.random) + .build() + val registry = RegistryBuilder.create() + .register("https", SSLConnectionSocketFactory(sslContext, getDefaultHostnameVerifier())) + .build() + return HttpClients.custom() + .setConnectionManager(BasicHttpClientConnectionManager(registry).apply { + socketConfig = httpSocketConfig + }) + .setDefaultRequestConfig(httpRequestConfig) + .build() + } + + private fun signatureOf(publicKey: ECPublicKey, peerKey: ECPublicKey): ByteArray { + val signature = Signature.getInstance("SHA256WithECDSA").let { signer -> + signer.initSign(serviceKeyPair.private, crypto.random) + signer.update(publicKey.toLittleEndian()) + signer.update(peerKey.toLittleEndian()) + signer.sign() + } + return ByteBuffer.allocate(KEY_SIZE).let { buf -> + ASN1InputStream(signature).use { input -> + for (number in input.readObject() as DLSequence) { + val pos = (number as ASN1Integer).positiveValue.toLittleEndian(KEY_SIZE / 2) + buf.put(pos) + } + buf.array() + } + } + } + + private fun ByteArray.decodeBase64(): ByteArray = Base64.getDecoder().decode(this) + private fun String.decodeURL(): String = URLDecoder.decode(this, "UTF-8") + private fun ByteArray.removeHeader(headerSize: Int) = copyOfRange(headerSize, size) + private fun ByteArray.ifNotZeros(): ByteArray? { + for (i in 0 until size) { + if (this[i] != 0.toByte()) return this + } + return null + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/SgxApplication.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/SgxApplication.kt new file mode 100644 index 0000000000..7cbd2fdbae --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/SgxApplication.kt @@ -0,0 +1,13 @@ +package net.corda.attestation + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.security.Security +import javax.ws.rs.ApplicationPath +import javax.ws.rs.core.Application + +@ApplicationPath("/") +class SgxApplication : Application() { + init { + Security.addProvider(BouncyCastleProvider()) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/AttestationError.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/AttestationError.kt new file mode 100644 index 0000000000..eedf3a29c2 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/AttestationError.kt @@ -0,0 +1,7 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("message") +class AttestationError(@param:JsonProperty("message") val message: String) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ChallengeResponse.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ChallengeResponse.kt new file mode 100644 index 0000000000..f6c6943452 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ChallengeResponse.kt @@ -0,0 +1,16 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("nonce", "serviceKey") +@JsonInclude(NON_NULL) +class ChallengeResponse( + @param:JsonProperty("nonce") + val nonce: String, + + @param:JsonProperty("serviceKey") + val serviceKey: ByteArray +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message0.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message0.kt new file mode 100644 index 0000000000..840f695e29 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message0.kt @@ -0,0 +1,13 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("extendedGID") +@JsonInclude(NON_NULL) +class Message0( + @param:JsonProperty("extendedGID") + val extendedGID: Int +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message1.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message1.kt new file mode 100644 index 0000000000..a5d58867cf --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message1.kt @@ -0,0 +1,15 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("ga", "platformGID") +class Message1( + // The client's 512 bit public DH key (Little Endian) + @param:JsonProperty("ga") + val ga: ByteArray, + + // Platform GID value from the SGX client + @param:JsonProperty("platformGID") + val platformGID: String +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message2.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message2.kt new file mode 100644 index 0000000000..d1f5e13cdb --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message2.kt @@ -0,0 +1,37 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder( + "gb", + "spid", + "linkableQuote", + "keyDerivationFuncId", + "signatureGbGa", + "aesCMAC", + "revocationList" +) +class Message2( + // The server's 512 bit public DH key (Little Endian) + @param:JsonProperty("gb") + val gb: ByteArray, + + @param:JsonProperty("spid") + val spid: String, + + @param:JsonProperty("linkableQuote") + val linkableQuote: Boolean = true, + + @param:JsonProperty("keyDerivationFuncId") + val keyDerivationFuncId: Int, + + @param:JsonProperty("signatureGbGa") + val signatureGbGa: ByteArray, // Not ASN.1 encoded + + @param:JsonProperty("aesCMAC") + val aesCMAC: ByteArray, // Not ASN.1 encoded + + @param:JsonProperty("revocationList") + val revocationList: ByteArray +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message3.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message3.kt new file mode 100644 index 0000000000..609472f0b2 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message3.kt @@ -0,0 +1,26 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("aesCMAC", "ga", "quote", "securityManifest", "nonce") +@JsonInclude(NON_NULL) +class Message3( + @param:JsonProperty("aesCMAC") + val aesCMAC: ByteArray, // Not ASN.1 encoded + + // The client's 512 bit public DH key (Little Endian) + @param:JsonProperty("ga") + val ga: ByteArray, + + @param:JsonProperty("quote") + val quote: ByteArray, + + @param:JsonProperty("securityManifest") + val securityManifest: ByteArray? = null, + + @param:JsonProperty("nonce") + val nonce: String? = null +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message4.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message4.kt new file mode 100644 index 0000000000..c17232b205 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/Message4.kt @@ -0,0 +1,67 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.annotation.JsonFormat +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder +import java.time.LocalDateTime + +@JsonPropertyOrder( + "reportID", + "quoteStatus", + "quoteBody", + "aesCMAC", + "securityManifestStatus", + "securityManifestHash", + "platformInfo", + "epidPseudonym", + "nonce", + "secret", + "secretHash", + "secretIV", + "timestamp" +) +@JsonInclude(NON_NULL) +class Message4( + @param:JsonProperty("reportID") + val reportID: String, + + @param:JsonProperty("quoteStatus") + val quoteStatus: String, + + @param:JsonProperty("quoteBody") + val quoteBody: ByteArray, + + @param:JsonProperty("aesCMAC") + val aesCMAC: ByteArray, + + @param:JsonProperty("securityManifestStatus") + val securityManifestStatus: String? = null, + + @param:JsonProperty("securityManifestHash") + val securityManifestHash: String? = null, + + @param:JsonProperty("platformInfo") + @get:JsonInclude(NON_EMPTY) + val platformInfo: ByteArray? = null, + + @param:JsonProperty("epidPseudonym") + val epidPseudonym: ByteArray? = null, + + @param:JsonProperty("nonce") + val nonce: String? = null, + + @param:JsonProperty("secret") + val secret: ByteArray, + + @param:JsonProperty("secretHash") + val secretHash: ByteArray, + + @param:JsonProperty("secretIV") + val secretIV: ByteArray, + + @param:JsonProperty("timestamp") + @field:JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS", timezone = "UTC") + val timestamp: LocalDateTime +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/HexadecimalSerialisers.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/HexadecimalSerialisers.kt new file mode 100644 index 0000000000..ccc0ac55d7 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/HexadecimalSerialisers.kt @@ -0,0 +1,19 @@ +@file:JvmName("HexadecimalSerialisers") +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import net.corda.attestation.hexToBytes +import net.corda.attestation.toHexString + +class HexadecimalSerialiser : StdSerializer(ByteArray::class.java) { + override fun serialize(value: ByteArray, gen: JsonGenerator, provider: SerializerProvider) = gen.writeString(value.toHexString()) +} + +class HexadecimalDeserialiser : StdDeserializer(ByteArray::class.java) { + override fun deserialize(p: JsonParser, context: DeserializationContext) = p.valueAsString.hexToBytes() +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ManifestStatus.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ManifestStatus.kt new file mode 100644 index 0000000000..ec707be9cc --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ManifestStatus.kt @@ -0,0 +1,10 @@ +package net.corda.attestation.message.ias + +enum class ManifestStatus { + OK, + UNKNOWN, + INVALID, + OUT_OF_DATE, + REVOKED, + RL_VERSION_MISMATCH +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/QuoteStatus.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/QuoteStatus.kt new file mode 100644 index 0000000000..c84d27fec8 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/QuoteStatus.kt @@ -0,0 +1,11 @@ +package net.corda.attestation.message.ias + +enum class QuoteStatus { + OK, + SIGNATURE_INVALID, + GROUP_REVOKED, + SIGNATURE_REVOKED, + KEY_REVOKED, + SIGRL_VERSION_MISMATCH, + GROUP_OUT_OF_DATE +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportProxyResponse.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportProxyResponse.kt new file mode 100644 index 0000000000..6ddac73ce3 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportProxyResponse.kt @@ -0,0 +1,19 @@ +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("signature", "certificatePath", "report") +@JsonInclude(NON_NULL) +class ReportProxyResponse( + @param:JsonProperty("signature") + val signature: String, + + @param:JsonProperty("certificatePath") + val certificatePath: String, + + @param:JsonProperty("report") + val report: ByteArray +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportRequest.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportRequest.kt new file mode 100644 index 0000000000..7bf5002795 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportRequest.kt @@ -0,0 +1,19 @@ +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("isvEnclaveQuote", "pseManifest", "nonce") +@JsonInclude(NON_NULL) +class ReportRequest( + @param:JsonProperty("isvEnclaveQuote") + val isvEnclaveQuote: ByteArray, + + @param:JsonProperty("pseManifest") + val pseManifest: ByteArray? = null, + + @param:JsonProperty("nonce") + val nonce: String? = null +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportResponse.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportResponse.kt new file mode 100644 index 0000000000..bdff14718e --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/ReportResponse.kt @@ -0,0 +1,58 @@ +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.annotation.JsonFormat +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import java.time.LocalDateTime + +@JsonPropertyOrder( + "nonce", + "id", + "timestamp", + "epidPseudonym", + "isvEnclaveQuoteStatus", + "isvEnclaveQuoteBody", + "pseManifestStatus", + "pseManifestHash", + "platformInfoBlob", + "revocationReason" +) +@JsonInclude(NON_NULL) +class ReportResponse( + @param:JsonProperty("id") + val id: String, + + @param:JsonProperty("isvEnclaveQuoteStatus") + val isvEnclaveQuoteStatus: QuoteStatus, + + @param:JsonProperty("isvEnclaveQuoteBody") + val isvEnclaveQuoteBody: ByteArray, + + @param:JsonProperty("platformInfoBlob") + @get:JsonSerialize(using = HexadecimalSerialiser::class) + @get:JsonDeserialize(using = HexadecimalDeserialiser::class) + val platformInfoBlob: ByteArray? = null, + + @param:JsonProperty("revocationReason") + val revocationReason: Int? = null, + + @param:JsonProperty("pseManifestStatus") + val pseManifestStatus: ManifestStatus? = null, + + @param:JsonProperty("pseManifestHash") + val pseManifestHash: String? = null, + + @param:JsonProperty("nonce") + val nonce: String? = null, + + @param:JsonProperty("epidPseudonym") + val epidPseudonym: ByteArray? = null, + + @param:JsonProperty("timestamp") + @field:JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS", timezone = "UTC") + val timestamp: LocalDateTime +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponse.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponse.kt new file mode 100644 index 0000000000..aa046d56f5 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponse.kt @@ -0,0 +1,16 @@ +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.* +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder("spid", "revocationList") +@JsonInclude(NON_NULL) +class RevocationListProxyResponse ( + @param:JsonProperty("spid") + val spid: String, + + @param:JsonProperty("revocationList") + val revocationList: ByteArray +) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/MockIAS.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/MockIAS.kt new file mode 100644 index 0000000000..c0ef8e0859 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/MockIAS.kt @@ -0,0 +1,59 @@ +package net.corda.mockias + +import net.corda.attestation.message.ias.ManifestStatus +import net.corda.attestation.message.ias.QuoteStatus +import net.corda.attestation.message.ias.ReportRequest +import net.corda.attestation.message.ias.ReportResponse +import java.time.Clock +import java.time.LocalDateTime +import javax.servlet.http.HttpServletResponse.* +import javax.ws.rs.* +import javax.ws.rs.core.MediaType.* +import javax.ws.rs.core.Response + +@Path("/attestation/sgx/v2") +class MockIAS { + private companion object { + private const val requestID = "de305d5475b4431badb2eb6b9e546014" + private const val QUOTE_BODY_SIZE = 432 + + private val platformInfo = byteArrayOf(0x12, 0x34, 0x56, 0x78, 0x9a.toByte(), 0xbc.toByte(), 0xde.toByte(), 0xf0.toByte(), 0x11, 0x22) + } + + @GET + @Path("/sigrl/{gid}") + fun getSigRL(@PathParam("gid") gid: String): Response { + return Response.ok(if (gid.toLowerCase() == "0000000b") "" else "AAIADgAAAAEAAAABAAAAAGSf/es1h/XiJeCg7bXmX0S/NUpJ2jmcEJglQUI8VT5sLGU7iMFu3/UTCv9uPADal3LhbrQvhBa6+/dWbj8hnsE=") + .header("Request-ID", requestID) + .build() + } + + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + @IASReport + @POST + @Path("/report") + fun getReport(req: ReportRequest?): Response { + val request = req ?: return Response.status(SC_BAD_REQUEST) + .header("Request-ID", requestID) + .build() + val report = ReportResponse( + id = "9497457846286849067596886882708771068", + isvEnclaveQuoteStatus = QuoteStatus.OK, + isvEnclaveQuoteBody = if (request.isvEnclaveQuote.size > QUOTE_BODY_SIZE) + request.isvEnclaveQuote.copyOf(QUOTE_BODY_SIZE) + else + request.isvEnclaveQuote, + pseManifestStatus = req.pseManifest?.toStatus(), + platformInfoBlob = platformInfo, + nonce = request.nonce, + timestamp = LocalDateTime.now(Clock.systemUTC()) + ) + return Response.ok(report) + .header("Request-ID", requestID) + .build() + } + + private fun ByteArray.toStatus(): ManifestStatus + = if (this.isEmpty() || this[0] == 0.toByte()) ManifestStatus.INVALID else ManifestStatus.OK +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/ReportSigner.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/ReportSigner.kt new file mode 100644 index 0000000000..33f5fe24dc --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/ReportSigner.kt @@ -0,0 +1,72 @@ +@file:JvmName("ReportSigner") +package net.corda.mockias + +import net.corda.mockias.io.SignatureOutputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.net.URLEncoder +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.KeyStore +import java.security.PrivateKey +import java.security.Signature +import java.security.cert.Certificate +import java.util.* +import javax.ws.rs.NameBinding +import javax.ws.rs.ext.Provider +import javax.ws.rs.ext.WriterInterceptor +import javax.ws.rs.ext.WriterInterceptorContext + + +@Provider +@IASReport +class ReportSigner : WriterInterceptor { + private companion object { + private const val BEGIN_CERT = "-----BEGIN CERTIFICATE-----\n" + private const val END_CERT = "\n-----END CERTIFICATE-----\n" + private const val signatureAlias = "ias" + private val storePassword = "attestation".toCharArray() + private val keyStore: KeyStore = KeyStore.getInstance("PKCS12").apply { + ReportSigner::class.java.classLoader.getResourceAsStream("dummyIAS.pfx")?.use { input -> + load(input, storePassword) + } + } + private val signingKey = keyStore.getKey(signatureAlias, storePassword) as PrivateKey + private val signingCertHeader: String = keyStore.getCertificateChain(signatureAlias).let { chain -> + StringBuilder().apply { + chain.forEach { cert -> append(cert.toPEM()) } + }.toString().encodeURL() + } + + private fun ByteArray.encodeBase64(): ByteArray = Base64.getEncoder().encode(this) + private fun String.encodeURL(): String = URLEncoder.encode(this, "UTF-8") + + private fun Certificate.toPEM(): String = ByteArrayOutputStream().let { out -> + out.write(BEGIN_CERT.toByteArray()) + out.write(encoded.encodeBase64()) + out.write(END_CERT.toByteArray()) + String(out.toByteArray(), UTF_8) + } + } + + @Throws(IOException::class) + override fun aroundWriteTo(context: WriterInterceptorContext) { + val contentStream = context.outputStream + val baos = ByteArrayOutputStream() + val signature = Signature.getInstance("SHA256withRSA").apply { + initSign(signingKey) + } + context.outputStream = SignatureOutputStream(baos, signature) + try { + context.proceed() + context.headers?.apply { + add("X-IASReport-Signature", signature.sign().encodeBase64().toString(UTF_8)) + add("X-IASReport-Signing-Certificate", signingCertHeader) + } + } finally { + baos.writeTo(contentStream) + } + } +} + +@NameBinding +annotation class IASReport \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/io/SignatureOutputStream.kt b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/io/SignatureOutputStream.kt new file mode 100644 index 0000000000..c923a039af --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/kotlin/net/corda/mockias/io/SignatureOutputStream.kt @@ -0,0 +1,42 @@ +package net.corda.mockias.io + +import java.io.FilterOutputStream +import java.io.IOException +import java.io.OutputStream +import java.security.Signature +import java.security.SignatureException + +class SignatureOutputStream(wrapped: OutputStream, val signature: Signature) : FilterOutputStream(wrapped) { + @Throws(IOException::class) + override fun write(data: Int) { + try { + signature.update(data.toByte()) + } catch (e: SignatureException) { + throw IOException(e.message, e) + } + super.write(data) + } + + @Throws(IOException::class) + override fun write(data: ByteArray, offset: Int, length: Int) { + try { + signature.update(data, offset, length) + } catch (e: SignatureException) { + throw IOException(e.message, e) + } + super.out.write(data, offset, length) + } + + @Throws(IOException::class) + override fun flush() { + super.flush() + } + + @Throws(IOException::class) + override fun close() { + super.close() + } + + @Throws(SignatureException::class) + fun sign(): ByteArray = signature.sign() +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/resources/dummyIAS.pfx b/sgx-jvm/remote-attestation/attestation-server/src/main/resources/dummyIAS.pfx new file mode 100644 index 0000000000000000000000000000000000000000..d9aea1caeb6cd42a18deb804e3d99b562426bdb4 GIT binary patch literal 2401 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 E-&r1izyJUM literal 0 HcmV?d00001 diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/resources/dummyIAStrust.pfx b/sgx-jvm/remote-attestation/attestation-server/src/main/resources/dummyIAStrust.pfx new file mode 100644 index 0000000000000000000000000000000000000000..0e46286671e7f0f32c9926881fa465e4c9f616b1 GIT binary patch literal 1002 zcmV2`Yw2hW8Bt2LUiC1_~;MNQUx`E1#m~E z6d=Mn%CAcx_HJ$7@5%Z;D)iVYY~XDbil3Q5p=E7yQN$R){$HSV|HJfHij=`v?UENb z@Eaxo6Vem4lzInAa(WP?Bp$IXv#^Q%0t-&0ktUcEt5-Cd;luqjxVca~g1Y3xS)_vJ z7C06G1tqH8_KdJUwg@pSQrzAU?OxgVOd(t27VLrUH))wL4Ouipxd_dN=nQQ3HOlp! z;^LywNJgJ9dCdlEgdo?g0h9<(2?Ps(vMaC>poQYId1BaJgp`HbO{_lY(#ZKlLU>VL z`09nIOT&&=$nqpPRYtiRQ=^c;O5B;>*bddacq~WYbwI~cb+bms3C`^}>D*Yoh4eNU zJ*XPQPDM9c#L~w%|}rO*GI9 z3FU?Z5{b=^jjZtkZn)r1V@hB(5+DN8LArEt5Xr~QLWBl7s0=D=tWFWCse7vE6P zifofXn##2dh*H--9V0xZ=1r}$3_h zobXU?tPI;}r7%7)AutIB1uG5%0vZJX1QdeVfmU*f%e0Ijir$?>%4RDGkc$Kq@x-T# YCtsxiwa7~{pLy;is?Y8<0s{etpy?UZ%K!iX literal 0 HcmV?d00001 diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/resources/log4j2.xml b/sgx-jvm/remote-attestation/attestation-server/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..5711c1a9c8 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/resources/log4j2.xml @@ -0,0 +1,49 @@ + + + + + ${sys:attestation.home} + attestation-server + ${sys:log-path}/archive + info + debug + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/security.properties b/sgx-jvm/remote-attestation/attestation-server/src/main/security.properties new file mode 100644 index 0000000000..28b8893e70 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/security.properties @@ -0,0 +1 @@ +crypto.policy=unlimited diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/.gitignore b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/.gitignore new file mode 100644 index 0000000000..92409802c8 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/.gitignore @@ -0,0 +1 @@ +client.key diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/AttestationReportSigningCACert.pem b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/AttestationReportSigningCACert.pem new file mode 100644 index 0000000000..27332a1d1f --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/AttestationReportSigningCACert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV +BAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0 +YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy +MzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL +U2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD +DCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G +CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR+tXc8u1EtJzLA10Feu1Wg+p7e +LmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh +rgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT +L/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe +NpEJUmg4ktal4qgIAxk+QHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ +byinkNndn+Bgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H +afuVeLHcDsRp6hol4P+ZFIhu8mmbI1u0hH3W/0C2BuYXB5PC+5izFFh/nP0lc2Lf +6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM +RoOaX4AS+909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX +MFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50 +L0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW +BBR4Q3t2pn680K9+QjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9+Qjfr +NXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq +hkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir +IEqucRiJSSx+HjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi+ripMtPZ +sFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi +zLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra +Ud4APK0wZTGtfPXU7w+IBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA +152Sq049ESDz+1rRGc2NVEqh1KaGXmtXvqxXcTB+Ljy5Bw2ke0v8iGngFBPqCTVB +3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5+xmBc388v9Dm21HGfcC8O +DD+gT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R+mJTLwPXVMrv +DaVzWh5aiEx+idkSGMnX +-----END CERTIFICATE----- diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/client.crt b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/client.crt new file mode 100644 index 0000000000..0e1132974f --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/client.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDhDCCAmygAwIBAgIJAMJY5H3wxR1xMA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMQswCQYDVQQKDAJSMzEQMA4GA1UE +AwwHUjMgVGVzdDAeFw0xNzEwMjUxMzEwMjJaFw0xODEwMjUxMzEwMjJaMEExCzAJ +BgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMQswCQYDVQQKDAJSMzEQMA4G +A1UEAwwHUjMgVGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALqc +lg7AXm5QPdJ/Yl3oUsIC+kfU4+OV6cvmJjAoThgah64h2bDjl7bYKnssSGbxZ2kH +21jTP6PxxB27XAPlkgGJEx0F7+Ss8Tq8XxtgfpARVNbQvwt4R7iujbeAS/1QIbJc +xpCCVsp4rAfWKV/DqzfBpSi/Rhdw5YmCfbsVZ/1GBY8kFDCI/KMLvZ67y1ZbqBYN +c146xp6uHxCiW5xhyOaIEgoefVyLCpK/SjzN9a3dQHyz07axOayjSRyK/gM8WDRU +K1+oiJOIvhp5zX7H5D5eRgksTNH3bwN24TVob63ltcs/N7MC/i09Omq+u9E99yo9 +7JqSCzwnanxwafLYVbcCAwEAAaN/MH0wCwYDVR0PBAQDAgKkMB0GA1UdDgQWBBQ0 +Rq9OPXGihG1iE6HUm7aMKorgeTAfBgNVHSMEGDAWgBQ0Rq9OPXGihG1iE6HUm7aM +KorgeTAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDwYDVR0TBAgwBgEB +/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAFUr+hlaixchIQZkzXzvCZFu+gfe0I6YJ +rZd9RKUJQfNteVoC29emEORQazjOn69Q2T9iNFy3COHQrzHhS0kZZEdm72283Yj5 +h6C/krEp784/cjWRObDcu8CP8o9FdFcXZb52NpC+xlTv9Re8rhV8NcitlGW2z3eW +9mBVePSvhilL0ssPXjfDQY2GALck5+ZxiA8gAmzvCkoI23fxUTwQ3u+qhjyPxjCk +kST2Ir7ilCNdeZTJErQhBtcQY+K39W5C98aVSJQDRDgeTT5nClBBMBlMrVE4ohaO +eMgJJkraVMDXiJqXXG1YccsoXb5ezmPCYFoTPOPtY7TyPMZb1Jxgag== +-----END CERTIFICATE----- diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/generate-keystores.sh b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/generate-keystores.sh new file mode 100755 index 0000000000..c8c8f268fe --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/ssl/intel-ssl/generate-keystores.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +rm -f isv.pfx ias.pfx + +ALIAS=isv +KEYPASS=attestation +STOREPASS=attestation + +DIRHOME=$(dirname $0) +KEYFILE=${DIRHOME}/client.key +CRTFILE=${DIRHOME}/client.crt +INTEL_CRTFILE=${DIRHOME}/AttestationReportSigningCACert.pem + +openssl verify -x509_strict -purpose sslclient -CAfile ${CRTFILE} ${CRTFILE} + +if [ ! -r ${KEYFILE} ]; then + echo "Development private key missing. This is the key that IAS expects our HTTP client to be using for Mutual TLS." + exit 1 +fi + +openssl pkcs12 -export -out client.pfx -inkey ${KEYFILE} -in ${CRTFILE} -passout pass:${STOREPASS} + +keytool -importkeystore -srckeystore client.pfx -srcstoretype pkcs12 -destkeystore isv.pfx -deststoretype pkcs12 -srcstorepass ${STOREPASS} -deststorepass ${STOREPASS} + +keytool -keystore isv.pfx -storetype pkcs12 -changealias -alias 1 -destalias ${ALIAS} -storepass ${STOREPASS} + +rm -rf client.pfx + +# Generate trust store for connecting with IAS. +if [ -r ${INTEL_CRTFILE} ]; then + keytool -import -keystore ias.pfx -storetype pkcs12 -file ${INTEL_CRTFILE} -alias ias -storepass ${STOREPASS} < + + + + + diff --git a/sgx-jvm/remote-attestation/attestation-server/src/main/webapp/WEB-INF/web.xml b/sgx-jvm/remote-attestation/attestation-server/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..5ef0bb1716 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,9 @@ + + + + 10 + + diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/ByteUtils.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/ByteUtils.kt new file mode 100644 index 0000000000..e20a9c953c --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/ByteUtils.kt @@ -0,0 +1,10 @@ +@file:JvmName("ByteUtils") +package net.corda.attestation + +fun byteArray(size: Int, of: Int) = ByteArray(size, { of.toByte() }) + +fun unsignedByteArrayOf(vararg values: Int) = ByteArray(values.size).apply { + for (i in 0 until values.size) { + this[i] = values[i].toByte() + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/CryptoProvider.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/CryptoProvider.kt new file mode 100644 index 0000000000..d8482e0492 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/CryptoProvider.kt @@ -0,0 +1,16 @@ +package net.corda.attestation + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.security.Security + +class CryptoProvider : TestRule { + override fun apply(statement: Statement, description: Description?): Statement { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(BouncyCastleProvider()) + } + return statement + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/CryptoTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/CryptoTest.kt new file mode 100644 index 0000000000..30b2d06bac --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/CryptoTest.kt @@ -0,0 +1,103 @@ +package net.corda.attestation + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.nio.charset.StandardCharsets.* +import java.security.KeyFactory +import java.security.interfaces.ECPublicKey + +class CryptoTest { + private lateinit var keyFactory: KeyFactory + private lateinit var crypto: Crypto + + @Rule + @JvmField + val cryptoProvider = CryptoProvider() + + @Before + fun setup() { + keyFactory = KeyFactory.getInstance("EC") + crypto = Crypto() + } + + @Test + fun testKeyConversions() { + val keyPair = crypto.generateKeyPair() + val ecPublicKey = keyPair.public as ECPublicKey + val ecParameters = ecPublicKey.params + val bytes = ecPublicKey.toLittleEndian() + val resultKey = keyFactory.generatePublic(bytes.toBigEndianKeySpec(ecParameters)) as ECPublicKey + assertEquals(ecPublicKey, resultKey) + assertArrayEquals(ecPublicKey.encoded, resultKey.encoded) + } + + @Test + fun testSharedSecret() { + val keyPairA = crypto.generateKeyPair() + val keyPairB = crypto.generateKeyPair() + + val secretA = crypto.generateSharedSecret(keyPairA.private, keyPairB.public) + val secretB = crypto.generateSharedSecret(keyPairB.private, keyPairA.public) + assertArrayEquals(secretB, secretA) + assertEquals(32, secretA.size) + } + + @Test + fun testEncryption() { + val keyPairA = crypto.generateKeyPair() + val keyPairB = crypto.generateKeyPair() + val secretKeyA = crypto.generateSecretKey(keyPairA.private, keyPairB.public) + val secretKeyB = crypto.generateSecretKey(keyPairB.private, keyPairA.public) + assertEquals(secretKeyA, secretKeyB) + assertArrayEquals(secretKeyA.encoded, secretKeyB.encoded) + + val iv = crypto.createIV() + val data = crypto.encrypt("Sooper secret string value!".toByteArray(), secretKeyA, iv) + assertEquals("Sooper secret string value!", String(crypto.decrypt(data, secretKeyB, iv), UTF_8)) + } + + @Test + fun testAesCMAC() { + val messageBytes = "Hello World".toByteArray() + val keyBytes = "0123456789012345".toByteArray() + val cmac = crypto.aesCMAC(keyBytes, messageBytes) + assertArrayEquals("3AFAFFFC4EB9274ABD6C9CC3D8B6984A".hexToBytes(), cmac) + } + + @Test + fun testToHexArrayString() { + val bytes = unsignedByteArrayOf(0xf5, 0x04, 0x83, 0x71) + assertEquals("[0xf5,0x04,0x83,0x71]", bytes.toHexArrayString()) + } + + @Test + fun `test vectors from RFC4493 and NIST 800-38B`() { + // Key as defined on https://tools.ietf.org/html/rfc4493.html#appendix-A + val testKey = "2b7e151628aed2a6abf7158809cf4f3c".hexToBytes() + + // Example 1: len = 0 + val out0 = crypto.aesCMAC(testKey, ByteArray(0)) + assertArrayEquals("bb1d6929e95937287fa37d129b756746".hexToBytes(), out0) + + // Example 2: len = 16 + val out16 = crypto.aesCMAC(testKey, "6bc1bee22e409f96e93d7e117393172a".hexToBytes()) + assertArrayEquals("070a16b46b4d4144f79bdd9dd04a287c".hexToBytes(), out16) + + // Example 3: len = 40 + val messageBytes40 = ("6bc1bee22e409f96e93d7e117393172a" + + "ae2d8a571e03ac9c9eb76fac45af8e51" + + "30c81c46a35ce411").hexToBytes() + val out40 = crypto.aesCMAC(testKey, messageBytes40) + assertArrayEquals("dfa66747de9ae63030ca32611497c827".hexToBytes(), out40) + + // Example 4: len = 64 + val messageBytes64 = ("6bc1bee22e409f96e93d7e117393172a" + + "ae2d8a571e03ac9c9eb76fac45af8e51" + + "30c81c46a35ce411e5fbc1191a0a52ef" + + "f69f2445df4f9b17ad2b417be66c3710").hexToBytes() + val out64 = crypto.aesCMAC(testKey, messageBytes64) + assertArrayEquals("51f0bebf7e3b9d92fc49741779363cfe".hexToBytes(), out64) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/DummyRandom.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/DummyRandom.kt new file mode 100644 index 0000000000..ad05129616 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/DummyRandom.kt @@ -0,0 +1,15 @@ +package net.corda.attestation + +import java.security.SecureRandom +import java.util.* + +class DummyRandom : SecureRandom(byteArrayOf()) { + override fun nextBoolean() = false + override fun nextInt() = 9 + override fun nextInt(bound: Int) = 0 + override fun nextBytes(bytes: ByteArray) = Arrays.fill(bytes, 9) + override fun nextDouble() = 9.0 + override fun nextFloat() = 9.0f + override fun nextLong() = 9L + override fun nextGaussian() = 0.0 +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/EndianUtilsTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/EndianUtilsTest.kt new file mode 100644 index 0000000000..2ea1634098 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/EndianUtilsTest.kt @@ -0,0 +1,86 @@ +package net.corda.attestation + +import org.junit.Assert.* +import org.junit.Test +import sun.security.ec.ECPublicKeyImpl +import java.math.BigInteger +import java.security.KeyPairGenerator +import java.security.interfaces.ECPublicKey +import java.security.spec.ECGenParameterSpec +import java.security.spec.ECPoint + +class EndianUtilsTest { + @Test + fun testBigIntegerToLittleEndian() { + val source = BigInteger("8877665544332211", 16) + assertArrayEquals(unsignedByteArrayOf(0x11), source.toLittleEndian(1)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22), source.toLittleEndian(2)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33), source.toLittleEndian(3)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44), source.toLittleEndian(4)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44, 0x55), source.toLittleEndian(5)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44, 0x55, 0x66), source.toLittleEndian(6)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77), source.toLittleEndian(7)) + assertArrayEquals(unsignedByteArrayOf(0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88), source.toLittleEndian(8)) + } + + @Test + fun testRemovingLeadingZeros() { + assertArrayEquals(byteArrayOf(), byteArrayOf().removeLeadingZeros()) + assertArrayEquals(byteArrayOf(), byteArrayOf(0x00, 0x00, 0x00, 0x00).removeLeadingZeros()) + assertArrayEquals(byteArrayOf(0x7F, 0x63), byteArrayOf(0x00, 0x00, 0x7F, 0x63).removeLeadingZeros()) + assertArrayEquals(byteArrayOf(0x7F, 0x43), byteArrayOf(0x7F, 0x43).removeLeadingZeros()) + } + + @Test + fun testUnsignedBytes() { + val source = BigInteger("FFAA", 16) + assertArrayEquals(unsignedByteArrayOf(0xFF, 0xAA), source.toUnsignedBytes()) + } + + @Test + fun testToHexString() { + val source = unsignedByteArrayOf(0xFF, 0xAA, 0x00) + assertEquals("FFAA00", source.toHexString().toUpperCase()) + } + + @Test + fun testShortToLittleEndian() { + assertArrayEquals(unsignedByteArrayOf(0x0F, 0x00), 15.toShort().toLittleEndian()) + assertArrayEquals(unsignedByteArrayOf(0xFF, 0xFF), 65535.toShort().toLittleEndian()) + assertArrayEquals(unsignedByteArrayOf(0x00, 0x01), 256.toShort().toLittleEndian()) + } + + @Test + fun testLittleEndianPublicKey() { + val ecParameters = (KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1")) }.generateKeyPair().public as ECPublicKey).params + val publicKey = ECPublicKeyImpl(ECPoint(BigInteger.TEN, BigInteger.ONE), ecParameters) + + val littleEndian2 = publicKey.toLittleEndian(2) + assertArrayEquals(byteArrayOf(0x0A, 0x01), littleEndian2) + val littleEndian4 = publicKey.toLittleEndian(4) + assertArrayEquals(byteArrayOf(0x0A, 0x00, 0x01, 0x00), littleEndian4) + val littleEndian8 = publicKey.toLittleEndian(8) + assertArrayEquals(byteArrayOf(0x0A, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00), littleEndian8) + } + + @Test + fun testLittleEndianUnsigned() { + val veryBigNumber1 = BigInteger("FFFFFFFFEEEEEEEE", 16) + val veryBigNumber2 = BigInteger("AAAAAAAABBBBBBBB", 16) + val ecParameters = (KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1")) }.generateKeyPair().public as ECPublicKey).params + val publicKey = ECPublicKeyImpl(ECPoint(veryBigNumber1, veryBigNumber2), ecParameters) + + val littleEndian16 = publicKey.toLittleEndian(16) + assertArrayEquals(byteArray(4, 0xEE) + .plus(byteArray(4, 0xFF) + .plus(byteArray(4, 0xBB)) + .plus(byteArray(4, 0xAA))), littleEndian16) + } + + @Test + fun testLittleEndianUnderflow() { + val number = BigInteger("007FEDCB", 16) + val littleEndian = number.toLittleEndian(4) + assertArrayEquals(unsignedByteArrayOf(0xcb, 0xed, 0x7f, 0x00), littleEndian) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/SampleTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/SampleTest.kt new file mode 100644 index 0000000000..513697d572 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/SampleTest.kt @@ -0,0 +1,84 @@ +package net.corda.attestation + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.security.* +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.spec.* + +class SampleTest { + private companion object { + private val privateKeyBytesBE = unsignedByteArrayOf( + 0x40, 0x8A, 0x06, 0x1A, 0xE6, 0x81, 0x3A, 0x7B, 0xAC, 0x4E, 0x34, 0xAA, 0x4D, 0x61, 0x3C, 0x86, + 0xF3, 0xDD, 0x9C, 0x48, 0x82, 0xA8, 0x68, 0x57, 0xFC, 0xB3, 0x9D, 0xF9, 0x81, 0xB6, 0x56, 0x2D + ) + private val publicKeyBytesBE = unsignedByteArrayOf( + 0x39, 0x16, 0xB0, 0x69, 0xB2, 0xBB, 0x0F, 0x92, 0xC4, 0x10, 0x27, 0x30, 0x9E, + 0x45, 0x61, 0x93, 0xB1, 0x67, 0x4F, 0xFB, 0x3E, 0xBC, 0x1F, 0xC5, 0xAE, 0x9F, 0x1A, 0x59, 0x45, 0x9F, 0x8C, 0xC0, + 0x4F, 0x96, 0x00, 0x30, 0x6E, 0x6C, 0x1F, 0x23, 0xF1, 0x5A, 0x2B, 0x1C, 0xC8, 0x32, 0xEB, 0xB8, 0xDC, 0x6A, 0x1A, + 0xA9, 0xE0, 0xCA, 0x35, 0x2A, 0x72, 0x46, 0x52, 0x2B, 0x24, 0x6B, 0x98, 0x5D + ) + + private val privateKeyBytesLE = unsignedByteArrayOf( + 0x90, 0xe7, 0x6c, 0xbb, 0x2d, 0x52, 0xa1, 0xce, 0x3b, 0x66, 0xde, 0x11, 0x43, 0x9c, 0x87, 0xec, + 0x1f, 0x86, 0x6a, 0x3b, 0x65, 0xb6, 0xae, 0xea, 0xad, 0x57, 0x34, 0x53, 0xd1, 0x03, 0x8c, 0x01 + ) + private val publicKeyBytesLE = unsignedByteArrayOf( + 0x72, 0x12, 0x8a, 0x7a, 0x17, 0x52, 0x6e, 0xbf, 0x85, 0xd0, 0x3a, 0x62, 0x37, 0x30, 0xae, 0xad, + 0x3e, 0x3d, 0xaa, 0xee, 0x9c, 0x60, 0x73, 0x1d, 0xb0, 0x5b, 0xe8, 0x62, 0x1c, 0x4b, 0xeb, 0x38, + 0xd4, 0x81, 0x40, 0xd9, 0x50, 0xe2, 0x57, 0x7b, 0x26, 0xee, 0xb7, 0x41, 0xe7, 0xc6, 0x14, 0xe2, + 0x24, 0xb7, 0xbd, 0xc9, 0x03, 0xf2, 0x9a, 0x28, 0xa8, 0x3c, 0xc8, 0x10, 0x11, 0x14, 0x5e, 0x06 + ) + } + + private lateinit var ecParameters: ECParameterSpec + private lateinit var keyFactory: KeyFactory + private lateinit var crypto: Crypto + + private lateinit var publicKey: ECPublicKey + private lateinit var privateKey: ECPrivateKey + + @Before + fun setup() { + crypto = Crypto(DummyRandom()) + keyFactory = KeyFactory.getInstance("EC") + ecParameters = (crypto.generateKeyPair().public as ECPublicKey).params + publicKey = keyFactory.generatePublic(publicKeyBytesLE.toBigEndianKeySpec(ecParameters)) as ECPublicKey + privateKey = keyFactory.generatePrivate(ECPrivateKeySpec(privateKeyBytesLE.reversedArray().toPositiveInteger(), ecParameters)) as ECPrivateKey + } + + @Test + fun checkKey() { + val myData = "And now for something completely different!" + val signature = Signature.getInstance("SHA256WithECDSA").let { signer -> + signer.initSign(privateKey, crypto.random) + signer.update(myData.toByteArray()) + signer.sign() + } + val verified = Signature.getInstance("SHA256WithECDSA").let { verifier -> + verifier.initVerify(publicKey) + verifier.update(myData.toByteArray()) + verifier.verify(signature) + } + assertTrue(verified) + } + + @Test + fun reversePublicKey() { + val transformedKey = keyFactory.generatePublic(publicKey.toLittleEndian().toBigEndianKeySpec(ecParameters)) as ECPublicKey + assertArrayEquals(publicKey.encoded, transformedKey.encoded) + } +} + +fun ByteArray.toKeySpec(ecParameters: ECParameterSpec): KeySpec { + if (size != KEY_SIZE) { + throw IllegalArgumentException("Public key has incorrect size ($size bytes)") + } + val ecPoint = ECPoint( + copyOf(size / 2).toPositiveInteger(), + copyOfRange(size / 2, size).toPositiveInteger() + ) + return ECPublicKeySpec(ecPoint, ecParameters) +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ChallengeResponseTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ChallengeResponseTest.kt new file mode 100644 index 0000000000..b3f634c610 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ChallengeResponseTest.kt @@ -0,0 +1,35 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class ChallengeResponseTest { + private companion object { + private val serviceKeyData = byteArrayOf(0x10, 0x00, 0x22, 0x00) + private val serviceKeyBase64 = serviceKeyData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialise() { + val challenge = ChallengeResponse("", serviceKeyData) + val str = mapper.writeValueAsString(challenge) + assertEquals("""{"nonce":"","serviceKey":"$serviceKeyBase64"}""", str) + } + + @Test + fun testDeserialise() { + val str = """{"nonce":"","serviceKey":"$serviceKeyBase64"}""" + val challenge = mapper.readValue(str, ChallengeResponse::class.java) + assertEquals("", challenge.nonce) + assertArrayEquals(serviceKeyData, challenge.serviceKey) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message0Test.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message0Test.kt new file mode 100644 index 0000000000..99e539868b --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message0Test.kt @@ -0,0 +1,29 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class Message0Test { + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialise() { + val msg0 = Message0(0x000F207F) + val str = mapper.writeValueAsString(msg0) + assertEquals("""{"extendedGID":991359}""", str) + } + + @Test + fun testDeserialise() { + val str = """{"extendedGID":991359}""" + val msg0 = mapper.readValue(str, Message0::class.java) + assertEquals(0x000F207F, msg0.extendedGID) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message1Test.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message1Test.kt new file mode 100644 index 0000000000..a0a4689bee --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message1Test.kt @@ -0,0 +1,38 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class Message1Test { + private companion object { + private val gaData = byteArrayOf(0x10, 0x00, 0x22, 0x00) + private val gaBase64 = gaData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialise() { + val msg1 = Message1( + ga = gaData, + platformGID = "2139062143" + ) + val str = mapper.writeValueAsString(msg1) + assertEquals("""{"ga":"$gaBase64","platformGID":"2139062143"}""", str) + } + + @Test + fun testDeserialise() { + val str = """{"ga":"$gaBase64","platformGID":"2139062143"}""" + val msg1 = mapper.readValue(str, Message1::class.java) + assertArrayEquals(gaData, msg1.ga) + assertEquals("2139062143", msg1.platformGID) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message2Test.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message2Test.kt new file mode 100644 index 0000000000..d7995cad23 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message2Test.kt @@ -0,0 +1,72 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class Message2Test { + private companion object { + private const val SPID = "8F42710C36029FA11744" + + private val gbData = byteArrayOf(0x10, 0x00, 0x22, 0x00) + private val gbBase64 = gbData.toBase64() + private val revocationListData = byteArrayOf(0x7F, 0x7F, 0x7F, 0x7F) + private val revocationListBase64 = revocationListData.toBase64() + private val signatureData = byteArrayOf(0x31, 0x35, 0x5D, 0x1A, 0x27, 0x44) + private val signatureBase64 = signatureData.toBase64() + private val aesCMACData = byteArrayOf(0x7C, 0x62, 0x50, 0x2B, 0x47, 0x0E) + private val aesCMACBase64 = aesCMACData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialise() { + val msg2 = Message2( + gb = gbData, + spid = SPID, + linkableQuote = true, + keyDerivationFuncId = 1, + signatureGbGa = signatureData, + aesCMAC = aesCMACData, + revocationList = revocationListData + ) + val str = mapper.writeValueAsString(msg2) + assertEquals("{" + + "\"gb\":\"$gbBase64\"," + + "\"spid\":\"$SPID\"," + + "\"linkableQuote\":true," + + "\"keyDerivationFuncId\":1," + + "\"signatureGbGa\":\"$signatureBase64\"," + + "\"aesCMAC\":\"$aesCMACBase64\"," + + "\"revocationList\":\"$revocationListBase64\"" + + "}", str) + } + + @Test + fun testDeserialise() { + val str = """{ + "gb":"$gbBase64", + "spid":"$SPID", + "linkableQuote":true, + "keyDerivationFuncId":1, + "signatureGbGa":"$signatureBase64", + "aesCMAC":"$aesCMACBase64", + "revocationList":"$revocationListBase64" + }""" + val msg2 = mapper.readValue(str, Message2::class.java) + assertArrayEquals(gbData, msg2.gb) + assertEquals(SPID, msg2.spid) + assertTrue(msg2.linkableQuote) + assertEquals(1, msg2.keyDerivationFuncId) + assertArrayEquals(signatureData, msg2.signatureGbGa) + assertArrayEquals(aesCMACData, msg2.aesCMAC) + assertArrayEquals(revocationListData, msg2.revocationList) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message3Test.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message3Test.kt new file mode 100644 index 0000000000..7e61a12fe7 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message3Test.kt @@ -0,0 +1,90 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class Message3Test { + private companion object { + private val cmacData = byteArrayOf(0x50, 0x60, 0x70, 0x7F) + private val cmacBase64 = cmacData.toBase64() + private val gaData = byteArrayOf(0x10, 0x00, 0x22, 0x00) + private val gaBase64 = gaData.toBase64() + private val quoteData = byteArrayOf(0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F) + private val quoteBase64 = quoteData.toBase64() + private val manifestData = byteArrayOf(0x44, 0x44, 0x44, 0x44, 0x44, 0x44) + private val manifestBase64 = manifestData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testBasicSerialise() { + val msg3 = Message3( + aesCMAC = cmacData, + ga = gaData, + quote = quoteData + ) + val str = mapper.writeValueAsString(msg3) + assertEquals("{" + + "\"aesCMAC\":\"$cmacBase64\"," + + "\"ga\":\"$gaBase64\"," + + "\"quote\":\"$quoteBase64\"" + + "}", str) + } + + @Test + fun testFullSerialise() { + val msg3 = Message3( + aesCMAC = cmacData, + ga = gaData, + quote = quoteData, + securityManifest = manifestData, + nonce = "" + ) + val str = mapper.writeValueAsString(msg3) + assertEquals("{" + + "\"aesCMAC\":\"$cmacBase64\"," + + "\"ga\":\"$gaBase64\"," + + "\"quote\":\"$quoteBase64\"," + + "\"securityManifest\":\"$manifestBase64\"," + + "\"nonce\":\"\"" + + "}", str) + } + + @Test + fun testBasicDeserialise() { + val str = """{ + "aesCMAC":"$cmacBase64", + "ga":"$gaBase64", + "quote":"$quoteBase64" + }""" + val msg3 = mapper.readValue(str, Message3::class.java) + assertArrayEquals(cmacData, msg3.aesCMAC) + assertArrayEquals(gaData, msg3.ga) + assertArrayEquals(quoteData, msg3.quote) + } + + @Test + fun testFullDeserialise() { + val str = """{ + "aesCMAC":"$cmacBase64", + "ga":"$gaBase64", + "quote":"$quoteBase64", + "securityManifest":"$manifestBase64", + "nonce":"" + }""" + val msg3 = mapper.readValue(str, Message3::class.java) + assertArrayEquals(cmacData, msg3.aesCMAC) + assertArrayEquals(gaData, msg3.ga) + assertArrayEquals(quoteData, msg3.quote) + assertArrayEquals(manifestData, msg3.securityManifest) + assertEquals("", msg3.nonce) + } +} diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message4Test.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message4Test.kt new file mode 100644 index 0000000000..ae054f7c2f --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/Message4Test.kt @@ -0,0 +1,156 @@ +package net.corda.attestation.message + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.unsignedByteArrayOf +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class Message4Test { + private companion object { + private val iso8601Time = "2017-11-09T15:03:46.345678" + private val testTimestamp = LocalDateTime.from(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(iso8601Time)) + private val quoteBodyData = byteArrayOf(0x7D, 0x7D, 0x7D, 0x7D, 0x7D, 0x7D, 0x7D, 0x7D) + private val quoteBodyBase64 = quoteBodyData.toBase64() + private val cmacData = byteArrayOf(0x17, 0x1F, 0x73, 0x66, 0x2E, 0x3F) + private val cmacBase64 = cmacData.toBase64() + private val platformInfoData = unsignedByteArrayOf(0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF) + private val platformInfoBase64 = platformInfoData.toBase64() + private val pseudonymData = byteArrayOf(0x31, 0x7E, 0x4A, 0x14) + private val pseudonymBase64 = pseudonymData.toBase64() + private val secretData = "Mystery data".toByteArray() + private val secretBase64 = secretData.toBase64() + private val secretHashData = byteArrayOf(0x02, 0x08, 0x25, 0x74) + private val secretHashBase64 = secretHashData.toBase64() + private val secretIVData = byteArrayOf(0x62, 0x72, 0x2A, 0x4F, 0x0E, -0x44) + private val secretIVBase64 = secretIVData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper().registerModule(JavaTimeModule()) + } + + @Test + fun testBasicSerialise() { + val msg4 = Message4( + reportID = "", + quoteStatus = "OK", + quoteBody = quoteBodyData, + aesCMAC = cmacData, + platformInfo = byteArrayOf(), + secret = secretData, + secretHash = secretHashData, + secretIV = secretIVData, + timestamp = testTimestamp + ) + val str = mapper.writeValueAsString(msg4) + assertEquals("{" + + "\"reportID\":\"\"," + + "\"quoteStatus\":\"OK\"," + + "\"quoteBody\":\"$quoteBodyBase64\"," + + "\"aesCMAC\":\"$cmacBase64\"," + + "\"secret\":\"$secretBase64\"," + + "\"secretHash\":\"$secretHashBase64\"," + + "\"secretIV\":\"$secretIVBase64\"," + + "\"timestamp\":\"$iso8601Time\"" + + "}", str) + } + + @Test + fun testFullSerialise() { + val msg4 = Message4( + reportID = "", + quoteStatus = "GROUP_OUT_OF_DATE", + quoteBody = quoteBodyData, + aesCMAC = cmacData, + securityManifestStatus = "INVALID", + securityManifestHash = "", + platformInfo = platformInfoData, + epidPseudonym = pseudonymData, + nonce = "", + secret = secretData, + secretHash = secretHashData, + secretIV = secretIVData, + timestamp = testTimestamp + ) + val str = mapper.writeValueAsString(msg4) + assertEquals("{" + + "\"reportID\":\"\"," + + "\"quoteStatus\":\"GROUP_OUT_OF_DATE\"," + + "\"quoteBody\":\"$quoteBodyBase64\"," + + "\"aesCMAC\":\"$cmacBase64\"," + + "\"securityManifestStatus\":\"INVALID\"," + + "\"securityManifestHash\":\"\"," + + "\"platformInfo\":\"$platformInfoBase64\"," + + "\"epidPseudonym\":\"$pseudonymBase64\"," + + "\"nonce\":\"\"," + + "\"secret\":\"$secretBase64\"," + + "\"secretHash\":\"$secretHashBase64\"," + + "\"secretIV\":\"$secretIVBase64\"," + + "\"timestamp\":\"$iso8601Time\"" + + "}", str) + } + + @Test + fun testBasicDeserialise() { + val str = """{ + "reportID":"", + "quoteStatus":"OK", + "quoteBody":"$quoteBodyBase64", + "aesCMAC":"$cmacBase64", + "secret":"$secretBase64", + "secretHash":"$secretHashBase64", + "secretIV":"$secretIVBase64", + "timestamp":"$iso8601Time" + }""" + val msg4 = mapper.readValue(str, Message4::class.java) + assertEquals("", msg4.reportID) + assertEquals("OK", msg4.quoteStatus) + assertArrayEquals(quoteBodyData, msg4.quoteBody) + assertArrayEquals(cmacData, msg4.aesCMAC) + assertNull(msg4.platformInfo) + assertArrayEquals(secretData, msg4.secret) + assertArrayEquals(secretHashData, msg4.secretHash) + assertArrayEquals(secretIVData, msg4.secretIV) + assertEquals(testTimestamp, msg4.timestamp) + } + + @Test + fun testFullDeserialise() { + val str = """{ + "reportID":"", + "quoteStatus":"GROUP_OUT_OF_DATE", + "quoteBody":"$quoteBodyBase64", + "aesCMAC":"$cmacBase64", + "securityManifestStatus":"INVALID", + "securityManifestHash":"", + "platformInfo":"$platformInfoBase64", + "epidPseudonym":"$pseudonymBase64", + "nonce":"", + "secret":"$secretBase64", + "secretHash":"$secretHashBase64", + "secretIV":"$secretIVBase64", + "timestamp":"$iso8601Time" + }""" + val msg4 = mapper.readValue(str, Message4::class.java) + assertEquals("", msg4.reportID) + assertEquals("GROUP_OUT_OF_DATE", msg4.quoteStatus) + assertArrayEquals(quoteBodyData, msg4.quoteBody) + assertArrayEquals(cmacData, msg4.aesCMAC) + assertEquals("INVALID", msg4.securityManifestStatus) + assertEquals("", msg4.securityManifestHash) + assertArrayEquals(platformInfoData, msg4.platformInfo) + assertArrayEquals(pseudonymData, msg4.epidPseudonym) + assertEquals("", msg4.nonce) + assertArrayEquals(secretData, msg4.secret) + assertArrayEquals(secretHashData, msg4.secretHash) + assertArrayEquals(secretIVData, msg4.secretIV) + assertEquals(testTimestamp, msg4.timestamp) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/MessageUtils.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/MessageUtils.kt new file mode 100644 index 0000000000..7603136b5c --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/MessageUtils.kt @@ -0,0 +1,6 @@ +@file:JvmName("MessageUtils") +package net.corda.attestation.message + +import java.util.* + +fun ByteArray.toBase64(): String = Base64.getEncoder().encodeToString(this) diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportProxyResponseTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportProxyResponseTest.kt new file mode 100644 index 0000000000..0745cd196b --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportProxyResponseTest.kt @@ -0,0 +1,49 @@ +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.toBase64 +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class ReportProxyResponseTest { + private companion object { + private val reportData = byteArrayOf(0x51, 0x62, 0x43, 0x24, 0x75, 0x4D) + private val reportBase64 = reportData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialiseBasic() { + val response = ReportProxyResponse( + signature = "", + certificatePath = "", + report = reportData + ) + val str = mapper.writeValueAsString(response) + assertEquals("{" + + "\"signature\":\"\"," + + "\"certificatePath\":\"\"," + + "\"report\":\"$reportBase64\"" + + "}", str) + } + + @Test + fun testDeserialiseBasic() { + val str = """{ + "signature":"", + "certificatePath":"", + "report":"$reportBase64" + }""" + val response = mapper.readValue(str, ReportProxyResponse::class.java) + assertEquals("", response.signature) + assertEquals("", response.certificatePath) + assertArrayEquals(reportData, response.report) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportRequestTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportRequestTest.kt new file mode 100644 index 0000000000..db464fecbd --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportRequestTest.kt @@ -0,0 +1,62 @@ +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.toBase64 +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class ReportRequestTest { + private companion object { + private val quoteData = byteArrayOf(0x41, 0x42, 0x43, 0x44, 0x45, 0x46) + private val quoteBase64 = quoteData.toBase64() + + private val manifestData = byteArrayOf(0x55, 0x72, 0x19, 0x5B) + private val manifestBase64 = manifestData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialise() { + val request = ReportRequest( + isvEnclaveQuote = quoteData, + pseManifest = manifestData, + nonce = "" + ) + val str = mapper.writeValueAsString(request) + assertEquals("{\"isvEnclaveQuote\":\"$quoteBase64\",\"pseManifest\":\"$manifestBase64\",\"nonce\":\"\"}", str) + } + + @Test + fun testSerialiseEmpty() { + val request = ReportRequest( + isvEnclaveQuote = byteArrayOf() + ) + val str = mapper.writeValueAsString(request) + assertEquals("{\"isvEnclaveQuote\":\"\"}", str) + } + + @Test + fun testDeserialise() { + val str = """{"isvEnclaveQuote":"$quoteBase64","pseManifest":"$manifestBase64","nonce":""}""" + val request = mapper.readValue(str, ReportRequest::class.java) + assertArrayEquals(quoteData, request.isvEnclaveQuote) + assertArrayEquals(manifestData, request.pseManifest) + assertEquals("", request.nonce) + } + + @Test + fun testDeserialiseQuoteOnly() { + val str = """{"isvEnclaveQuote":"$quoteBase64"}""" + val request = mapper.readValue(str, ReportRequest::class.java) + assertArrayEquals(quoteData, request.isvEnclaveQuote) + assertNull(request.pseManifest) + assertNull(request.nonce) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportResponseTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportResponseTest.kt new file mode 100644 index 0000000000..4a7856d2a9 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/ReportResponseTest.kt @@ -0,0 +1,129 @@ +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.attestation.message.toBase64 +import net.corda.attestation.message.ias.ManifestStatus.* +import net.corda.attestation.message.ias.QuoteStatus.* +import net.corda.attestation.unsignedByteArrayOf +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class ReportResponseTest { + private companion object { + private val iso8601Time = "2017-11-08T18:19:27.123456" + private val testTimestamp = LocalDateTime.from(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(iso8601Time)) + + private val quoteBodyData = byteArrayOf(0x61, 0x62, 0x63, 0x64, 0x65, 0x66) + private val quoteBodyBase64 = quoteBodyData.toBase64() + + private val platformInfoData = unsignedByteArrayOf(0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef) + + private val pseudonymData = byteArrayOf(0x63, 0x18, 0x33, 0x72) + private val pseudonymBase64 = pseudonymData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper().registerModule(JavaTimeModule()) + } + + @Test + fun testSerialiseBasic() { + val response = ReportResponse( + id = "", + isvEnclaveQuoteStatus = QuoteStatus.OK, + isvEnclaveQuoteBody = quoteBodyData, + timestamp = testTimestamp + ) + val str = mapper.writeValueAsString(response) + assertEquals("{" + + "\"id\":\"\"," + + "\"timestamp\":\"$iso8601Time\"," + + "\"isvEnclaveQuoteStatus\":\"OK\"," + + "\"isvEnclaveQuoteBody\":\"$quoteBodyBase64\"" + + "}", str) + } + + @Test + fun testSerialiseFull() { + val response = ReportResponse( + id = "", + isvEnclaveQuoteStatus = GROUP_OUT_OF_DATE, + isvEnclaveQuoteBody = quoteBodyData, + platformInfoBlob = platformInfoData, + revocationReason = 1, + pseManifestStatus = INVALID, + pseManifestHash = "", + nonce = "", + epidPseudonym = pseudonymData, + timestamp = testTimestamp + ) + val str = mapper.writeValueAsString(response) + assertEquals("{" + + "\"nonce\":\"\"," + + "\"id\":\"\"," + + "\"timestamp\":\"$iso8601Time\"," + + "\"epidPseudonym\":\"$pseudonymBase64\"," + + "\"isvEnclaveQuoteStatus\":\"GROUP_OUT_OF_DATE\"," + + "\"isvEnclaveQuoteBody\":\"$quoteBodyBase64\"," + + "\"pseManifestStatus\":\"INVALID\"," + + "\"pseManifestHash\":\"\"," + + "\"platformInfoBlob\":\"123456789abcdef\"," + + "\"revocationReason\":1" + + "}", str) + } + + @Test + fun testDeserialiseBasic() { + val str = """{ + "id":"", + "isvEnclaveQuoteStatus":"OK", + "isvEnclaveQuoteBody":"$quoteBodyBase64", + "timestamp":"$iso8601Time" + }""" + val response = mapper.readValue(str, ReportResponse::class.java) + assertEquals("", response.id) + assertEquals(QuoteStatus.OK, response.isvEnclaveQuoteStatus) + assertArrayEquals(quoteBodyData, response.isvEnclaveQuoteBody) + assertNull(response.platformInfoBlob) + assertNull(response.revocationReason) + assertNull(response.pseManifestStatus) + assertNull(response.pseManifestHash) + assertNull(response.nonce) + assertNull(response.epidPseudonym) + assertEquals(testTimestamp, response.timestamp) + } + + @Test + fun testDeserialiseFull() { + val str = """{ + "id":"", + "isvEnclaveQuoteStatus":"GROUP_OUT_OF_DATE", + "isvEnclaveQuoteBody":"$quoteBodyBase64", + "platformInfoBlob":"0123456789ABCDEF", + "revocationReason":1, + "pseManifestStatus":"OK", + "pseManifestHash":"", + "nonce":"", + "epidPseudonym":"$pseudonymBase64", + "timestamp":"$iso8601Time" + }""" + val response = mapper.readValue(str, ReportResponse::class.java) + assertEquals("", response.id) + assertEquals(QuoteStatus.GROUP_OUT_OF_DATE, response.isvEnclaveQuoteStatus) + assertArrayEquals(quoteBodyData, response.isvEnclaveQuoteBody) + assertArrayEquals(platformInfoData, response.platformInfoBlob) + assertEquals(1, response.revocationReason) + assertEquals(ManifestStatus.OK, response.pseManifestStatus) + assertEquals("", response.pseManifestHash) + assertEquals("", response.nonce) + assertArrayEquals(pseudonymData, response.epidPseudonym) + assertEquals(testTimestamp, response.timestamp) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponseTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponseTest.kt new file mode 100644 index 0000000000..70f4819dba --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/attestation/message/ias/RevocationListProxyResponseTest.kt @@ -0,0 +1,46 @@ +package net.corda.attestation.message.ias + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.attestation.message.toBase64 +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class RevocationListProxyResponseTest { + private companion object { + private const val SPID = "84D402C36BA9EF9B0A86EF1A9CC8CE4F" + private val revocationListData = byteArrayOf(0x51, 0x62, 0x43, 0x24, 0x75, 0x4D) + private val revocationListBase64 = revocationListData.toBase64() + } + + private lateinit var mapper: ObjectMapper + + @Before + fun setup() { + mapper = ObjectMapper() + } + + @Test + fun testSerialiseBasic() { + val response = RevocationListProxyResponse( + spid = SPID, + revocationList = revocationListData + ) + val str = mapper.writeValueAsString(response) + assertEquals("{" + + "\"spid\":\"$SPID\"," + + "\"revocationList\":\"$revocationListBase64\"" + + "}", str) + } + + @Test + fun testDeserialiseBasic() { + val str = """{ + "spid":"$SPID", + "revocationList":"$revocationListBase64" + }""" + val response = mapper.readValue(str, RevocationListProxyResponse::class.java) + assertEquals(SPID, response.spid) + assertArrayEquals(revocationListData, response.revocationList) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/mockias/io/SignatureOutputStreamTest.kt b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/mockias/io/SignatureOutputStreamTest.kt new file mode 100644 index 0000000000..a7ce271d32 --- /dev/null +++ b/sgx-jvm/remote-attestation/attestation-server/src/test/kotlin/net/corda/mockias/io/SignatureOutputStreamTest.kt @@ -0,0 +1,53 @@ +package net.corda.mockias.io + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.security.KeyPairGenerator +import java.security.Signature + +class SignatureOutputStreamTest { + + private lateinit var output: ByteArrayOutputStream + private lateinit var signedOutput: SignatureOutputStream + private lateinit var reference: Signature + + @Before + fun setup() { + val keyPairGenerator = KeyPairGenerator.getInstance("RSA") + val keyPair = keyPairGenerator.genKeyPair() + val signature = Signature.getInstance("SHA256withRSA").apply { + initSign(keyPair.private) + } + output = ByteArrayOutputStream() + signedOutput = SignatureOutputStream(output, signature) + reference = Signature.getInstance("SHA256withRSA").apply { + initSign(keyPair.private) + } + } + + @Test + fun testSignValue() { + signedOutput.write(-0x74) + signedOutput.write(0x00) + signedOutput.write(0x11) + reference.update(ByteArrayOutputStream().let { baos -> + baos.write(-0x74) + baos.write(0x00) + baos.write(0x11) + baos.toByteArray() + }) + assertArrayEquals(byteArrayOf(-0x74, 0x00, 0x11), output.toByteArray()) + assertArrayEquals(reference.sign(), signedOutput.sign()) + } + + @Test + fun testSignBuffer() { + val buffer = byteArrayOf(0x01, -0x7F, 0x64, -0x52, 0x00) + signedOutput.write(buffer) + reference.update(buffer) + assertArrayEquals(buffer, output.toByteArray()) + assertArrayEquals(reference.sign(), signedOutput.sign()) + } +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/build.gradle b/sgx-jvm/remote-attestation/build.gradle new file mode 100644 index 0000000000..5f77b998ad --- /dev/null +++ b/sgx-jvm/remote-attestation/build.gradle @@ -0,0 +1,63 @@ +buildscript { + // For sharing constants between builds + Properties constants = new Properties() + file("../../constants.properties").withInputStream { constants.load(it) } + + ext.kotlin_version = constants.getProperty("kotlinVersion") + ext.bouncycastle_version = constants.getProperty("bouncycastleVersion") + ext.resteasy_version = '3.1.4.Final' + ext.jackson_version = '2.9.2' + ext.slf4j_version = '1.7.25' + ext.log4j_version = '2.9.1' + ext.junit_version = '4.12' + + repositories { + mavenLocal() + mavenCentral() + jcenter() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" + } +} + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +allprojects { + tasks.withType(KotlinCompile).all { + kotlinOptions { + languageVersion = "1.1" + apiVersion = "1.1" + jvmTarget = "1.8" + javaParameters = true // Useful for reflection. + } + } + + tasks.withType(Test) { + // Prevent the project from creating temporary files outside of the build directory. + systemProperties['java.io.tmpdir'] = buildDir + } + + group 'com.r3.corda.enterprise' + version '1.0-SNAPSHOT' + + repositories { + mavenLocal() + mavenCentral() + jcenter() + } + + configurations { + compile { + // We want to use SLF4J's version of these bindings: jcl-over-slf4j + // Remove any transitive dependency on Apache's version. + exclude group: 'commons-logging', module: 'commons-logging' + } + } +} + +task wrapper(type: Wrapper) { + gradleVersion = "4.3.1" +} diff --git a/sgx-jvm/remote-attestation/gradle/wrapper/gradle-wrapper.jar b/sgx-jvm/remote-attestation/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..6b6ea3ab4ff4f69d55c5fd9c0a6ac70f47d41008 GIT binary patch literal 54731 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�A``0fKpc0WAU$bg;D1GyIQa;7hdW#MKpuc+fPkN63DB8-QCon}w55{+ zK*~kT%+g5EzyQD?t7onEf4S{n5`Y%HU(o_IxB{TT|7ske_`f6&x7P#Y(^Lk?K1$eF z8Ohk#SUB1Kx@itd&0`AyGX5-pngUK={i!e#{R8yBlYsrOyWkeC%j+RF5iuzNd@qNfB>rpz#I8f@s{`{nv9Lfe|_68v(EoSLz4a_*l*>a ze`!-MGn)To!IAynEWd7smziIGVoE6e67%Kc<1cw&U)K0#-o>Bj6zac3|F@C;A`9b7 z=$C2lenNw4{S)+GliV}2G}0N|fG z)sG;wmk+am&9E z{i*){Mdkk`{7ZrQpYTT3{{;VHviuVEr7rtV*j}4|g8j3f;U(rv3E-cYWBU$VtD_%M1M5Tztu!v61)^^{7FFK`5OfPAl>+q z>7}IGPbO2}-(dPJfwz~OFNL~(a)Jl^2IoJ?cDeQ`pGlj`8S@I)6PGc(f|02zx2HPq$%$HzbV~+ v^TYh7&j0bc{Ml*p!|U?1+ylV=n-AuVG#FqV^dowa3FsZ*6oN|6kH7vOrq \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/sgx-jvm/remote-attestation/gradlew.bat b/sgx-jvm/remote-attestation/gradlew.bat new file mode 100644 index 0000000000..e95643d6a2 --- /dev/null +++ b/sgx-jvm/remote-attestation/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sgx-jvm/remote-attestation/settings.gradle b/sgx-jvm/remote-attestation/settings.gradle new file mode 100644 index 0000000000..45be9e2c58 --- /dev/null +++ b/sgx-jvm/remote-attestation/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'remote-attestation' +include 'attestation-server' From 96d8ec964042c0feece297ff56a7cf38778755d0 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 12 Dec 2017 17:07:35 +0000 Subject: [PATCH 2/2] Dynamically add BouncyCastle provider (#185) --- network-management/build.gradle | 4 ++++ .../main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/network-management/build.gradle b/network-management/build.gradle index 128a31cf06..625ebb42e6 100644 --- a/network-management/build.gradle +++ b/network-management/build.gradle @@ -114,4 +114,8 @@ dependencies { //TODO remove once we can put driver jar into a predefined directory //JDBC driver can be passed to the Node at startup using setting the jarDirs property in the Node configuration file. compile 'com.microsoft.sqlserver:mssql-jdbc:6.2.1.jre8' + + // Bouncy Castle for HSM signing + compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}" + compile "org.bouncycastle:bcpkix-jdk15on:${bouncycastle_version}" } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt index 153411eebb..e148e2007c 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt @@ -15,6 +15,9 @@ import com.r3.corda.networkmanage.hsm.persistence.DBSignedCertificateRequestStor import com.r3.corda.networkmanage.hsm.signer.HsmCsrSigner import com.r3.corda.networkmanage.hsm.signer.HsmNetworkMapSigner import com.r3.corda.networkmanage.hsm.utils.mapCryptoServerException +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.security.Security + fun main(args: Array) { run(parseParameters(*args)) @@ -22,6 +25,11 @@ fun main(args: Array) { fun run(parameters: Parameters) { parameters.run { + // Ensure the BouncyCastle provider is installed + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(BouncyCastleProvider()) + } + // Create DB connection. checkNotNull(dataSourceProperties) val database = configureDatabase(dataSourceProperties, databaseConfig)