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
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 a developer can add to the
a couple of lines like this:
0.3 here means use beta 3.
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 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
Most of this boilerplate isn't strictly necessary except for the highlighted region. However, the rest works around
a bug in IntelliJ IDEA if you wish to use that IDE in which
interactive JavaDocs would otherwise not be available. To benefit from the workaround you should run
to generate your IntelliJ project. If IntelliJ is running at the time it should automatically notice the changes and
apply them. If you click "import from Gradle" in the notification popup workflow then everything will work fine
except JavaDoc integration.
Configure the host module¶
SGX enclaves can be used in one of four modes, in order of increasing realism:
- Mock: your enclave class is created in the host JVM and no native or SGX specific code is used.
- Simulation: an enclave is compiled to native and loaded, but SGX hardware doesn't need to be present.
- Debug: the enclave is loaded using SGX hardware and drivers, but with a back door that allows debugger access to the memory.
- Release: the enclave is loaded using SGX hardware and drivers, and there's no back door. This is the real deal.
Only release mode locks out the host and provides the standard SGX security model. At this time it requires the enclave to be signed with a key whitelisted by Intel. Future versions of Conclave will remove this restriction for modern hardware that supports flexible launch control.
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.
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
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. We'll stick with
Avian for now.
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 picked it.
The revocation level should be incremented if a weakness in the enclave code 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
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.
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
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!
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 17
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 implement the
There's one method we must supply:
invoke 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.
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
At first we will be building and running our enclave in simulation mode. This does not require the platform hardware to support SGX. However, when we want to load 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.
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.
- 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.
1 2 3 4 5 6
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.
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. There's a useful
1 2 3
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.
Now get the serialized bytes to a client via whatever network mechanism you want. The bytes are essentially a large,
complex digital signature, so it's safe to publish them publicly. 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.
Get a SPID¶
To use SGX remote attestation for real we need to do some additional work. Remember how we wrote
enclave.start(null, null, null); above? The first two parameters are API keys required to use the Intel attestation
servers. The first is called a "Service Provider ID" or SPID, and the second is called the "attestation key".
You can sign-up easily and for free. Learn more about IAS.
Once you have a SPID and attestation key you can either hard code them, or load them from a config file, or as in
our sample code just pass them via the command line. Replace the call to
EnclaveHost.start above with this snippet:
1 2 3 4 5
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.
Future versions of Conclave will allow you to use Intel DCAP, which is an alternative way to do attestation which will let you run your own attestation servers, and thus skip getting these keys from Intel.
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.
During the build you should see output like this:
1 2 3
The measurement should correspond to the value found in the
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 a machine with SGX enabled and also
provide your EPID SPID and attestation key. See here for more information.
gradlew -PenclaveMode=debug host:run --args="<SPID> <attestation key>"
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
invoke 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 two parameters: 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 an
EnclaveMail. This object gives us access to the body bytes that the client sent, but it
also exposes some other header fields:
- The authenticated sender public key. This is the public key of the client that sent the mail. 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.
- 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.
- The from field. This is a string that the sender can pick to identify itself. Unlike the authenticated sender key this is entirely arbitrary, and nothing stops a sender picking any value it likes. Often this field won't be used. It's intended to hold some sort of routing address where the sender would like to receive replies - sometimes this is implied, but an explicit routing address can be useful when implementing more complex hosts and enclave that may not reply straight away.
- 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.
- The sequence number. This must increment by one for every mail delivered on a topic. Conclave will automatically reject messages for which this doesn't hold true, within the scope of messages seen whilst the enclave is loaded.
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 this simple tutorial we only care about the body and sender public key. 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
createMail method to do this. It gives us back a
MutableMail object, which is a builder with setters we can use
to control the sequence number, topic and so on. Once we've done configuring it to our liking we pass it to
postMail which performs the encryption and delivers the newly encrypted mail 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
invoke (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:
1 2 3 4 5 6 7 8
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 enclave can provide a routing hint to tell the host where it'd like the message delivered. For example, this
could be set to the value of the from header. 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 you could provide an
EnclaveHost.MailCallbacks that has access to your connected clients, a database, a durable queue,
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. 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 22 23 24 25 26 27 28 29 30 31 32
This code is straightforward. In order, it:
- Opens a socket using the Java sockets API and enters a loop listening for connections.
- 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 keep an incrementing counter because the enclave wants a unique value which it can use to signal acknowledgement. We aren't using acknowledgement here though, because this app is too simple to need it.
- We deliver the encrypted mail bytes to the enclave.
- We pick up the response from the
AtomicReferencebox that was set by the callback.
In future we will provide APIs to bind enclaves to common transports, to avoid this sort of boilerplate.
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
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.
1 2 3
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.
Keys and mail¶
We want to receive a response from the enclave, and we want that to be encrypted/tamperproofed too. That means we need a key pair of our 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.createMail method, which returns a
MutableMail object. We must set our private key on the mail
so it's mixed in to the calculations when the mail is encrypted and thus becomes available in
inside the enclave: without the call to
setPrivateKey the authenticated sender will be null. This can sometimes be
appropriate, if for example, the enclave already knows the sender's private key via some other mechanism.
The sequence number for this newly created mail is zero. That means if we re-run the program twice we'll try to send the enclave two mails with the same topic, sender and sequence number. This is invalid: the enclave will reject it as an apparent replay attack until it's restarted and forgets what mail it received. We could keep a counter on disk and increment it, but it's easier to simply set the topic to a random UUID. After that, we can encrypt the mail.
1 2 3 4 5
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
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
EnclaveInstanceInfo.decryptMail, passing in the encrypted reply we got and our own private key.
This method checks the bytes 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
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 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 correct OS. Native tests are ideal for
To test the enclave in debug mode on real secure hardware the
-PenclaveMode=debug flag needs to be specified and the
SPID and attestation key need to be passed into the test. This can be done with system properties.
In your host module build file:
1 2 3 4 5
Pass these values to
EnclaveHost.start in your test:
1 2 3 4 5 6 7 8 9
gradlew -PenclaveMode=debug -Pspid=<SPID> -Pattestation-key=<attestation key> host:test
Note that native tests are located in the host module while mock tests in the enclave module.