Writing the sample enclave¶
The sample "hello world" enclave just reverses whatever string is passed into it. We'll do these things to make our own version of the hello enclave project:
- Configure Gradle. At this time Conclave projects must use Gradle as their build system.
- Implement an enclave object that accepts both local calls from the host, and encrypted messages from a client.
- Write the host program that loads the enclave.
- Run the host and enclave in simulation and debug modes.
- Write the client that sends the enclave encrypted messages via the host.
Configure your modules¶
Create a new Gradle project via whatever mechanism you prefer, e.g. IntelliJ can do this via the New Project wizard. Create three modules defined in the project: one for the host, one for the enclave and one for the client.
The host program may be an existing server program of some kind, e.g. a web server, but in this tutorial we'll write a command line host. The client may likewise be a GUI app or integrated with some other program (like a server), but in this case to keep it simple the client will also be a command line app.
In the unzipped SDK there is a directory called
repo that contains a local Maven repository. This is where the libraries
and Gradle plugin can be found. We need to tell Gradle to look there for plugins.
Create or modify a file called
settings.gradle in your project root directory so it looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
This boilerplate is unfortunately necessary to copy/paste into each project that uses Conclave. It sets up Gradle to locate the plugin that configures the rest of the boilerplate build logic for you
pluginManagement block tells Gradle to use a property called
conclaveRepo to find the
in your SDK download. Because developers on your team could unpack the SDK anywhere, they must configure the path
before the build will work. The code above will print a helpful error if they forget or get it wrong.
To set the value, add a couple of lines to
Gradle properties can be set using a file in the project directory, or more usefully in the developer's home directory.
You may wish to put the version in the project's
gradle.properties file and the path in each developer's personal
gradle.properties. Alternatively just add a
sdk directory to the
.gitignore and require everyone to unpack the
SDK to the source tree.
Add the following code to your root
build.gradle file to import the repository:
1 2 3 4 5 6 7 8
Configure the host module¶
Add this bit of code to your host
build.gradle file to let the mode be chosen from the command line:
Then add the following dependencies, also to the host's
1 2 3 4 5 6 7
This says that at runtime (but not compile time) the
:enclave module must be on the classpath, and configures
dependencies to respect the three different variants of the enclave. That is, the enclave module will expose tasks
to compile and use either simulation, debug or release mode (in mock mode you're just using regular Java, so no
special compilation is necessary). Which task to use is actually selected by the host build.
Don't worry if you see the error
Could not resolve project :enclave. This will be resolved when
we configure the enclave module in the next section.
For this simple tutorial we also add a runtime-only dependency on the popular SLF4J library which Conclave uses to do logging. SLF4J enables you to send Conclave's logging to any of the major logging frameworks used in Java, but here, we add the "simple" backend which just causes it to log to the console. Finally we configure unit testing using JUnit 5.
If you intend to use an external signing process to sign your enclave then add the following lines to the Gradle file:
1 2 3 4
This creates a new task that can be invoked using Gradle to halt the build after generating materials that need to be signed by an external signing process. After the material has been signed the build can be resumed.
Configure the enclave module¶
Add the Conclave Gradle plugin:
1 2 3
and a dependency on the Conclave enclave library:
1 2 3 4 5
This time you don't have to specify the Conclave version because the plugin will set that for you automatically.
Specify the enclave's runtime environment, product ID and revocation level:
1 2 3 4 5
These settings are described in detail in the page on enclave configuration. A summary of these settings follows:
The runtime setting tells Conclave which runtime environment to use inside the enclave and can either be
graalvm_native_image. If the setting is omitted then it defaults to
Architecture overview for details on the differences between the two supported runtime
graalvm_native_image value is new and has a few limitations, but runs much faster.
Conclave needs access to a Linux build environment in order to build enclaves with the
On MacOS and Windows this is automatically created during the build process using Docker. If you do not have Docker
installed then the build will generate an error prompting you to switch to using either the
avian runtime or to
install Docker on your system. Once Docker is installed and added to your
PATH environment variable you can proceed
graalvm_native_image enclaves. Docker is not required for enclaves using the
The product ID is an arbitrary number that can be used to distinguish between different enclaves produced by the same organisation (which may for internal reasons wish to use a single signing key). This value should not change once you have picked it.
The revocation level should be incremented if a weakness in the enclave code is discovered and fixed; doing this will enable clients to avoid connecting to old, compromised enclaves. The revocation level should not be incremented on every new release, but only when security improvements have been made.
Specify the signing methods for each of the build types. You could keep your private key in a file for both debug and release enclaves if you like, but some organisations require private keys to be held in an offline system or HSM. In that case, configure it like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
The example configuration above specifies different signing configurations for each of the different build types.
- Simulation builds use the default dummy key.
- Debug builds use a private key stored in a file.
- Release builds use a private key managed by some external signing process.
hello-world sample in the SDK contains some example keys that can be used with the
externalKey signing types. These can be found in
|sample_private_key.pem||A 3072 bit RSA private key that can be used to test the
|external_signing_*.pem||An AES encrypted 3072 bit RSA public/private key pair that can be used to test the
signing directory from the SDK into your project and/or update the paths in the enclave
Alternatively you can provide or generate your own keys.
These keys aren't whitelisted by Intel so you can't use them for real release builds. Only use these sample keys for the tutorial. Don't use them for signing your own enclaves!
Configure the client build¶
The client build is the simplest of all. This is literally a bog-standard hello world command line app Gradle build, with a single dependency on the Conclave client library:
1 2 3 4 5 6 7 8 9 10 11 12
And with that, we're done configuring the build.
Create a new subclass of
Enclaves are similar to standalone programs and as such have an equivalent to a "main class". This class must be a
Create your enclave class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Enclave class by itself doesn't require you to support direct communication with the host. This is because
sometimes you don't need that and shouldn't have to implement message handlers. In this case we'll use that
functionality because it's a good place to start learning, so we also override and implement the
method which takes a byte array and optionally returns a byte array back. Here we just reverse the contents.
In a real app you would use the byte array to hold serialised data structures. You can use whatever data formats you like. You could use a simple string format or a binary format like protocol buffers.
In this tutorial we won't write a multi-threaded enclave. If you want to do this, you'll need to override the
boolean isThreadSafe() method in the
Enclave class (use
override val threadSafe: Boolean get() = true in Kotlin).
This tells Conclave to allow multiple threads into the enclave simultaneously. You're required to opt-in to allowing
multi-threading to avoid accidents when someone writes a simple enclave that isn't thread safe, and forgets that the host
is malicious and can enter your code with multiple threads simultaneously even if you aren't ready for it, corrupting
your application level data via race conditions. By blocking multi-threading until you indicate readiness, the hope
is that some types of attack can be avoided. See the page on enclave threading to learn more.
Write a simple host program¶
An enclave by itself is just a library: you must therefore load it from inside a host program.
It's easy to load then pass data to and from an enclave. Let's start with the skeleton of a little command line app:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
At first we will be building and running our enclave in simulation mode. This does not require the platform hardware to support SGX. However simulation mode does require us to be using Linux. If we are not using Linux as our host OS then we can use a Linux container or virtual machine as described in Running the host. Alternatively we could use mock mode instead of simulation mode. When we want to switch to loading either a debug or release build of the enclave we need to ensure the platform supports SGX.
By adding the code below to the main method we can determine whether the platform can load debug and release enclaves. This method reports the actual hardware status even if you are currently working with simulation enclaves.
1 2 3 4 5 6 7 8
If SGX is not supported the function throws an exception which describes the reason why. There are a number of common reasons why SGX may not be supported including:
- The CPU or the system BIOS does not support SGX.
- The host operating system is Windows or Mac OS. Conclave currently only supports loading enclaves in simulation, debug or release modes on Linux.
- SGX is disabled in the BIOS and must be manually enabled by the user.
- SGX is disabled but can be enabled in software.
If SGX is disabled but can be enabled in software the code below attempts to automatically enable SGX support by specifying the 'true' parameter. It might be necessary to run this application with root access and/or reboot the system in order to successfully enable SGX. The exception message will describe if this is the case.
To load the enclave we'll put this after the platform check:
1 2 3 4 5 6 7 8 9
This code starts by creating an
EnclaveHost object. This names the
class and then attempts to load it inside another JVM running inside an enclave. A remote attestation procedure is
then performed involving Intel's servers. This procedure can fail if attempting to load a debug or release enclave
and the platform does not support SGX. This is why it is important to perform the platform check we made in the code
above. If the enclave does fail to load for any reason then an exception is thrown describing the reason why.
You can use the command
EnclaveHost.getCapabilitiesDiagnostics() to print out some diagnostic information about the CPU, which can be helpful for troubleshooting.
We then call
start which initialises the enclave and the
MyEnclave class inside it.
You can load multiple enclaves at once but they must all use same mode, and each enclave will get its own isolated
Note that an
EnclaveHost allocates memory out of a pool called the "enclave page cache" which is a machine-wide
limited resource. It'll be freed if the host JVM quits, but it's good practice to close the
EnclaveHost object by
close on it when done. Therefore we also make sure the
.close() method is called on the enclave no
matter what using a try-with-resources statement. This doesn't actually matter in such a tiny hello world sample,
because the enclave will be unloaded by the kernel once we exit like any other resource. It's just here to remind
you that an enclave must be explicitly unloaded if you need to reinitialise it for whatever reason, or if you need
the memory back.
Starting and stopping/closing an enclave is not free, so don't load the enclave, use it and immediately close it again as in the above example. Cost-wise it's like starting a regular process even though no process will actually exist. Treat the enclave like any other expensive resource and keep it around for as long as you might need it.
Once we started the enclave, we call it passing in a string as bytes. The enclave will reverse it and we'll print out
the answer. This is as easy as calling
EnclaveHost.callEnclave, so put this in the
callEnclave static method
So we just convert the string to bytes, send it to the enclave, and convert the response from bytes back to a string.
There's no point in using an enclave to protect purely local data, as the data must ultimately come from the (assumed malicious/compromised) host in that scenario. That's why you need remote attestation, which lets an enclave prove its identity to the third parties who will upload secret data. If this paragraph doesn't make sense please review the Architecture overview and the Enclaves section.
Before we can set up communication with a client, we must therefore get remote attestation working.
Using remote attestation is easy! Just obtain an
EnclaveInstanceInfo and serialize/deserialize it using the
provided methods. Add these lines to the end of the
1 2 3
EnclaveInstanceInfo has a useful
toString function that will print out something like this:
1 2 3 4 5 6 7 8 9 10
The hash in the first line is the measurement. This is a hash of the code of the enclave. It includes both all
the Java code inside the enclave as a fat-JAR, and all the support and JVM runtime code required. As such it will
change any time you alter the code of your enclave, the version of Conclave in use or the mode
(simulation/debug/release) of the enclave. The enclave measurement should be stable across builds and machines, so
clients can audit the enclave by repeating the Gradle build and comparing the value they get in the
EnclaveInstanceInfo against what the build process prints out.
- All this data is available via individual getters on the
EnclaveInstanceInfoso you should never feel a need to parse the output of
EnclaveInstanceInfois an interface so you can easily build mock attestations in your tests.
- When not in simulation mode the timestamp is signed by Intel and comes from their servers.
An instance has a security assessment, which can change in response to discovery of vulnerabilities in the
infrastructure (i.e. without anything changing about the host or enclave itself). As we can see this enclave isn't
actually considered secure yet because we're running in simulation mode still. An enclave can be
INSECURE. A assessment of
STALE means there is a software/firmware/microcode update available for the platform
that improves security in some way. The client may wish to observe when this starts being reported and define a
time span in which the remote enclave operator must upgrade.
We can send the serialized bytes to a client via whatever network mechanism we want. The bytes are essentially a large, complex digital signature, so it's safe to publish them publicly. For simplicity in this tutorial we are just going to copy them manually and hard-code them in the client, but more on that later.
An attestation doesn't inherently expire but because the SGX ecosystem is always moving, client code will typically have
some frequency with which it expects the host code to refresh the
EnclaveInstanceInfo. At present this is done by
stopping/closing and then restarting the enclave.
To use SGX remote attestation for real we need to do some additional work. Remember how we wrote
enclave.start(null, null); above? The first parameter contains configuration data required to use an attestation
service. There are three kinds of attestation service:
- EPID. This older protocol is supported by some desktop/laptop class Intel CPUs. The EPID protocol includes some consumer privacy cryptography, and involves talking directly to Intel's IAS service to generate an attestation. For that you need to obtain an API key and service provider ID from Intel. You can sign-up easily and for free. Learn more about IAS.
- Azure DCAP. The datacenter attestation primitives protocol is newer and designed for servers. When running on a Microsoft Azure Confidential Compute VM or Kubernetes pod, you don't need any parameters. It's all configured out of the box.
- Generic DCAP. When not running on Azure, you need to obtain API keys for Intel's PCCS service.
We'll target Azure for now to keep things simple. Replace the call to
EnclaveHost.start above with this snippet:
Why does Conclave need to contact Intel's servers? It's because those servers contain the most up to date information about what CPUs and system enclave versions are considered secure. Over time these servers will change their assessment of the host system and this will be visible in the responses, as security improvements are made.
Run what we've got so far¶
We can apply the Gradle
application plugin and set the
in the usual manner to let us run
the host from the command line.
gradlew host:run and it should print "Hello World!" backwards along with the security info as shown above.
If you are using Windows or macOS then please follow the instructions for your operating system, which you can find in the Running the host section.
During the build you should see output like this:
1 2 3 4
The code hash will correspond to the value found in the
and the code signer will be
Make a note of the value of
Enclave code signer. We will need it later on to verify the enclave's identity from the client.
You can switch to debug mode by specifying the
enclaveMode property. In debug mode the real hardware is used and
virtually everything is identical to how it will be in production, but there's a small back door that can be used
by debuggers to read/write the enclave's memory.
You will need to run this on an Azure Confidential VM.
gradlew -PenclaveMode=debug host:run
The enclave isn't of much use to the host, because the host already trusts itself. It's only useful to remote clients that want to use the enclave for computation without having to trust the host machine or software.
We're now going to wire up encrypted messaging. Conclave provides an API for this called Mail. Conclave Mail handles all the encryption for you, but leaves it up to you how the bytes themselves are moved around. You could use a REST API, a gRPC API, JMS message queues, a simple TCP socket as in this tutorial, or even files.
Receiving and posting mail in the enclave¶
Firstly we need to upgrade our little enclave to be able to receive mails from clients. This is easy! Just override
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
receiveFromUntrustedHost method here isn't really needed, it's just because we're already using this to demonstrate local calls.
The new part is
receiveMail. This method takes three parameters: the first is an identifier that the host gets to pick, which doesn't
mean anything but we can use to acknowledge the mail if we want to using
Enclave.acknowledgeMail. Acknowledgement can be used
to tell the host the enclave is done processing the mail if it doesn't want to reply immediately. It will be discussed
more in future tutorials. In this simple tutorial we reply immediately so don't need to use this feature, and thus we
ignore the ID.
The second parameter is a routing hint string. It's also provided by the host and it helps the host route replies when
dealing with multiple clients. It's passed into
postMail when the enclave posts a reply. In our example the host only
deals with one client and so it's not used.
The third parameter is an
EnclaveMail. This object gives us access to the body bytes that the client sent, but it
also exposes some other header fields:
- A topic. This can be used to distinguish between different streams of mail from the same client. It's a string and can be thought of as equivalent to a URL path or port number. Topics are scoped per-sender and are not global. The client can send multiple streams of related mail by using a different topic for each stream, and it can do this concurrently.
- The sequence number. Starting from zero, this is incremented by one for every mail delivered on a topic. Conclave will automatically reject messages if this doesn't hold true, thus ensuring to the client that the stream of related mail is received by the enclave in the order they were sent, and that the host is unable to re-order or drop them.
- The envelope. This is a slot that can hold any arbitrary byte array the sender likes. It's a holding zone for app specific data that should be authenticated but unencrypted.
These header fields are available to the host and therefore should not contain secrets. It may seem odd to have
data that's unencrypted, but it's often useful for the client, host and enclave to collaborate in various ways related
to storage and routing of data. Even when the host is untrusted it may still be useful for the client to send data
that is readable by the host and enclave simultaneously, but which the host cannot tamper with. Inside the enclave
you can be assured that the header fields contain the values set by the client, because they're checked before
receiveMail is invoked.
In addition to the headers there is also the authenticated sender public key. This is the public key of the client that sent the mail. Like the body it's encrypted so that the host cannot learn the client identities. It's called "authenticated" because the encryption used by Conclave means you can trust that the mail was encrypted by an entity holding the private key matching this public key. If your enclave recognises the public key this feature can be used as a form of user authentication.
In this simple tutorial we only care about the body. We reverse the bytes in the mail body and then create a response
mail that will be encrypted to the sender. It contains the reversed bytes. We use the
postOffice method to do this. It
gives us back a post office object, which is a factory for creating encrypted mail. Because we want to create a
response, we pass in the original mail to
postOffice and it will give us an instance which is configured to encrypt mail
back to the sender. It will also use the same topic as the original mail.
encryptMail will encrypt the reversed bytes
and add on the authenticated header. The resulting mail bytes are passed to
postMail which delivers it to the host.
It will emerge in a callback we're about to configure.
You can post mail anytime and to anyone you like. It doesn't have to be a response to the sender, you can post
multiple mails at once and you can post mails inside
receiveFromUntrustedHost (i.e. during a local call).
Receiving and posting mail in the host¶
Mail posted by an enclave appears in a callback we pass to
EnclaveHost.start. Let's use a really simple
implementation: we'll just store the encrypted bytes in a variable, so we can pick it up later.
Replace the call to
EnclaveHost.start with this snippet:
1 2 3 4 5 6 7 8 9
Java doesn't let us directly change variables from a callback, so we use an
AtomicReference here as a box.
Kotlin lets you alter mutable variables from callbacks directly, without needing this sort of trick.
The callback is a list of
MailCommand objects, and what we're interested in are requests for delivery which are
MailCommand.PostMail objects. They contain the encrypted mail bytes to send. More information about the
mail commands can be found below.
The enclave can provide a routing hint to tell the host where it'd like the message delivered.
It's called a "hint" because the enclave must always remember that
the host is untrusted. It can be arbitrarily malicious and could, for example, not deliver the mail at all, or
it could deliver it to the wrong place. However if it does deliver it wrongly, the encryption will ensure the
bogus recipient can't do anything with the mail. In this simple hello world tutorial we can only handle one client
at once so we're going to ignore the routing hint here. In a more sophisticated server your callback implementation can
have access to your connected clients, a database, a durable queue, a
ThreadLocal containing a servlet connection and so on.
At the bottom of our main method let's add some code to accept TCP connections and send the
whomever connects. You will also need to add
throws IOException to the method signature of
main. Then we'll accept a
mail uploaded by the client, send it to the enclave, and deliver the response back. We'll write the client code in a moment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
This code is straightforward. In order, it:
- Opens a socket using the Java sockets API and listens for a connection.
- Accepts a connection and then sends the serialized
EnclaveInstanceInfoto the client. We first send the length so the client knows how many bytes to read.
- The client will send us a byte array back, which contains an encrypted string. This code can't read the body, it's just encrypted bytes except for a few fields in the headers, which are available to the host.
- We deliver the encrypted mail bytes to the enclave.
- We pick up the response from the
AtomicReferencebox that was set by the callback.
The first parameter to
deliverMail is a "mail ID" that the enclave can use to
identify this mail to the host. This feature is intended for use with acknowledgement, which allows the enclave to
signal that it's done with that message and the work it represents can be atomically/transactionally completed.
The routing hint is an arbitrary string that can be used to identify the sender of the mail from the host's
perspective, e.g. a connection ID, username, identity - it's up to you. The enclave can use this string to
signal to the host that a mail should go to that location. It's called a "hint" to remind you that the host code may
be modified or writter by an attacker, so the enclave can't trust it. However, the encryption on the mail makes it
useless for the host to mis-direct mail.
In future we will provide APIs to bind enclaves to common transports, to avoid this sort of boilerplate.
The second parameter to
EnclaveHost.start is a callback which returns a list of
MailCommand objects from the enclave.
There are two commands the host can receive:
- Post mail. This is when the enclave wants to send mail over the network to a client. The enclave may provide a routing hint with the mail to help the host route the message. The host is also expected to safely store the message in case the enclave is restarted. If that happens then it needs to redeliver all the (unacknowledged) mail back to the enclave in order.
- Acknowledge mail. This is when the enclave no longer needs the mail to be redelivered to it on restart and the host is thus expected to delete it from its store. There are many reasons why an enclave may not want a message redelivered. For example, the conversation with the client has reached its end and so it acknowledges all the mail in that thread; or the enclave can checkpoint in the middle by creating a mail to itself which condenses all the previous mail, which are then all acknowledged.
The host receives these commands grouped together within the scope of a single
call. This allows the host to add transactionality when processing the commands. So for example, the delivery of the mail
from the client to the enclave and the subsequent reply back can be processed atomically within the same database transaction
when the host is providing persistent, durable messaging. Likewise the acknowledgement of any mail can occur within the
Writing the client¶
The client app will do three things:
- Connect to the host server and download the
- Verify the enclave is acceptable: i.e. that it will do what's expected.
- Send it the command line arguments as a string to reverse and get back the answer, using encrypted mail.
Here's the initial boilerplate to grab the user input, connect, download and deserialize the
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
How do you know the
EnclaveInstanceInfo you've got is for the enclave you really intend to interact with? In normal
client/server programming you connect to a host using some sort of identity, like a domain name or IP address. TLS
is used to ensure the server that picks up is the rightful owner of the domain name you intended to connect to. In
enclave programming the location of the enclave might not matter much because the host is untrusted. Instead you have
to verify what is running, rather than where it's running.
The domain name of the server can still be important in some applications, in which case you should use TLS instead of raw sockets as is the case here.
One way to do this is by inspecting the properties on the
EnclaveInstanceInfo object and hard-coding some logic. That
works fine, but testing an
EnclaveInstanceInfo is a common pattern in enclave programming, so we provide an API to
do it for you.
EnclaveConstraint class takes an
performs some matching against it. A constraint object can be built in code, or it can be loaded from a small domain
specific language encoded as a one-line string. The string form is helpful if you anticipate frequent upgrades that
should be whitelisted or other frequent changes to the acceptable enclave, as it can be easily put into a
configuration file, JSON, XML or command line flags.
The constraint lets you specify:
- Acceptable code hashes (measurements)
- Acceptable signing public keys
- The minimum revocation level
- The product ID
- The security level of the instance:
If you specify a signing public key then you must also specify the product ID, otherwise if the organisation that created the enclave makes a second different kind of enclave in future, a malicious host might connect you with the wrong one. If the input/output commands are similar then a confusion attack could be opened up. That's why you must always specify the product ID even if it's zero.
The simplest possible string-form constraint looks like this:
It says "accept exactly one program, with that measurement hash". In this case the value came from the output of the build process as shown above. This is useful when you don't trust the author nor host of the enclave, and want to audit the source code and then reproduce the build.
Often that's too rigid. We trust the developer of the enclave, just not the host. In that case we'll accept any enclave signed by the developer's public key. We can express that by listing code signing key hashes, like this:
When constraining to a signing key we must also specify the product ID, because a key can be used to sign more than one product.
Add this line to the end of the client's
Replace the signing key in the snippet above with the enclave signer hash that was printed when you built the enclave.
This line of code parses a simple constraint that says any enclave (even if run in simulation mode) signed by this
hash of a code signing key with product ID of 1 is acceptable. Obviously in a real app, you would remove the part
SEC:INSECURE, but it's convenient to have this whilst developing. You'd probably also retrieve the
constraint from a configuration file, system property or command line flag. Finally it uses the
check method with
the attestation object. If anything is amiss an exception is thrown, so past this point we know we're talking to the
ReverseEnclave we wrote earlier.
If needed, more than one key hash could be added to the list of enclave constraints (e.g. if simulation and debug modes use a distinct key from release mode). The enclave is accepted if one key hash matches.
1 2 3
Keys and mail¶
The client wants to receive a response from the enclave, and we want that to be encrypted/tamperproofed too. That means it need a key pair of its own. Conclave uses Curve25519, a state of the art elliptic curve algorithm. For reasons of implementation robustness and avoidance of side channel attacks, this is the only algorithm supported by Conclave Mail. If you want to use other algorithms for some reason you would need to implement your own messaging system on top of host-local calls. Alternatively, use that other algorithm to encrypt/decrypt a Curve25519 private key. Generating such a key is straightforward:
Unfortunately the Java Cryptography Architecture only introduced official support for Curve25519 in Java 11. At the
moment in Conclave therefore, you must utilize our
classes. In future we may offer support for using the Java 11 JCA types directly. A Curve25519 private key is simply
32 random bytes, which you can access using the
getEncoded() method on
Now we have a key with which to receive the response, we create a mail to the enclave. This is done using the
EnclaveInstanceInfo.createPostOffice method, which returns a new
PostOffice object. This is similar to the post office
inside the enclave and let's us create mail with increasing sequence numbers. We pass in our private key when creating
the post office so it's mixed in to the calculations when the mail is encrypted and thus becomes available in
getAuthenticatedSender() inside the enclave.
We've chosen a topic value of "reverse" but any will do as the client uses a random key and only sends one mail using it.
However, if a client needs to send multiple mail which are related to each other such that it's important they reach the
enclave in the same order then these mail need to all use the same topic and private key. In other words, they need to
be created from the same
PostOffice instance. The enclave will automatically detect any dropped or reordered messages
and throw an exception.
Make sure there's only one
PostOffice instance per (destination public key, sender private key, topic) triple.
This can be done very easily in Kotlin by using a data class with these three properties as a key to a
The same can be done in Java except it will be slightly more verbose as you will
need to override the
hashCodemethods of your key class. Modern IDEs let you do this very quickly.
In more complex apps it might be smart to use the topic in more complex ways, like how a web app can use the URL to separate different functions of the app.
Now we have an encrypted message we can write it to the socket and receive the enclave's response.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
We write out the length of the mail, then the mail bytes, then read the length of the response and read the response
bytes. Finally we use
PostOffice.decryptMail on the same instance we used to create our request, passing in the encrypted
reply. This method decrypts the bytes using our private key, checks they really did come from that enclave (by checking
the authenticated sender key against the enclave's public key in the attestation), decodes the bytes and yields the reply.
We can then access the body of the message using
Finally we close the socket, and we're done. Phew! 😅
There are two ways you can test the enclave: as a mock or natively.
conclave-testing library has a
MockHost class which lets you whitebox test your enclave by running the enclave
fully in-memory. There is no need for SGX hardware or a specific OS and thus ideal for cross-platform unit testing. The
underlying enclave object is also exposed enabling you to make assertions on the enclave's state, something that
cannot be done on real hardware or even in simulation mode.
In your enclave module build.gradle file add the following test dependency
You create your mock enclave by calling
1 2 3
MockHost is a
EnclaveHost so you call the enclave as normal with
callEnclave. You have direct assess to the
enclave object instance with
Testing the enclave natively is relatively straightforward: the enclave needs to be loaded with
default this will run the tests in a simulated environment and will require the Linux OS. Native tests are ideal for
1 2 3 4 5 6 7 8 9 10
You'll notice that we annotated the test class with
@EnabledOnOs(OS.LINUX). This is from
JUnit 5 and it will make sure
the native test isn't run on non-Linux environments.
will execute the test using a simulation enclave, or not at all if the OS is not Linux. You can switch to a debug enclave
and test on real secure hardware by using the
gradlew -PenclaveMode=debug host:test
To run the native tests on a non-Linux machine you can use Docker, which manages Linux VMs for you. See the instructions for compiling and running the host for more information.