com.r3.conclave.host.EnclaveHost.kt Maven / Gradle / Ivy
The newest version!
package com.r3.conclave.host
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.r3.conclave.common.*
import com.r3.conclave.common.internal.*
import com.r3.conclave.common.internal.InternalCallType.*
import com.r3.conclave.common.internal.attestation.Attestation
import com.r3.conclave.common.internal.kds.EnclaveKdsConfig
import com.r3.conclave.common.internal.kds.KDSErrorResponse
import com.r3.conclave.common.kds.KDSKeySpec
import com.r3.conclave.host.EnclaveHost.CallState.*
import com.r3.conclave.host.EnclaveHost.HostState.*
import com.r3.conclave.host.internal.*
import com.r3.conclave.host.internal.EnclaveScanner.ScanResult
import com.r3.conclave.host.internal.attestation.*
import com.r3.conclave.host.internal.fatfs.FileSystemHandler
import com.r3.conclave.host.internal.gramine.GramineEnclaveHandle
import com.r3.conclave.host.internal.kds.KDSPrivateKeyRequest
import com.r3.conclave.host.internal.kds.KDSPrivateKeyResponse
import com.r3.conclave.host.kds.KDSConfiguration
import com.r3.conclave.mail.Curve25519PublicKey
import com.r3.conclave.mail.MailDecryptionException
import com.r3.conclave.utilities.internal.*
import java.io.DataOutputStream
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.nio.ByteBuffer
import java.nio.file.Path
import java.security.PublicKey
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.function.Consumer
import java.util.function.Function
/**
* Represents an enclave running on the local CPU. Instantiating this object loads and
* initialises the enclave, making it ready to receive connections.
*
* You can get a [EnclaveHost] using one of the static factory methods.
*
* An enclave won't actually be loaded and initialised immediately until the [start] method is explicitly called.
* This gives you time to configure the [EnclaveHost] before startup.
*
* Multiple enclaves can be loaded at once, however, you may not mix
* simulation/debug/production enclaves together.
*
* Although the enclave must currently run against Java 8, the host can use any
* version of Java that is supported.
*/
class EnclaveHost private constructor(
private val enclaveHandle: EnclaveHandle
) : AutoCloseable {
/**
* Suppress kotlin specific companion objects from our API documentation.
* The public items within the object are still published in the documentation.
* @suppress
*/
companion object {
private val log = loggerFor()
private val signatureScheme = SignatureSchemeEdDSA()
private val jsonMapper = JsonMapper.builder().enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS).build()
/**
* Diagnostics output outlining CPU capabilities. This is a free text field and should only be used for
* debugging, logging. Don't try to parse the output.
*/
@JvmStatic
val capabilitiesDiagnostics: String
get() = Native.getCpuCapabilitiesSummary()
/**
* Load the signed enclave for the given enclave class name.
*
* @param enclaveClassName The name of the enclave class to load.
*
* @throws IllegalArgumentException if there is no enclave file for the given class name.
* @throws IllegalStateException if more than one enclave file is found.
* @throws EnclaveLoadException if the enclave does not load correctly or if the platform does
* not support hardware enclaves or if enclave support is disabled.
* @throws PlatformSupportException if the mode is not mock and the host OS is not Linux or if the CPU doesn't
* support SGX enclave in simulation mode or higher.
*/
@JvmStatic
@Throws(EnclaveLoadException::class, PlatformSupportException::class)
fun load(enclaveClassName: String): EnclaveHost = load(enclaveClassName, null)
/**
* Scan the classpath and load the single signed enclave that is found.
*
* @throws IllegalStateException if no enclave file is found or more than one enclave file is found.
* @throws EnclaveLoadException if the enclave does not load correctly or if the platform does
* not support hardware enclaves or if enclave support is disabled.
* @throws PlatformSupportException if the mode is not mock and the host OS is not Linux or if the CPU doesn't
* support SGX enclave in simulation mode or higher.
*/
@JvmStatic
@Throws(EnclaveLoadException::class, PlatformSupportException::class)
fun load(): EnclaveHost = load(null)
/**
* Load the signed enclave for the given enclave class name.
*
* @param enclaveClassName The name of the enclave class to load.
* @param mockConfiguration Defines the configuration to use when loading the enclave in mock mode.
* If no configuration is provided when using mock mode then a default set
* of configuration parameters are used. This parameter is ignored when
* not using mock mode.
*
* @throws IllegalArgumentException if there is no enclave file for the given class name or if
* an unexpected error occurs when trying to check platform support.
* @throws IllegalStateException if more than one enclave file is found.
* @throws EnclaveLoadException if the enclave does not load correctly or if the platform does
* not support enclaves in the required mode.
* @throws PlatformSupportException if the mode is not mock and the host OS is not Linux or if the CPU doesn't
* support SGX enclave in simulation mode or higher.
*/
@JvmStatic
@Throws(EnclaveLoadException::class, PlatformSupportException::class)
fun load(enclaveClassName: String, mockConfiguration: MockConfiguration?): EnclaveHost {
return createEnclaveHost(EnclaveScanner().findEnclave(enclaveClassName), mockConfiguration)
}
/**
* Scan the classpath and load the single signed enclave that is found.
*
* @param mockConfiguration Defines the configuration to use when loading the enclave in mock mode.
* If no configuration is provided when using mock mode then a default set
* of configuration parameters are used. This parameter is ignored when
* not using mock mode.
*
* @throws IllegalStateException if no enclave file is found or if more than one enclave file is found.
* @throws EnclaveLoadException if the enclave does not load correctly or if the platform does
* not support enclaves in the required mode.
* @throws PlatformSupportException if the mode is not mock and the host OS is not Linux or if the CPU doesn't
* support SGX enclave in simulation mode or higher.
*/
@JvmStatic
@Throws(EnclaveLoadException::class, PlatformSupportException::class)
fun load(mockConfiguration: MockConfiguration?): EnclaveHost {
return createEnclaveHost(EnclaveScanner().findEnclave(), mockConfiguration)
}
/**
* Determine whether simulated enclaves are supported on the current platform. Irrespective of the
* current project mode. To support simulated enclaves, the platform needs to be Linux.
*
* @return Boolean true if simulated enclaves are supported, false otherwise.
*/
@JvmStatic
fun isSimulatedEnclaveSupported(): Boolean = isNativeEnclaveSupported(requireHardwareSupport = false)
/**
* Determine whether hardware enclaves are supported on the current platform. Irrespective of the
* current project mode. To support hardware enclaves, the platform needs to be Linux and the system
* needs a CPU which supports Intel SGX.
*
* @return Boolean true if hardware enclaves are supported, false otherwise.
*/
@JvmStatic
fun isHardwareEnclaveSupported(): Boolean = isNativeEnclaveSupported(requireHardwareSupport = true)
/**
* Determine which enclave modes are supported on the current platform. Irrespective of the current
* project mode.
*
* @return Set containing the set of enclave modes that are supported by the current platform.
*/
@JvmStatic
fun getSupportedModes(): Set {
val supportedModes = EnumSet.of(EnclaveMode.MOCK)
if (isSimulatedEnclaveSupported()) {
supportedModes.add(EnclaveMode.SIMULATION)
if (isHardwareEnclaveSupported()) {
supportedModes.add(EnclaveMode.DEBUG)
supportedModes.add(EnclaveMode.RELEASE)
}
}
return supportedModes
}
/**
* Some platforms support software enablement of SGX, this method will attempt to do this. This may
* require elevated privileges and/or a reboot in order to work. The method is safe to call on machines
* where SGX is already enabled.
*
* @throws PlatformSupportException If SGX could not be enabled.
*/
@JvmStatic
@Throws(PlatformSupportException::class)
fun enableHardwareEnclaveSupport() {
checkNotLinux()
NativeShared.enablePlatformHardwareEnclaveSupport()
}
// The internal modifier prevents this from appearing in the API docs, however because we shade Kotlin it will
// still be available to Java users. We solve that by making it synthetic which hides it from the Java compiler.
@JvmSynthetic
@JvmStatic
internal fun internalCreateNonMock(scanResult: ScanResult): EnclaveHost {
val enclaveHandle = when (scanResult) {
is ScanResult.GraalVM -> NativeEnclaveHandle(
scanResult.enclaveMode,
scanResult.enclaveClassName,
scanResult.soFileUrl
)
is ScanResult.Gramine -> GramineEnclaveHandle(
scanResult.enclaveMode,
scanResult.enclaveClassName,
scanResult.zipFileUrl
)
is ScanResult.Mock -> throw IllegalArgumentException()
}
return EnclaveHost(enclaveHandle)
}
// The internal modifier prevents this from appearing in the API docs, however because we shade Kotlin it will
// still be available to Java users. We solve that by making it synthetic which hides it from the Java compiler.
@JvmSynthetic
@JvmStatic
internal fun internalCreateMock(
enclaveClass: Class<*>,
mockConfiguration: MockConfiguration? = null,
kdsConfig: EnclaveKdsConfig? = null
): EnclaveHost {
// For mock mode ensure the host can access the enclave constructor. It may have been set as private.
val constructor = enclaveClass.getDeclaredConstructor().apply { isAccessible = true }
val enclaveHandle = MockEnclaveHandle(
constructor.newInstance(),
mockConfiguration,
kdsConfig
)
return EnclaveHost(enclaveHandle)
}
private fun createEnclaveHost(result: ScanResult, mockConfiguration: MockConfiguration?): EnclaveHost {
try {
return when (result) {
is ScanResult.Mock -> {
// Here we do not call checkPlatformEnclaveSupport as mock mode is supported on any platform
val enclaveClass = Class.forName(result.enclaveClassName)
internalCreateMock(enclaveClass, mockConfiguration)
}
else -> {
checkPlatformEnclaveSupport(result.enclaveMode)
internalCreateNonMock(result)
}
}
} catch (e: EnclaveLoadException) {
throw e
} catch (e: Exception) {
throw EnclaveLoadException("Unable to load enclave", e)
}
}
private fun isNativeEnclaveSupported(requireHardwareSupport: Boolean): Boolean {
if (!UtilsOS.isLinux()) {
return false
}
return try {
NativeShared.checkPlatformEnclaveSupport(requireHardwareSupport)
true
} catch (e: PlatformSupportException) {
false
}
}
/**
* Checks to see if the platform supports enclaves in a given mode and throws an exception if not.
*
* @throws PlatformSupportException if the requested mode is not supported on the system.
*/
private fun checkPlatformEnclaveSupport(enclaveMode: EnclaveMode) {
// All platforms support MOCK mode
if (enclaveMode == EnclaveMode.MOCK)
return
checkNotLinux()
val requireHardwareSupport = enclaveMode.isHardware
try {
NativeShared.checkPlatformEnclaveSupport(requireHardwareSupport)
} catch (e: PlatformSupportException) {
// Improve error message in case that SSE4.1 is missing
val features = NativeApi.cpuFeatures
if (!features.contains(CpuFeature.SSE4_1)) {
val sb = StringBuilder()
sb.append(e.message)
sb.append(
features.joinToString(
prefix = "\nCPU features: ", separator = ", ",
postfix = "\nReason: SSE4.1 is required but was not found."
)
)
throw PlatformSupportException(sb.toString(), e)
} else {
throw e
}
} catch (e: Exception) {
throw IllegalStateException("Unable to check platform support", e)
}
}
private fun checkNotLinux() {
if (!UtilsOS.isLinux()) {
val message =
"This system does not support hardware enclaves. " +
"If you wish to run enclaves built in simulation, release or debug mode, " +
"you must run in a linux environment. Consult the conclave documentation " +
"for platform specific instructions."
throw PlatformSupportException(message)
}
}
}
private var kdsConfiguration: KDSConfiguration? = null
private var fileSystemHandler: FileSystemHandler? = null
private val hostStateManager = StateManager(New)
private val setEnclaveInfoCallHandler = SetEnclaveInfoCallHandler()
@PotentialPackagePrivate("Access for EnclaveHostMockTest")
private val enclaveMessageHandler = EnclaveMessageHandler()
private var _enclaveInstanceInfo: EnclaveInstanceInfoImpl? = null
private lateinit var commandsCallback: Consumer>
/**
* The name of the sub-class of Enclave that was loaded.
*/
val enclaveClassName: String get() = enclaveHandle.enclaveClassName
/**
* The mode the enclave is running in.
*/
val enclaveMode: EnclaveMode get() = enclaveHandle.enclaveMode
/**
* For mock mode, the instance of the Enclave that is loaded by the host. This should be cast
* to the type of Enclave that has been loaded and can be used to examine the state of the
* enclave.
*
* For anything other than mock mode attempting to access this property will result
* in an IllegalStateException being thrown.
*/
val mockEnclave: Any get() = enclaveHandle.mockEnclave
private lateinit var attestationService: AttestationService
@Throws(EnclaveLoadException::class)
@Synchronized
fun start(
attestationParameters: AttestationParameters?,
sealedState: ByteArray?,
enclaveFileSystemFile: Path?,
commandsCallback: Consumer>
) {
start(attestationParameters, sealedState, enclaveFileSystemFile, null, commandsCallback)
}
/**
* Causes the enclave to be loaded and the [com.r3.conclave.enclave.Enclave] object constructed inside.
* This method must be called before sending is possible. Remember to call
* [close] to free the associated enclave resources when you're done with it.
*
* @param attestationParameters Either an [AttestationParameters.EPID] object initialised with the required API keys,
* or an [AttestationParameters.DCAP] object (which requires no extra parameters) when the host operating system is
* pre-configured for DCAP attestation, typically by a cloud provider. This parameter is ignored if the enclave is
* in mock or simulation mode and a mock attestation is used instead. Likewise, null can also be used for development
* purposes.
*
* @param sealedState The last sealed state that was emitted by the enclave via [MailCommand.StoreSealedState]. The
* sealed state is an encrypted blob of the enclave's internal state and it's updated by the enclave as it processes
* mail (the contents of [com.r3.conclave.enclave.Enclave.persistentMap] is also part of this state). Each new sealed state that is
* emitted via [MailCommand.StoreSealedState] supercedes the previous one and must be securely persisted. Failure
* to do this will result in the enclave's clients detecting a "rollback" attack if the enclave is restarted.
* Typically the sealed state should be stored in a database, inside the same database transaction that
* processes thhe other mail commands, such as [MailCommand.PostMail]. More information can be found
* [here](https://github.com/R3Conclave/conclave-core-sdk/wiki/Enclave-Persistence).
*
* @param enclaveFileSystemFile File where the enclave's encrypted file system will be persisted to. This can be null
* if the enclave's configured to use one. If it is then a file path must be provided. More information can be found
* [here](https://github.com/R3Conclave/conclave-core-sdk/wiki/Enclave-Persistence).
*
* @param kdsConfiguration Configuration for connecting to a key derivation service (KDS) in case the enclave needs
* to use one for encrypting persisted data. More information can be found [here](https://github.com/R3Conclave/conclave-core-sdk/wiki/KDS-Configuration).
*
* @param commandsCallback A callback that is automatically invoked after the end of every [callEnclave] and
* [deliverMail] call. The callback returns a list of actions, or [MailCommand]s, which need to be actioned together,
* ideally within the scope single transaction.
*
* The callback is invoked serially, never concurrently, and in the order that they need to be actioned. This
* means there's no need to do any external synchronization.
*
* @throws IllegalArgumentException If the [enclaveMode] is either release or debug and no attestation parameters
* are provided.
* @throws EnclaveLoadException If the enclave could not be started.
* @throws IllegalStateException If the host has been closed.
*/
@Throws(EnclaveLoadException::class)
@Synchronized
fun start(
attestationParameters: AttestationParameters?,
sealedState: ByteArray?,
enclaveFileSystemFile: Path?,
kdsConfiguration: KDSConfiguration?,
commandsCallback: Consumer>
) {
if (hostStateManager.state is Started) return
hostStateManager.checkStateIsNot { "The host has been closed." }
// This can throw IllegalArgumentException which we don't want wrapped in a EnclaveLoadException.
attestationService = AttestationServiceFactory.getService(enclaveMode, attestationParameters)
try {
this.commandsCallback = commandsCallback
// Register call handlers
enclaveHandle.enclaveInterface.apply {
registerCallHandler(HostCallType.GET_ATTESTATION, GetAttestationHandler())
registerCallHandler(HostCallType.SET_ENCLAVE_INFO, setEnclaveInfoCallHandler)
registerCallHandler(HostCallType.CALL_MESSAGE_HANDLER, enclaveMessageHandler)
}
// Initialise the enclave before fetching enclave instance info
enclaveHandle.initialise(attestationParameters)
updateAttestation()
log.debug { enclaveInstanceInfo.toString() }
// Once the EnclaveInstanceInfo has been updated, we can do a KDS request for the persistence key.
if (kdsConfiguration != null) {
this.kdsConfiguration = kdsConfiguration
// TODO We can avoid this ECALL if we get the enclave to send its persistence key spec when it's
// first initialised.
val persistenceKeySpec = enclaveHandle.getKdsPersistenceKeySpec()
// If the enclave is configured also with KDS spec for persistence, we trigger the private key request.
// Note that the kdsConfiguration is also used in the context of KdsPostOffice
if (persistenceKeySpec != null) {
val kdsResponse = executeKdsPrivateKeyRequest(persistenceKeySpec, kdsConfiguration)
enclaveHandle.setKdsPersistenceKey(kdsResponse)
}
}
if (enclaveFileSystemFile != null) {
log.info("Setting up persistent enclave file system...")
}
fileSystemHandler = prepareFileSystemHandler(enclaveFileSystemFile)
enclaveHandle.startEnclave(sealedState)
if (enclaveFileSystemFile != null) {
log.info("Setup of the file system completed successfully.")
}
hostStateManager.state = Started
} catch (e: Exception) {
throw EnclaveLoadException("Unable to start enclave", e)
}
}
private fun prepareFileSystemHandler(enclaveFileSystemFile: Path?): FileSystemHandler? {
return if (isFileSystemSupported()) {
val fileSystemFilePaths = if (enclaveFileSystemFile != null) listOf(enclaveFileSystemFile) else emptyList()
FileSystemHandler(fileSystemFilePaths, enclaveMode)
} else {
null
}
}
private fun isFileSystemSupported(): Boolean {
// This filesystem implementation is only supported in SIMULATION, DEBUG, RELEASE mode in Graal VM mode only
// TODO: Fix this once we have filesystem supported in Gramine
return enclaveMode != EnclaveMode.MOCK && enclaveHandle is NativeEnclaveHandle
}
private fun executeKdsPrivateKeyRequest(
keySpec: KDSKeySpec,
kdsConfiguration: KDSConfiguration
): KDSPrivateKeyResponse {
val url = URL("${kdsConfiguration.url}/private")
val con: HttpURLConnection = url.openConnection() as HttpURLConnection
con.connectTimeout = kdsConfiguration.timeout.toMillis().toInt()
con.readTimeout = kdsConfiguration.timeout.toMillis().toInt()
con.requestMethod = "POST"
con.setRequestProperty("Content-Type", "application/json; utf-8")
con.setRequestProperty("API-VERSION", "1")
con.doOutput = true
val kdsPrivateKeyRequest = KDSPrivateKeyRequest(
// TODO Cache the serialised bytes
appAttestationReport = enclaveInstanceInfo.serialize(),
name = keySpec.name,
masterKeyType = keySpec.masterKeyType,
policyConstraint = keySpec.policyConstraint
)
con.outputStream.use {
jsonMapper.writeValue(it, kdsPrivateKeyRequest)
}
if (con.responseCode != HttpURLConnection.HTTP_OK) {
val errorText = (con.errorStream ?: con.inputStream).use { it.reader().readText() }
val kdsErrorResponse = try {
jsonMapper.readValue(errorText, KDSErrorResponse::class.java)
} catch (e: Exception) {
// It is likely that the error response is not a KDSErrorResponse if an exception is raised
// The best thing to do in those cases is to return the response code
throw IOException("HTTP response code: ${con.responseCode}, HTTP response message: $errorText")
}
throw IOException(kdsErrorResponse.reason)
}
val kdsPrivateKeyResponse = con.inputStream.use {
jsonMapper.readValue(it, KDSPrivateKeyResponse::class.java)
}
return kdsPrivateKeyResponse
}
/**
* Perform a fresh attestation with the attestation service. On successful completion the [enclaveInstanceInfo]
* property may be updated to a newer one. If so make sure to provide this to end clients.
*
* Note that an attestation is already performed on startup. It's recommended to call this method if a long time
* has passed and clients may want a more fresh version.
*/
@Synchronized
fun updateAttestation() {
val attestation = getAttestation()
updateEnclaveInstanceInfo(attestation)
}
private fun getAttestation(): Attestation {
val signedQuote = enclaveHandle.getEnclaveInstanceInfoQuote()
log.debug { "Got quote $signedQuote" }
return attestationService.attestQuote(signedQuote)
}
private fun updateEnclaveInstanceInfo(attestation: Attestation) {
_enclaveInstanceInfo = EnclaveInstanceInfoImpl(
setEnclaveInfoCallHandler.enclaveInfo.signatureKey,
setEnclaveInfoCallHandler.enclaveInfo.encryptionKey,
attestation
)
}
/**
* Provides the info of this specific loaded instance. Note that the enclave
* instance info will remain valid across restarts of the host JVM/reloads of the
* enclave.
*
* @throws IllegalStateException if the enclave has not been started.
*/
val enclaveInstanceInfo: EnclaveInstanceInfo
get() = checkNotNull(_enclaveInstanceInfo) { "The enclave host has not been started." }
/**
* Passes the given byte array to the enclave. The format of the byte
* arrays are up to you but will typically use some sort of serialization
* mechanism, alternatively, [DataOutputStream] is a convenient way to lay out
* pieces of data in a fixed order.
*
* For this method to work the enclave class must override and implement [com.r3.conclave.enclave.Enclave.receiveFromUntrustedHost] The return
* value from that method (which can be null) is returned here. It will not be received via the provided callback.
*
* With the provided callback the enclave also has the option of using
* [com.r3.conclave.enclave.Enclave.callUntrustedHost] and sending/receiving byte arrays in the opposite
* direction. By chaining callbacks together, a kind of virtual stack can be constructed
* allowing complex back-and-forth conversations between enclave and untrusted host.
*
* Any uncaught exceptions thrown by [com.r3.conclave.enclave.Enclave.receiveFromUntrustedHost] will propagate across the enclave-host boundary and
* will be rethrown here.
*
* @param bytes Bytes to send to the enclave.
* @param callback Bytes received from the enclave via [com.r3.conclave.enclave.Enclave.callUntrustedHost].
*
* @return The return value of the enclave's [com.r3.conclave.enclave.Enclave.receiveFromUntrustedHost].
*
* @throws UnsupportedOperationException If the enclave has not provided an implementation of
* [com.r3.conclave.enclave.Enclave.receiveFromUntrustedHost].
* @throws IllegalStateException If the host has not been started.
* @throws EnclaveException If an exception is raised from within the enclave.
*/
fun callEnclave(bytes: ByteArray, callback: Function): ByteArray? {
return callEnclaveInternal(bytes, callback)
}
/**
* Passes the given byte array to the enclave. The format of the byte
* arrays are up to you but will typically use some sort of serialization
* mechanism, alternatively, [DataOutputStream] is a convenient way to lay out
* pieces of data in a fixed order.
*
* For this method to work the enclave class must override and implement [com.r3.conclave.enclave.Enclave.receiveFromUntrustedHost] The return
* value from that method (which can be null) is returned here. It will not be received via the provided callback.
*
* The enclave does not have the option of using [com.r3.conclave.enclave.Enclave.callUntrustedHost] for
* sending bytes back to the host. Use the overload which takes in a callback [Function] instead.
*
* Any uncaught exceptions thrown by [com.r3.conclave.enclave.Enclave.receiveFromUntrustedHost] will propagate
* across the enclave-host boundary and will be rethrown here.
*
* @param bytes Bytes to send to the enclave.
*
* @return The return value of the enclave's [com.r3.conclave.enclave.Enclave.receiveFromUntrustedHost].
*
* @throws UnsupportedOperationException If the enclave has not provided an implementation of [com.r3.conclave.enclave.Enclave.receiveFromUntrustedHost].
* @throws IllegalStateException If the host has not been started.
* @throws EnclaveException If an exception is raised from within the enclave.
*/
fun callEnclave(bytes: ByteArray): ByteArray? = callEnclaveInternal(bytes, null)
private fun callEnclaveInternal(bytes: ByteArray, callback: EnclaveCallback?): ByteArray? {
return checkStateFirst { enclaveMessageHandler.callEnclave(bytes, callback) }
}
/**
* Delivers the given encrypted mail bytes to the enclave. The enclave is required to override and implement
* [com.r3.conclave.enclave.Enclave.receiveMail]
* to receive it. If the enclave throws an exception it will be rethrown.
* It's up to the caller to decide what to do with mails that don't seem to be
* handled properly: discarding it and logging an error is a simple option, or
* alternatively queuing it to disk in anticipation of a bug fix or upgrade
* is also workable.
*
* It's possible the callback provided to [start] will receive a [MailCommand.PostMail]
* on the same thread, requesting mail to be sent back in response. However, it's
* also possible the enclave will hold the mail without requesting any action.
*
* If the enclave is not unable to decrypt the mail bytes then a [MailDecryptionException] is thrown. This can
* happen if the mail is not encrypted to the enclave's key, which will most likely occur if the enclave was
* restarted and the client had used the enclave's old encryption key. In such a scenerio the client must be
* informed so that it re-send the mail using the enclave's new encryption key.
*
* @param mail The encrypted mail received from a remote client.
* @param routingHint An arbitrary bit of data identifying the sender on the host side. The enclave can pass this
* back through to [MailCommand.PostMail] to ask the host to deliver the reply to the right location.
* @param callback If the enclave calls [com.r3.conclave.enclave.Enclave.callUntrustedHost] then the
* bytes will be passed to this object for consumption and generation of the
* response.
*
* @throws UnsupportedOperationException If the enclave has not provided an implementation for
* [com.r3.conclave.enclave.Enclave.receiveMail].
* @throws MailDecryptionException If the enclave was unable to decrypt the mail due to either key mismatch or
* corrupted mail bytes.
* @throws IOException If the mail is encrypted with a KDS private key and the host was unable to communicate
* with the KDS to get it.
* @throws IllegalStateException If the host has not been started.
* @throws EnclaveException If an exception is raised from within the enclave.
*/
@Throws(MailDecryptionException::class, IOException::class)
fun deliverMail(mail: ByteArray, routingHint: String?, callback: Function) {
deliverMailInternal(mail, routingHint, callback)
}
/**
* Delivers the given encrypted mail bytes to the enclave. The enclave is required to override and implement
* [com.r3.conclave.enclave.Enclave.receiveMail]
* to receive it. If the enclave throws an exception it will be rethrown.
* It's up to the caller to decide what to do with mails that don't seem to be
* handled properly: discarding it and logging an error is a simple option, or
* alternatively queuing it to disk in anticipation of a bug fix or upgrade
* is also workable.
*
* It's possible the callback provided to [start] will receive a [MailCommand.PostMail]
* on the same thread, requesting mail to be sent back in response. However, it's
* also possible the enclave will hold the mail without requesting any action.
*
* If the enclave is not unable to decrypt the mail bytes then a [MailDecryptionException] is thrown. This can
* happen if the mail is not encrypted to the enclave's key, which will most likely occur if the enclave was
* restarted and the client had used the enclave's old encryption key. In such a scenerio the client must be
* informed so that it re-send the mail using the enclave's new encryption key.
*
* Note: The enclave does not have the option of using [com.r3.conclave.enclave.Enclave.callUntrustedHost] for
* sending bytes back to the host. Use the overload which takes in a callback [Function] instead.
*
* @param mail the encrypted mail received from a remote client.
* @param routingHint An arbitrary bit of data identifying the sender on the host side. The enclave can pass this
* back through to [MailCommand.PostMail] to ask the host to deliver the reply to the right location.
*
* @throws UnsupportedOperationException If the enclave has not provided an implementation for [com.r3.conclave.enclave.Enclave.receiveMail].
* @throws MailDecryptionException If the enclave was unable to decrypt the mail due to either key mismatch or
* corrupted mail bytes.
* @throws IOException If the mail is encrypted with a KDS private key and the host was unable to communicate
* with the KDS to get it.
* @throws IllegalStateException If the host has not been started.
* @throws EnclaveException If an exception is raised from within the enclave.
*/
@Throws(MailDecryptionException::class, IOException::class)
fun deliverMail(mail: ByteArray, routingHint: String?) = deliverMailInternal(mail, routingHint, null)
private fun deliverMailInternal(mail: ByteArray, routingHint: String?, callback: EnclaveCallback?) {
return checkStateFirst { enclaveMessageHandler.deliverMail(mail, callback, routingHint) }
}
private inline fun checkStateFirst(block: () -> T): T {
return when (hostStateManager.state) {
New -> throw IllegalStateException("The enclave host has not been started.")
Closed -> throw IllegalStateException("The enclave host has been closed.")
Started -> block()
}
}
@Synchronized
override fun close() {
// Closing an unstarted or already closed EnclaveHost is allowed, because this makes it easier to use
// Java try-with-resources and makes finally blocks more forgiving, e.g.
//
// try {
// enclave.start()
// } finally {
// enclave.close()
// }
//
// could yield a secondary error if an exception was thrown in enclave.start without this.
if (hostStateManager.state !is Started) return
try {
// Ask the enclave to close so all its resources are released before the enclave is destroyed
enclaveHandle.stopEnclave()
// Destroy the enclave
enclaveHandle.destroy()
fileSystemHandler?.close()
} finally {
hostStateManager.state = Closed
}
}
private class EnclaveInfo(val signatureKey: PublicKey, val encryptionKey: Curve25519PublicKey)
/**
* Handler for servicing attestation requests from the enclave.
*/
private inner class GetAttestationHandler : CallHandler {
override fun handleCall(parameterBuffer: ByteBuffer): ByteBuffer {
val attestationBytes = writeData { _enclaveInstanceInfo!!.attestation.writeTo(this) }
return ByteBuffer.wrap(attestationBytes)
}
}
/**
* Handler for receiving enclave info from the enclave on initialisation.
* TODO: It would be better to return enclave info from the initialise enclave call
* but that doesn't work in mock mode at the moment.
*/
private inner class SetEnclaveInfoCallHandler : CallHandler {
private var _enclaveInfo: EnclaveInfo? = null
val enclaveInfo: EnclaveInfo get() = checkNotNull(_enclaveInfo) { "Not received enclave info" }
override fun handleCall(parameterBuffer: ByteBuffer): ByteBuffer? {
val signatureKey = signatureScheme.decodePublicKey(parameterBuffer.getBytes(44))
val encryptionKey = Curve25519PublicKey(parameterBuffer.getBytes(32))
_enclaveInfo = EnclaveInfo(signatureKey, encryptionKey)
return null
}
}
private class Transaction {
val stateManager = StateManager(Ready)
val mailCommands = LinkedList()
fun fireMailCommands(commandsCallback: Consumer>) {
check(mailCommands.isNotEmpty())
val commandsCopy = ArrayList(mailCommands)
mailCommands.clear()
commandsCallback.accept(commandsCopy)
}
}
@PotentialPackagePrivate("Access for EnclaveHostMockTest")
private inner class EnclaveMessageHandler : CallHandler {
private val callTypeValues = InternalCallType.values()
@PotentialPackagePrivate("Access for EnclaveHostMockTest")
private val threadIDToTransaction = ConcurrentHashMap()
// Try to reduce the number of HTTP requests to the KDS, which also has the benefit for reducing the number
// large ECALLs containing the KDS mail response and the KDE EII bytes (since the enclave also caches the
// private key).
private val seenKdsKeySpecs = ConcurrentHashMap.newKeySet()
override fun handleCall(parameterBuffer: ByteBuffer): ByteBuffer? {
val type = callTypeValues[parameterBuffer.get().toInt()]
val threadID = parameterBuffer.getLong()
val transaction = threadIDToTransaction.getValue(threadID)
val callStateManager = transaction.stateManager
val intoEnclaveState = callStateManager.checkStateIs()
when (type) {
MAIL -> onMail(transaction, parameterBuffer)
UNTRUSTED_HOST -> onUntrustedHost(intoEnclaveState, threadID, parameterBuffer)
CALL_RETURN -> onCallReturn(callStateManager, parameterBuffer)
SEALED_STATE -> onSealedState(transaction, parameterBuffer)
}
return null
}
private fun onMail(transaction: Transaction, input: ByteBuffer) {
// routingHint can be null/missing.
val routingHint = input.getNullable { getIntLengthPrefixString() }
// rest of the body to deliver (should be encrypted).
val encryptedBytes = input.getRemainingBytes()
val cmd = MailCommand.PostMail(encryptedBytes, routingHint)
transaction.mailCommands.add(cmd)
}
private fun onUntrustedHost(intoEnclaveState: IntoEnclave, threadID: Long, input: ByteBuffer) {
val bytes = input.getRemainingBytes()
requireNotNull(intoEnclaveState.callback) {
"Enclave responded via callUntrustedHost but a callback was not provided to callEnclave."
}
val response = intoEnclaveState.callback.apply(bytes)
if (response != null) {
sendToEnclave(CALL_RETURN, threadID, response.size) { buffer ->
buffer.put(response)
}
}
}
private fun onCallReturn(callStateManager: StateManager, input: ByteBuffer) {
callStateManager.state = Response(input.getRemainingBytes())
}
private fun onSealedState(transaction: Transaction, input: ByteBuffer) {
val sealedState = input.getRemainingBytes()
val cmd = MailCommand.StoreSealedState(sealedState)
transaction.mailCommands.add(cmd)
// If a sealed state is received from the the enclave then it should be the last command the enclave sends
// to the host. It triggers an execution of the commands callback. We do this here whilst the thread still
// has the internal enclave lock, thus making sure the sealed states are emitted in the order the enclave
// wishes.
transaction.fireMailCommands(commandsCallback)
}
fun callEnclave(bytes: ByteArray, callback: EnclaveCallback?): ByteArray? {
// To support concurrent calls into the enclave, the current thread's ID is used a call ID which is passed between
// the host and enclave. This enables each thread to have its own state for managing the calls.
try {
return callIntoEnclave(callback) { threadID ->
sendToEnclave(UNTRUSTED_HOST, threadID, bytes.size) { buffer ->
buffer.put(bytes)
}
}
} catch (e: MailDecryptionException) {
// callEnclave does not have throws declaration for MailDecryptionException (since it doesn't directly
// deal with mail) and so must be wrapped in an unchecked exception.
throw RuntimeException(e)
}
}
fun deliverMail(mailBytes: ByteArray, callback: EnclaveCallback?, routingHint: String?) {
// The host checks if the mail is encrypted with a KDS private key and makes the KDS HTTP request to get
// it. This is safe to do as the key derivation field is authenticated and the KDS response is encrypted.
// The enclave will check itself anyway that the response matches the key derivation field.
// Doing this has two benefits:
// 1. Avoids an unnecessary OCALL-ECALL cycle and thus simplifying the enclave code.
// 2. Avoids any IOException that might have been thrown by the HTTP request from being swallowed in
// release mode enclaves.
val mailKeyDerivation = MailKeyDerivation.deserialiseFromMailBytes(mailBytes)
val kdsKeySpec = (mailKeyDerivation as? KdsKeySpecKeyDerivation)?.keySpec
val privateKeyResponse = kdsKeySpec?.let { getKdsPrivateKeyResponse(kdsKeySpec) }
callIntoEnclave(callback) { threadID ->
val routingHintBytes = routingHint?.toByteArray()
val routingHintSize = nullableSize(routingHintBytes) { it.intLengthPrefixSize }
val privateKeyResponseSize = nullableSize(privateKeyResponse) { it.size }
val size = routingHintSize + privateKeyResponseSize + mailBytes.size
sendToEnclave(MAIL, threadID, size) { buffer ->
buffer.putNullable(routingHintBytes) { putIntLengthPrefixBytes(it) }
buffer.putNullable(privateKeyResponse) { putKdsPrivateKeyResponse(it) }
buffer.put(mailBytes)
}
}
if (privateKeyResponse != null) {
// It's important that we mark this key spec as having been seen only after the enclave has processed
// the mail. In the presence of multiple threads, only here can we guarantee that it has cached the
// private key for itself.
seenKdsKeySpecs += kdsKeySpec
}
}
private fun getKdsPrivateKeyResponse(keySpec: KDSKeySpec): KDSPrivateKeyResponse? {
// As an optimisation avoid sending the KDS response mail and KDS EII if the enclave has already cached
// the private key. However we can't guarantee that the enclave has cached the private key until after
// deliverMail has returned which is why we don't update the cache here.
return if (keySpec !in seenKdsKeySpecs) {
val kdsConfig = checkNotNull(kdsConfiguration) {
"Mail is encrypted with KDS private key but host has not been provided with KDS configuration."
}
executeKdsPrivateKeyRequest(keySpec, kdsConfig)
} else {
null
}
}
// Sets up the state tracking and handle re-entrancy.
private fun callIntoEnclave(callback: EnclaveCallback?, body: (Long) -> Unit): ByteArray? {
val threadID = Thread.currentThread().id
val transaction = threadIDToTransaction.computeIfAbsent(threadID) { Transaction() }
val callStateManager = transaction.stateManager
// It's allowed for the host to recursively call back into the enclave with callEnclave via the callback. In this
// scenario previousCallState would represent the previous call into the enclave. Once this recursive step is
// complete we restore the call state so that the recursion can unwind.
val intoEnclaveState = IntoEnclave(callback)
// We take note of the current state so that once this callEnclave has finished we revert back to it. This
// allows nested callEnclave each with potentially their own callback.
val previousCallState = callStateManager.transitionStateFrom(to = intoEnclaveState)
// Going into a callEnclave, the call state should only be Ready or IntoEnclave.
check(previousCallState !is Response)
var response: Response? = null
try {
body(threadID)
} catch (t: Throwable) {
throw when (t) {
// No need to wrap an Enclave exception inside another Enclave exception
is EnclaveException -> t
// Unchecked exceptions propagate as is.
is RuntimeException, is Error -> t
// MailDecryptionException needs to propagate as is for deliverMail.
is MailDecryptionException -> t
else -> EnclaveException(null, t)
}
} finally {
// We revert the state even if an exception was thrown in the callback. This enables the user to have
// their own exception handling and reuse of the host-enclave communication channel for another call.
if (callStateManager.state === intoEnclaveState) {
// If the state hasn't changed then it means the enclave didn't have a response.
callStateManager.state = previousCallState
} else {
response = callStateManager.transitionStateFrom(to = previousCallState)
}
}
// If fully unwound and we still have mail commands to deliver (because a sealed state wasn't emitted) ...
if (callStateManager.state == Ready && transaction.mailCommands.isNotEmpty()) {
// ... the transaction ends here so pass mail commands to the host for processing.
transaction.fireMailCommands(commandsCallback)
}
return response?.bytes
}
private fun sendToEnclave(
type: InternalCallType,
threadID: Long,
payloadSize: Int,
payload: (ByteBuffer) -> Unit
) {
val buffer = ByteBuffer.allocate(1 + Long.SIZE_BYTES + payloadSize).apply {
put(type.ordinal.toByte())
putLong(threadID)
payload(this)
}
enclaveHandle.sendMessageHandlerCommand(buffer)
}
}
private sealed class CallState {
object Ready : CallState()
class IntoEnclave(val callback: EnclaveCallback?) : CallState()
class Response(val bytes: ByteArray) : CallState()
}
private sealed class HostState {
object New : HostState()
object Started : HostState()
object Closed : HostState()
}
}
// Typealias to make this code easier to read.
private typealias EnclaveCallback = Function