diff --git a/build.gradle b/build.gradle index 818cc24129..f877f50f18 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,7 @@ import com.r3.testing.DistributeTestsBy import com.r3.testing.PodLogLevel +import java.nio.file.Files +import java.nio.file.Paths import static org.gradle.api.JavaVersion.VERSION_11 import static org.gradle.api.JavaVersion.VERSION_1_8 @@ -189,6 +191,18 @@ buildscript { mavenCentral() jcenter() } + + maven { + def path = Paths.get(rootDir.absolutePath).resolve("./repo").toAbsolutePath().normalize() + if (!Files.isDirectory(path.resolve("com"))) { + if (Files.isDirectory(Paths.get("/repo/com"))) { + path = Paths.get("/repo") + } else { + throw new Exception("Neither $path nor /repo seem to exist, or they aren't Maven repositories; it should be the SDK 'repo' subdirectory.") + } + } + url = path.toFile() + } } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" @@ -424,6 +438,18 @@ allprojects { } mavenCentral() jcenter() + + maven { + def path = Paths.get(rootDir.absolutePath).resolve(conclaveRepo).toAbsolutePath().normalize() + if (!Files.isDirectory(path.resolve("com"))) { + if (Files.isDirectory(Paths.get("/repo/com"))) { + path = Paths.get("/repo") + } else { + throw new Exception("Neither $path nor /repo seem to exist, or they aren't Maven repositories; it should be the SDK 'repo' subdirectory.") + } + } + url = path.toFile() + } } } diff --git a/common/enclave/build.gradle b/common/enclave/build.gradle new file mode 100644 index 0000000000..79dc483ac3 --- /dev/null +++ b/common/enclave/build.gradle @@ -0,0 +1,7 @@ +apply plugin: 'kotlin' + +dependencies { + implementation "com.r3.conclave:conclave-common:$conclaveVersion" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation "net.i2p.crypto:eddsa:0.3.0" +} diff --git a/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/EnclaveCommand.kt b/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/EnclaveCommand.kt new file mode 100644 index 0000000000..4adab57b97 --- /dev/null +++ b/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/EnclaveCommand.kt @@ -0,0 +1,29 @@ +package com.r3.conclave.cordapp.common + +/** + * Generic type for an enclave command. + * This type contains no properties or methods and simply instructs the enclave to perform a specific action. + */ +interface EnclaveCommand { + + /** + * Convert this type to a [String]. + * @return serialized [String] of this type. + */ + fun serialize(): String { + return this.javaClass.name + } +} + +/** + * Deserialize an [EnclaveCommand]. + * Convert a serialized string into an [EnclaveCommand] instance. + */ +fun String.toEnclaveCommand(): EnclaveCommand { + return Class.forName(this).newInstance() as EnclaveCommand +} + +/** + * An [EnclaveCommand] that instructs the enclave to register a host identity. + */ +class RegisterHostIdentity : EnclaveCommand \ No newline at end of file diff --git a/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/EnclaveUtils.kt b/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/EnclaveUtils.kt new file mode 100644 index 0000000000..cbf2320733 --- /dev/null +++ b/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/EnclaveUtils.kt @@ -0,0 +1,44 @@ +package com.r3.conclave.cordapp.common + +import java.security.MessageDigest +import java.util.* + +/** + * Parse [routingHint] of the format flowId:route and return a pair of flowId [String] and [EnclaveCommand] class. + * @param routingHint the routing hint from receiveMail to parse. + * @return a pair of flowId [String] and [EnclaveCommand]. + * @throws [IllegalArgumentException] if the hint cannot be parsed, or if the enclave command is not known. + */ +fun getRoutingInfo(routingHint: String): Pair { + val routingElements = routingHint.split(":") + if (routingElements.size != 2) { + throw IllegalArgumentException("Expected routingHint in the form flowId:route") + } + + val (flowId, routeName) = routingElements + try { + UUID.fromString(flowId) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Invalid flowId: $flowId") + } + + val route = try { + routeName.trim().toEnclaveCommand() + } catch (e: IllegalArgumentException) { + throw java.lang.IllegalArgumentException("Unknown command: $routeName") + } + + return Pair(flowId, route) +} + +/** + * Return the hash of the input [String] using the given hashing algorithm. + * @param input the [String] to generate a hash of. + * @param algorithm the hashing algorithm to use. + * @return the generated hash [String]. + */ +fun hashString(input: String, algorithm: String = "SHA-256"): String { + return MessageDigest.getInstance(algorithm) + .digest(input.toByteArray()) + .fold("") { str, it -> str + "%02x".format(it) } +} \ No newline at end of file diff --git a/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/EncryptedTxEnclaveCommands.kt b/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/EncryptedTxEnclaveCommands.kt new file mode 100644 index 0000000000..50862a99f1 --- /dev/null +++ b/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/EncryptedTxEnclaveCommands.kt @@ -0,0 +1,7 @@ +package com.r3.conclave.cordapp.common + +/** + * An [EnclaveCommand] that instructs the enclave to initialise a post office to a remote enclave using the serialized + * attestation contained in the mail body (requires an enclave host to be registered). + */ +class InitPostOfficeToRemoteEnclave : EnclaveCommand \ No newline at end of file diff --git a/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/SenderIdentity.kt b/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/SenderIdentity.kt new file mode 100644 index 0000000000..0429d0286a --- /dev/null +++ b/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/SenderIdentity.kt @@ -0,0 +1,23 @@ +package com.r3.conclave.cordapp.common + +import java.security.PublicKey + +/** + * Defines the interface for querying the sender's identity properties. The SenderIdentity is accessible to the + * the Enclave and it can be used to uniquely identify the sender if the sender decided to share + * its verifiable identity. Any identity shared by the sender goes through a verification process to ensure the identity + * is part of the same certificate chain as the root certificate hardcoded into the enclave. + */ +interface SenderIdentity { + + /** + * The verified X.500 subject name of the sender + */ + val name: String + + /** + * The verified public key of the sender's identity. Specifically, this is the public key from the sender's X.509 certificate. + * Note, this public key is different to the encryption key used in Mail ([EnclaveMail.authenticatedSender]) + */ + val publicKey: PublicKey +} \ No newline at end of file diff --git a/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/internal/SenderIdentityImpl.kt b/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/internal/SenderIdentityImpl.kt new file mode 100644 index 0000000000..f3bd76b537 --- /dev/null +++ b/common/enclave/src/main/kotlin/com/r3/conclave/cordapp/common/internal/SenderIdentityImpl.kt @@ -0,0 +1,137 @@ +package com.r3.conclave.cordapp.common.internal + +import com.r3.conclave.common.internal.SignatureSchemeEdDSA +import com.r3.conclave.cordapp.common.SenderIdentity +import net.i2p.crypto.eddsa.EdDSASecurityProvider +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.DataInputStream +import java.io.DataOutputStream +import java.security.PublicKey +import java.security.Security +import java.security.SignatureException +import java.security.cert.* + +/** + * Implements the [SenderIdentity] interface and allows a sender to prepare and send its identity to en Enclave, and + * allows the Enclave to privately and securely verify the sender X509 name and public key belonging to a network where + * parties are identified by a X509 certificate issued by the CA trusted by the Enclave. + */ +class SenderIdentityImpl( + private val signerCertPath: CertPath, + private val signatureData: ByteArray +) : SenderIdentity { + private val signerCertificate: X509Certificate get() = (signerCertPath.certificates[0] as X509Certificate) + + init { + require(signerCertPath.type == "X.509") { "Only X.509 certificates supported" } + val certs = signerCertPath.certificates + require(certs.size >= 2) { "Certificate path must at least include subject and issuing certificates" } + } + + /** + * Verifies the sender identified by the [signerCertificate] is the party that signed the [sharedSecret] producing + * the [signatureData] + */ + fun didSign(sharedSecret: ByteArray): Boolean { + return try { + signatureScheme.verify(signerCertificate.publicKey, signatureData, sharedSecret) + true + } catch (e: SignatureException) { + false + } + } + + /** + * Verifies the [signerCertPath] is valid and issued by the given trusted root + * + * @param caRoot the root certificate that is used to validate the instance's certificate path [signerCertPath] + */ + fun isTrusted(caRoot: X509Certificate): Boolean { + val trustAnchor = TrustAnchor(caRoot, null) + val parameters = PKIXParameters(setOf(trustAnchor)).apply { isRevocationEnabled = false } + return try { + CertPathValidator.getInstance("PKIX").validate(signerCertPath, parameters) + true + } catch (e: CertPathValidatorException) { + false + } + } + + /** + * The verified subject name of the sender + */ + override val name: String get() = signerCertificate.subjectX500Principal.name + + /** + * The verified public key of the sender + */ + override val publicKey: PublicKey get() = signerCertificate.publicKey + + /** + * Serializes the [SenderIdentity] instance into a byte array. + * + * Used by the mail sender to serialize the [SenderIdentity] instance and privately send it to the Enclave message + * handler where is then deserialized. + */ + fun serialize(): ByteArray { + val baos = ByteArrayOutputStream() + val dos = DataOutputStream(baos) + + serialize(dos) + + return baos.toByteArray() + } + + fun serialize(dos: DataOutputStream) { + dos.writeIntLengthPrefixBytes(signerCertPath.getEncoded("PkiPath")) + dos.writeIntLengthPrefixBytes(signatureData) + } + + companion object { + private val signatureScheme = SignatureSchemeEdDSA() + + init { + Security.addProvider(EdDSASecurityProvider()) + } + + private fun deserializeCertPath(dis: DataInputStream): CertPath { + val certBytes = dis.readIntLengthPrefixBytes() + val certFactory = CertificateFactory.getInstance("X.509") + return certFactory.generateCertPath(ByteArrayInputStream(certBytes), "PkiPath") + } + + private fun deserializeSignedSecret(dis: DataInputStream): ByteArray = dis.readIntLengthPrefixBytes() + + /** + * Deserialzes a serialized [SenderIdentityImpl] instance + */ + fun deserialize(from: ByteArray): SenderIdentityImpl { + val bais = ByteArrayInputStream(from) + val dis = DataInputStream(bais) + return deserialize(dis) + } + + fun deserialize(dis: DataInputStream): SenderIdentityImpl { + try { + val signerCertPath = deserializeCertPath(dis) + val signedSecret = deserializeSignedSecret(dis) + + return SenderIdentityImpl(signerCertPath, signedSecret) + } catch (e: Exception) { + throw IllegalArgumentException("Corrupted MailerIdentity bytes", e) + } + } + } +} + +private fun DataOutputStream.writeIntLengthPrefixBytes(data: ByteArray) { + this.writeInt(data.size) + this.write(data) +} + +private fun DataInputStream.readIntLengthPrefixBytes(): ByteArray { + val data = ByteArray(this.readInt()) + this.readFully(data) + return data +} diff --git a/enclave/build.gradle b/enclave/build.gradle new file mode 100644 index 0000000000..385c4424bb --- /dev/null +++ b/enclave/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'kotlin' + id 'com.r3.conclave.enclave' +} + +/** + * The default Java version for enclaves is 11 so we need to set it to 8 for the CorDapp to work. + */ +tasks.withType(JavaCompile) { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + options.encoding = 'UTF-8' +} + +/** + * There is no Kotlin code in this project, + * but if there were, this section would be required. + * Note: Java versions above and here have to match. + */ +/* +tasks.withType(AbstractCompile) { + if (it.class.name.startsWith('org.jetbrains.kotlin.gradle.tasks.KotlinCompile')) { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + apiVersion = '1.5' + languageVersion = '1.5' + } + } +} +*/ + +dependencies { + implementation project(':common-enclave') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation 'junit:junit:4.13.1' + + testImplementation(platform('org.junit:junit-bom:5.7.0')) +} + +conclave { + productID = 1 + revocationLevel = 0 + + simulation { + signingType = privateKey + signingKey = file("sample_private_key.pem") + } + + debug { + signingType = privateKey + signingKey = file("sample_private_key.pem") + } + + release { + signingType = privateKey + signingKey = file("sample_private_key.pem") + } +} diff --git a/enclave/sample_private_key.pem b/enclave/sample_private_key.pem new file mode 100644 index 0000000000..090ce1661b --- /dev/null +++ b/enclave/sample_private_key.pem @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/enclave/src/main/kotlin/com/r3/conclave/cordapp/sample/enclave/CommandEnclave.kt b/enclave/src/main/kotlin/com/r3/conclave/cordapp/sample/enclave/CommandEnclave.kt new file mode 100644 index 0000000000..f85c118b7f --- /dev/null +++ b/enclave/src/main/kotlin/com/r3/conclave/cordapp/sample/enclave/CommandEnclave.kt @@ -0,0 +1,113 @@ +package com.r3.conclave.cordapp.sample.enclave + +import com.r3.conclave.cordapp.common.EnclaveCommand +import com.r3.conclave.cordapp.common.RegisterHostIdentity +import com.r3.conclave.cordapp.common.getRoutingInfo +import com.r3.conclave.cordapp.common.internal.SenderIdentityImpl +import com.r3.conclave.enclave.Enclave +import com.r3.conclave.mail.EnclaveMail +import com.r3.conclave.enclave.EnclavePostOffice +import java.lang.IllegalStateException +import java.security.PublicKey +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.logging.Logger + +abstract class CommandEnclave : Enclave() { + + // the public key of the authenticated and registered host + protected lateinit var authenticatedHostKey: PublicKey + + // Post office to communicate with our host + // To be initialised when registerHostIdentity is called + protected lateinit var hostPostOffice: EnclavePostOffice + + companion object { + private const val trustedRootCertificateResourcePath = "/trustedroot.cer" + + @JvmStatic + val trustedRootCertificate: X509Certificate = run { + try { + EncryptedTxEnclave::class.java.getResourceAsStream(trustedRootCertificateResourcePath).use { + val certificateFactory = CertificateFactory.getInstance("X509") + certificateFactory.generateCertificate(it) as X509Certificate + } + } catch (exception: Exception) { + Logger.getGlobal().severe( + "Failed to load trusted root certificate. Please ensure the resource exists and it is not " + + "corrupted. Resource path: $trustedRootCertificateResourcePath" + ) + throw exception + } + } + } + + /** + * Receive incoming mail and route according to the [EnclaveCommand] class contained in the [routingHint]. + * @param mail the received [EnclaveMail]. + * @param routingHint a [String] containing the executing flowId and enclave command to execute. + * @throws [IllegalArgumentException] if [routingHint] is null, or cannot be parsed. + */ + override fun receiveMail(mail: EnclaveMail, routingHint: String?) { + routingHint ?: throw IllegalArgumentException( + "routingHint must be set for this enclave: ${this.javaClass.simpleName}") + + val (flowId, route) = getRoutingInfo(routingHint) + + val registeredHost = if (isHostRegistered()) authenticatedHostKey else null + println("Received route: $route, registered host: $registeredHost") + + if (route is RegisterHostIdentity) { + registerHostIdentity(mail, flowId) + } else if (isHostRegistered()) { + receiveMail(mail, flowId, route) + } else { + throw IllegalStateException( + "Host must be registered before executing enclave commands" + ) + } + } + + abstract fun receiveMail(mail: EnclaveMail, flowId: String, route: EnclaveCommand) + + /** + * Deserialize the [SenderIdentityImpl] from the mail body and authenticate the contained identity. + * If the sender is trusted and is the signer of the shared secret, register the host's public key and initialise + * the [hostPostOffice] property. Respond with "ack" if the registration was successful or "nak" otherwise. + * @param mail an incoming [EnclaveMail] containing a serialized [SenderIdentityImpl] instance. + * @param flowId the ID of the flow executing on our host. + */ + protected fun registerHostIdentity(mail: EnclaveMail, flowId: String) { + val identity = SenderIdentityImpl.deserialize(mail.bodyAsBytes) + val sharedSecret = mail.authenticatedSender.encoded + + val authenticated = authenticateIdentity(sharedSecret, identity) + if (authenticated) { + authenticatedHostKey = mail.authenticatedSender + hostPostOffice = postOffice(mail) + } + + val responseString = if (authenticated) "ack" else "nak" + + val reply = postOffice(mail).encryptMail(responseString.toByteArray(Charsets.UTF_8)) + postMail(reply, "$flowId:${RegisterHostIdentity().serialize()}") + } + + /** + * Return true if a host has successfully registered and authenticated their identity. + * @return [Boolean] indicating if a host has registered with this enclave. + */ + private fun isHostRegistered() = this::authenticatedHostKey.isInitialized + + /** + * Return true if [identity] is trusted and is the signer of [sharedSecret]. + * @param sharedSecret check identity signature against this shared secret. + * @param identity the [SenderIdentityImpl] instance to authenticate. + */ + private fun authenticateIdentity(sharedSecret: ByteArray, identity: SenderIdentityImpl): Boolean { + val isTrusted = identity.isTrusted(trustedRootCertificate) + val didSign = identity.didSign(sharedSecret) + + return isTrusted && didSign + } +} \ No newline at end of file diff --git a/enclave/src/main/kotlin/com/r3/conclave/cordapp/sample/enclave/EncryptedTxEnclave.kt b/enclave/src/main/kotlin/com/r3/conclave/cordapp/sample/enclave/EncryptedTxEnclave.kt new file mode 100644 index 0000000000..e7dd34e818 --- /dev/null +++ b/enclave/src/main/kotlin/com/r3/conclave/cordapp/sample/enclave/EncryptedTxEnclave.kt @@ -0,0 +1,50 @@ +package com.r3.conclave.cordapp.sample.enclave + +import com.r3.conclave.common.EnclaveInstanceInfo +import com.r3.conclave.cordapp.common.* +import com.r3.conclave.enclave.EnclavePostOffice +import com.r3.conclave.mail.EnclaveMail +import java.lang.IllegalStateException +import java.util.* + +/** + * Provide commands to exchange a randomly generated message and hash with another instance of this enclave. + */ +class EncryptedTxEnclave : CommandEnclave() { + + private lateinit var remoteEnclavePostOffice: EnclavePostOffice + + private val commandToCallableMap = mapOf( + InitPostOfficeToRemoteEnclave::class.java.name to ::initPostOfficeToRemoteEnclave + ) + + override fun receiveMail(mail: EnclaveMail, flowId: String, route: EnclaveCommand) { + if (commandToCallableMap[route.javaClass.name] != null) { + commandToCallableMap[route.javaClass.name]!!.invoke(mail, flowId) + } else { + throw IllegalArgumentException( + "No callable registered for command $route in enclave: ${this.javaClass.simpleName}") + } + } + + /** + * Deserialize the enclave attestation and create a post office to the remote enclave. + * @param mail an incoming [EnclaveMail] containing the attestation to deserialize. + * @param flowId the ID of the flow executing on our host. + * @throws [IllegalStateException] if the remote enclave has already been initialised, or if the host identity has + * not been registered. + * @throws [IllegalArgumentException] if the mail sender is not the authenticated host key. + */ + private fun initPostOfficeToRemoteEnclave(mail: EnclaveMail, flowId: String) { + if (this::remoteEnclavePostOffice.isInitialized) { + throw IllegalStateException("Post office for remote enclave has already been initialized") + } else if (mail.authenticatedSender != authenticatedHostKey) { + throw IllegalArgumentException("Mail sender does not match the authenticated host key") + } + + val attestation = EnclaveInstanceInfo.deserialize(mail.bodyAsBytes) + // TODO: should we check an enclave constraint? + + remoteEnclavePostOffice = postOffice(attestation, flowId) + } +} \ No newline at end of file diff --git a/enclave/src/main/resources/trustedroot.cer b/enclave/src/main/resources/trustedroot.cer new file mode 100644 index 0000000000..4b63d46378 --- /dev/null +++ b/enclave/src/main/resources/trustedroot.cer @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICCTCCAbCgAwIBAgIIcFe0qctqSucwCgYIKoZIzj0EAwIwWDEbMBkGA1UEAwwS +Q29yZGEgTm9kZSBSb290IENBMQswCQYDVQQKDAJSMzEOMAwGA1UECwwFY29yZGEx +DzANBgNVBAcMBkxvbmRvbjELMAkGA1UEBhMCVUswHhcNMTcwNTIyMDAwMDAwWhcN +MjcwNTIwMDAwMDAwWjBYMRswGQYDVQQDDBJDb3JkYSBOb2RlIFJvb3QgQ0ExCzAJ +BgNVBAoMAlIzMQ4wDAYDVQQLDAVjb3JkYTEPMA0GA1UEBwwGTG9uZG9uMQswCQYD +VQQGEwJVSzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGlm6LFHrVkzfuUHin36 +Jrm1aUMarX/NUZXw8n8gSiJmsZPlUEplJ+f/lzZMky5EZPTtCciG34pnOP0eiMd/ +JTCjZDBiMB0GA1UdDgQWBBR8rqnfuUgBKxOJC5rmRYUcORcHczALBgNVHQ8EBAMC +AYYwIwYDVR0lBBwwGgYIKwYBBQUHAwEGCCsGAQUFBwMCBgRVHSUAMA8GA1UdEwEB +/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgDaL4SguKsNeTT7SeUkFdoCBACeG8 +GqO4M1KlfimphQwCICiq00hDanT5W8bTLqE7GIGuplf/O8AABlpWrUg6uiUB +-----END CERTIFICATE----- diff --git a/gradle.properties b/gradle.properties index d70b133fa3..87325104aa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,3 +5,4 @@ owasp.failOnError=false owasp.failBuildOnCVSS=11.0 compilation.allWarningsAsErrors=false test.parallel=false +conclaveVersion=1.2.1 diff --git a/settings.gradle b/settings.gradle index 97dfbdd783..6a3f2f7e6b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,8 +22,13 @@ pluginManagement { mavenLocal() gradlePluginPortal() maven { url "${artifactory_contextUrl}/corda-dependencies" } + maven { url "$settingsDir/repo" } } } + + plugins { + id 'com.r3.conclave.enclave' version conclaveVersion apply false + } } // The project is named 'corda-project' and not 'corda' because if this is named the same as the // output JAR from the capsule then the buildCordaJAR task goes into an infinite loop. @@ -102,6 +107,9 @@ include 'testing:cordapps:dbfailure:dbfworkflows' include 'testing:cordapps:missingmigration' include 'testing:cordapps:sleeping' +// Conclave +include 'enclave' + // Common libraries - start include 'common-validation' project(":common-validation").projectDir = new File("$settingsDir/common/validation") @@ -112,6 +120,9 @@ project(":common-configuration-parsing").projectDir = new File("$settingsDir/com include 'common-logging' project(":common-logging").projectDir = new File("$settingsDir/common/logging") + +include 'common-enclave' +project(":common-enclave").projectDir = new File("$settingsDir/common/enclave") // Common libraries - end apply from: 'buildCacheSettings.gradle'