From 83d6a248a8842709423941a3673b76e66a6a169a Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Wed, 13 Dec 2017 17:45:33 +0000 Subject: [PATCH] ENT-970 - SGX remote attestation host (#173) * ENT-970 - SGX remote attestation host * Remote attestation enclave * Client for the remote attestation host * Communicates with ISV / RA server, which in turn communicates with the Intel Attestation Service * Native library bridging the client code running on the JVM with the native bits controlling and communicating with the enclave * ENT-970 - Address comments from code review * ENT-970 - More updates addressing review comments * ENT-970 - Integrate with root Gradle project for SGX --- sgx-jvm/.gitignore | 6 + sgx-jvm/remote-attestation/README.md | 45 ++ sgx-jvm/remote-attestation/enclave/.gitignore | 6 + sgx-jvm/remote-attestation/enclave/Makefile | 213 ++++++++++ .../enclave/config/debug.xml | 11 + .../enclave/config/release.xml | 11 + .../remote-attestation/enclave/enclave.cpp | 216 ++++++++++ .../remote-attestation/enclave/enclave.edl | 104 +++++ .../remote-attestation/enclave/enclave.lds | 10 + sgx-jvm/remote-attestation/enclave/sgx.mk | 84 ++++ sgx-jvm/remote-attestation/host/Makefile | 72 ++++ sgx-jvm/remote-attestation/host/build.gradle | 82 ++++ .../remote-attestation/host/native/.gitignore | 1 + .../remote-attestation/host/native/Makefile | 115 ++++++ .../host/native/enclave-manager.cpp | 95 +++++ .../host/native/enclave-manager.hpp | 55 +++ .../remote-attestation/host/native/jni.hpp | 13 + .../host/native/logging.cpp | 32 ++ .../host/native/logging.hpp | 32 ++ .../host/native/remote-attestation.cpp | 285 +++++++++++++ .../host/native/remote-attestation.hpp | 181 +++++++++ .../host/native/sealing.cpp | 15 + .../host/native/sealing.hpp | 24 ++ .../host/native/wrapper.cpp | 384 ++++++++++++++++++ .../attestation/AttestationManagerTests.kt | 212 ++++++++++ .../corda/sgx/bridge/EnclaveConfiguration.kt | 12 + .../sgx/attestation/AttestationEnclave.kt | 83 ++++ .../sgx/attestation/AttestationManager.kt | 289 +++++++++++++ .../entities/AttestationContext.kt | 6 + .../entities/AttestationException.kt | 8 + .../attestation/entities/AttestationResult.kt | 53 +++ .../attestation/entities/AttestationStatus.kt | 20 + .../sgx/attestation/entities/Challenge.kt | 21 + .../corda/sgx/attestation/entities/Quote.kt | 38 ++ .../sgx/attestation/entities/QuoteStatus.kt | 71 ++++ .../sgx/attestation/entities/QuoteType.kt | 23 ++ .../attestation/service/ChallengerDetails.kt | 57 +++ .../sgx/attestation/service/ISVClient.kt | 71 ++++ .../sgx/attestation/service/ISVHttpClient.kt | 220 ++++++++++ .../service/messages/ChallengeResponse.kt | 19 + .../attestation/service/messages/Message0.kt | 16 + .../attestation/service/messages/Message1.kt | 18 + .../attestation/service/messages/Message2.kt | 39 ++ .../attestation/service/messages/Message3.kt | 28 ++ .../attestation/service/messages/Message4.kt | 72 ++++ .../attestation/NativeAttestationEnclave.kt | 213 ++++++++++ .../corda/sgx/bridge/enclave/NativeEnclave.kt | 99 +++++ .../sgx/bridge/system/NativeSgxSystem.kt | 41 ++ .../corda/sgx/bridge/wrapper/EnclaveResult.kt | 27 ++ .../wrapper/ExtendedGroupIdentifierResult.kt | 22 + .../bridge/wrapper/InitializationResult.kt | 21 + .../corda/sgx/bridge/wrapper/LaunchToken.kt | 13 + .../corda/sgx/bridge/wrapper/NativeWrapper.kt | 99 +++++ .../wrapper/PublicKeyAndGroupIdentifier.kt | 28 ++ .../corda/sgx/bridge/wrapper/QuoteResult.kt | 45 ++ .../bridge/wrapper/SealingOperationResult.kt | 22 + .../sgx/bridge/wrapper/VerificationResult.kt | 28 ++ .../main/kotlin/net/corda/sgx/cli/Program.kt | 51 +++ .../kotlin/net/corda/sgx/enclave/ECKey.kt | 42 ++ .../kotlin/net/corda/sgx/enclave/Enclave.kt | 86 ++++ .../corda/sgx/enclave/EnclaveIdentifier.kt | 6 + .../net/corda/sgx/enclave/SgxException.kt | 39 ++ .../kotlin/net/corda/sgx/enclave/SgxStatus.kt | 291 +++++++++++++ .../net/corda/sgx/sealing/SealedSecret.kt | 6 + .../net/corda/sgx/sealing/SealingException.kt | 10 + .../net/corda/sgx/sealing/SealingResult.kt | 21 + .../kotlin/net/corda/sgx/sealing/Secret.kt | 6 + .../net/corda/sgx/sealing/SecretManager.kt | 59 +++ .../sgx/system/ExtendedGroupIdentifier.kt | 15 + .../net/corda/sgx/system/GroupIdentifier.kt | 13 + .../net/corda/sgx/system/SgxDeviceStatus.kt | 62 +++ .../kotlin/net/corda/sgx/system/SgxSystem.kt | 76 ++++ .../sgx/system/SgxUnavailableException.kt | 10 + .../host/src/main/resources/log4j2.xml | 50 +++ .../corda/sgx/bridge/EnclaveConfiguration.kt | 12 + .../NativeAttestationEnclaveTests.kt | 53 +++ .../sgx/bridge/enclave/NativeEnclaveTests.kt | 92 +++++ .../sgx/bridge/system/NativeSgxSystemTests.kt | 24 ++ .../net/corda/sgx/enclave/ECKeyTests.kt | 24 ++ .../net/corda/sgx/enclave/SgxStatusTests.kt | 26 ++ .../system/ExtendedGroupIdentifierTests.kt | 26 ++ .../corda/sgx/system/SgxDeviceStatusTests.kt | 22 + sgx-jvm/remote-attestation/settings.gradle | 1 + 83 files changed, 5249 insertions(+) create mode 100644 sgx-jvm/.gitignore create mode 100644 sgx-jvm/remote-attestation/README.md create mode 100644 sgx-jvm/remote-attestation/enclave/.gitignore create mode 100644 sgx-jvm/remote-attestation/enclave/Makefile create mode 100644 sgx-jvm/remote-attestation/enclave/config/debug.xml create mode 100644 sgx-jvm/remote-attestation/enclave/config/release.xml create mode 100644 sgx-jvm/remote-attestation/enclave/enclave.cpp create mode 100644 sgx-jvm/remote-attestation/enclave/enclave.edl create mode 100644 sgx-jvm/remote-attestation/enclave/enclave.lds create mode 100644 sgx-jvm/remote-attestation/enclave/sgx.mk create mode 100644 sgx-jvm/remote-attestation/host/Makefile create mode 100644 sgx-jvm/remote-attestation/host/build.gradle create mode 100644 sgx-jvm/remote-attestation/host/native/.gitignore create mode 100644 sgx-jvm/remote-attestation/host/native/Makefile create mode 100644 sgx-jvm/remote-attestation/host/native/enclave-manager.cpp create mode 100644 sgx-jvm/remote-attestation/host/native/enclave-manager.hpp create mode 100644 sgx-jvm/remote-attestation/host/native/jni.hpp create mode 100644 sgx-jvm/remote-attestation/host/native/logging.cpp create mode 100644 sgx-jvm/remote-attestation/host/native/logging.hpp create mode 100644 sgx-jvm/remote-attestation/host/native/remote-attestation.cpp create mode 100644 sgx-jvm/remote-attestation/host/native/remote-attestation.hpp create mode 100644 sgx-jvm/remote-attestation/host/native/sealing.cpp create mode 100644 sgx-jvm/remote-attestation/host/native/sealing.hpp create mode 100644 sgx-jvm/remote-attestation/host/native/wrapper.cpp create mode 100644 sgx-jvm/remote-attestation/host/src/integration-test/kotlin/net/corda/sgx/attestation/AttestationManagerTests.kt create mode 100644 sgx-jvm/remote-attestation/host/src/integration-test/kotlin/net/corda/sgx/bridge/EnclaveConfiguration.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/AttestationEnclave.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/AttestationManager.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationContext.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationException.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationResult.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationStatus.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/Challenge.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/Quote.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/QuoteStatus.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/QuoteType.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/ChallengerDetails.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/ISVClient.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/ISVHttpClient.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/ChallengeResponse.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message0.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message1.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message2.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message3.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message4.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/attestation/NativeAttestationEnclave.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/enclave/NativeEnclave.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/system/NativeSgxSystem.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/EnclaveResult.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/ExtendedGroupIdentifierResult.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/InitializationResult.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/LaunchToken.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/NativeWrapper.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/PublicKeyAndGroupIdentifier.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/QuoteResult.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/SealingOperationResult.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/VerificationResult.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/cli/Program.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/ECKey.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/Enclave.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/EnclaveIdentifier.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/SgxException.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/SgxStatus.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SealedSecret.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SealingException.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SealingResult.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/Secret.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SecretManager.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/ExtendedGroupIdentifier.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/GroupIdentifier.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/SgxDeviceStatus.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/SgxSystem.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/SgxUnavailableException.kt create mode 100644 sgx-jvm/remote-attestation/host/src/main/resources/log4j2.xml create mode 100644 sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/EnclaveConfiguration.kt create mode 100644 sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/attestation/NativeAttestationEnclaveTests.kt create mode 100644 sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/enclave/NativeEnclaveTests.kt create mode 100644 sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/system/NativeSgxSystemTests.kt create mode 100644 sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/enclave/ECKeyTests.kt create mode 100644 sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/enclave/SgxStatusTests.kt create mode 100644 sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/system/ExtendedGroupIdentifierTests.kt create mode 100644 sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/system/SgxDeviceStatusTests.kt diff --git a/sgx-jvm/.gitignore b/sgx-jvm/.gitignore new file mode 100644 index 0000000000..2d9cc6c751 --- /dev/null +++ b/sgx-jvm/.gitignore @@ -0,0 +1,6 @@ +.idea/ +.vscode/ +obj/ +build/ +log/ +tags diff --git a/sgx-jvm/remote-attestation/README.md b/sgx-jvm/remote-attestation/README.md new file mode 100644 index 0000000000..60ce64c6be --- /dev/null +++ b/sgx-jvm/remote-attestation/README.md @@ -0,0 +1,45 @@ +# Remote Attestation + +## Project Organisation + + * **Enclave** + + The enclave (`enclave/`) is responsible for initialising and coordinating + the remote attestation process from the client side, and will eventually + operate on a secret provisioned from the challenger (once successfully + attested by Intel's Attestation Service). + + * **Host** + + The host JVM (`host/`) is running in an untrusted environment and + facilitates the communication between the challenger and its enclave. + To coordinate with the enclave, the host uses a native JNI library (in + `host/native/`) + + * **Challenger** + + The challenger JVM does not require SGX-enabled hardware and is essentially + the party asking the host to prove that it has spun up a program in an + enclave on trusted hardware (that cannot be tampered with), so that + consequently, it can provision an encrypted secret to said enclave. + + * **IAS Proxy** + + The proxy is responsible for talking to the Intel Attestation Service over + mutual TLS to verify attestation evidence received from the host. The proxy + needs a client certificate and a service provider identifier (SPID) issued + by Intel. In turn, it will forward any received proof from Intel to the + host and challenger, making it possible for the challenger to trust the + host and thus provision the secret. The proof is signed with Intel's root + certificate. + +## Getting Started + +To get started, run the following commands in `sgx-jvm`: + +```bash +> source environment +> sx help +``` + +Further documentation is available in `sgx-jvm/tools/sx/README.md`. diff --git a/sgx-jvm/remote-attestation/enclave/.gitignore b/sgx-jvm/remote-attestation/enclave/.gitignore new file mode 100644 index 0000000000..4652f2b807 --- /dev/null +++ b/sgx-jvm/remote-attestation/enclave/.gitignore @@ -0,0 +1,6 @@ +*_t.c +*_t.h +*_t.o +*_u.c +*_u.h +*_u.o diff --git a/sgx-jvm/remote-attestation/enclave/Makefile b/sgx-jvm/remote-attestation/enclave/Makefile new file mode 100644 index 0000000000..491879e28c --- /dev/null +++ b/sgx-jvm/remote-attestation/enclave/Makefile @@ -0,0 +1,213 @@ +.PHONY: info all clean \ + unsigned signed-openssl signed-hsm \ + obj-trusted obj-untrusted + +# === GENERAL PARAMETERS ========================================================================== + +SHELL = /bin/bash +MAKEFILE_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +MODE ?= DEBUG + +GRADLE_FILE = $(MAKEFILE_DIR)/../host/build.gradle +NAME = corda_sgx_ra +VERSION := $(shell sed -n "s/^version = '\([^']*\)'.*/\\1/p" $(GRADLE_FILE)) +PLATFORM := $(shell uname -s | tr [:upper:] [:lower:]) + +OUT_DIR = $(MAKEFILE_DIR)/build +OBJ_DIR = $(MAKEFILE_DIR)/obj + +ENCLAVE = $(OUT_DIR)/$(NAME)_enclave.a +ENCLAVE_SIGNED = $(OUT_DIR)/$(NAME)_enclave.so +ENCLAVE_UNSIGNED = $(OUT_DIR)/$(NAME)_enclave_unsigned.so +ENCLAVE_BLOB = $(OUT_DIR)/$(NAME)_enclave_blob.bin + +ENCLAVE_OPENSSL = $(OUT_DIR)/$(NAME)_enclave_openssl.so +OPENSSL_PRIVATE_KEY = $(MAKEFILE_DIR)/../../sign_helper/selfsigning.pem +OPENSSL_PUBLIC_KEY = $(OUT_DIR)/selfsigning.public.pem +OPENSSL_SIGNATURE = $(OUT_DIR)/$(NAME)_enclave.signature.openssl.sha256 + +ENCLAVE_HSM = $(OUT_DIR)/$(NAME)_enclave_hsm.so +HSM_PROFILE ?= dev_sim +HSM_PUBLIC_KEY = $(OUT_DIR)/hsm.public.pem +HSM_SIGNATURE = $(OUT_DIR)/$(NAME)_enclave.signature.hsm.sha256 + +HSM_BUILD_DIR = $(MAKEFILE_DIR)/../../hsm-tool/build/libs/sgx-jvm +HSM_TOOL_VERSION = 1.0-SNAPSHOT +HSM_TOOL = $(HSM_BUILD_DIR)/hsm-tool-$(HSM_TOOL_VERSION).jar + +# === BUILD PARAMETERS ============================================================================ + +CPP = g++ +GRADLE = $(MAKEFILE_DIR)/../../../gradlew + +CPPFLAGS_BASE = $(INC_DIRS) -Wall -fPIC +CPPFLAGS_DEBUG = $(CPPFLAGS_BASE) -g +CPPFLAGS_RELEASE = $(CPPFLAGS_BASE) -s -DNDEBUG + +LDFLAGS_BASE = +LDFLAGS_DEBUG = $(LDFLAGS_BASE) +LDFLAGS_RELEASE = $(LDFLAGS_BASE) -s + +# === SGX-SPECIFIC BUILD PARAMETERS =============================================================== + +SGX_SDK := $(MAKEFILE_DIR)/../../linux-sgx +include $(MAKEFILE_DIR)/sgx.mk + +SGX_CONFIG = $(MAKEFILE_DIR)/config/$(SGX_MODE_NAME).xml +RPC_DIR = $(MAKEFILE_DIR)/rpc + +# === MODE-SPECIFIC BUILD PARAMETERS ============================================================== + +ifeq ($(subst release,RELEASE,$(MODE)),RELEASE) + CPPFLAGS = $(CPPFLAGS_RELEASE) $(SGX_CPPFLAGS_RELEASE) -I$(RPC_DIR) + LDFLAGS = $(LDFLAGS_RELEASE) $(SGX_LDFLAGS_RELEASE) +else + CPPFLAGS = $(CPPFLAGS_DEBUG) $(SGX_CPPFLAGS_DEBUG) -I$(RPC_DIR) + LDFLAGS = $(LDFLAGS_DEBUG) $(SGX_LDFLAGS_DEBUG) +endif + +# === ENCLAVE SOURCES AND OUTPUTS ================================================================= + +ENCLAVE_SOURCES = $(wildcard *.cpp) +ENCLAVE_OBJECTS = $(addprefix $(OBJ_DIR)/, $(ENCLAVE_SOURCES:.cpp=.o)) + +TGEN_SOURCES = $(addprefix $(RPC_DIR)/, enclave_t.c) +TGEN_HEADERS = $(TGEN_SOURCES:.c=.h) +TGEN_OBJECTS = $(patsubst $(RPC_DIR)/%,$(OBJ_DIR)/%,$(TGEN_SOURCES:.c=.o)) + +UGEN_SOURCES = $(addprefix $(RPC_DIR)/, enclave_u.c) +UGEN_HEADERS = $(UGEN_SOURCES:.c=.h) +UGEN_OBJECTS = $(patsubst $(RPC_DIR)/%,$(OBJ_DIR)/%,$(UGEN_SOURCES:.c=.o)) + +# === PSEUDO TARGETS ============================================================================== + +info: # Show available targets + @echo "Build Targets:" + @sed -n 's/^\([a-z-]*\): .*# \(.*\)$$/ \1 - \2/p' Makefile | expand -t24 | sort + @echo + @echo "Configuration:" + @echo " HSM_PROFILE = dev_sim (or dev_hsm, prod)" + @echo " MODE = DEBUG (or RELEASE)" + @echo " SGX_DEBUG_MODE = TRUE (or FALSE)" + @echo " SGX_IS_PRERELEASE = FALSE (or TRUE)" + @echo " SGX_USE_HARDWARE = FALSE (or TRUE)" + @echo + +all: $(ENCLAVE) signed-openssl # Build enclave (self-signed using OpenSSL) + +clean: # Clean build files + @$(RM) -rf $(OUT_DIR) + @$(RM) -rf $(OBJ_DIR) + @$(RM) -rf $(RPC_DIR) + +# === SIGNING ===================================================================================== + +mode: # Show state of SGX specific build variables + @echo "SGX_MODE: $(SGX_MODE)" + @echo "SGX_DEBUG: $(SGX_DEBUG)" + @echo "SGX_PRERELEASE: $(SGX_PRERELEASE)" + @echo "HSM_PROFILE: $(HSM_PROFILE)" + +unsigned: $(ENCLAVE_BLOB) # Construct an unsigned enclave + +signed-openssl: $(ENCLAVE_OPENSSL) # Construct a self-signed enclave using OpenSSL + +signed-hsm: $(ENCLAVE_HSM) # Construct an enclave signed by an HSM + mev_sim|dev_hsm|prod) + +$(ENCLAVE_UNSIGNED): $(ENCLAVE) + @echo "Using mode=$(MODE), config=$(SGX_CONFIG)" + $(CPP) -o $(ENCLAVE_UNSIGNED) $(LDFLAGS) + +$(ENCLAVE_BLOB): $(ENCLAVE_UNSIGNED) + $(SGX_SIGNER) gendata \ + -enclave $(ENCLAVE_UNSIGNED) \ + -out $(ENCLAVE_BLOB) \ + -config $(SGX_CONFIG) + +$(ENCLAVE_OPENSSL): $(ENCLAVE_BLOB) $(OPENSSL_PUBLIC_KEY) $(OPENSSL_SIGNATURE) + $(SGX_SIGNER) catsig \ + -enclave $(ENCLAVE_UNSIGNED) \ + -key $(OPENSSL_PUBLIC_KEY) \ + -sig $(OPENSSL_SIGNATURE) \ + -unsigned $(ENCLAVE_BLOB) \ + -config $(SGX_CONFIG) \ + -out $(ENCLAVE_OPENSSL) + @cp $(ENCLAVE_OPENSSL) $(ENCLAVE_SIGNED) + +$(OPENSSL_PUBLIC_KEY): $(OPENSSL_PRIVATE_KEY) + openssl rsa \ + -in $(OPENSSL_PRIVATE_KEY) \ + -pubout -out $(OPENSSL_PUBLIC_KEY) + +$(OPENSSL_SIGNATURE): $(OPENSSL_PRIVATE_KEY) $(ENCLAVE_BLOB) + openssl dgst \ + -sha256 \ + -sign $(OPENSSL_PRIVATE_KEY) \ + -out $(OPENSSL_SIGNATURE) \ + $(ENCLAVE_BLOB) + +$(ENCLAVE_HSM): $(ENCLAVE_BLOB) $(HSM_PUBLIC_KEY) $(HSM_SIGNATURE) + $(SGX_SIGNER) catsig \ + -enclave $(ENCLAVE_UNSIGNED) \ + -key $(HSM_PUBLIC_KEY) \ + -sig $(HSM_SIGNATURE) \ + -unsigned $(ENCLAVE_BLOB) \ + -config $(SGX_CONFIG) \ + -out $(ENCLAVE_HSM) + @cp $(ENCLAVE_HSM) $(ENCLAVE_SIGNED) + +$(HSM_PUBLIC_KEY) $(HSM_SIGNATURE): $(HSM_TOOL) $(ENCLAVE_BLOB) + @echo "Using HSM profile=$(HSM_PROFILE)" + java -jar $(HSM_TOOL) \ + --mode=Sign \ + --source=$(ENCLAVE_BLOB) \ + --pubkey=$(HSM_PUBLIC_KEY) \ + --signature=$(HSM_SIGNATURE) \ + --profile=$(HSM_PROFILE) + +$(HSM_TOOL): + $(GRADLE) sgx-hsm-tool:jar + +# === ENCLAVE ===================================================================================== + +obj-trusted: $(TGEN_OBJECTS) # Object files for trusted zone + +obj-untrusted: $(UGEN_OBJECTS) # Object files for untrusted zone + +$(ENCLAVE): $(TGEN_OBJECTS) $(ENCLAVE_OBJECTS) | $(OUT_DIR) + ar qc $@ $^ + ranlib $@ + +$(TGEN_SOURCES) $(TGEN_HEADERS) $(UGEN_SOURCES) $(UGEN_HEADERS): enclave.edl | $(RPC_DIR) + $(SGX_EDGER8R) \ + --search-path $(SGX_INC_DIR) \ + --trusted-dir $(RPC_DIR) \ + --untrusted-dir $(RPC_DIR) \ + enclave.edl + +$(ENCLAVE_OBJECTS): $(ENCLAVE_SOURCES) | $(OBJ_DIR) + +$(TGEN_OBJECTS): $(TGEN_SOURCES) + +$(UGEN_OBJECTS): $(UGEN_SOURCES) + +$(OBJ_DIR)/%.o: $(RPC_DIR)/%.c + @mkdir -p $(@D) + $(CPP) $(CPPFLAGS) $(SGX_DEBUG_FLAGS) $(SGX_DEFS) -o $@ -c $< + +$(OBJ_DIR)/%.o: %.cpp + @mkdir -p $(@D) + $(CPP) $(CPPFLAGS) $(SGX_DEBUG_FLAGS) $(SGX_DEFS) -o $@ -c $< + +# === BUILD DIRECTORIES =========================================================================== + +$(OUT_DIR): + @mkdir -p $(OUT_DIR) + +$(OBJ_DIR): + @mkdir -p $(OBJ_DIR) + +$(RPC_DIR): + @mkdir -p $(RPC_DIR) diff --git a/sgx-jvm/remote-attestation/enclave/config/debug.xml b/sgx-jvm/remote-attestation/enclave/config/debug.xml new file mode 100644 index 0000000000..36465f4b20 --- /dev/null +++ b/sgx-jvm/remote-attestation/enclave/config/debug.xml @@ -0,0 +1,11 @@ + + 0 + 0 + 0x40000 + 0x100000 + 1 + 1 + 0 + 0 + 0xFFFFFFFF + diff --git a/sgx-jvm/remote-attestation/enclave/config/release.xml b/sgx-jvm/remote-attestation/enclave/config/release.xml new file mode 100644 index 0000000000..d694143ccf --- /dev/null +++ b/sgx-jvm/remote-attestation/enclave/config/release.xml @@ -0,0 +1,11 @@ + + 0 + 0 + 0x40000 + 0x100000 + 1 + 1 + 1 + 0 + 0xFFFFFFFF + diff --git a/sgx-jvm/remote-attestation/enclave/enclave.cpp b/sgx-jvm/remote-attestation/enclave/enclave.cpp new file mode 100644 index 0000000000..47228c0318 --- /dev/null +++ b/sgx-jvm/remote-attestation/enclave/enclave.cpp @@ -0,0 +1,216 @@ +#include +#include +#include + +#include "enclave_t.h" + +#define CHECKED(expr) { \ + sgx_status_t _status = (expr); \ + if (SGX_SUCCESS != _status) { return _status; } \ +} + +#define MAX_SECRET_SIZE 128 + +extern "C" { + +// === Initialization and Finalization ======================================= + +static inline sgx_status_t create_pse_session( + bool use_platform_services +) { + // If desired, try up to three times to establish a PSE session. + sgx_status_t status = SGX_SUCCESS; + if (use_platform_services) { + int retry_count = 3; + do { status = sgx_create_pse_session(); } + while (SGX_ERROR_BUSY == status && --retry_count); + } + return status; +} + +static inline sgx_status_t close_pse_session( + bool use_platform_services +) { + // If a PSE session was created, close it properly. + sgx_status_t status = SGX_SUCCESS; + if (use_platform_services) { + status = sgx_close_pse_session(); + } + return status; +} + +// Initialize the remote attestation process. +sgx_status_t initializeRemoteAttestation( + bool use_platform_services, + sgx_ec256_public_t *challenger_key, + sgx_ra_context_t *context +) { + sgx_status_t status; + + // Abort if the public key of the challenger and/or the output context + // variable is not provided. + if (NULL == challenger_key || NULL == context) { + return SGX_ERROR_INVALID_PARAMETER; + } + + // If desired, try to establish a PSE session. + CHECKED(create_pse_session(use_platform_services)); + + // Initialize the remote attestation and key exchange process, and place + // the resulting context in `context`. + status = sgx_ra_init(challenger_key, use_platform_services, context); + + // If a PSE session was created, close it properly. + CHECKED(close_pse_session(use_platform_services)); + return status; +} + +// Clean up and finalize the remote attestation process. +sgx_status_t finalizeRemoteAttestation( + sgx_ra_context_t context +) { + // Release the remote attestation and key exchange context after the + // process has been completed. + return sgx_ra_close(context); +} + +// === Remote Attestation Verification ======================================= + +/* + * This function compares `len` bytes from buffer `b1` and `b2` in constant + * time to protect against side-channel attacks. For sensitive code running + * inside an enclave, this function is preferred over `memcmp`. + */ +static int consttime_memequal(const void *b1, const void *b2, size_t len) { + // Written by Matthias Drochner . Public domain. + const unsigned char + *c1 = (unsigned char*)b1, + *c2 = (unsigned char*)b2; + unsigned int res = 0; + + while (len--) res |= *c1++ ^ *c2++; + + /* + * Map 0 to 1 and [1, 256) to 0 using only constant-time + * arithmetic. + * + * This is not simply `!res' because although many CPUs support branchless + * conditional moves and many compilers will take advantage of them, + * certain compilers generate branches on certain CPUs for `!res'. + */ + return (1 & ((res - 1) >> 8)); +} + +// Verify CMAC from the challenger to protect against spoofed results. +sgx_status_t verifyCMAC( + sgx_ra_context_t context, + uint8_t *message, + size_t message_size, + uint8_t *cmac, + size_t cmac_size +) { + // Check inputs. + if (sizeof(sgx_mac_t) != cmac_size || NULL == cmac) { + return SGX_ERROR_INVALID_PARAMETER; + } + + if (UINT32_MAX < message_size || NULL == message) { + return SGX_ERROR_INVALID_PARAMETER; + } + + // Get negotiated MK key of remote attestation and key exchange session. + sgx_ec_key_128bit_t mk_key = { 0 }; + CHECKED(sgx_ra_get_keys(context, SGX_RA_KEY_MK, &mk_key)); + + // Perform 128-bit CMAC hash over the first four bytes of the status + // obtained from the challenger. + uint8_t computed_cmac[SGX_CMAC_MAC_SIZE] = { 0 }; + CHECKED(sgx_rijndael128_cmac_msg( + &mk_key, message, message_size, &computed_cmac + )); + + // Compare the computed CMAC-SMK with the provided one. + if (0 == consttime_memequal(computed_cmac, cmac, sizeof(computed_cmac))) { + return SGX_ERROR_MAC_MISMATCH; + } + + // Can further test number of uses of the secret data, and require + // re-attestation after X uses... But, for now, we're happy! + return SGX_SUCCESS; +} + +// Verify attestation response from the challenger. +sgx_status_t verifyAttestationResponse( + sgx_ra_context_t context, + uint8_t *secret, + size_t secret_size, + uint8_t *gcm_iv, + uint8_t *gcm_mac, + size_t gcm_mac_size, + uint8_t *sealed_secret, + size_t sealed_secret_size +) { + // Check inputs. + if (secret_size > MAX_SECRET_SIZE || NULL == secret) { + return SGX_ERROR_INVALID_PARAMETER; + } + if (gcm_mac_size != SGX_AESGCM_MAC_SIZE || NULL == gcm_mac) { + return SGX_ERROR_INVALID_PARAMETER; + } + if (sealed_secret_size - sizeof(sgx_sealed_data_t) > MAX_SECRET_SIZE) { + return SGX_ERROR_INVALID_PARAMETER; + } + + // Get negotiated SK key of remote attestation and key exchange session. + sgx_ec_key_128bit_t sk_key; + CHECKED(sgx_ra_get_keys(context, SGX_RA_KEY_SK, &sk_key)); + + // Decrypt using Rijndael AES-GCM. + uint8_t *decrypted_secret = (uint8_t*)malloc(secret_size); + CHECKED(sgx_rijndael128GCM_decrypt( + &sk_key, secret, secret_size, decrypted_secret, gcm_iv, + SGX_AESGCM_IV_SIZE, NULL, 0, (sgx_aes_gcm_128bit_tag_t*)gcm_mac + )); + + // Return sealed secret if requested. + if (NULL != sealed_secret && secret_size <= sealed_secret_size) { + // Seal the secret so that it can be returned to the untrusted + // environment. + CHECKED(sgx_seal_data( + 0, NULL, secret_size, decrypted_secret, + sealed_secret_size, (sgx_sealed_data_t*)sealed_secret + )); + } + + // Free temporary memory. + free(decrypted_secret); + + return SGX_SUCCESS; +} + +// Check whether the sealed secret is unsealable or not. +sgx_status_t unsealSecret( + uint8_t *sealed_secret, + size_t sealed_secret_size +) { + // Allocate temporary buffer for the output of the operation. + uint8_t *buffer = (uint8_t*)malloc(sealed_secret_size); + uint32_t buffer_size = sealed_secret_size; + + if (NULL == buffer) { + return SGX_ERROR_OUT_OF_MEMORY; + } + + // Attempt to unseal the secret. + sgx_status_t status = sgx_unseal_data( + (sgx_sealed_data_t*)sealed_secret, NULL, NULL, + buffer, &buffer_size + ); + + // Free up the temporary memory buffer. + free(buffer); + + return status; +} + +} diff --git a/sgx-jvm/remote-attestation/enclave/enclave.edl b/sgx-jvm/remote-attestation/enclave/enclave.edl new file mode 100644 index 0000000000..6019bf6256 --- /dev/null +++ b/sgx-jvm/remote-attestation/enclave/enclave.edl @@ -0,0 +1,104 @@ +enclave { + from "sgx_tkey_exchange.edl" import *; + include "sgx_key_exchange.h" + include "sgx_tseal.h" + + trusted { + + /** + * Initialize the remote attestation process. + * + * @param usePlatformServices If true, the enclave establishes a + * session with the PSE before initializing the attestation context. + * This provides additional nonce replay protection and a reliable + * monotonic counter. + * @param challengerKey ECDSA public key of the challenger with the 8 + * magic bytes removed, and X and Y components changed to little + * endian. + * @param context The variable receiving the context constructed during + * initialization. + * + * @return Status code indicative of the outcome of the operation. + */ + public sgx_status_t initializeRemoteAttestation( + bool usePlatformServices, + [in] sgx_ec256_public_t *challengerKey, + [out] sgx_ra_context_t *context + ); + + /** + * Clean up and finalize the remote attestation process. + * + * @param context The context constructed during initialization. + * + * @return SGX_SUCCESS if successful, or SGX_ERROR_INVALID_PARAMETER if + * an invalid context is provided. + */ + public sgx_status_t finalizeRemoteAttestation( + sgx_ra_context_t context + ); + + /** + * Verify CMAC of attestation result from challenger using the MK key. + * + * @param context The context constructed during initialization. + * @param message The status obtained from the challenger as part of + * the attestation result. + * @param messageSize The size of the attestation status payload. + * @param cmac The CMAC received from the challenger. + * @param cmacSize The size of the CMAC received. + * + * @return Status code indicative of the outcome of the operation. + */ + public sgx_status_t verifyCMAC( + sgx_ra_context_t context, + [in,size=messageSize] uint8_t *message, + size_t messageSize, + [in,size=cmacSize] uint8_t *cmac, + size_t cmacSize + ); + + /** + * Verify an attestation response from the service provider. + * + * @param context The context constructed during initialization. + * @param secret Message containing the secret. + * @param secretSize Size of the secret message, in bytes. + * @param gcmIV The initialization vector used in the decryption. + * @param gcmMac Pointer to the AES-GCM MAC for the secret message. + * @param gcmMacSize Size of the AES-GCM MAC. + * @param sealedSecret Pre-allocated buffer receiving the sealed + * secret. If NULL, the sealed secret will not be returned. + * @param maxSealedSecretSize The maximum size of the sealed secret. + * This must be less than or equal to the size of the pre-allocated + * buffer above, and no larger than the upper limit of 128 bytes. + * + * @return Status code indicative of the outcome of the operation. + */ + public sgx_status_t verifyAttestationResponse( + sgx_ra_context_t context, + [in,size=secretSize] uint8_t *secret, + size_t secretSize, + [in,count=12] uint8_t *gcmIV, + [in,size=gcmMacSize] uint8_t *gcmMac, + size_t gcmMacSize, + [out,size=maxSealedSecretSize] uint8_t *sealedSecret, + size_t maxSealedSecretSize + ); + + /** + * Check whether the application enclave is able to unseal a secret. + * + * @param sealedSecret The previously sealed secret. + * @param sealedSecretSize The size of the sealed secret. + * + * @return Status code indicative of the outcome of the operation. + */ + public sgx_status_t unsealSecret( + [in,size=sealedSecretSize] uint8_t *sealedSecret, + size_t sealedSecretSize + ); + + }; + +}; diff --git a/sgx-jvm/remote-attestation/enclave/enclave.lds b/sgx-jvm/remote-attestation/enclave/enclave.lds new file mode 100644 index 0000000000..f15b9561c3 --- /dev/null +++ b/sgx-jvm/remote-attestation/enclave/enclave.lds @@ -0,0 +1,10 @@ +corda_sgx_ra_enclave.so +{ + global: + enclave_entry; + g_global_data_sim; + g_global_data; + g_peak_heap_used; + local: + *; +}; \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/enclave/sgx.mk b/sgx-jvm/remote-attestation/enclave/sgx.mk new file mode 100644 index 0000000000..cafc3b0e19 --- /dev/null +++ b/sgx-jvm/remote-attestation/enclave/sgx.mk @@ -0,0 +1,84 @@ +# === SGX-SPECIFIC BUILD PARAMETERS =============================================================== + +SGX_LIB_DIR := $(SGX_SDK)/build/linux +SGX_INC_DIR := $(SGX_SDK)/common/inc + +SGX_EDGER8R = $(SGX_SDK)/build/linux/sgx_edger8r +SGX_SIGNER := $(SGX_SDK)/build/linux/sgx_sign + +SGX_MODE_NAME := $(shell echo $(MODE) | tr [:upper:] [:lower:]) +SGX_USE_HARDWARE ?= FALSE +SGX_IS_PRERELEASE ?= FALSE +SGX_DEBUG_MODE ?= TRUE + +SGX_CPPFLAGS_RELEASE = -fvisibility=hidden -fpie -fstack-protector \ + -I$(SGX_INC_DIR) \ + -s -DNDEBUG + +SGX_CPPFLAGS_DEBUG = -fvisibility=hidden -fpie -fstack-protector \ + -I$(SGX_INC_DIR) + +ifeq ($(SGX_USE_HARDWARE),TRUE) + URTS_LIB = sgx_urts + TRTS_LIB = sgx_trts + SGX_SERVICE_LIB = sgx_tservice + CAPABLE_LIB = sgx_capable + UAE_SERVICE_LIB = sgx_uae_service + UKEY_EXCHNG = sgx_ukey_exchange + SGX_SIM = 0 + SGX_MODE = HW +else + URTS_LIB = sgx_urts_sim + TRTS_LIB = sgx_trts_sim + SGX_SERVICE_LIB = sgx_tservice_sim + CAPABLE_LIB = sgx_capable + UAE_SERVICE_LIB = sgx_uae_service_sim + UKEY_EXCHNG = sgx_ukey_exchange + SGX_SIM = 1 + SGX_MODE = SIM +endif + +ifeq ($(SGX_DEBUG_MODE),TRUE) + SGX_DEBUG = 1 + SGX_DEBUG_FLAGS = -DDEBUG -UNDEBUG -UEDEBUG +else + SGX_DEBUG = 0 + SGX_DEBUG_FLAGS = -DNDEBUG -UEDEBUG -UDEBUG +endif + +ifeq ($(SGX_IS_PRERELEASE),TRUE) + SGX_PRERELEASE = 1 + SGX_DEBUG = 0 + SGX_DEBUG_FLAGS = -DNDEBUG -DEDEBUG -UDEBUG +else + SGX_PRERELEASE = 0 +endif + +SGX_LIBS = -lsgx_tstdc -lsgx_tstdcxx -lsgx_tcrypto \ + -lsgx_tkey_exchange \ + -l$(SGX_SERVICE_LIB) + +SGX_DEFS = -DSGX_SIM=$(SGX_SIM) \ + -DSGX_MODE=$(SGX_MODE) \ + -DSGX_DEBUG=$(SGX_DEBUG) \ + -DSGX_PRERELEASE=$(SGX_PRERELEASE) + +LINK_SCRIPT = $(MAKEFILE_DIR)/enclave.lds + +SGX_LDFLAGS_BASE = -Wl,--no-undefined \ + -nostdlib -nodefaultlibs -nostartfiles \ + -L$(SGX_LIB_DIR) \ + -Wl,--whole-archive \ + -l$(TRTS_LIB) \ + -Wl,--no-whole-archive \ + -Wl,--start-group \ + $(ENCLAVE) $(SGX_LIBS) \ + -Wl,--end-group \ + -Wl,-Bstatic -Wl,-Bsymbolic \ + -Wl,-pie,-eenclave_entry \ + -Wl,--export-dynamic \ + -Wl,--defsym,__ImageBase=0 \ + -Wl,--version-script=$(LINK_SCRIPT) + +SGX_LDFLAGS_RELEASE = $(SGX_LDFLAGS_BASE) +SGX_LDFLAGS_DEBUG = $(SGX_LDFLAGS_BASE) diff --git a/sgx-jvm/remote-attestation/host/Makefile b/sgx-jvm/remote-attestation/host/Makefile new file mode 100644 index 0000000000..fab45e1cf5 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/Makefile @@ -0,0 +1,72 @@ +.PHONY: host host-native docs \ + unit-tests unit-tests-force \ + integration-tests integration-tests-force \ + run run-local run-remote run-sgx \ + clean + +# === GENERAL PARAMETERS ========================================================================== + +SHELL = /bin/bash +MAKEFILE_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +GRADLE ?= $(MAKEFILE_DIR)/../gradlew + +# TODO Update to Dokka 0.9.16 (when available) to fix: https://github.com/Kotlin/dokka/issues/184 +DOKKA_FILTER = grep -v "Can't find node by signature" + +# === PSEUDO TARGETS ============================================================================== + +host: + $(GRADLE) compileKotlin + +host-native: + make -C native all + +docs: + $(GRADLE) dokka | $(DOKKA_FILTER) + +docs-force: + rm -rf $(MAKEFILE_DIR)/build/javadoc + $(GRADLE) dokka | $(DOKKA_FILTER) + +unit-tests: host-native + $(GRADLE) test + +unit-tests-force: host-native + $(GRADLE) cleanTest test + +integration-tests: host-native + $(GRADLE) integrationTest + +integration-tests-force: host-native + $(GRADLE) cleanIntegrationTest integrationTest + +~/.classpath: + $(GRADLE) -q getClasspath > ~/.classpath + +clean: + $(GRADLE) clean + rm -f ~/.classpath + +# === TEST TARGETS ================================================================================ + +JAVA_PROG = $(JAVA_HOME)/bin/java +SGX_GDB = /sgx/sgxsdk/bin/sgx-gdb +HOST = localhost:2000 +MAIN_CLASS = net.corda.sgx.cli.Program +JVM_DEFS = -Dcorda.sgx.enclave.path=/code/sgx-jvm/remote-attestation/enclave/build \ + -Djava.library.path=/code/sgx-jvm/remote-attestation/host/native/build \ + -Dfile.encoding=US-ASCII -Duser.country=US -Duser.language=en -Duser.variant \ + -Dattestation.home=/code/sgx-jvm/remote-attestation/host/build/logs +JVM_ARGS = -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 + +run: ~/.classpath + $(JAVA_PROG) $(JVM_DEFS) $(JVM_ARGS) -cp `cat ~/.classpath` $(MAIN_CLASS) + +run-local: ~/.classpath + gdb --args $(JAVA_PROG) $(JVM_DEFS) $(JVM_ARGS) -cp `cat ~/.classpath` $(MAIN_CLASS) + +run-sgx: ~/.classpath + $(SGX_GDB) --args $(JAVA_PROG) $(JVM_DEFS) $(JVM_ARGS) -cp `cat ~/.classpath` $(MAIN_CLASS) + +run-remote: ~/.classpath + gdbserver $(HOST) $(JAVA_PROG) $(JVM_DEFS) $(JVM_ARGS) -cp `cat ~/.classpath` $(MAIN_CLASS) diff --git a/sgx-jvm/remote-attestation/host/build.gradle b/sgx-jvm/remote-attestation/host/build.gradle new file mode 100644 index 0000000000..8307f092ec --- /dev/null +++ b/sgx-jvm/remote-attestation/host/build.gradle @@ -0,0 +1,82 @@ +version = '0.1' + +buildscript { + ext.dokka_version = "0.9.15" + ext.httpclient_version = "4.5.2" + + ext.nativeBuildDir = "$projectDir/native/build" + ext.enclaveBuildDir = "$projectDir/../enclave/build" + ext.debugArgs = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005" + + repositories { + mavenLocal() + mavenCentral() + jcenter() + } + + dependencies { + classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" + } +} + +apply plugin: 'kotlin' +apply plugin: 'application' +apply plugin: 'org.jetbrains.dokka' + +mainClassName = 'net.corda.sgx.cli.Program' +description "Client side of SGX remote attestation process" + +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +sourceSets { + integrationTest { + kotlin { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/kotlin') + } + } +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + testCompile "junit:junit:$junit_version" + testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + + compile "com.fasterxml.jackson.core:jackson-core:$jackson_version" + compile "com.fasterxml.jackson.core:jackson-databind:$jackson_version" + compile "com.fasterxml.jackson.core:jackson-annotations:$jackson_version" + compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" + + compile "org.apache.httpcomponents:httpclient:$httpclient_version" + compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + compile "org.apache.logging.log4j:log4j-core:$log4j_version" + runtime "org.apache.logging.log4j:log4j-web:$log4j_version" + compile "org.slf4j:jcl-over-slf4j:$slf4j_version" +} + +dokka { + outputFormat = 'html' + outputDirectory = "$buildDir/javadoc" +} + +task integrationTest(type: Test) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath +} + +tasks.withType(Test) { + systemProperty "java.library.path", "$nativeBuildDir" + systemProperty "corda.sgx.enclave.path", "$enclaveBuildDir" + systemProperty "attestation.home", "$buildDir/logs" + jvmArgs "$debugArgs" +} + +task getClasspath(type: Task) { + doLast { + println sourceSets.main.runtimeClasspath.collect { it.toString() }.join(":") + } +} diff --git a/sgx-jvm/remote-attestation/host/native/.gitignore b/sgx-jvm/remote-attestation/host/native/.gitignore new file mode 100644 index 0000000000..8762f1c2da --- /dev/null +++ b/sgx-jvm/remote-attestation/host/native/.gitignore @@ -0,0 +1 @@ +wrapper.hpp diff --git a/sgx-jvm/remote-attestation/host/native/Makefile b/sgx-jvm/remote-attestation/host/native/Makefile new file mode 100644 index 0000000000..064bfc4039 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/native/Makefile @@ -0,0 +1,115 @@ +.PHONY: all clean + +# === GENERAL PARAMETERS ========================================================================== + +SHELL = /bin/bash +MAKEFILE_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +MODE ?= DEBUG # or RELEASE + +GRADLE_FILE = $(MAKEFILE_DIR)/../build.gradle +NAME = corda_sgx_ra +VERSION := $(shell sed -n "s/^version = '\([^']*\)'.*/\\1/p" $(GRADLE_FILE)) +PLATFORM := $(shell uname -s | tr [:upper:] [:lower:]) + +OUT_DIR = $(MAKEFILE_DIR)/build +OBJ_DIR = $(MAKEFILE_DIR)/obj + +HOST_LIBRARY = $(OUT_DIR)/lib$(NAME).so + +# === BUILD PARAMETERS ============================================================================ + +ifeq ($(PLATFORM),linux) + JDK_HOME ?= $(dir $(word 1,$(wildcard /usr/lib/jvm/*/))) + JDK_INC_DIRS = -I$(JDK_HOME)include -I$(JDK_HOME)include/linux +endif + +LOGGING_DEFS = +ifeq ($(LOGGING),TRUE) + LOGGING_DEFS = -DLOGGING +endif + +CPP = g++ + +CPPFLAGS_BASE = $(JDK_INC_DIRS) -Wall -fPIC \ + $(SGX_DEFS) $(LOGGING_DEFS) +CPPFLAGS_DEBUG = $(CPPFLAGS_BASE) -g -O0 -DDEBUG +CPPFLAGS_RELEASE = $(CPPFLAGS_BASE) -s -DNDEBUG + +LDFLAGS_BASE = \ + -shared \ + -Wl,-soname,lib$(NAME).so \ + -Wl,-rpath,$(SGX_LIB_DIR):../../linux-sgx/build/linux \ + -Wl,-z,defs \ + -Wl,--no-as-needed \ + -L$(SGX_LIB_DIR) \ + -l$(URTS_LIB) \ + -l$(CAPABLE_LIB) \ + -l$(UAE_SERVICE_LIB) \ + -lpthread +LDFLAGS_DEBUG = $(LDFLAGS_BASE) +LDFLAGS_RELEASE = $(LDFLAGS_BASE) -s + +# === SGX-SPECIFIC BUILD PARAMETERS =============================================================== + +SGX_SDK := $(MAKEFILE_DIR)/../../../linux-sgx +include $(MAKEFILE_DIR)/../../enclave/sgx.mk + +# === MODE-SPECIFIC BUILD PARAMETERS ============================================================== + +ifeq ($(subst release,RELEASE,$(MODE)),RELEASE) + CPPFLAGS = $(CPPFLAGS_RELEASE) $(SGX_CPPFLAGS_RELEASE) + LDFLAGS = $(LDFLAGS_RELEASE) +else + CPPFLAGS = $(CPPFLAGS_DEBUG) $(SGX_CPPFLAGS_DEBUG) + LDFLAGS = $(LDFLAGS_DEBUG) +endif + +# === PSEUDO TARGETS ============================================================================== + +all: $(HOST_LIBRARY) + +clean: + @rm -f $(JNI_HEADERS) + @$(RM) -rf $(OUT_DIR) + @$(RM) -rf $(OBJ_DIR) + +# === HOST ======================================================================================== + +HOST_SOURCES = $(wildcard *.cpp) +HOST_OBJECTS = $(addprefix $(OBJ_DIR)/, $(HOST_SOURCES:.cpp=.o)) + +ENCLAVE_PROJECT = $(MAKEFILE_DIR)/../../enclave +ENCLAVE_OBJECTS = $(ENCLAVE_PROJECT)/obj/enclave_u.o +ENCLAVE_INC_DIR = $(MAKEFILE_DIR)/../../enclave/rpc + +JNI_HEADERS = wrapper.hpp +JNI_SOURCES = $(JNI_HEADERS:.hpp=.cpp) +JNI_OBJECTS = $(addprefix $(OBJ_DIR)/, $(JNI_SOURCES:.cpp=.o)) +CLASSPATH = $(MAKEFILE_DIR)/../build/classes/kotlin/main + +$(HOST_LIBRARY): $(HOST_OBJECTS) $(ENCLAVE_OBJECTS) $(JNI_OBJECTS) | $(OUT_DIR) + $(CPP) $(LDFLAGS) -o $@ $^ \ + $(SGX_LIB_DIR)/lib$(UKEY_EXCHNG).a + +$(HOST_OBJECTS): $(HOST_SOURCES) | $(OBJ_DIR) + +$(ENCLAVE_OBJECTS): + make -C $(ENCLAVE_PROJECT) obj-untrusted + +$(JNI_OBJECTS): $(JNI_SOURCES) | $(JNI_HEADERS) + +$(JNI_HEADERS): | $(JNI_SOURCES) + javah -o $@ -cp $(CLASSPATH) net.corda.sgx.bridge.wrapper.NativeWrapper + +$(OBJ_DIR)/%.o: %.cpp + @mkdir -p $(@D) + $(CPP) $(CPPFLAGS) -I$(ENCLAVE_INC_DIR) -o $@ -c $< + +# === BUILD DIRECTORIES =========================================================================== + +$(OUT_DIR): + @mkdir -p $(OUT_DIR) + +$(OBJ_DIR): + @mkdir -p $(OBJ_DIR) diff --git a/sgx-jvm/remote-attestation/host/native/enclave-manager.cpp b/sgx-jvm/remote-attestation/host/native/enclave-manager.cpp new file mode 100644 index 0000000000..11ef5365ae --- /dev/null +++ b/sgx-jvm/remote-attestation/host/native/enclave-manager.cpp @@ -0,0 +1,95 @@ +#include + +#include +#include +#include + +#include "enclave-manager.hpp" +#include "logging.hpp" + +// Instantiate a new enclave from a signed enclave binary, and return the +// identifier of the instance. +sgx_enclave_id_t create_enclave( + const char *path, + bool use_platform_services, + sgx_status_t *result, + sgx_launch_token_t *token +) { + int updated = 0; // Indication of whether the launch token was updated. + sgx_enclave_id_t enclave_id = 0; // The identifier of the created enclave. + + // If the launch token is empty, then create a new enclave. Otherwise, try + // to re-activate the existing enclave. `SGX_DEBUG_FLAG` is automatically + // set to 1 in debug mode, and 0 in release mode. + sgx_status_t status = sgx_create_enclave( + path, SGX_DEBUG_FLAG, token, &updated, &enclave_id, NULL + ); + + LOG(enclave_id, status, 0, "sgx_create_enclave()"); + + // Store the return value of the operation. + if (NULL != result) { + *result = status; + } + + // Return the identifier of the created enclave. Remember that if `status` + // is `SGX_ERROR_ENCLAVE_LOST`, the enclave should be destroyed and then + // re-created. + return (SGX_SUCCESS == status) ? enclave_id : 0; +} + +// Destroy enclave if currently loaded. +bool destroy_enclave(sgx_enclave_id_t enclave_id) { + if (enclave_id != 0){ + // Attempt to destroy the enclave if we are provided with a valid + // enclave identifier. + sgx_status_t status = sgx_destroy_enclave(enclave_id); + + LOG(enclave_id, status, 0, "sgx_destroy_enclave()"); + + return SGX_SUCCESS == status; + } + return false; +} + +// Check the status of the SGX device on the current machine. +sgx_device_status_t get_device_status(void) { +#if SGX_SIM == 1 +#pragma message "get_device_status() is being simulated" + // If in simulation mode, simulate device capabilities. + return SGX_ENABLED; +#endif + + // Try to retrieve the current status of the SGX device. + sgx_device_status_t status; + sgx_status_t ret = sgx_cap_enable_device(&status); + + LOG(0, ret, 0, "sgx_cap_enable_device() = { status = %x }", status); + + if (SGX_SUCCESS != ret) { + return SGX_DISABLED; + } + + return status; +} + +// Report which extended Intel EPID Group the client uses by default. +uint32_t get_extended_group_id(sgx_status_t *result) { + uint32_t egid; + + // The extended EPID group identifier is indicative of which attestation + // service the client is supposed to be communicating with. Currently, only + // a value of zero is supported, which is referring to Intel. The user + // should verify the retrieved extended group identifier, as any other + // value than zero will be disregarded by the service provider. + sgx_status_t status = sgx_get_extended_epid_group_id(&egid); + + LOG(0, status, 0, "sgx_get_extended_epid_group_id() = %u", egid); + + // Store the return value of the operation. + if (NULL != result) { + *result = status; + } + + return egid; +} diff --git a/sgx-jvm/remote-attestation/host/native/enclave-manager.hpp b/sgx-jvm/remote-attestation/host/native/enclave-manager.hpp new file mode 100644 index 0000000000..81396b6ccb --- /dev/null +++ b/sgx-jvm/remote-attestation/host/native/enclave-manager.hpp @@ -0,0 +1,55 @@ +#ifndef __ENCLAVE_MANAGER_H__ +#define __ENCLAVE_MANAGER_H__ + +#include +#include + +/** + * Instantiate a new enclave from a signed enclave binary, and return the + * identifier of the instance. + * + * @param path The file name of the signed enclave binary to load. + * @param use_platform_services If true, Intel's platform services are used to + * add extra protection against replay attacks during nonce generation and to + * provide a trustworthy monotonic counter. + * @param result Variable receiving the result of the operation, if not NULL. + * @param token Pointer to launch token; cannot be NULL. + * + * @return The identifier of the created enclave. + */ +sgx_enclave_id_t create_enclave( + const char *path, + bool use_platform_services, + sgx_status_t *result, + sgx_launch_token_t *token +); + +/** + * Destroy enclave if currently loaded. + * + * @param enclave_id The identifier of the enclave to destroy. + * + * @return True if the enclave was active and got destroyed. False otherwise. + */ +bool destroy_enclave( + sgx_enclave_id_t enclave_id +); + +/** + * Check the status of the SGX device on the current machine. + */ +sgx_device_status_t get_device_status(void); + +/** + * Report which extended Intel EPID Group the client uses by default. The key + * used to sign a Quote will be a member of the extended EPID Group reported in + * this API. The application will typically use this value to tell the ISV + * Service Provider which group to use during remote attestation. + * + * @param result Variable receiving the result of the operation, if not NULL. + * + * @return The extended EPID group identifier. + */ +uint32_t get_extended_group_id(sgx_status_t *result); + +#endif /* __ENCLAVE_MANAGER_H__ */ diff --git a/sgx-jvm/remote-attestation/host/native/jni.hpp b/sgx-jvm/remote-attestation/host/native/jni.hpp new file mode 100644 index 0000000000..302efee1d0 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/native/jni.hpp @@ -0,0 +1,13 @@ +#ifndef __JNI_HPP__ +#define __JNI_HPP__ + +#include + +#define NATIVE_WRAPPER(return_type, method) \ + JNIEXPORT return_type JNICALL \ + Java_net_corda_sgx_bridge_wrapper_NativeWrapper_##method + +#define KLASS(name) \ + ("net/corda/sgx/bridge/wrapper/" name) + +#endif /* __JNI_HPP__ */ diff --git a/sgx-jvm/remote-attestation/host/native/logging.cpp b/sgx-jvm/remote-attestation/host/native/logging.cpp new file mode 100644 index 0000000000..a798565038 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/native/logging.cpp @@ -0,0 +1,32 @@ +#include + +#include "logging.hpp" + +void log( + sgx_enclave_id_t enclave_id, + sgx_status_t status, + sgx_ra_context_t context, + const char *message, + ... +) { + char mode[4] = { 0 }; + mode[0] = (SGX_SIM == 0) ? 'H' : 'S'; + mode[1] = (SGX_DEBUG == 0) ? 'R' : 'D'; + mode[2] = (SGX_PRERELEASE == 0) ? 'x' : 'P'; + mode[3] = 0; + + char buffer[1024]; + va_list args; + va_start(args, message); + vsnprintf(buffer, sizeof(buffer), message, args); + va_end(args); + + printf( + "SGX(id=%lx,status=%x,ctx=%u,mode=%s): %s\n", + (uint64_t)enclave_id, + (uint32_t)status, + (uint32_t)context, + mode, + buffer + ); +} diff --git a/sgx-jvm/remote-attestation/host/native/logging.hpp b/sgx-jvm/remote-attestation/host/native/logging.hpp new file mode 100644 index 0000000000..cc24228edc --- /dev/null +++ b/sgx-jvm/remote-attestation/host/native/logging.hpp @@ -0,0 +1,32 @@ +#ifndef __LOGGING_HPP__ +#define __LOGGING_HPP__ + +#include + +#include +#include + +#ifdef LOGGING +#define LOG(enclave_id, status, context, message, ...) \ + log(enclave_id, (sgx_status_t)(status), context, message, ##__VA_ARGS__) +#else +#define LOG(enclave_id, status, context, message, ...) ; +#endif + +/** + * Log message to standard output. + * + * @param enclave_id The enclave identifier. + * @param status The outcome of the last SGX operation. + * @param context The remote attestation context. + * @param message The message. + */ +void log( + sgx_enclave_id_t enclave_id, + sgx_status_t status, + sgx_ra_context_t context, + const char *message, + ... +); + +#endif /* __LOGGING_HPP__ */ diff --git a/sgx-jvm/remote-attestation/host/native/remote-attestation.cpp b/sgx-jvm/remote-attestation/host/native/remote-attestation.cpp new file mode 100644 index 0000000000..e25354aca9 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/native/remote-attestation.cpp @@ -0,0 +1,285 @@ +#include +#include + +#include + +#include "enclave_u.h" +#include "logging.hpp" +#include "remote-attestation.hpp" + +// Initialize the remote attestation. +sgx_status_t initialize_remote_attestation( + // Inputs + sgx_enclave_id_t enclave_id, + bool use_platform_services, + sgx_ec256_public_t *key_challenger, + + // Outputs + sgx_ra_context_t *context +) { + sgx_status_t ret; + + // Perform ECALL into the application enclave to initialize the remote + // attestation. The resulting attestation context will be stored in the + // variable referenced by the `context` parameter. + sgx_status_t _ret = initializeRemoteAttestation( + enclave_id, &ret, use_platform_services, key_challenger, context + ); + + LOG(enclave_id, _ret | ret, *context, "initialize_remote_attestation()"); + + // If the ECALL itself failed, report why. Otherwise, return the status of + // the underlying function call. + return (SGX_SUCCESS != _ret) ? _ret : ret; +} + +// Clean up and finalize the remote attestation process. +sgx_status_t finalize_remote_attestation( + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context +) { + sgx_status_t ret; + + // Perform ECALL into the application enclave to close the current + // attestation context and tidy up. + sgx_status_t _ret = finalizeRemoteAttestation(enclave_id, &ret, context); + + LOG(enclave_id, _ret | ret, context, "finalize_remote_attestation()"); + + // If the ECALL itself failed, report why. Otherwise, return the status of + // the underlying function call. + return (SGX_SUCCESS != _ret) ? _ret : ret; +} + +// Retrieve the application enclave's public key and the platform's group +// identifier. +sgx_status_t get_public_key_and_group_identifier( + // Inputs + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context, + + // Outputs + sgx_ec256_public_t *public_key, + sgx_epid_group_id_t *group_id, + + // Retry logic + int max_retry_count, + unsigned int retry_wait_in_secs +) { + sgx_status_t ret; + sgx_ra_msg1_t message; + + // It is generally recommended that the caller should wait (typically + // several seconds to tens of seconds) and retry `sgx_ra_get_msg1()` if + // `SGX_ERROR_BUSY` is returned. + int retry_count = max_retry_count; + + while (retry_count-- >= 0) { + // Using an ECALL proxy to `sgx_ra_get_ga()` in the `sgx_tkey_exchange` + // library to retrieve the public key of the application enclave. + ret = sgx_ra_get_msg1(context, enclave_id, sgx_ra_get_ga, &message); + + LOG(enclave_id, ret, context, "sgx_ra_get_msg1()"); + + if (SGX_ERROR_BUSY == ret) { + // Wait before retrying... + sleep(retry_wait_in_secs); + } else if (SGX_SUCCESS != ret) { + return ret; + } else { + break; + } + } + + // Store the public key; components X and Y, each 256 bits long. + if (NULL != public_key) { + memcpy(public_key, &message.g_a, sizeof(sgx_ec256_public_t)); + } + + // Store the EPID group identifier. Note, this is not the same as the + // extended group identifier. + if (NULL != group_id) { + memcpy(group_id, &message.gid, sizeof(sgx_epid_group_id_t)); + } + + return ret; +} + +// Process details received from challenger via the service provider, and +// generate quote. +sgx_status_t process_challenger_details_and_generate_quote( + // Inputs + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context, + sgx_ec256_public_t *challenger_public_key, + sgx_spid_t *service_provider_id, + uint16_t quote_type, + uint16_t key_derivation_function, + sgx_ec256_signature_t *signature, + sgx_mac_t *challenger_mac, + uint32_t revocation_list_size, + uint8_t *revocation_list, + + // Outputs + sgx_mac_t *enclave_mac, + sgx_ec256_public_t *enclave_public_key, + sgx_ps_sec_prop_desc_t *security_properties, + uint8_t **quote, + size_t *quote_size, + + // Retry logic + int max_retry_count, + unsigned int retry_wait_in_secs +) { + sgx_status_t ret = SGX_SUCCESS; + size_t msg_in_size = sizeof(sgx_ra_msg2_t) + revocation_list_size; + sgx_ra_msg2_t *msg_in = (sgx_ra_msg2_t*)malloc(msg_in_size); + sgx_ra_msg3_t *msg_out = NULL; + uint32_t msg_out_size; + + if (NULL == msg_in) { + return SGX_ERROR_OUT_OF_MEMORY; + } + + // Populate input message (message 2 in the Intel attestation flow). + memcpy(&msg_in->g_b, challenger_public_key, sizeof(sgx_ec256_public_t)); + memcpy(&msg_in->spid, service_provider_id, sizeof(sgx_spid_t)); + msg_in->quote_type = quote_type; + msg_in->kdf_id = key_derivation_function; + memcpy(&msg_in->sign_gb_ga, signature, sizeof(sgx_ec256_signature_t)); + memcpy(&msg_in->mac, challenger_mac, sizeof(sgx_mac_t)); + msg_in->sig_rl_size = revocation_list_size; + if (revocation_list_size > 0) { + memcpy(&msg_in->sig_rl, revocation_list, revocation_list_size); + } + + // Nullify outputs. + *quote = NULL; + + // It is generally recommended that the caller should wait (typically + // several seconds to tens of seconds) and retry `sgx_ra_proc_msg2()` if + // `SGX_ERROR_BUSY` is returned. + int retry_count = max_retry_count; + + while (retry_count-- >= 0) { + // Using an ECALL proxy to `sgx_ra_proc_msg2_trusted()` in the + // `sgx_tkey_exchange` library to process the incoming details from the + // challenger, and `sgx_ra_get_msg3_trusted()` in the same library to + // generate the quote. + ret = sgx_ra_proc_msg2( + context, + enclave_id, + sgx_ra_proc_msg2_trusted, + sgx_ra_get_msg3_trusted, + msg_in, + sizeof(sgx_ra_msg2_t) + revocation_list_size, + &msg_out, + &msg_out_size + ); + + LOG(enclave_id, ret, context, "sgx_ra_proc_msg2()"); + + if (SGX_ERROR_BUSY == ret) { + // Wait before retrying... + sleep(retry_wait_in_secs); + } else { + break; + } + } + + // Populate outputs from the returned message structure. + if (NULL != msg_out) { + memcpy(enclave_mac, &msg_out->mac, sizeof(sgx_mac_t)); + memcpy(enclave_public_key, &msg_out->g_a, sizeof(sgx_ec256_public_t)); + + size_t z_sec_prop = sizeof(sgx_ps_sec_prop_desc_t); + memcpy(security_properties, &msg_out->ps_sec_prop, z_sec_prop); + } + + // Populate the quote structure. + if (NULL != msg_out) { + *quote_size = msg_out_size - offsetof(sgx_ra_msg3_t, quote); + *quote = (uint8_t*)malloc(*quote_size); + if (NULL != quote) { + memcpy(*quote, &msg_out->quote, *quote_size); + } + } else { + *quote = NULL; + } + + // The output message is generated by the library and thus has to be freed + // upon completion. + free(msg_out); + + // Allocated due to the variable size revocation list. Free up the + // temporary structure. + free(msg_in); + + // Check if the malloc() call for the output quote failed above; if it did, + // it was due to an out-of-memory condition. + if (NULL == quote && SGX_SUCCESS == ret) { + return SGX_ERROR_OUT_OF_MEMORY; + } + + return ret; +} + +sgx_status_t verify_attestation_response( + // Inputs + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context, + uint8_t *message, + size_t message_size, + uint8_t *cmac, + size_t cmac_size, + uint8_t *secret, + size_t secret_size, + uint8_t *gcm_iv, + uint8_t *gcm_mac, + size_t gcm_mac_size, + + // Outputs + uint8_t *sealed_secret, + size_t *sealed_secret_size, + sgx_status_t *cmac_status +) { + // Check the generated CMAC from the service provider. + sgx_status_t ret = SGX_SUCCESS; + sgx_status_t _ret = verifyCMAC( + enclave_id, &ret, context, message, message_size, cmac, cmac_size + ); + + *cmac_status = ret; + LOG(enclave_id, _ret, context, "verify_cmac() = %x", (uint32_t)ret); + + // Abort if call failed. Otherwise, forward the outcome to the caller. + if (SGX_SUCCESS != _ret) { + return _ret; + } + + // Try to decrypt and verify the attestation response. + _ret = verifyAttestationResponse( + enclave_id, &ret, context, secret, secret_size, + gcm_iv, gcm_mac, gcm_mac_size, + sealed_secret, sizeof(sgx_sealed_data_t) + secret_size + ); + + LOG( + enclave_id, _ret, context, + "verify_attestation_response() = %x", + (uint32_t)ret + ); + + // Abort if unable to verify attestation response. + if (SGX_SUCCESS != (_ret | ret)) { + return (SGX_SUCCESS != _ret) ? _ret : ret; + } + + // Return sealed secret if requested. The buffer is populated by the ECALL + // above, if sealed_secret is non-null. + if (NULL != sealed_secret_size) { + *sealed_secret_size = sizeof(sgx_sealed_data_t) + secret_size; + } + + return SGX_SUCCESS; +} diff --git a/sgx-jvm/remote-attestation/host/native/remote-attestation.hpp b/sgx-jvm/remote-attestation/host/native/remote-attestation.hpp new file mode 100644 index 0000000000..caebbd3750 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/native/remote-attestation.hpp @@ -0,0 +1,181 @@ +#ifndef __REMOTE_ATTESTATION_H__ +#define __REMOTE_ATTESTATION_H__ + +#include +#include +#include + +/** + * Initialize the remote attestation. + * + * @param enclave_id The identifier of the enclave facilitating the remote + * attestation. + * @param use_platform_services If true, the enclave establishes a session with + * the PSE before initializing the attestation context. This provides + * additional nonce replay protection and a reliable monotonic counter. + * @param key_challenger ECDSA public key of the challenger with the 8 magic + * bytes removed, and X and Y components changed to little-endian. + * @param context The variable receiving the context constructed during + * initialization. + * + * @return Status code indicative of the outcome of the operation. + */ +sgx_status_t initialize_remote_attestation( + // Inputs + sgx_enclave_id_t enclave_id, + bool use_platform_services, + sgx_ec256_public_t *key_challenger, + + // Outputs + sgx_ra_context_t* context +); + +/** + * Clean up and finalize the remote attestation process. + * + * @param enclave_id The identifier of the enclave facilitating the remote + * attestation. + * @param context The context constructed during initialization. + * + * @return SGX_SUCCESS if successful, or SGX_ERROR_INVALID_PARAMETER if + * an invalid context is provided. + */ +sgx_status_t finalize_remote_attestation( + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context +); + +/** + * Get the public key of the application enclave, and the identifier of the + * EPID group the platform belongs to. + * + * @param enclave_id The identifier of the application enclave. + * @param context The context constructed during initialization. + * @param public_key Variable receiving the elliptic curve public key of the + * application enclave, based on the NIST P-256 elliptic curve.. + * @param group_id Variable receiving the identifier of the platform's EPID + * group. + * @param max_retry_count The maximum number of times to retry the operation. + * @param retry_wait_in_secs The number of seconds to wait between each retry. + * + * @return SGX_SUCCESS if successful, otherwise an error code indicative of + * what went wrong. + */ +sgx_status_t get_public_key_and_group_identifier( + // Inputs + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context, + + // Outputs + sgx_ec256_public_t *public_key, + sgx_epid_group_id_t *group_id, + + // Retry logic + int max_retry_count, + unsigned int retry_wait_in_secs +); + +/** + * Process details received from the challenger via the service provider, and + * generate quote. If the service provider accepts the quote, negotiated + * session keys between the application enclave and the challenger are ready + * for use. However, if it fails, the application should notify the service + * provider of the error or the service provider needs a time-out mechanism to + * terminate the remote attestation transaction when it does not receive the + * message. + * + * @param enclave_id The identifier of the application enclave. + * @param context The context constructed during initialization. + * @param challenger_public_key The public key of the challenger. + * @param service_provider_id The identifier of the service provider. + * @param quote_type Indicates the quote type, linkable (1) or unlinkable (0). + * @param key_derivation_function The ID of the key derivation function used. + * @param signature An ECDSA signature over the challenger and application + * enclave's public keys. + * @param mac A 128-bit AES-CMAC generated by the service provider. + * @param revocation_list_size The size of revocation_list, in bytes. + * @param revocation_list The signature revocation list certificate of the + * Intel EPID group identified by the group identifier in message 1. + * @param enclave_mac AES-CMAC generated by the enclave. + * @param enclave_public_key Variable receiving the public key of the + * application enclave. + * @param security_properties Variable receiving the security properties of the + * Intel SGX platform service. If the security property information is not + * required in the remote attestation and key exchange process, this field will + * be all zeros. + * @param quote Variable receiving a pointer to the quote returned from + * sgx_get_quote. This should be freed after use. + * @param quote_size Variable receiving the size of the quote. + * @param max_retry_count The maximum number of times to retry the operation. + * @param retry_wait_in_secs The number of seconds to wait between each retry. + * + * @return SGX_SUCCESS if successful, otherwise an error code indicative of + * what went wrong. + */ +sgx_status_t process_challenger_details_and_generate_quote( + // Inputs + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context, + sgx_ec256_public_t *challenger_public_key, + sgx_spid_t *service_provider_id, + uint16_t quote_type, + uint16_t key_derivation_function, + sgx_ec256_signature_t *signature, + sgx_mac_t *challenger_mac, + uint32_t revocation_list_size, + uint8_t *revocation_list, + + // Outputs + sgx_mac_t *enclave_mac, + sgx_ec256_public_t *enclave_public_key, + sgx_ps_sec_prop_desc_t *security_properties, + uint8_t **quote, + size_t *quote_size, + + // Retry logic + int max_retry_count, + unsigned int retry_wait_in_secs +); + +/** + * Verify attestation response from service provider. + * + * @param enclave_id The identifier of the application enclave. + * @param context The context constructed during initialization. + * @param message The received attestation result message. + * @param message_size The size of the attestation result message. + * @param cmac The CMAC computed over the attestation result message. + * @param cmac_size The size of the CMAC. + * @param secret The encrypted secret provisioned by the challenger. + * @param secret_size The size of the encrypted secret. + * @param gcm_iv The 12-byte initialization vector used in the decryption. + * @param gcm_mac The GCM-MAC generated over the secret. + * @param gcm_mac_size The size of the GCM-MAC. + * @param sealed_secret Pre-allocated buffer receiving the sealed secret. + * @param sealed_secret_size The size of the sealed secret. + * @param cmac_status The variable receiving the outcome of the CMAC check. + * + * @return SGX_SUCCESS if successful, otherwise an error code indicative of + * what went wrong. + */ +sgx_status_t verify_attestation_response( + // Inputs + sgx_enclave_id_t enclave_id, + sgx_ra_context_t context, + uint8_t *message, + size_t message_size, + uint8_t *cmac, + size_t cmac_size, + uint8_t *secret, + size_t secret_size, + uint8_t *gcm_iv, + uint8_t *gcm_mac, + size_t gcm_mac_size, + + // Outputs + uint8_t *sealed_secret, + size_t *sealed_secret_size, + sgx_status_t *cmac_status +); + +#endif /* __REMOTE_ATTESTATION_H__ */ diff --git a/sgx-jvm/remote-attestation/host/native/sealing.cpp b/sgx-jvm/remote-attestation/host/native/sealing.cpp new file mode 100644 index 0000000000..be64121190 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/native/sealing.cpp @@ -0,0 +1,15 @@ +#include "enclave_u.h" +#include "sealing.hpp" + +// Check whether the application enclave is able to unseal a secret. +sgx_status_t unseal_secret( + sgx_enclave_id_t enclave_id, + uint8_t *sealed_secret, + size_t sealed_secret_size +) { + sgx_status_t status = SGX_SUCCESS; + sgx_status_t ret = unsealSecret( + enclave_id, &status, sealed_secret, sealed_secret_size + ); + return SGX_SUCCESS != ret ? ret : status; +} diff --git a/sgx-jvm/remote-attestation/host/native/sealing.hpp b/sgx-jvm/remote-attestation/host/native/sealing.hpp new file mode 100644 index 0000000000..2ba607aa3f --- /dev/null +++ b/sgx-jvm/remote-attestation/host/native/sealing.hpp @@ -0,0 +1,24 @@ +#ifndef __SEALING_H__ +#define __SEALING_H__ + +#include +#include + +/** + * Check whether the application enclave is able to unseal a persisted, sealed + * secret. + * + * @param enclave_id The identifier of the application enclave. + * @param sealed_secret The pre-existing, sealed secret. + * @param sealed_secret_size The size of the sealed secret. + * + * @return An indication of whether or not the enclave was able to unseal the + * secret. + */ +sgx_status_t unseal_secret( + sgx_enclave_id_t enclave_id, + uint8_t *sealed_secret, + size_t sealed_secret_size +); + +#endif /* __SEALING_H__ */ diff --git a/sgx-jvm/remote-attestation/host/native/wrapper.cpp b/sgx-jvm/remote-attestation/host/native/wrapper.cpp new file mode 100644 index 0000000000..441eddf6fe --- /dev/null +++ b/sgx-jvm/remote-attestation/host/native/wrapper.cpp @@ -0,0 +1,384 @@ +#include +#include + +#include +#include + +#include "wrapper.hpp" +#include "jni.hpp" + +#include "logging.hpp" +#include "enclave-manager.hpp" +#include "remote-attestation.hpp" +#include "sealing.hpp" + +NATIVE_WRAPPER(jint, getDeviceStatus) + (JNIEnv *, jobject) +{ + // Get the status of the SGX device on the local machine. + return get_device_status(); +} + +NATIVE_WRAPPER(jobject, getExtendedGroupIdentifier) + (JNIEnv *env, jobject) +{ + // FindClass/GetMethodID both throw an exception upon failure, so we don't + // need to perform any further NULL-checks. + jclass klass = env->FindClass(KLASS("ExtendedGroupIdentifierResult")); + jmethodID cid = env->GetMethodID(klass, "", "(IJ)V"); + if (cid == NULL) { return NULL; } + + // Get the extended EPID group identifier from SGX. + sgx_status_t status = SGX_ERROR_UNEXPECTED; + uint32_t extended_group_id = get_extended_group_id(&status); + + // Construct and return ExtendedGroupIdentifierResult(identifier, status). + return env->NewObject(klass, cid, extended_group_id, status); +} + +NATIVE_WRAPPER(jobject, createEnclave) + (JNIEnv *env, jobject, jstring path, jboolean use_platform_services, + jbyteArray in_launch_token) +{ + // FindClass/GetMethodID both throw an exception upon failure, so we don't + // need to perform any further NULL-checks. + jclass klass = env->FindClass(KLASS("EnclaveResult")); + jmethodID cid = env->GetMethodID(klass, "", "(J[BJ)V"); + if (cid == NULL) { return NULL; } + + // Marshall inputs. + const char *n_path = env->GetStringUTFChars(path, NULL); + sgx_status_t status; + + // Initialize launch token. + sgx_launch_token_t launch_token = { 0 }; + env->GetByteArrayRegion( + in_launch_token, 0, sizeof(sgx_launch_token_t), (jbyte*)&launch_token + ); + + // Create the enclave. + sgx_enclave_id_t enclave_id = create_enclave( + n_path, (bool)use_platform_services, &status, + &launch_token + ); + + // Construct resulting launch token (could be the same as the input). + jbyteArray _launch_token = env->NewByteArray(sizeof(sgx_launch_token_t)); + env->SetByteArrayRegion( + _launch_token, 0, sizeof(sgx_launch_token_t), (jbyte*)&launch_token + ); + + // Free up memory. + env->ReleaseStringUTFChars(path, n_path); + + // Construct and return EnclaveResult(identifier, launch_token, status). + return env->NewObject(klass, cid, enclave_id, _launch_token, status); +} + +NATIVE_WRAPPER(jboolean, destroyEnclave) + (JNIEnv *, jobject, jlong enclave_id) +{ + // Destroy the enclave if a valid identifier has been passed in. + if (enclave_id != 0) { + return destroy_enclave((sgx_enclave_id_t)enclave_id); + } else { + return false; + } +} + +NATIVE_WRAPPER(jobject, initializeRemoteAttestation) + (JNIEnv *env, jobject, jlong enclave_id, jboolean use_platform_services, + jbyteArray in_key_challenger) +{ + // FindClass/GetMethodID both throw an exception upon failure, so we don't + // need to perform any further NULL-checks. + jclass klass = env->FindClass(KLASS("InitializationResult")); + jmethodID cid = env->GetMethodID(klass, "", "(IJ)V"); + if (cid == NULL) { return NULL; } + + // Marshall the public key passed in from the JVM. + sgx_ec256_public_t key_challenger = { 0 }; + env->GetByteArrayRegion( + in_key_challenger, 0, sizeof(sgx_ec256_public_t), + (jbyte*)&key_challenger + ); + + // Initialize the remote attestation context. + sgx_ra_context_t context; + sgx_status_t status = initialize_remote_attestation( + enclave_id, use_platform_services, &key_challenger, &context + ); + + // Construct and return InitializationResult(context, status). + return env->NewObject(klass, cid, context, status); +} + +NATIVE_WRAPPER(jlong, finalizeRemoteAttestation) + (JNIEnv *, jobject, jlong enclave_id, jint context) +{ + // Finalize the remote attestation + return finalize_remote_attestation(enclave_id, context); +} + +NATIVE_WRAPPER(jobject, getPublicKeyAndGroupIdentifier) + (JNIEnv *env, jobject, jlong enclave_id, jint context, jint max_retry_count, + jint retry_wait_in_secs) +{ + // FindClass/GetMethodID both throw an exception upon failure, so we don't + // need to perform any further NULL-checks. + jclass klass = env->FindClass(KLASS("PublicKeyAndGroupIdentifier")); + jmethodID cid = env->GetMethodID(klass, "", "([BIJ)V"); + if (cid == NULL) { return NULL; } + + // Get the public key of the application enclave, and the group identifier + // of the platform. + sgx_ec256_public_t public_key; + sgx_epid_group_id_t group_id; + sgx_status_t status = get_public_key_and_group_identifier( + enclave_id, context, &public_key, &group_id, max_retry_count, + retry_wait_in_secs + ); + + // Cast group identifier into an unsigned integer. + uint32_t gid = *((uint32_t*)group_id); + + // Create managed array used to return the enclave's public key. + jbyteArray _public_key = env->NewByteArray(sizeof(sgx_ec256_public_t)); + if (NULL == _public_key) { + // Out of memory - abort + return NULL; + } + + // Copy public key bytes over to managed array. + env->SetByteArrayRegion( + _public_key, 0, sizeof(sgx_ec256_public_t), (jbyte*)&public_key + ); + + // Return PublicKeyAndGroupIdentifier(publicKey, groupIdentifier, status). + return env->NewObject(klass, cid, _public_key, gid, status); +} + +NATIVE_WRAPPER(jobject, processServiceProviderDetailsAndGenerateQuote) + (JNIEnv *env, jobject, jlong enclave_id, jint context, + jbyteArray in_challenger_public_key, jbyteArray in_service_provider_id, + jshort quote_type, jshort key_derivation_function, jbyteArray in_signature, + jbyteArray in_mac, jint revocation_list_size, + jbyteArray in_revocation_list, jint max_retry_count, + jint retry_wait_in_secs) +{ + // FindClass/GetMethodID both throw an exception upon failure, so we don't + // need to perform any further NULL-checks. + jclass klass = env->FindClass(KLASS("QuoteResult")); + jmethodID cid = env->GetMethodID(klass, "", "([B[B[B[BJ)V"); + if (cid == NULL) { return NULL; } + + // Marshal inputs. + sgx_ec256_public_t challenger_public_key; + sgx_spid_t service_provider_id; + sgx_ec256_signature_t signature; + sgx_mac_t mac; + uint8_t *revocation_list = (uint8_t*)malloc(revocation_list_size); + + // Check if there's enough free memory to allocate a buffer for the + // revocation list. + if (NULL == revocation_list) { + return NULL; + } + + env->GetByteArrayRegion( + in_challenger_public_key, 0, sizeof(sgx_ec256_public_t), + (jbyte*)&challenger_public_key + ); + env->GetByteArrayRegion( + in_service_provider_id, 0, sizeof(sgx_spid_t), + (jbyte*)&service_provider_id + ); + env->GetByteArrayRegion( + in_signature, 0, sizeof(sgx_ec256_signature_t), + (jbyte*)&signature + ); + env->GetByteArrayRegion( + in_mac, 0, sizeof(sgx_mac_t), + (jbyte*)&mac + ); + env->GetByteArrayRegion( + in_revocation_list, 0, revocation_list_size, (jbyte*)revocation_list + ); + + // Output variables. + sgx_mac_t enclave_mac = { 0 }; + sgx_ec256_public_t enclave_public_key = { 0 }; + sgx_ps_sec_prop_desc_t security_properties = { 0 }; + uint8_t *quote = NULL; + size_t quote_size = 0; + + // Process details received from challenger via the service provider, and + // generate quote. + sgx_status_t status = process_challenger_details_and_generate_quote( + // Inputs + enclave_id, context, &challenger_public_key, &service_provider_id, + quote_type, key_derivation_function, &signature, &mac, + revocation_list_size, revocation_list, + // Outputs + &enclave_mac, &enclave_public_key, &security_properties, "e, + "e_size, + // Retry logic + max_retry_count, retry_wait_in_secs + ); + + LOG( + enclave_id, status, context, + "process_challenger_details_and_generate_quote() = quote(size=%u)", + quote_size + ); + + // Create output objects. + jbyteArray _enclave_mac = env->NewByteArray(sizeof(sgx_mac_t)); + env->SetByteArrayRegion( + _enclave_mac, 0, sizeof(sgx_mac_t), (jbyte*)&enclave_mac + ); + jbyteArray _enclave_public_key = env->NewByteArray( + sizeof(sgx_ec256_public_t) + ); + env->SetByteArrayRegion( + _enclave_public_key, 0, sizeof(sgx_ec256_public_t), + (jbyte*)&enclave_public_key + ); + jbyteArray _security_properties = env->NewByteArray( + sizeof(sgx_ps_sec_prop_desc_t) + ); + env->SetByteArrayRegion( + _security_properties, 0, sizeof(sgx_ps_sec_prop_desc_t), + (jbyte*)&security_properties + ); + + jbyteArray _quote = NULL; + + // Free up memory. + if (NULL != quote) { + _quote = env->NewByteArray(quote_size); + env->SetByteArrayRegion(_quote, 0, quote_size, (jbyte*)quote); + free(quote); + } else { + _quote = env->NewByteArray(0); + } + free(revocation_list); + + // Return QuoteResult(mac, publicKey, securityProperties, quote, status). + return env->NewObject( + klass, cid, + _enclave_mac, _enclave_public_key, _security_properties, _quote, status + ); +} + +NATIVE_WRAPPER(jobject, verifyAttestationResponse) + (JNIEnv *env, jobject, jlong enclave_id, jint context, + jbyteArray message, jbyteArray cmac, jbyteArray secret, + jbyteArray gcm_iv, jbyteArray gcm_mac) +{ + // FindClass/GetMethodID both throw an exception upon failure, so we don't + // need to perform any further NULL-checks. + jclass klass = env->FindClass(KLASS("VerificationResult")); + jmethodID cid = env->GetMethodID(klass, "", "([BJJ)V"); + if (cid == NULL) { return NULL; } + + // Get buffer sizes. + size_t message_size = env->GetArrayLength(message); + size_t cmac_size = env->GetArrayLength(cmac); + size_t secret_size = env->GetArrayLength(secret); + size_t gcm_mac_size = env->GetArrayLength(gcm_mac); + + // Allocate buffers. + uint8_t *_message = (uint8_t*)malloc(message_size); + uint8_t *_cmac = (uint8_t*)malloc(cmac_size); + uint8_t *_secret = (uint8_t*)malloc(secret_size); + uint8_t *_gcm_iv = (uint8_t*)malloc(SGX_AESGCM_IV_SIZE); + uint8_t *_gcm_mac = (uint8_t*)malloc(gcm_mac_size); + + // Length of secret is preserved during encryption, but prepend header. + size_t _sealed_secret_size = sizeof(sgx_sealed_data_t) + secret_size; + uint8_t *_sealed_secret = (uint8_t*)malloc(_sealed_secret_size); + + // Check if we ran out of memory. + if (NULL == _message || NULL == _cmac || NULL == _secret || + NULL == _gcm_iv || NULL == _gcm_mac || NULL == _sealed_secret) { + free(_message); + free(_cmac); + free(_secret); + free(_gcm_iv); + free(_gcm_mac); + free(_sealed_secret); + return NULL; + } + + // Marshal inputs. + env->GetByteArrayRegion(message, 0, message_size, (jbyte*)_message); + env->GetByteArrayRegion(cmac, 0, cmac_size, (jbyte*)_cmac); + env->GetByteArrayRegion(secret, 0, secret_size, (jbyte*)_secret); + env->GetByteArrayRegion(gcm_iv, 0, SGX_AESGCM_IV_SIZE, (jbyte*)_gcm_iv); + env->GetByteArrayRegion(gcm_mac, 0, gcm_mac_size, (jbyte*)_gcm_mac); + + // Verify the attestation response received from the service provider. + sgx_status_t cmac_status = SGX_SUCCESS; + sgx_status_t status = verify_attestation_response( + enclave_id, context, _message, message_size, _cmac, cmac_size, + _secret, secret_size, _gcm_iv, _gcm_mac, gcm_mac_size, _sealed_secret, + &_sealed_secret_size, &cmac_status + ); + + // Free temporary allocations. + free(_message); + free(_cmac); + free(_secret); + free(_gcm_iv); + free(_gcm_mac); + + // Marshal outputs. + jbyteArray sealed_secret; + if (NULL != _sealed_secret) { + sealed_secret = env->NewByteArray(_sealed_secret_size); + env->SetByteArrayRegion( + sealed_secret, 0, _sealed_secret_size, (jbyte*)_sealed_secret + ); + free(_sealed_secret); + } else { + sealed_secret = env->NewByteArray(0); + } + + // Return VerificationResult(sealedSecret, cmacValidationStatus, status). + return env->NewObject(klass, cid, sealed_secret, cmac_status, status); +} + +NATIVE_WRAPPER(jlong, unsealSecret) + (JNIEnv *env, jobject, jlong enclave_id, jbyteArray sealed_secret) +{ + // Check if we've actually got a sealed secret to unseal. + uint8_t *_sealed_secret = NULL; + size_t sealed_secret_size = env->GetArrayLength(sealed_secret); + if (0 == sealed_secret_size) { + return SGX_ERROR_INVALID_PARAMETER; + } + + // Allocate buffer. + _sealed_secret = (uint8_t*)malloc(sealed_secret_size); + + // Check if we ran out of memory. + if (NULL == _sealed_secret) { + return 0; + } + + // Marshal inputs. + env->GetByteArrayRegion( + sealed_secret, 0, sealed_secret_size, (jbyte*)_sealed_secret + ); + + // Try to unseal the secret. + sgx_status_t result = unseal_secret( + enclave_id, _sealed_secret, sealed_secret_size + ); + + // Free temporary allocations. + free(_sealed_secret); + + return result; +} diff --git a/sgx-jvm/remote-attestation/host/src/integration-test/kotlin/net/corda/sgx/attestation/AttestationManagerTests.kt b/sgx-jvm/remote-attestation/host/src/integration-test/kotlin/net/corda/sgx/attestation/AttestationManagerTests.kt new file mode 100644 index 0000000000..0c45a0fcac --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/integration-test/kotlin/net/corda/sgx/attestation/AttestationManagerTests.kt @@ -0,0 +1,212 @@ +package net.corda.sgx.attestation + +import net.corda.sgx.attestation.entities.QuoteType +import net.corda.sgx.attestation.service.ISVHttpClient +import net.corda.sgx.bridge.EnclaveConfiguration +import net.corda.sgx.bridge.attestation.NativeAttestationEnclave +import net.corda.sgx.enclave.SgxStatus +import net.corda.sgx.sealing.SecretManager +import org.hamcrest.CoreMatchers.anyOf +import org.hamcrest.CoreMatchers.containsString +import org.junit.Assert.* +import org.junit.After +import org.junit.Test +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import kotlin.test.assertEquals +import kotlin.test.assertFails + +@Suppress("KDocMissingDocumentation") +class AttestationManagerTests { + + private companion object { + + @JvmStatic + private val log: Logger = LoggerFactory + .getLogger(AttestationManagerTests::class.java) + + } + + private val usePlatformServices = false + + private lateinit var manager: AttestationManager + + private fun enclave() = NativeAttestationEnclave( + EnclaveConfiguration.path, + usePlatformServices + ) + + private fun enclaveAndManager( + name: String + ): Pair { + val enclave = enclave() + manager = AttestationManager( + enclave = enclave, + usePlatformServices = usePlatformServices, + secretManager = SecretManager(enclave), + isvClient = ISVHttpClient(name = name) + ) + return Pair(enclave, manager) + } + + @After + fun cleanup() { + manager.cleanUp() + } + + @Test + fun `step 1 - can initialize attestation process`() { + // No interaction with ISV. + val (_, manager) = enclaveAndManager("Step1") + val challenge = manager.requestChallenge() + manager.initialize(challenge) + } + + @Test + fun `step 2 - can request challenge provisioning`() { + // Request challenge from ISV. + val (_, manager) = enclaveAndManager("Step2") + val challenge = manager.requestChallenge() + assertNotEquals(0, challenge.nonce.length) + } + + @Test + fun `step 3 - can send extended group identifier`() { + // Send MSG0. + val (_, manager) = enclaveAndManager("Step3") + val challenge = manager.requestChallenge() + manager.initialize(challenge) + manager.sendExtendedGroupIdentifier() + } + + @Test + fun `step 4a - can retrieve public key and group identifier`() { + // No interaction with ISV. + val (enclave, manager) = enclaveAndManager("Step4a") + val challenge = manager.requestChallenge() + manager.initialize(challenge) + val (publicKey, groupId) = enclave.getPublicKeyAndGroupIdentifier() + assertNotEquals(0, publicKey.bytes.size) + assertNotEquals(0, groupId) + } + + @Test + fun `step 4b - can send public key and group identifier`() { + // Send MSG1. + val (_, manager) = enclaveAndManager("Step4b") + val challenge = manager.requestChallenge() + manager.initialize(challenge) + manager.sendPublicKeyAndGroupIdentifier() + } + + @Test + fun `step 5 - can receive challenger details`() { + // Send MSG1 and receive MSG2. + val (_, manager) = enclaveAndManager("Step5") + val challenge = manager.requestChallenge() + manager.initialize(challenge) + val details = manager.sendPublicKeyAndGroupIdentifier() + assertEquals(1, details.keyDerivationFunctionIdentifier) + assertEquals(QuoteType.LINKABLE, details.quoteType) + } + + @Test + fun `step 6a - can generate quote`() { + // Send MSG1, receive MSG2, and generate MSG3. + val (enclave, manager) = enclaveAndManager("Step6a") + val challenge = manager.requestChallenge() + manager.initialize(challenge) + val details = manager.sendPublicKeyAndGroupIdentifier() + val quote = enclave + .processChallengerDetailsAndGenerateQuote(details) + assertNotNull(quote.publicKey) + assertNotEquals(0, quote.publicKey.bytes.size) + assertNotNull(quote.messageAuthenticationCode) + assertNotEquals(0, quote.messageAuthenticationCode.size) + assertNotNull(quote.payload) + assertNotEquals(0, quote.payload.size) + assertNotNull(quote.securityProperties) + assertNotEquals(0, quote.securityProperties.size) + } + + @Test + fun `step 6b - can generate and submit quote`() { + // Request challenge from ISV, send MSG1, receive MSG2, and send MSG3. + val (_, manager) = enclaveAndManager("Step6b") + val challenge = manager.requestChallenge() + manager.initialize(challenge) + val details = manager.sendPublicKeyAndGroupIdentifier() + assertEquals(64, details.signature.size) + val response = manager.generateAndSubmitQuote(challenge, details) + assertThat(response.quoteStatus.name, anyOf( + containsString("OK"), + + // If the attestation request to IAS returns + // GROUP_OUT_OF_DATE, the machine needs a microcode + // upgrade. In this case, it's up to the challenger + // whether or not to trust the content of the quote. + containsString("GROUP_OUT_OF_DATE") + )) + } + + @Test + fun `step 7a - can verify attestation response`() { + // Request challenge from ISV, send MSG0 and MSG1, receive MSG2, send + // MSG3, and receive MSG4. + val sealingHeaderSize = 560 + val (_, manager) = enclaveAndManager("Step7a") + val challenge = manager.requestChallenge() + manager.initialize(challenge) + val details = manager.sendPublicKeyAndGroupIdentifier() + val response = manager.generateAndSubmitQuote(challenge, details) + val (macStatus, sealedSecret) = + manager.verifyAttestationResponse(response) + assertEquals(SgxStatus.SUCCESS, macStatus) + assertNotNull(sealedSecret) + assertEquals( + sealingHeaderSize + response.secret.size, + sealedSecret.size + ) + } + + @Test + fun `step 7b - cannot verify attestation response with invalid IV`() { + // Request challenge from ISV, send MSG0 and MSG1, receive MSG2, send + // MSG3, and receive MSG4. + val (_, manager) = enclaveAndManager("Step7b") + val challenge = manager.requestChallenge() + manager.initialize(challenge) + val details = manager.sendPublicKeyAndGroupIdentifier() + val response = manager.generateAndSubmitQuote(challenge, details) + response.secretIV[0] = ((response.secretIV[0] + 1) % 255).toByte() + assertFails { manager.verifyAttestationResponse(response) } + } + + @Test + fun `step 8 - can seal and reuse provisioned secret`() { + // Request challenge from ISV, send MSG0 and MSG1, receive MSG2, send + // MSG3, and receive MSG4. Seal the secret and then unseal it again. + val sealingHeaderSize = 560 + val (enclave, manager) = enclaveAndManager("Step8") + val challenge = manager.requestChallenge() + manager.initialize(challenge) + val details = manager.sendPublicKeyAndGroupIdentifier() + val response = manager.generateAndSubmitQuote(challenge, details) + val (macStatus, sealedSecret) = + manager.verifyAttestationResponse(response) + assertEquals(SgxStatus.SUCCESS, macStatus) + assertNotNull(sealedSecret) + assertEquals( + sealingHeaderSize + response.secret.size, + sealedSecret.size + ) + val unsealingResult = enclave.unseal(sealedSecret) + assertEquals(SgxStatus.SUCCESS, unsealingResult) + + val invalidSecret = sealedSecret.copyOf(sealedSecret.size - 10) + + byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) + val unsealingResultInvalid = enclave.unseal(invalidSecret) + assertNotEquals(SgxStatus.SUCCESS, unsealingResultInvalid) + } + +} diff --git a/sgx-jvm/remote-attestation/host/src/integration-test/kotlin/net/corda/sgx/bridge/EnclaveConfiguration.kt b/sgx-jvm/remote-attestation/host/src/integration-test/kotlin/net/corda/sgx/bridge/EnclaveConfiguration.kt new file mode 100644 index 0000000000..8654de00fd --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/integration-test/kotlin/net/corda/sgx/bridge/EnclaveConfiguration.kt @@ -0,0 +1,12 @@ +package net.corda.sgx.bridge + +object EnclaveConfiguration { + + private val dir: String = System.getProperty("corda.sgx.enclave.path") + + /** + * The path of the signed attestation enclave binary. + */ + val path: String = "$dir/corda_sgx_ra_enclave.so" + +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/AttestationEnclave.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/AttestationEnclave.kt new file mode 100644 index 0000000000..0d37d99575 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/AttestationEnclave.kt @@ -0,0 +1,83 @@ +package net.corda.sgx.attestation + +import net.corda.sgx.attestation.entities.AttestationResult +import net.corda.sgx.attestation.entities.Quote +import net.corda.sgx.attestation.service.ChallengerDetails +import net.corda.sgx.enclave.ECKey +import net.corda.sgx.enclave.Enclave +import net.corda.sgx.enclave.SgxException +import net.corda.sgx.enclave.SgxStatus +import net.corda.sgx.sealing.SealedSecret +import net.corda.sgx.system.GroupIdentifier + +/** + * Enclave used in remote attestation. + */ +interface AttestationEnclave : Enclave { + + /** + * The platform services offer an architectural enclave which provides a + * trusted time source and a monotonic counter, which in turn can be used + * for replay protection during nonce generation and for securely + * calculating the length of time for which a secret shall be valid. + */ + val usePlatformServices: Boolean + + /** + * Create a context for the remote attestation and key exchange process. + * + * @param challengerKey The elliptic curve public key of the challenger + * (NIST P-256 elliptic curve). + * + * @throws SgxException If unable to create context. + */ + fun initializeKeyExchange(challengerKey: ECKey) + + /** + * Finalize the remote attestation and key exchange process. + */ + fun finalizeKeyExchange() + + /** + * Get the public key of the application enclave, based on NIST P-256 + * elliptic curve, and the identifier of the EPID group to which the + * platform belongs. + */ + fun getPublicKeyAndGroupIdentifier(): Pair + + /** + * Process the response from the challenger and generate a quote for the + * final step of the attestation process. + * + * @param challengerDetails Details received from the challenger. + */ + fun processChallengerDetailsAndGenerateQuote( + challengerDetails: ChallengerDetails + ): Quote + + /** + * Verify the attestation response received from the service provider. + * + * @param attestationResult The received attestation response. + * + * @return A pair of (1) the outcome of the validation of the CMAC over + * the security manifest, and (2) the sealed secret, if successful. + * + * @throws SgxException If unable to verify the response and seal the + * secret. + */ + fun verifyAttestationResponse( + attestationResult: AttestationResult + ): Pair + + /** + * Attempt to unseal a secret inside the enclave and report the outcome of + * the operation. + * + * @param sealedSecret The sealed secret provisioned by the challenger. + * + * @return A status code indicative of the outcome of the operation. + */ + fun unseal(sealedSecret: SealedSecret): SgxStatus + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/AttestationManager.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/AttestationManager.kt new file mode 100644 index 0000000000..0d32dc29f6 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/AttestationManager.kt @@ -0,0 +1,289 @@ +package net.corda.sgx.attestation + +import net.corda.sgx.attestation.entities.* +import net.corda.sgx.attestation.service.ChallengerDetails +import net.corda.sgx.attestation.service.ISVClient +import net.corda.sgx.enclave.SgxStatus +import net.corda.sgx.sealing.SealedSecret +import net.corda.sgx.sealing.SecretManager +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * Remote attestation flow. + * + * @property enclave The enclave used for the remote attestation process. + * @property secretManager The secret manager used to store and manage secrets. + * @property isvClient Client used to communicate with the remote attestation + * service provider. + * @property usePlatformServices Indication of whether to use platform services + * or not to. Platform services supplies a trusted time source in addition to a + * monotonic counter, which in combination can be used to protect against + * replay attacks during nonce generation and to securely set time validity of + * secrets. + */ +class AttestationManager @JvmOverloads constructor( + private val enclave: AttestationEnclave, + private val secretManager: SecretManager, + private val isvClient: ISVClient, + private val usePlatformServices: Boolean = false +) { + + private companion object { + + @JvmStatic + private val log: Logger = LoggerFactory + .getLogger(AttestationManager::class.java) + + } + + /** + * The backbone of the remote attestation flow. Ensure that the attestation + * context gets closed (if needed) upon completion. + */ + fun attest() { + val challenge = requestChallenge() + initialize(challenge) + try { + sendExtendedGroupIdentifier() + val details = sendPublicKeyAndGroupIdentifier() + val response = generateAndSubmitQuote(challenge, details) + val (status, secret) = verifyAttestationResponse(response) + if (status != SgxStatus.SUCCESS) { + throw AttestationException( + "Failed to validate CMAC manifest (${status.name})" + ) + } + persistSecret(secret) + } finally { + cleanUp() + } + } + + /** + * Ask service provider to provision challenge. + */ + fun requestChallenge(): Challenge { + // Request challenge from service provider. + try { + return isvClient.requestChallenge() + } catch (ex: Exception) { + log.error("Failed to request challenge.", ex) + throw ex + } + } + + /** + * Activate enclave and initialize the key exchange and remote attestation + * flow. + */ + fun initialize(challenge: Challenge) { + // Make sure the system is SGX-enabled and that we have a running + // enclave to work with. + try { + enclave.activate() + } catch (ex: Exception) { + log.error("Failed to activate enclave.", ex) + throw ex + } + + // If we have already been through an attestation, chances are that the + // existing sealed secret might still be valid. So, if we have a sealed + // secret, we first try to unseal it to see whether we need to + // re-attest or not. + if (!checkIfAttestationIsNeeded()) { + // The secret was already provisioned and is still valid, so we can + // relinquish control to the enclave to operate further on the + // provisioned data. + return + } + + // Initialize the key exchange and remote attestation process. Platform + // services supplies a trusted time source in addition to a monotonic + // counter, which in combination can be used to protect against replay + // attacks during nonce generation and to securely set time validity of + // secrets. + + try { + enclave.initializeKeyExchange(challenge.publicKey) + } catch (ex: Exception) { + log.error("Failed to initialize key exchange.", ex) + throw ex + } + } + + + /** + * Send extended group identifier to service provider. + */ + fun sendExtendedGroupIdentifier() { + // Next, we need to send our extended group identifier to let the + // service provider know what EPID group to use during remote + // attestation. This corresponds to message 0 in the Intel remote + // attestation flow. + val egid = try { + enclave.system.getExtendedGroupIdentifier() + } catch (ex: Exception) { + log.error("Failed to get extended group identifier.", ex) + throw ex + } + try { + if (!isvClient.validateExtendedGroupIdentifier(egid)) { + throw AttestationException("Extended group not accepted.") + } + } catch (ex: Exception) { + log.error("Failed validating extended group identifier.", ex) + throw ex + } + } + + /** + * Send public key and group identifier, and receive details about the + * challenger. + */ + fun sendPublicKeyAndGroupIdentifier(): ChallengerDetails { + // Call into the Intel Provisioning Server to provision attestation + // key, if necessary. Otherwise, just go ahead and retrieve the + // public key and the group identifier. + val (publicKey, gid) = try { + enclave.getPublicKeyAndGroupIdentifier() + } catch (ex: Exception) { + log.error("Failed to get public key and group identifier.", ex) + throw ex + } + + // Send our public key and our group identifier to the service + // provider, which in turn will check our details against the current + // version of the revocation list. The service provider will forward + // this list together with its service provider ID, signatures, quote + // type, etc. to us (the client). This request corresponds to message 1 + // in the Intel remote attestation flow. + try { + return isvClient.sendPublicKeyAndGroupIdentifier(publicKey, gid) + } catch (ex: Exception) { + log.error("Failed sending PK and group identifier to ISV.", ex) + throw ex + } + } + + /** + * Process response from the service provider, generate a quote and send + * it to the service provider to get it verified. + */ + fun generateAndSubmitQuote( + challenge: Challenge, + details: ChallengerDetails + ): AttestationResult { + // Process response from the service provider and generate a quote. + val quote = try { + enclave.processChallengerDetailsAndGenerateQuote(details) + } catch (ex: Exception) { + log.error("Failed to process challenger details and generate " + + "quote.", ex) + throw ex + } + + // Send the quote to the service provider, which in turn will verify + // the attestation evidence with the Intel attestation service. + return submitQuote(challenge, quote) + } + + /** + * Send quote to service provider, which in turn will verify the + * attestation evidence with the Intel attestation service. + */ + fun submitQuote( + challenge: Challenge, + quote: Quote + ): AttestationResult { + // Send the quote to the service provider, which in turn will verify + // the attestation evidence with the Intel attestation service. + val attestationResponse = try { + isvClient.submitQuote(challenge, quote) + } catch (ex: Exception) { + log.error("Failed to submit quote to ISV.", ex) + throw ex + } + if (!attestationResponse.isSuccessful) { + val status = attestationResponse.status + throw AttestationException("Failed to verify quote. $status") + } + return attestationResponse + } + + /** + * Verify the received attestation response from service provider. + */ + fun verifyAttestationResponse( + response: AttestationResult + ): Pair { + if (response.quoteStatus == QuoteStatus.GROUP_OUT_OF_DATE) { + log.warn(QuoteStatus.GROUP_OUT_OF_DATE.description) + } else if (response.quoteStatus != QuoteStatus.OK) { + val code = response.quoteStatus + throw AttestationException("${code.description} (${code.name})") + } + + if (response.secretHash.isEmpty()) { + throw AttestationException("No secret hash available") + } + + // The attestation service is happy with the quote, and the remote + // attestation has been verified. Now, we need to verify the response, + // decrypt the secret and seal it in the enclave. + try { + return enclave.verifyAttestationResponse(response) + } catch (ex: Exception) { + log.error("Failed to verify attestation response.", ex) + throw ex + } + } + + /** + * Finalize remote attestation and key exchange process. + */ + fun cleanUp() { + try { + // The secret has been provisioned, so we are now ready to + // relinquish control to the enclave to operate further on the + // provisioned data. + enclave.finalizeKeyExchange() + } catch (ex: Exception) { + log.error("Failed to finalize key exchange.", ex) + throw ex + } + } + + /** + * Persist the secret sealed by the enclave. + */ + fun persistSecret(secret: SealedSecret) { + // The attestation response was verified, so we persist the secret. + try { + secretManager.persistSealedSecret(secret) + } catch (ex: Exception) { + log.error("Failed to persist sealed secret.", ex) + throw ex + } + } + + /** + * Checks if we already have a sealed secret from a previous attestation + * that is still valid. + */ + private fun checkIfAttestationIsNeeded(): Boolean { + // If we do not have a secret at hand, we need to be provisioned one. + // This is accomplished by running through the attestation process. If + // we have already been provisioned a secret, that does not necessarily + // mean that we are validly attested. We also need to check that we can + // unseal said secret and consequently that the attestation has not + // expired. + return try { + !secretManager.isValid() + } catch (ex: Exception) { + log.warn("Failed to unseal and validate secret.", ex) + true + } + } + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationContext.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationContext.kt new file mode 100644 index 0000000000..d765eb1f50 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationContext.kt @@ -0,0 +1,6 @@ +package net.corda.sgx.attestation.entities + +/** + * Remote attestation context. + */ +typealias AttestationContext = Int \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationException.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationException.kt new file mode 100644 index 0000000000..31f6ccaaa8 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationException.kt @@ -0,0 +1,8 @@ +package net.corda.sgx.attestation.entities + +/** + * Exception thrown during remote attestation. + */ +class AttestationException( + message: String +) : Exception(message) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationResult.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationResult.kt new file mode 100644 index 0000000000..5212c07ba6 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationResult.kt @@ -0,0 +1,53 @@ +package net.corda.sgx.attestation.entities + +import net.corda.sgx.sealing.ProvisionedSecret +import java.time.LocalDateTime + +/** + * The outcome of a remote attestation process. + */ +class AttestationResult( + + /** + * THe status of the provided quote. + */ + val quoteStatus: QuoteStatus, + + /** + * The received attestation result message. + */ + val attestationResultMessage: ByteArray, + + /** + * The CMAC over the attestation result message. + */ + val aesCMAC: ByteArray, + + /** + * Provisioned, encrypted secret if the attestation was successful. + */ + val secret: ProvisionedSecret, + + /** + * The initialization vector used as part of the decryption. + */ + val secretIV: ByteArray, + + /** + * The GCM MAC returned as part of the attestation response. + */ + val secretHash: ByteArray, + + /** + * The end result of the remote attestation process. + */ + val status: AttestationStatus + +) { + + /** + * Result of the remote attestation. + */ + val isSuccessful: Boolean = status == AttestationStatus.SUCCESS + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationStatus.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationStatus.kt new file mode 100644 index 0000000000..9b4414274c --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/AttestationStatus.kt @@ -0,0 +1,20 @@ +package net.corda.sgx.attestation.entities + +/** + * The status of the remote attestation process. + * + * @property message A human readable representation of the state. + */ +enum class AttestationStatus(val message: String) { + + /** + * The remote attestation was successful. + */ + SUCCESS("Remote attestation succeeded."), + + /** + * The remote attestation failed. + */ + FAIL("Remote attestation failed."), + +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/Challenge.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/Challenge.kt new file mode 100644 index 0000000000..7179a5e75e --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/Challenge.kt @@ -0,0 +1,21 @@ +package net.corda.sgx.attestation.entities + +import net.corda.sgx.enclave.ECKey + +/** + * Remote attestation challenge from service provider. + */ +class Challenge( + + /** + * A one-time number generated by the service provider and used + * throughout the attestation to protect against replay attacks. + */ + val nonce: String, + + /** + * The long-term identity key of the challenger. + */ + val publicKey: ECKey + +) diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/Quote.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/Quote.kt new file mode 100644 index 0000000000..ab3f63d4bd --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/Quote.kt @@ -0,0 +1,38 @@ +package net.corda.sgx.attestation.entities + +import net.corda.sgx.enclave.ECKey + +/** + * Signed report from the application enclave. The corresponds to message 3 in + * the Intel remote attestation flow. + */ +class Quote( + + /** + * The 128-bit AES-CMAC generated by the application enclave. See + * [sgx_ra_msg3_t](https://software.intel.com/en-us/node/709238) for + * more details on its derivation. + */ + val messageAuthenticationCode: ByteArray, + + /** + * The public elliptic curve key of the application enclave. + */ + val publicKey: ECKey, + + /** + * Security property of the Intel SGX Platform Service. If the Intel + * SGX Platform Service security property information is not required + * in the remote attestation and key exchange process, this field will + * be all zeros. The buffer is 256 bytes long. + */ + val securityProperties: ByteArray, + + /** + * Quote returned from sgx_get_quote. More details about how the quote + * is derived can be found in Intel's documentation: + * [sgx_ra_msg3_t](https://software.intel.com/en-us/node/709238) + */ + val payload: ByteArray + +) diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/QuoteStatus.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/QuoteStatus.kt new file mode 100644 index 0000000000..0f80107749 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/QuoteStatus.kt @@ -0,0 +1,71 @@ +package net.corda.sgx.attestation.entities + +/** + * The status of an enclave's quote as it has been processed by the Intel + * Attestation service. + * + * @property description A human-readable description of the status code. + */ +enum class QuoteStatus(val description: String) { + + /** + * EPID signature of the ISV enclave quote was verified correctly and the + * TCB level of the SGX platform is up-to- date. + */ + OK("EPID signature of the ISV enclave quote was verified correctly and " + + "the TCB level of the SGX platform is up-to-date."), + + /** + * EPID signature of the ISV enclave quote was invalid. The content of the + * quote is not trustworthy. + */ + SIGNATURE_INVALID("EPID signature of the ISV enclave quote was invalid."), + + /** + * The EPID group has been revoked. When this value is returned, the + * revocation reason field of the Attestation Verification Report will + * contain a revocation reason code for this EPID group as reported in the + * EPID Group CRL. The content of the quote is not trustworthy. + */ + GROUP_REVOKED("The EPID group has been revoked."), + + /** + * The EPID private key used to sign the quote has been revoked by + * signature. The content of the quote is not trustworthy. + */ + SIGNATURE_REVOKED("The EPID private key used to sign the quote has been " + + "revoked by signature."), + + /** + * The EPID private key used to sign the quote has been directly revoked + * (not by signature). The content of the quote is not trustworthy. + */ + KEY_REVOKED("The EPID private key used to sign the quote has been " + + "directly revoked (not by signature)."), + + /** + * SigRL version in ISV enclave quote does not match the most recent + * version of the SigRL. In rare situations, after SP retrieved the SigRL + * from IAS and provided it to the platform, a newer version of the SigRL + * is made available. As a result, the Attestation Verification Report will + * indicate SIGRL_VERSION_MISMATCH. SP can retrieve the most recent version + * of SigRL from the IAS and request the platform to perform remote + * attestation again with the most recent version of SigRL. If the platform + * keeps failing to provide a valid quote matching with the most recent + * version of the SigRL, the content of the quote is not trustworthy. + */ + SIGRL_VERSION_MISMATCH("SigRL version in ISV enclave quote does not " + + "match the most recent version of the SigRL."), + + /** + * The EPID signature of the ISV enclave quote has been verified correctly, + * but the TCB level of SGX platform is outdated. The platform has not been + * identified as compromised and thus it is not revoked. It is up to the + * Service Provider to decide whether or not to trust the content of the + * quote. + */ + GROUP_OUT_OF_DATE("The EPID signature of the ISV enclave quote has " + + "been verified correctly, but the TCB level of SGX platform " + + "is outdated.") + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/QuoteType.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/QuoteType.kt new file mode 100644 index 0000000000..f6f5f04659 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/entities/QuoteType.kt @@ -0,0 +1,23 @@ +package net.corda.sgx.attestation.entities + +/** + * The type of quote used in the attestation. + * + * @property value The native value of the quote type. + */ +enum class QuoteType(val value: Short) { + + /** + * Unlinkable is random value-based, meaning that having two quotes you + * cannot identify whether they are from the same source or not. + */ + UNLINKABLE(0), + + /** + * Linkable is name-based, meaning that having two quotes you can identify + * if they come from the same enclave or not. Note that you can not + * determine which enclave it is though. + */ + LINKABLE(1) + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/ChallengerDetails.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/ChallengerDetails.kt new file mode 100644 index 0000000000..c181c93aec --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/ChallengerDetails.kt @@ -0,0 +1,57 @@ +package net.corda.sgx.attestation.service + +import net.corda.sgx.attestation.entities.QuoteType +import net.corda.sgx.enclave.ECKey + +/** + * Information retrieved from message 2 in the Intel remote attestation flow. + */ +class ChallengerDetails( + + /** + * The public elliptic curve key of the challenger, based on the NIST + * P-256 elliptic curve. + */ + val publicKey: ECKey, + + /** + * The identifier of the service provider, also known as SPID. + */ + val serviceProviderIdentifier: ByteArray, + + /** + * Indicates the quote type, i.e., whether it is linkable or + * un-linkable. + */ + val quoteType: QuoteType, + + /** + * The identifier of the key derivation function. + */ + val keyDerivationFunctionIdentifier: Short, + + /** + * An ECDSA signature of (g_b||g_a), using the challenger's ECDSA + * private key corresponding to the public key specified in the + * sgx_ra_init function, where g_b is the public elliptic curve key of + * the challenger and g_a is the public key of application enclave, + * provided by the application enclave, in the remote attestation and + * key exchange message (message 1). + */ + val signature: ByteArray, + + /** + * The 128-bit AES-CMAC generated by the service provider. See + * [sgx_ra_msg2_t](https://software.intel.com/en-us/node/709237) + * for more details on its derivation. + */ + val messageAuthenticationCode: ByteArray, + + /** + * The Intel EPID signature revocation list certificate of the Intel + * EPID group identified by the group identifier in the remote + * attestation and key exchange message (message 1). + */ + val signatureRevocationList: ByteArray + +) diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/ISVClient.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/ISVClient.kt new file mode 100644 index 0000000000..6ec22508b8 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/ISVClient.kt @@ -0,0 +1,71 @@ +package net.corda.sgx.attestation.service + +import net.corda.sgx.attestation.entities.AttestationResult +import net.corda.sgx.attestation.entities.Challenge +import net.corda.sgx.attestation.entities.Quote +import net.corda.sgx.enclave.ECKey +import net.corda.sgx.system.ExtendedGroupIdentifier +import net.corda.sgx.system.GroupIdentifier + +/** + * A proxy providing an interface for calling into required functionality of + * the remote attestation service. Communication with the service takes place + * over HTTPS. + */ +interface ISVClient { + + /** + * Send request to the remote attestation service asking to get provisioned + * a challenge. + * + * @return A challenge from the attestation service. + */ + fun requestChallenge(): Challenge + + /** + * Send request to the remote attestation service to validate the extended + * group identifier. This corresponds to message 0 in Intel's remote + * attestation flow. + * + * @param extendedGroupId The extended group identifier obtained from + * calling into the architectural enclave. + * + * @return True if the service successfully validated the identifier and is + * happy to proceed. False otherwise. + */ + fun validateExtendedGroupIdentifier( + extendedGroupId: ExtendedGroupIdentifier + ): Boolean + + /** + * Send request containing the application enclave's public key and the + * platform's EPID group identifier. This corresponds to message 1 in + * Intel's remote attestation flow. + * + * @param publicKey The application enclave's public key. + * @param groupIdentifier The platform's EPID group identifier. + * + * @return Details about the service provider, such as its public key, SPID + * and quote type. + */ + fun sendPublicKeyAndGroupIdentifier( + publicKey: ECKey, + groupIdentifier: GroupIdentifier + ): ChallengerDetails + + /** + * Send request containing the generated quote to the service provider so + * that they can verify the attestation evidence with Intel's attestation + * service. + * + * @param challenge The challenge received in the first step. + * @param quote The generated quote. + * + * @return The outcome of the remote attestation. + */ + fun submitQuote( + challenge: Challenge, + quote: Quote + ): AttestationResult + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/ISVHttpClient.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/ISVHttpClient.kt new file mode 100644 index 0000000000..31b4fe8850 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/ISVHttpClient.kt @@ -0,0 +1,220 @@ +package net.corda.sgx.attestation.service + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import net.corda.sgx.attestation.entities.* +import net.corda.sgx.attestation.service.messages.* +import net.corda.sgx.enclave.ECKey +import net.corda.sgx.system.ExtendedGroupIdentifier +import net.corda.sgx.system.GroupIdentifier +import net.corda.sgx.system.SgxSystem +import net.corda.sgx.system.value +import org.apache.http.HttpStatus.SC_OK +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.HttpGet +import org.apache.http.client.methods.HttpPost +import org.apache.http.config.SocketConfig +import org.apache.http.entity.ContentType.APPLICATION_JSON +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.BasicHttpClientConnectionManager +import org.apache.http.util.EntityUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * Client used to talk to the Remote Attestation ISV. + * + * @property endpoint The base URL of the ISV service. + */ +open class ISVHttpClient( + private val endpoint: String = "http://localhost:${portNumber()}", + private val name: String = "Client" +) : ISVClient { + + private companion object { + + @JvmStatic + private val log: Logger = LoggerFactory + .getLogger(ISVHttpClient::class.java) + + fun portNumber(): Int { + val portNumber = System.getenv("PORT") ?: "9080" + return Integer.parseUnsignedInt(portNumber) + } + + } + + private var httpClient: CloseableHttpClient + private val mapper = ObjectMapper().registerModule(JavaTimeModule()) + + init { + val httpRequestConfig = RequestConfig.custom() + .setConnectTimeout(5_000) + .setSocketTimeout(5_000) + .build() + val httpSocketConfig = SocketConfig.custom() + .setSoReuseAddress(true) + .setTcpNoDelay(true) + .build() + val connectionManager = BasicHttpClientConnectionManager() + .apply { socketConfig = httpSocketConfig } + httpClient = HttpClients.custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(httpRequestConfig) + .build() + log.debug("$name : Talking to ISV on $endpoint") + } + + override fun requestChallenge(): Challenge { + val request = HttpGet("$endpoint/attest/provision") + httpClient.execute(request).use { response -> + val body = EntityUtils.toString(response.entity) + log.debug("$name : Challenge <- ${response.statusLine}") + log.debug("$name : Challenge <- $body") + if (response.statusLine.statusCode < 200 || + response.statusLine.statusCode >= 300) + { + throw AttestationException(response.statusLine.toString()) + } + val challenge = read(body) + + if (SC_OK != response.statusLine.statusCode + || challenge.nonce.isEmpty()) { + throw AttestationException("Failed to request challenge") + } + + return Challenge( + challenge.nonce, + ECKey.fromBytes(challenge.serviceKey) + ) + } + } + + override fun validateExtendedGroupIdentifier( + extendedGroupId: ExtendedGroupIdentifier + ): Boolean { + val message0 = Message0(extendedGroupId.value) + val request = HttpPost("$endpoint/attest/msg0") + log.debug("$name : MSG0 -> ${mapper.writeValueAsString(message0)}") + request.entity = StringEntity( + mapper.writeValueAsString(message0), + APPLICATION_JSON + ) + httpClient.execute(request).use { response -> + log.debug("$name : MSG0 <- ${response.statusLine}") + return SC_OK == response.statusLine.statusCode + } + } + + override fun sendPublicKeyAndGroupIdentifier( + publicKey: ECKey, + groupIdentifier: GroupIdentifier + ): ChallengerDetails { + val message1 = Message1( + ga = publicKey.bytes, + platformGID = groupIdentifier.value() + ) + val request = HttpPost("$endpoint/attest/msg1") + log.debug("$name : MSG1 -> ${mapper.writeValueAsString(message1)}") + request.entity = StringEntity( + mapper.writeValueAsString(message1), + APPLICATION_JSON + ) + httpClient.execute(request).use { response -> + val body = EntityUtils.toString(response.entity) + log.debug("$name : MSG2 <- ${response.statusLine}") + log.debug("$name : MSG2 <- $body") + if (response.statusLine.statusCode < 200 || + response.statusLine.statusCode >= 300) + { + throw AttestationException(response.statusLine.toString()) + } + val message2 = read(body) + + if (SC_OK != response.statusLine.statusCode) { + throw AttestationException("Invalid response from server") + } + + message2.spid + val spid = hexStringToByteArray(message2.spid) + return ChallengerDetails( + publicKey = ECKey.fromBytes(message2.gb), + serviceProviderIdentifier = spid, + quoteType = if (message2.linkableQuote) { + QuoteType.LINKABLE + } else { + QuoteType.UNLINKABLE + }, + keyDerivationFunctionIdentifier = message2.keyDerivationFuncId.toShort(), + signature = message2.signatureGbGa, + messageAuthenticationCode = message2.aesCMAC, + signatureRevocationList = message2.revocationList + ) + } + } + + override fun submitQuote( + challenge: Challenge, + quote: Quote + ): AttestationResult { + val message3 = Message3( + aesCMAC = quote.messageAuthenticationCode, + ga = quote.publicKey.bytes, + quote = quote.payload, + securityManifest = quote.securityProperties, + nonce = challenge.nonce + ) + val request = HttpPost("$endpoint/attest/msg3") + log.debug("$name : MSG3 -> ${mapper.writeValueAsString(message3)}") + request.entity = StringEntity( + mapper.writeValueAsString(message3), + APPLICATION_JSON + ) + httpClient.execute(request).use { response -> + val body = EntityUtils.toString(response.entity) + log.debug("$name : MSG4 <- ${response.statusLine}") + for (header in response.allHeaders) { + log.trace("$name : MSG4 <- ${header.name} = ${header.value}") + } + log.debug("$name : MSG4 <- $body") + if (response.statusLine.statusCode < 200 || + response.statusLine.statusCode >= 300) + { + throw AttestationException(response.statusLine.reasonPhrase) + } + val message4 = read(body) + return AttestationResult( + quoteStatus = SgxSystem.quoteStatusFromString(message4.quoteStatus), + attestationResultMessage = message4.platformInfo ?: byteArrayOf(), + aesCMAC = message4.aesCMAC ?: byteArrayOf(), + secret = message4.secret, + secretHash = message4.secretHash, + secretIV = message4.secretIV, + status = if (SC_OK == response.statusLine.statusCode) { + AttestationStatus.SUCCESS + } else { + AttestationStatus.FAIL + } + ) + } + } + + private inline fun read(value: String): T = + mapper.readValue(value, T::class.java) as T + + private fun hexStringToByteArray(s: String): ByteArray { + val len = s.length + val data = ByteArray(len / 2) + var i = 0 + while (i < len) { + val c1 = Character.digit(s[i], 16) shl 4 + val c2 = Character.digit(s[i + 1], 16) + data[i / 2] = (c1 + c2).toByte() + i += 2 + } + return data + } + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/ChallengeResponse.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/ChallengeResponse.kt new file mode 100644 index 0000000000..794342c79a --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/ChallengeResponse.kt @@ -0,0 +1,19 @@ +package net.corda.sgx.attestation.service.messages + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@Suppress("KDocMissingDocumentation") +@JsonPropertyOrder("nonce", "serviceKey") +@JsonInclude(NON_NULL) +class ChallengeResponse( + + @param:JsonProperty("nonce") + val nonce: String, + + @param:JsonProperty("serviceKey") + val serviceKey: ByteArray + +) diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message0.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message0.kt new file mode 100644 index 0000000000..a7b8176e35 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message0.kt @@ -0,0 +1,16 @@ +package net.corda.sgx.attestation.service.messages + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@Suppress("KDocMissingDocumentation") +@JsonPropertyOrder("extendedGID") +@JsonInclude(NON_NULL) +class Message0( + + @param:JsonProperty("extendedGID") + val extendedGID: Int + +) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message1.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message1.kt new file mode 100644 index 0000000000..06a0d25f35 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message1.kt @@ -0,0 +1,18 @@ +package net.corda.sgx.attestation.service.messages + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@Suppress("KDocMissingDocumentation") +@JsonPropertyOrder("ga", "platformGID") +class Message1( + + // The client's 512 bit public DH key + @param:JsonProperty("ga") + val ga: ByteArray, + + // Platform GID value from the SGX client + @param:JsonProperty("platformGID") + val platformGID: String + +) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message2.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message2.kt new file mode 100644 index 0000000000..33baba78b9 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message2.kt @@ -0,0 +1,39 @@ +package net.corda.sgx.attestation.service.messages + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@Suppress("KDocMissingDocumentation") +@JsonPropertyOrder( + "gb", + "spid", + "linkableQuote", + "keyDerivationFuncId", + "signatureGbGa", + "aesCMAC", + "revocationList" +) +class Message2( + // The server's 512 bit public DH key (Little Endian) + @param:JsonProperty("gb") + val gb: ByteArray, + + @param:JsonProperty("spid") + val spid: String, + + @param:JsonProperty("linkableQuote") + val linkableQuote: Boolean = true, + + @param:JsonProperty("keyDerivationFuncId") + val keyDerivationFuncId: Int, + + @param:JsonProperty("signatureGbGa") + val signatureGbGa: ByteArray, // Not ASN.1 encoded + + @param:JsonProperty("aesCMAC") + val aesCMAC: ByteArray, // Not ASN.1 encoded + + @param:JsonProperty("revocationList") + val revocationList: ByteArray + +) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message3.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message3.kt new file mode 100644 index 0000000000..0680b69a88 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message3.kt @@ -0,0 +1,28 @@ +package net.corda.sgx.attestation.service.messages + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@Suppress("KDocMissingDocumentation") +@JsonPropertyOrder("aesCMAC", "ga", "quote", "securityManifest", "nonce") +@JsonInclude(NON_NULL) +class Message3( + @param:JsonProperty("aesCMAC") + val aesCMAC: ByteArray, // Not ASN.1 encoded + + // The client's 512 bit public DH key + @param:JsonProperty("ga") + val ga: ByteArray, + + @param:JsonProperty("quote") + val quote: ByteArray, + + @param:JsonProperty("securityManifest") + val securityManifest: ByteArray? = null, + + @param:JsonProperty("nonce") + val nonce: String? = null + +) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message4.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message4.kt new file mode 100644 index 0000000000..350230b283 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/attestation/service/messages/Message4.kt @@ -0,0 +1,72 @@ +package net.corda.sgx.attestation.service.messages + +import com.fasterxml.jackson.annotation.JsonFormat +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder +import java.time.LocalDateTime + +@Suppress("KDocMissingDocumentation") +@JsonPropertyOrder( + "reportID", + "quoteStatus", + "quoteBody", + "securityManifestStatus", + "securityManifestHash", + "platformInfo", + "aesCMAC", + "epidPseudonym", + "nonce", + "secret", + "secretHash", + "secretIV", + "timestamp" +) +@JsonInclude(NON_NULL) +class Message4( + + @param:JsonProperty("reportID") + val reportID: String, + + @param:JsonProperty("quoteStatus") + val quoteStatus: String, + + @param:JsonProperty("quoteBody") + val quoteBody: ByteArray, + + @param:JsonProperty("securityManifestStatus") + val securityManifestStatus: String? = null, + + @param:JsonProperty("securityManifestHash") + val securityManifestHash: String? = null, + + @param:JsonProperty("platformInfo") + val platformInfo: ByteArray? = null, + + @param:JsonProperty("aesCMAC") + val aesCMAC: ByteArray? = null, + + @param:JsonProperty("epidPseudonym") + val epidPseudonym: ByteArray? = null, + + @param:JsonProperty("nonce") + val nonce: String? = null, + + @param:JsonProperty("secret") + val secret: ByteArray, + + @param:JsonProperty("secretHash") + val secretHash: ByteArray, + + @param:JsonProperty("secretIV") + val secretIV: ByteArray, + + @param:JsonProperty("timestamp") + @field:JsonFormat( + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS", + timezone = "UTC" + ) + val timestamp: LocalDateTime + +) diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/attestation/NativeAttestationEnclave.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/attestation/NativeAttestationEnclave.kt new file mode 100644 index 0000000000..976a6ffdc1 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/attestation/NativeAttestationEnclave.kt @@ -0,0 +1,213 @@ +package net.corda.sgx.bridge.attestation + +import net.corda.sgx.attestation.AttestationEnclave +import net.corda.sgx.attestation.entities.AttestationContext +import net.corda.sgx.attestation.entities.AttestationException +import net.corda.sgx.attestation.entities.AttestationResult +import net.corda.sgx.attestation.entities.Quote +import net.corda.sgx.attestation.service.ChallengerDetails +import net.corda.sgx.bridge.enclave.NativeEnclave +import net.corda.sgx.bridge.wrapper.NativeWrapper +import net.corda.sgx.enclave.ECKey +import net.corda.sgx.enclave.SgxException +import net.corda.sgx.enclave.SgxStatus +import net.corda.sgx.sealing.* +import net.corda.sgx.system.GroupIdentifier +import net.corda.sgx.system.SgxSystem +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import kotlin.concurrent.withLock + +/** + * Enclave used in remote attestation. + */ +class NativeAttestationEnclave @JvmOverloads constructor( + enclavePath: String, + override val usePlatformServices: Boolean = false +) : NativeEnclave(enclavePath, usePlatformServices), AttestationEnclave { + + private companion object { + + @JvmStatic + private val log: Logger = LoggerFactory + .getLogger(NativeAttestationEnclave::class.java) + + } + + private val maxRetryCount: Int = 5 + + private val retryDelayInSeconds: Int = 5 + + private var context: AttestationContext? = null + + /** + * Create a context for the remote attestation and key exchange process. + * + * @param challengerKey The elliptic curve public key of the challenger + * (NIST P-256 elliptic curve). + * + * @throws SgxException If unable to create context. + */ + override fun initializeKeyExchange(challengerKey: ECKey) { + lock.withLock { + val result = NativeWrapper.initializeRemoteAttestation( + identifier, + usePlatformServices, + challengerKey.bytes + ) + val status = SgxSystem.statusFromCode(result.result) + context = result.context + if (status != SgxStatus.SUCCESS) { + throw SgxException(status, identifier, context) + } + } + } + + /** + * Clean up and finalize the remote attestation process. + */ + override fun finalizeKeyExchange() { + lock.withLock { + val oldContext = context + if (oldContext != null) { + val code = NativeWrapper.finalizeRemoteAttestation( + identifier, + oldContext + ) + context = null + val status = SgxSystem.statusFromCode(code) + if (status != SgxStatus.SUCCESS) { + throw SgxException(status, identifier, oldContext) + } + } + } + } + + /** + * Get the public key of the application enclave, based on NIST P-256 + * elliptic curve, and the identifier of the EPID group the platform + * belongs to. + */ + override fun getPublicKeyAndGroupIdentifier(): Pair { + lock.withLock { + val context = context + ?: throw AttestationException("Not initialized.") + val result = NativeWrapper.getPublicKeyAndGroupIdentifier( + identifier, + context, + maxRetryCount, + retryDelayInSeconds + ) + val status = SgxSystem.statusFromCode(result.result) + if (status != SgxStatus.SUCCESS) { + throw SgxException(status, identifier, context) + } + + return Pair( + ECKey.fromBytes(result.publicKey), + result.groupIdentifier + ) + } + } + + /** + * Process the response from the challenger and generate a quote for the + * final step of the attestation process. + * + * @param challengerDetails Details from the challenger. + */ + override fun processChallengerDetailsAndGenerateQuote( + challengerDetails: ChallengerDetails + ): Quote { + lock.withLock { + val context = context + ?: throw AttestationException("Not initialized.") + val result = NativeWrapper.processServiceProviderDetailsAndGenerateQuote( + identifier, + context, + challengerDetails.publicKey.bytes, + challengerDetails.serviceProviderIdentifier, + challengerDetails.quoteType.value, + challengerDetails.keyDerivationFunctionIdentifier, + challengerDetails.signature, + challengerDetails.messageAuthenticationCode, + challengerDetails.signatureRevocationList.size, + challengerDetails.signatureRevocationList, + maxRetryCount, + retryDelayInSeconds + ) + val status = SgxSystem.statusFromCode(result.result) + if (status != SgxStatus.SUCCESS) { + throw SgxException(status, identifier, context) + } + return Quote( + result.messageAuthenticationCode, + ECKey.fromBytes(result.publicKey), + result.securityProperties, + result.payload + ) + } + } + + /** + * Verify the attestation response received from the service provider. + * + * @param attestationResult The received attestation response. + * + * @return A pair of (1) the outcome of the validation of the CMAC over + * the security manifest, and (2) the sealed secret, if successful. + * + * @throws SgxException If unable to verify the response or seal the + * secret. + */ + override fun verifyAttestationResponse( + attestationResult: AttestationResult + ): Pair { + lock.withLock { + val context = context + ?: throw AttestationException("Not initialized.") + val result = NativeWrapper.verifyAttestationResponse( + identifier, + context, + attestationResult.attestationResultMessage, + attestationResult.aesCMAC, + attestationResult.secret, + attestationResult.secretIV, + attestationResult.secretHash + ) + val cmacValidationStatus = SgxSystem.statusFromCode( + result.cmacValidationStatus + ) + + if (cmacValidationStatus != SgxStatus.SUCCESS) { + if (attestationResult.aesCMAC.isEmpty()) { + log.warn("No CMAC available") + } else { + log.warn( + "Failed to validate AES-CMAC ({}).", + cmacValidationStatus.name + ) + } + } + + val status = SgxSystem.statusFromCode(result.result) + if (status != SgxStatus.SUCCESS) { + throw SgxException(status, identifier, context) + } + + return Pair(cmacValidationStatus, result.secret) + } + } + + /** + * Attempt to unseal a secret inside the enclave and report the outcome of + * the operation. + */ + override fun unseal(sealedSecret: SealedSecret): SgxStatus { + lock.withLock { + val result = NativeWrapper.unsealSecret(identifier, sealedSecret) + return SgxSystem.statusFromCode(result) + } + } + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/enclave/NativeEnclave.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/enclave/NativeEnclave.kt new file mode 100644 index 0000000000..a984e065b4 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/enclave/NativeEnclave.kt @@ -0,0 +1,99 @@ +package net.corda.sgx.bridge.enclave + +import net.corda.sgx.bridge.system.NativeSgxSystem +import net.corda.sgx.bridge.wrapper.LaunchToken +import net.corda.sgx.bridge.wrapper.NativeWrapper +import net.corda.sgx.bridge.wrapper.newLaunchToken +import net.corda.sgx.enclave.Enclave +import net.corda.sgx.enclave.EnclaveIdentifier +import net.corda.sgx.enclave.SgxStatus +import net.corda.sgx.system.SgxSystem +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +/** + * Representation of an enclave on an SGX-enabled system. + * + * @param enclavePath The path to the signed enclave binary. + * @param usePlatformServices Whether or not to leverage Intel's platform + * services (for replay protection in nonce generation, etc.). + */ +open class NativeEnclave @JvmOverloads constructor( + private val enclavePath: String, + private val usePlatformServices: Boolean = false +) : Enclave { + + /** + * Lock for ensuring single entry of enclave calls. + */ + protected val lock: Lock = ReentrantLock() + + private var enclaveId: EnclaveIdentifier = 0 + + private var launchToken: LaunchToken = newLaunchToken() + + /** + * The SGX-enabled system on which this enclave is running. + */ + override val system: SgxSystem + get() = NativeSgxSystem() + + /** + * The enclave identifier. + */ + override val identifier: EnclaveIdentifier + get() = enclaveId + + /** + * Create enclave used for remote attestation, and consequently for secret + * sealing and unsealing. + */ + override fun create(): SgxStatus { + lock.withLock { + val result = NativeWrapper.createEnclave( + enclavePath, // The path to the signed enclave binary + usePlatformServices, // Whether to use Intel's services + launchToken // New or pre-existing launch token. + ) + val status = SgxSystem.statusFromCode(result.result) + if (status == SgxStatus.ERROR_ENCLAVE_LOST) { + // If the enclave was lost, we need to destroy it. Not doing so + // will result in EPC memory leakage that could prevent + // subsequent enclaves from loading. + destroy() + } + enclaveId = result.identifier + launchToken = result.token + return status + } + } + + /** + * Destroy enclave if running. + */ + override fun destroy(): Boolean { + lock.withLock { + if (enclaveId != 0L) { + // Only attempt to destroy enclave if one has been created + val result = NativeWrapper.destroyEnclave(enclaveId) + enclaveId = 0L + launchToken = newLaunchToken() + return result + } + return true + } + } + + /** + * Check whether the enclave has been run before or not. + */ + override fun isFresh(): Boolean { + lock.withLock { + val nullByte = 0.toByte() + return identifier == 0L && + (0 until launchToken.size).all { launchToken[it] == nullByte } + } + } + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/system/NativeSgxSystem.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/system/NativeSgxSystem.kt new file mode 100644 index 0000000000..5ea99aff51 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/system/NativeSgxSystem.kt @@ -0,0 +1,41 @@ +package net.corda.sgx.bridge.system + +import net.corda.sgx.attestation.entities.AttestationException +import net.corda.sgx.bridge.wrapper.NativeWrapper +import net.corda.sgx.enclave.SgxException +import net.corda.sgx.enclave.SgxStatus +import net.corda.sgx.system.ExtendedGroupIdentifier +import net.corda.sgx.system.SgxDeviceStatus +import net.corda.sgx.system.SgxSystem + +/** + * Query system properties of an SGX-enabled environment. + */ +class NativeSgxSystem : SgxSystem { + + /** + * Check if the client platform is enabled for Intel SGX. The application + * must be run with administrator privileges to get the status + * successfully. + * + * @return The current status of the SGX device. + */ + override fun getDeviceStatus(): SgxDeviceStatus { + return SgxSystem.deviceStatusFromCode(NativeWrapper.getDeviceStatus()) + } + + /** + * Get the extended Intel EPID Group the client uses by default. The key + * used to sign a quote will be a member of the this group. + */ + override fun getExtendedGroupIdentifier(): ExtendedGroupIdentifier { + val result = NativeWrapper.getExtendedGroupIdentifier() + val status = SgxSystem.statusFromCode(result.result) + if (status != SgxStatus.SUCCESS) { + throw SgxException(status) + } + return SgxSystem.extendedGroupIdentifier(result.extendedGroupIdentifier) + ?: throw AttestationException("Invalid extended EPID group") + } + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/EnclaveResult.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/EnclaveResult.kt new file mode 100644 index 0000000000..793ea125b7 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/EnclaveResult.kt @@ -0,0 +1,27 @@ +package net.corda.sgx.bridge.wrapper + +import net.corda.sgx.enclave.EnclaveIdentifier +import net.corda.sgx.enclave.SgxStatus + +/** + * The result of a call to [NativeWrapper.createEnclave]. + */ +class EnclaveResult( + + /** + * The identifier of the created enclave, if any. + */ + val identifier: EnclaveIdentifier, + + /** + * The launch token of the enclave. + */ + val token: LaunchToken, + + /** + * The output status code of the enclave creation (of type [SgxStatus] + * downstream). + */ + val result: Long + +) diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/ExtendedGroupIdentifierResult.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/ExtendedGroupIdentifierResult.kt new file mode 100644 index 0000000000..8ce9ab123f --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/ExtendedGroupIdentifierResult.kt @@ -0,0 +1,22 @@ +package net.corda.sgx.bridge.wrapper + +import net.corda.sgx.enclave.SgxStatus +import net.corda.sgx.system.ExtendedGroupIdentifier + +/** + * The result of a call to [NativeWrapper.getExtendedGroupIdentifier]. + */ +data class ExtendedGroupIdentifierResult( + + /** + * The extended Intel EPID group identifier (of type + * [ExtendedGroupIdentifier] downstream). + */ + val extendedGroupIdentifier: Int, + + /** + * The result of the operation (of type [SgxStatus] downstream). + */ + val result: Long + +) diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/InitializationResult.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/InitializationResult.kt new file mode 100644 index 0000000000..b186072ac8 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/InitializationResult.kt @@ -0,0 +1,21 @@ +package net.corda.sgx.bridge.wrapper + +import net.corda.sgx.attestation.entities.AttestationContext +import net.corda.sgx.enclave.SgxStatus + +/** + * The result of a call to [NativeWrapper.initializeRemoteAttestation]. + */ +data class InitializationResult( + + /** + * The context returned from the call to sgx_ra_init(). + */ + val context: AttestationContext, + + /** + * The result of the operation (of type [SgxStatus] downstream). + */ + val result: Long + +) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/LaunchToken.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/LaunchToken.kt new file mode 100644 index 0000000000..93ffe933f2 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/LaunchToken.kt @@ -0,0 +1,13 @@ +package net.corda.sgx.bridge.wrapper + +/** + * An opaque type used to hold enclave launch information. Used by + * sgx_create_enclave to initialize an enclave. The license is generated by the + * launch enclave. + */ +typealias LaunchToken = ByteArray + +/** + * Create a new launch token. + */ +fun newLaunchToken(): LaunchToken = ByteArray(1024) diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/NativeWrapper.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/NativeWrapper.kt new file mode 100644 index 0000000000..00c985bdce --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/NativeWrapper.kt @@ -0,0 +1,99 @@ +package net.corda.sgx.bridge.wrapper + +import net.corda.sgx.attestation.entities.AttestationContext +import net.corda.sgx.enclave.EnclaveIdentifier +import net.corda.sgx.sealing.ProvisionedSecret +import net.corda.sgx.sealing.SealedSecret + +@Suppress("KDocMissingDocumentation") +internal object NativeWrapper { + + init { + System.loadLibrary("corda_sgx_ra") + } + + // enclave-management.cpp + + @Throws(Exception::class) + external fun getDeviceStatus(): Int + + @Throws(Exception::class) + external fun getExtendedGroupIdentifier(): ExtendedGroupIdentifierResult + + @Throws(Exception::class) + external fun createEnclave( + path: String, + usePlatformServices: Boolean, + token: LaunchToken + ): EnclaveResult + + @Throws(Exception::class) + external fun destroyEnclave( + id: Long + ): Boolean + + // remote-attestation.cpp + + @Throws(Exception::class) + external fun initializeRemoteAttestation( + enclaveIdentifier: EnclaveIdentifier, + usePlatformServices: Boolean, + challengerKey: ByteArray + ): InitializationResult + + @Throws(Exception::class) + external fun finalizeRemoteAttestation( + enclaveIdentifier: EnclaveIdentifier, + context: AttestationContext + ): Long + + @Throws(Exception::class) + external fun getPublicKeyAndGroupIdentifier( + enclaveIdentifier: EnclaveIdentifier, + context: AttestationContext, + maxRetryCount: Int, + retryWaitInSeconds: Int + ): PublicKeyAndGroupIdentifier + + @Throws(Exception::class) + external fun processServiceProviderDetailsAndGenerateQuote( + enclaveIdentifier: EnclaveIdentifier, + context: AttestationContext, + publicKey: ByteArray, + serviceProviderIdentifier: ByteArray, + quoteType: Short, + keyDerivationFunctionIdentifier: Short, + signature: ByteArray, + messageAuthenticationCode: ByteArray, + signatureRevocationSize: Int, + signatureRevocationList:ByteArray, + maxRetryCount: Int, + retryWaitInSeconds: Int + ): QuoteResult + + @Throws(Exception::class) + external fun verifyAttestationResponse( + enclaveIdentifier: EnclaveIdentifier, + context: AttestationContext, + attestationResultMessage: ByteArray, + aesCmac: ByteArray, + secret: ByteArray, + gcmIV: ByteArray, + gcmMac: ByteArray + ): VerificationResult + + // sealing.cpp + + @Throws(Exception::class) + external fun sealSecret( + enclaveIdentifier: EnclaveIdentifier, + provisionedSecret: ProvisionedSecret + ): SealingOperationResult + + @Throws(Exception::class) + external fun unsealSecret( + enclaveIdentifier: EnclaveIdentifier, + sealedSecret: SealedSecret + ): Long + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/PublicKeyAndGroupIdentifier.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/PublicKeyAndGroupIdentifier.kt new file mode 100644 index 0000000000..1028bb97bb --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/PublicKeyAndGroupIdentifier.kt @@ -0,0 +1,28 @@ +package net.corda.sgx.bridge.wrapper + +import net.corda.sgx.enclave.ECKey +import net.corda.sgx.enclave.SgxStatus +import net.corda.sgx.system.GroupIdentifier + +/** + * The result of a call to [NativeWrapper.getPublicKeyAndGroupIdentifier]. + */ +class PublicKeyAndGroupIdentifier( + + /** + * The public elliptic curve key of the application enclave (of type + * [ECKey] downstream). + */ + val publicKey: ByteArray, + + /** + * The identifier of the EPID group to which the enclave belongs. + */ + val groupIdentifier: GroupIdentifier, + + /** + * The result of the operation (of type [SgxStatus] downstream). + */ + val result: Long + +) diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/QuoteResult.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/QuoteResult.kt new file mode 100644 index 0000000000..40d3375f53 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/QuoteResult.kt @@ -0,0 +1,45 @@ +package net.corda.sgx.bridge.wrapper + +import net.corda.sgx.enclave.ECKey +import net.corda.sgx.enclave.SgxStatus + +/** + * The result of a call to + * [NativeWrapper.processServiceProviderDetailsAndGenerateQuote]. + */ +class QuoteResult( + + /** + * The 128-bit AES-CMAC generated by the application enclave. See + * [sgx_ra_msg3_t](https://software.intel.com/en-us/node/709238) for + * more details on its derivation. + */ + val messageAuthenticationCode: ByteArray, + + /** + * The public elliptic curve key of the application enclave (of type + * [ECKey] downstream). + */ + val publicKey: ByteArray, + + /** + * Security property of the Intel SGX Platform Service. If the Intel + * SGX Platform Service security property information is not required + * in the remote attestation and key exchange process, this field will + * be all zeros. The buffer is 256 bytes long. + */ + val securityProperties: ByteArray, + + /** + * Quote returned from sgx_get_quote. More details about how the quote + * is derived can be found in Intel's documentation: + * [sgx_ra_msg3_t](https://software.intel.com/en-us/node/709238) + */ + val payload: ByteArray, + + /** + * The result of the operation (of type [SgxStatus] downstream). + */ + val result: Long + +) diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/SealingOperationResult.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/SealingOperationResult.kt new file mode 100644 index 0000000000..d038746082 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/SealingOperationResult.kt @@ -0,0 +1,22 @@ +package net.corda.sgx.bridge.wrapper + +import net.corda.sgx.sealing.SealedSecret +import net.corda.sgx.sealing.SealingResult + +/** + * The result of a call to [NativeWrapper.sealSecret]. + */ +class SealingOperationResult( + + /** + * The sealed secret, if any. + */ + val sealedSecret: SealedSecret, + + /** + * The output result of the operation (of type [SealingResult] + * downstream). + */ + val result: Long + +) diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/VerificationResult.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/VerificationResult.kt new file mode 100644 index 0000000000..88c2263d19 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/bridge/wrapper/VerificationResult.kt @@ -0,0 +1,28 @@ +package net.corda.sgx.bridge.wrapper + +import net.corda.sgx.enclave.SgxStatus +import net.corda.sgx.sealing.SealedSecret + +/** + * The result of a call to [NativeWrapper.verifyAttestationResponse]. + */ +class VerificationResult( + + /** + * The sealed secret returned if the attestation result was + * successfully verified. + */ + val secret: SealedSecret, + + /** + * The outcome of the validation of the CMAC over the attestation + * result message (of type [SgxStatus] downstream). + */ + val cmacValidationStatus: Long, + + /** + * The result of the operation (of type [SgxStatus] downstream). + */ + val result: Long + +) diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/cli/Program.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/cli/Program.kt new file mode 100644 index 0000000000..06e162dc34 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/cli/Program.kt @@ -0,0 +1,51 @@ +@file:JvmName("Program") + +package net.corda.sgx.cli + +import net.corda.sgx.attestation.AttestationManager +import net.corda.sgx.attestation.service.ISVHttpClient +import net.corda.sgx.bridge.attestation.NativeAttestationEnclave +import net.corda.sgx.sealing.SecretManager +import org.slf4j.LoggerFactory + +object Flow + +fun main(args: Array) { + val log = LoggerFactory.getLogger(Flow::class.java) + + val dir = System.getProperty("corda.sgx.enclave.path") + val enclavePath = "$dir/corda_sgx_ra_enclave.so" + val enclave = NativeAttestationEnclave(enclavePath) + val isvClient = ISVHttpClient() + val secretManager = SecretManager(enclave) + val manager = AttestationManager(enclave, secretManager, isvClient) + + log.info("Request challenge ...") + val challenge = manager.requestChallenge() + + log.info("Initialize remote attestation context ...") + manager.initialize(challenge) + + try { + log.info("Send public key and group identifier ...") + val details = manager.sendPublicKeyAndGroupIdentifier() + + log.info("Process challenger details and generate quote ...") + val quote = enclave + .processChallengerDetailsAndGenerateQuote(details) + + log.info("Submit generated quote ...") + val response = manager.submitQuote(challenge, quote) + log.info("Quote status = ${response.quoteStatus}") + + log.info("Verify attestation response ...") + val (validMac, sealedSecret) = + manager.verifyAttestationResponse(response) + log.info("Has valid CMAC = $validMac") + log.info("Sealed secret size = ${sealedSecret.size}") + } catch (ex : Exception) { + log.error("Failed to complete remote attestation", ex) + } finally { + manager.cleanUp() + } +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/ECKey.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/ECKey.kt new file mode 100644 index 0000000000..a99590272a --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/ECKey.kt @@ -0,0 +1,42 @@ +package net.corda.sgx.enclave + +/** + * Public key based on NIST P-256 elliptic curve + * + * @param componentX The 256-bit X component of the key. + * @param componentY The 256-bit Y component of the key. + */ +class ECKey( + componentX: ByteArray, + componentY: ByteArray +) { + + /** + * The bytes constituting the elliptic curve public key. + */ + val bytes: ByteArray = componentX.plus(componentY) + + companion object { + + /** + * Create a public key from a byte array. + * + * @param bytes The 64 bytes forming the NIST P-256 elliptic curve. + */ + fun fromBytes(bytes: ByteArray): ECKey { + if (bytes.size != 64) { + throw Exception("Expected 64 bytes, but got ${bytes.size}") + } + val componentX = bytes.copyOfRange(0, 32) + val componentY = bytes.copyOfRange(32, 64) + return ECKey(componentX, componentY) + } + + } + +} + +/** + * Short-hand generator for supplying a constant X or Y component. + */ +fun ecKeyComponent(vararg bytes: Int) = bytes.map { it.toByte() }.toByteArray() diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/Enclave.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/Enclave.kt new file mode 100644 index 0000000000..15362a866d --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/Enclave.kt @@ -0,0 +1,86 @@ +package net.corda.sgx.enclave + +import net.corda.sgx.system.SgxSystem +import net.corda.sgx.system.SgxUnavailableException + +/** + * Representation an enclave. + */ +interface Enclave { + + /** + * The SGX-enabled system on which this enclave is running. + */ + val system: SgxSystem + + /** + * The enclave identifier. + */ + val identifier: EnclaveIdentifier + + /** + * Create enclave used for remote attestation, and consequently for secret + * sealing and unsealing. + */ + fun create(): SgxStatus + + /** + * Destroy enclave if running. + */ + fun destroy(): Boolean + + /** + * Destroy and re-create the enclave. This is normally done if the enclave + * is lost due to a power transition or similar events. + */ + fun recreate(): SgxStatus { + destroy() + return create() + } + + /** + * Check whether the enclave has been run before or not. + */ + fun isFresh(): Boolean + + /** + * Check whether an enclave has already been created and initialized. + * Otherwise, try to create required enclave or re-create one in the cases + * where an older one has been lost due to a power transition or similar. + * + * @throws SgxException If unable to create enclave. + * @throws SgxUnavailableException If SGX is unavailable or for some reason + * disabled. + */ + fun activate() { + // First, make sure SGX is available and that it is enabled. Under some + // circumstances, a reboot may be required to enable SGX. In either + // case, as long as the extensions aren't enabled, an + // [SgxUnavailableException] will be thrown. + system.ensureAvailable() + + // If the enclave has already been created and is active, we are good + // to proceed. + var status = create() + if (status == SgxStatus.SUCCESS) { + return + } + + // Check if an attestation enclave was previously created. If it was + // and it is no longer available, recreate one to the same + // specification. Note: Losing an enclave is normally the result of a + // power transition. + if (status == SgxStatus.ERROR_ENCLAVE_LOST) { + status = recreate() + if (status != SgxStatus.SUCCESS) { + throw SgxException(status, identifier) + } + return + } + + // Some other error occurred, let's abort + throw SgxException(status, identifier) + } + +} + diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/EnclaveIdentifier.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/EnclaveIdentifier.kt new file mode 100644 index 0000000000..64d67a777c --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/EnclaveIdentifier.kt @@ -0,0 +1,6 @@ +package net.corda.sgx.enclave + +/** + * The identifier of an enclave. + */ +typealias EnclaveIdentifier = Long diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/SgxException.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/SgxException.kt new file mode 100644 index 0000000000..ef03af6feb --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/SgxException.kt @@ -0,0 +1,39 @@ +package net.corda.sgx.enclave + +import net.corda.sgx.attestation.entities.AttestationContext + +/** + * Exception raised whenever there's a problem creating, destroying or + * interacting with an enclave or SGX. + * + * @property status The status or outcome of an SGX operation. + * @property enclaveIdentifier The identifier of the enclave, if available. + * @property context The established remote attestation context, if available. + */ +class SgxException @JvmOverloads constructor( + val status: SgxStatus, + private val enclaveIdentifier: EnclaveIdentifier? = null, + private val context: AttestationContext? = null + +) : Exception(status.message) { + + /** + * Human readable representation of the exception. + */ + override fun toString(): String { + val message = super.toString() + val identifierString = if (enclaveIdentifier != null) { + "0x${java.lang.Long.toHexString(enclaveIdentifier)}" + } else { + "null" + } + val contextString = if (context != null) { + "0x${java.lang.Integer.toHexString(context)}" + } else { + "null" + } + return "$message (enclave=$identifierString, " + + "context=$contextString, status=${status.name})" + } + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/SgxStatus.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/SgxStatus.kt new file mode 100644 index 0000000000..8c04266397 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/enclave/SgxStatus.kt @@ -0,0 +1,291 @@ +package net.corda.sgx.enclave + +/** + * The status of an SGX operation + * + * @property code The native status code returned from the SGX API. + * @property message A human readable representation of the state. + */ +enum class SgxStatus(val code: Long, val message: String) { + + /** + * Success. + */ + SUCCESS(0x0000, "Success"), + + /** + * Unexpected error. + */ + ERROR_UNEXPECTED(0x0001, "Unexpected error"), + + /** + * The parameter is incorrect. + */ + ERROR_INVALID_PARAMETER(0x0002, "The parameter is incorrect"), + + /** + * Not enough memory is available to complete this operation. + */ + ERROR_OUT_OF_MEMORY(0x0003, "Not enough memory is available to complete this operation"), + + /** + * Enclave lost after power transition or used in child process created by linux:fork(). + */ + ERROR_ENCLAVE_LOST(0x0004, "Enclave lost after power transition or used in child process created by linux:fork()"), + + /** + * SGX API is invoked in incorrect order or state. + */ + ERROR_INVALID_STATE(0x0005, "SGX API is invoked in incorrect order or state"), + + /** + * The ECALL/OCALL index is invalid. + */ + ERROR_INVALID_FUNCTION(0x1001, "The ecall/ocall index is invalid"), + + /** + * The enclave is out of TCS. + */ + ERROR_OUT_OF_TCS(0x1003, "The enclave is out of TCS"), + + /** + * The enclave is crashed. + */ + ERROR_ENCLAVE_CRASHED(0x1006, "The enclave is crashed"), + + /** + * The ECALL is not allowed at this time, e.g. ECALL is blocked by the dynamic entry table, or nested ECALL is not allowed during initialization. + */ + ERROR_ECALL_NOT_ALLOWED(0x1007, "The ECALL is not allowed at this time, e.g. ECALL is blocked by the dynamic entry table, or nested ECALL is not allowed during initialization"), + + /** + * The OCALL is not allowed at this time, e.g. OCALL is not allowed during exception handling. + */ + ERROR_OCALL_NOT_ALLOWED(0x1008, "The OCALL is not allowed at this time, e.g. OCALL is not allowed during exception handling"), + + /** + * The enclave is running out of stack. + */ + ERROR_STACK_OVERRUN(0x1009, "The enclave is running out of stack"), + + /** + * The enclave image has one or more undefined symbols. + */ + ERROR_UNDEFINED_SYMBOL(0x2000, "The enclave image has one or more undefined symbols"), + + /** + * The enclave image is not correct. + */ + ERROR_INVALID_ENCLAVE(0x2001, "The enclave image is not correct."), + + /** + * The enclave identifier is invalid. + */ + ERROR_INVALID_ENCLAVE_ID(0x2002, "The enclave identifier is invalid."), + + /** + * The signature is invalid. + */ + ERROR_INVALID_SIGNATURE(0x2003, "The signature is invalid"), + + /** + * The enclave is signed as product enclave, and can not be created as debuggable enclave. + */ + ERROR_NDEBUG_ENCLAVE(0x2004, "The enclave is signed as product enclave, and can not be created as debuggable enclave"), + + /** + * Not enough EPC is available to load the enclave. + */ + ERROR_OUT_OF_EPC(0x2005, "Not enough EPC is available to load the enclave"), + + /** + * Cannot open SGX device. + */ + ERROR_NO_DEVICE(0x2006, "Cannot open SGX device"), + + /** + * Page mapping failed in driver. + */ + ERROR_MEMORY_MAP_CONFLICT(0x2007, "Page mapping failed in driver"), + + /** + * The metadata is incorrect. + */ + ERROR_INVALID_METADATA(0x2009, "The metadata is incorrect"), + + /** + * Device is busy, mostly EINIT failed. + */ + ERROR_DEVICE_BUSY(0x200c, "Device is busy, mostly EINIT failed"), + + /** + * Metadata version is inconsistent between uRTS and sgx_sign, or uRTS is incompatible with current platform. + */ + ERROR_INVALID_VERSION(0x200d, "Metadata version is inconsistent between uRTS and sgx_sign, or uRTS is incompatible with current platform"), + + /** + * The target enclave 32/64 bit mode or SIM/HW mode is incompatible with the mode of current uRTS. + */ + ERROR_MODE_INCOMPATIBLE(0x200e, "The target enclave 32/64 bit mode or SIM/HW mode is incompatible with the mode of current uRTS"), + + /** + * Cannot open enclave file. + */ + ERROR_ENCLAVE_FILE_ACCESS(0x200f, "Cannot open enclave file"), + + /** + * The MiscSelct/MiscMask settings are not correct. + */ + ERROR_INVALID_MISC(0x2010, "The MiscSelct/MiscMask settings are not correct"), + + /** + * Indicates verification error for reports, sealed data, etc. + */ + ERROR_MAC_MISMATCH(0x3001, "Indicates verification error for reports, sealed data, etc"), + + /** + * The enclave is not authorized. + */ + ERROR_INVALID_ATTRIBUTE(0x3002, "The enclave is not authorized"), + + /** + * The CPU SVN is beyond platform's CPU SVN value. + */ + ERROR_INVALID_CPUSVN(0x3003, "The CPU SVN is beyond platform's CPU SVN value"), + + /** + * The ISV SVN is greater than the enclave's ISV SVN. + */ + ERROR_INVALID_ISVSVN(0x3004, "The ISV SVN is greater than the enclave's ISV SVN"), + + /** + * The key name is an unsupported value. + */ + ERROR_INVALID_KEYNAME(0x3005, "The key name is an unsupported value"), + + /** + * Indicates AESM didn't respond or the requested service is not supported. + */ + ERROR_SERVICE_UNAVAILABLE(0x4001, "Indicates AESM didn't respond or the requested service is not supported"), + + /** + * The request to AESM timed out. + */ + ERROR_SERVICE_TIMEOUT(0x4002, "The request to AESM timed out"), + + /** + * Indicates EPID blob verification error. + */ + ERROR_AE_INVALID_EPIDBLOB(0x4003, "Indicates EPID blob verification error"), + + /** + * Enclave has no privilege to get launch token. + */ + ERROR_SERVICE_INVALID_PRIVILEGE(0x4004, "Enclave has no privilege to get launch token"), + + /** + * The EPID group membership is revoked. + */ + ERROR_EPID_MEMBER_REVOKED(0x4005, "The EPID group membership is revoked"), + + /** + * SGX needs to be updated. + */ + ERROR_UPDATE_NEEDED(0x4006, "SGX needs to be updated"), + + /** + * Network connection or proxy settings issue is encountered. + */ + ERROR_NETWORK_FAILURE(0x4007, "Network connection or proxy settings issue is encountered"), + + /** + * Session is invalid or ended by server. + */ + ERROR_AE_SESSION_INVALID(0x4008, "Session is invalid or ended by server"), + + /** + * Requested service is temporarily not available. + */ + ERROR_BUSY(0x400a, "Requested service is temporarily not available"), + + /** + * Monotonic Counter doesn't exist or has been invalidated. + */ + ERROR_MC_NOT_FOUND(0x400c, "Monotonic Counter doesn't exist or has been invalidated"), + + /** + * Caller doesn't have access to specified VMC. + */ + ERROR_MC_NO_ACCESS_RIGHT(0x400d, "Caller doesn't have access to specified VMC"), + + /** + * Monotonic counters are used up. + */ + ERROR_MC_USED_UP(0x400e, "Monotonic counters are used up"), + + /** + * Monotonic counters exceeds quota limitation. + */ + ERROR_MC_OVER_QUOTA(0x400f, "Monotonic counters exceeds quota limitation"), + + /** + * Key derivation function doesn't match during key exchange. + */ + ERROR_KDF_MISMATCH(0x4011, "Key derivation function doesn't match during key exchange"), + + /** + * EPID provisioning failed due to platform not being recognized by backend server. + */ + ERROR_UNRECOGNIZED_PLATFORM(0x4012, "EPID provisioning failed due to platform not being recognized by backend server"), + + /** + * Not privileged to perform this operation. + */ + ERROR_NO_PRIVILEGE(0x5002, "Not privileged to perform this operation"), + + /** + * The file is in a bad state. + */ + ERROR_FILE_BAD_STATUS(0x7001, "The file is in a bad state, run sgx_clearerr to try and fix it"), + + /** + * The KeyID field is all zeros, cannot re-generate the encryption key. + */ + ERROR_FILE_NO_KEY_ID(0x7002, "The KeyID field is all zeros, cannot re-generate the encryption key"), + + /** + * The current file name is different then the original file name (not allowed due to potential substitution attack). + */ + ERROR_FILE_NAME_MISMATCH(0x7003, "The current file name is different then the original file name (not allowed due to potential substitution attack)"), + + /** + * The file is not an SGX file. + */ + ERROR_FILE_NOT_SGX_FILE(0x7004, "The file is not an SGX file"), + + /** + * A recovery file cannot be opened, so flush operation cannot continue (only used when no EXXX is returned). + */ + ERROR_FILE_CANT_OPEN_RECOVERY_FILE(0x7005, "A recovery file cannot be opened, so flush operation cannot continue (only used when no EXXX is returned)"), + + /** + * A recovery file cannot be written, so flush operation cannot continue (only used when no EXXX is returned). + */ + ERROR_FILE_CANT_WRITE_RECOVERY_FILE(0x7006, "A recovery file cannot be written, so flush operation cannot continue (only used when no EXXX is returned)"), + + /** + * When opening the file, recovery is needed, but the recovery process failed. + */ + ERROR_FILE_RECOVERY_NEEDED(0x7007, "When opening the file, recovery is needed, but the recovery process failed"), + + /** + * fflush operation (to disk) failed (only used when no EXXX is returned). + */ + ERROR_FILE_FLUSH_FAILED(0x7008, "fflush operation (to disk) failed (only used when no EXXX is returned)"), + + /** + * fclose operation (to disk) failed (only used when no EXXX is returned). + */ + ERROR_FILE_CLOSE_FAILED(0x7009, "fclose operation (to disk) failed (only used when no EXXX is returned)"), + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SealedSecret.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SealedSecret.kt new file mode 100644 index 0000000000..bd28cc2289 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SealedSecret.kt @@ -0,0 +1,6 @@ +package net.corda.sgx.sealing + +/** + * Representation of a sealed secret. + */ +typealias SealedSecret = ByteArray diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SealingException.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SealingException.kt new file mode 100644 index 0000000000..e64fa4590d --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SealingException.kt @@ -0,0 +1,10 @@ +package net.corda.sgx.sealing + +/** + * Exception raised whenever there is a problem with a sealing operation. + * + * @property status The status or outcome of the operation. + */ +class SealingException( + val status: SealingResult +) : Exception(status.message) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SealingResult.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SealingResult.kt new file mode 100644 index 0000000000..d5213e023b --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SealingResult.kt @@ -0,0 +1,21 @@ +package net.corda.sgx.sealing + +/** + * The outcome of a performed sealing operation. + * + * @property code The underlying status code. + * @property message A human readable representation of the state. + */ +enum class SealingResult(val code: Long, val message: String) { + + /** + * Sealing was successful. + */ + SUCCESS(0, "Sealing was successful."), + + /** + * Sealing was unsuccessful. + */ + FAIL(1, "Failed to seal secret."), + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/Secret.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/Secret.kt new file mode 100644 index 0000000000..a0a1195c43 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/Secret.kt @@ -0,0 +1,6 @@ +package net.corda.sgx.sealing + +/** + * Representation of a provisioned secret. + */ +typealias ProvisionedSecret = ByteArray diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SecretManager.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SecretManager.kt new file mode 100644 index 0000000000..8048535889 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/sealing/SecretManager.kt @@ -0,0 +1,59 @@ +package net.corda.sgx.sealing + +import net.corda.sgx.attestation.AttestationEnclave +import net.corda.sgx.enclave.SgxStatus + +/** + * Manager for storing and managing secrets. + * + * @property enclave The facilitating attestation enclave. + */ +open class SecretManager( + private val enclave: AttestationEnclave +) { + + /** + * Check that an existing secret (if available) is valid and hasn't + * expired. + */ + fun isValid(): Boolean { + // First off, check whether we actually have access to a secret + if (!hasSecret()) { + return false + } + + // Then, ensure that we can unseal the secret, that the lease has not + // expired, etc. + val result = unsealSecret() + return result == SgxStatus.SUCCESS + } + + /** + * Retrieve sealed secret, or null if not available. + */ + open fun getSealedSecret(): SealedSecret? = null + + /** + * Persist the sealed secret to disk or similar, for future use. + * + * @param sealedSecret The secret sealed to the enclave's context. + */ + open fun persistSealedSecret(sealedSecret: SealedSecret) { } + + /** + * Check whether we have a secret persisted already. + */ + private fun hasSecret(): Boolean { + return getSealedSecret() != null + } + + /** + * Check if we can unseal an existing secret. + */ + private fun unsealSecret(): SgxStatus { + val sealedSecret = getSealedSecret() + ?: return SgxStatus.ERROR_INVALID_PARAMETER + return enclave.unseal(sealedSecret) + } + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/ExtendedGroupIdentifier.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/ExtendedGroupIdentifier.kt new file mode 100644 index 0000000000..d036995889 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/ExtendedGroupIdentifier.kt @@ -0,0 +1,15 @@ +package net.corda.sgx.system + +/** + * Extended Intel EPID group identifier. + * + * @property value The identifier for the extended EPID group. + */ +enum class ExtendedGroupIdentifier(val value: Int) { + + /** + * Indicates that we are using Intel's Attestation Service. + */ + INTEL(0), + +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/GroupIdentifier.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/GroupIdentifier.kt new file mode 100644 index 0000000000..7d35c8d2aa --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/GroupIdentifier.kt @@ -0,0 +1,13 @@ +package net.corda.sgx.system + +/** + * The Intel EPID group identifier. + */ +typealias GroupIdentifier = Int + +/** + * Get the string representation of the group identifier. + */ +fun GroupIdentifier.value(): String = Integer + .toHexString(this) + .padStart(8, '0') diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/SgxDeviceStatus.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/SgxDeviceStatus.kt new file mode 100644 index 0000000000..6d7f8fe608 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/SgxDeviceStatus.kt @@ -0,0 +1,62 @@ +package net.corda.sgx.system + +/** + * The status of the SGX device on the current machine. + * + * @property code The native status code returned from the SGX API. + * @property message A human readable representation of the state. + */ +enum class SgxDeviceStatus(val code: Int, val message: String) { + + /** + * The platform is enabled for Intel SGX. + */ + ENABLED(0, "SGX device is available and enabled"), + + /** + * This platform is disabled for Intel SGX. It is configured to be enabled + * after the next reboot. + */ + DISABLED_REBOOT_REQUIRED(1, "Rebooted required"), + + /** + * The operating system does not support UEFI enabling of the Intel SGX + * device. If UEFI is supported by the operating system in general, but + * support for enabling the Intel SGX device does not exist, this function + * returns the more general [DISABLED]. + */ + DISABLED_LEGACY_OS(2, "Operating system with EFI support required"), + + /** + * This platform is disabled for Intel SGX. More details about the ability + * to enable Intel SGX are unavailable. There may be cases when Intel SGX + * can be enabled manually in the BIOS. + */ + DISABLED(3, "SGX device is not available"), + + /** + * The platform is disabled for Intel SGX but can be enabled using the + * Software Control Interface. + */ + DISABLED_SCI_AVAILABLE(4, "Needs enabling using the SCI"), + + /** + * The platform is disabled for Intel SGX but can be enabled manually + * through the BIOS menu. The Software Control Interface is not available + * to enable Intel SGX on this platform. + */ + DISABLED_MANUAL_ENABLE(5, "Needs enabling through the BIOS menu"), + + /** + * The detected version of Windows 10 is incompatible with Hyper-V. Intel + * SGX cannot be enabled on the target machine unless Hyper-V is disabled. + */ + DISABLED_HYPERV_ENABLED(6, "Hyper-V must be disabled"), + + + /** + * Intel SGX is not supported by this processor. + */ + DISABLED_UNSUPPORTED_CPU(7, "SGX not supported by processor"), + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/SgxSystem.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/SgxSystem.kt new file mode 100644 index 0000000000..262d551fd5 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/SgxSystem.kt @@ -0,0 +1,76 @@ +package net.corda.sgx.system + +import net.corda.sgx.attestation.entities.AttestationException +import net.corda.sgx.attestation.entities.QuoteStatus +import net.corda.sgx.enclave.SgxStatus + +/** + * Query system properties of an SGX-enabled environment. + */ +interface SgxSystem { + + companion object { + + /** + * Get [SgxDeviceStatus] from numeric code. + */ + fun deviceStatusFromCode(code: Int): SgxDeviceStatus = + enumValues().first { it.code == code } + + /** + * Get [SgxStatus] from numeric code. + */ + fun statusFromCode(code: Long): SgxStatus = + enumValues().first { it.code == code } + + /** + * Get [ExtendedGroupIdentifier] from a numeric identifier. + */ + fun extendedGroupIdentifier(id: Int): ExtendedGroupIdentifier? = + enumValues(). + firstOrNull { it.value == id } + + /** + * Get [QuoteStatus] from string. + */ + fun quoteStatusFromString( + code: String + ): QuoteStatus { + return enumValues() + .firstOrNull { it.name == code } + ?: throw AttestationException( + "Invalid quote status code '$code'") + } + + } + + /** + * Check if the client platform is enabled for Intel SGX. The application + * must be run with administrator privileges to get the status + * successfully. + * + * @return The current status of the SGX device. + */ + fun getDeviceStatus(): SgxDeviceStatus + + /** + * Get the extended Intel EPID Group the client uses by default. The key + * used to sign a quote will be a member of the this group. + */ + fun getExtendedGroupIdentifier(): ExtendedGroupIdentifier + + /** + * Check if SGX is available and enabled in the current runtime + * environment. + * + * @throws SgxUnavailableException If SGX is unavailable or for some reason + * disabled. + */ + fun ensureAvailable() { + val status = getDeviceStatus() + if (status != SgxDeviceStatus.ENABLED) { + throw SgxUnavailableException(status) + } + } + +} diff --git a/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/SgxUnavailableException.kt b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/SgxUnavailableException.kt new file mode 100644 index 0000000000..fea9b82279 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/kotlin/net/corda/sgx/system/SgxUnavailableException.kt @@ -0,0 +1,10 @@ +package net.corda.sgx.system + +/** + * Exception raised if SGX for some reason is unavailable on the system. + * + * @property status The status of the SGX device. + */ +class SgxUnavailableException( + val status: SgxDeviceStatus +) : Exception(status.message) \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/main/resources/log4j2.xml b/sgx-jvm/remote-attestation/host/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..431d635d46 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/main/resources/log4j2.xml @@ -0,0 +1,50 @@ + + + + + ${sys:attestation.home} + attestation-host + ${sys:log-path}/archive + info + debug + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/EnclaveConfiguration.kt b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/EnclaveConfiguration.kt new file mode 100644 index 0000000000..62c87c2e86 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/EnclaveConfiguration.kt @@ -0,0 +1,12 @@ +package net.corda.sgx.bridge + +object EnclaveConfiguration { + + private val dir: String = System.getProperty("corda.sgx.enclave.path") + + /** + * The path of the signed attestation enclave binary. + */ + val path: String = "$dir/corda_sgx_ra_enclave.so" + +} diff --git a/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/attestation/NativeAttestationEnclaveTests.kt b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/attestation/NativeAttestationEnclaveTests.kt new file mode 100644 index 0000000000..73f9bc405d --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/attestation/NativeAttestationEnclaveTests.kt @@ -0,0 +1,53 @@ +package net.corda.sgx.bridge.attestation + +import net.corda.sgx.bridge.EnclaveConfiguration +import net.corda.sgx.enclave.ECKey +import net.corda.sgx.enclave.ecKeyComponent +import org.junit.Ignore +import org.junit.Test + +@Suppress("KDocMissingDocumentation") +class NativeAttestationEnclaveTests { + + // Dummy key used to initialize the key exchange + private val challengerKey: ECKey = ECKey( + ecKeyComponent( + 0xC0, 0x8C, 0x9F, 0x45, 0x59, 0x1A, 0x9F, 0xAE, + 0xC5, 0x1F, 0xBC, 0x3E, 0xFB, 0x4F, 0x67, 0xB1, + 0x93, 0x61, 0x45, 0x9E, 0x30, 0x27, 0x10, 0xC4, + 0x92, 0x0F, 0xBB, 0xB2, 0x69, 0xB0, 0x16, 0x39 + ), + ecKeyComponent( + 0x5D, 0x98, 0x6B, 0x24, 0x2B, 0x52, 0x46, 0x72, + 0x2A, 0x35, 0xCA, 0xE0, 0xA9, 0x1A, 0x6A, 0xDC, + 0xB8, 0xEB, 0x32, 0xC8, 0x1C, 0x2B, 0x5A, 0xF1, + 0x23, 0x1F, 0x6C, 0x6E, 0x30, 0x00, 0x96, 0x4F + ) + ) + + @Test + fun `can initialize and finalize key exchange without platform services`() { + val enclave = NativeAttestationEnclave(EnclaveConfiguration.path, false) + enclave.activate() + enclave.initializeKeyExchange(challengerKey) + enclave.finalizeKeyExchange() + enclave.destroy() + } + + // TODO Use PSE to protect against replay attacks + // We currently don't leverage the PSE to protect against replay attacks, + // etc., and it seems like the box doesn't communicate properly with it. + // Longer term, we should get this enabled, which will require further + // changes to the enclave code as well. + + @Ignore("Requires platform services") + @Test + fun `can initialize and finalize key exchange with platform services`() { + val enclave = NativeAttestationEnclave(EnclaveConfiguration.path, true) + enclave.activate() + enclave.initializeKeyExchange(challengerKey) + enclave.finalizeKeyExchange() + enclave.destroy() + } + +} diff --git a/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/enclave/NativeEnclaveTests.kt b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/enclave/NativeEnclaveTests.kt new file mode 100644 index 0000000000..9a8bab83ea --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/enclave/NativeEnclaveTests.kt @@ -0,0 +1,92 @@ +package net.corda.sgx.bridge.enclave + +import net.corda.sgx.bridge.EnclaveConfiguration +import net.corda.sgx.bridge.attestation.NativeAttestationEnclave +import net.corda.sgx.enclave.SgxStatus +import org.junit.Assert.* +import org.junit.Test + +@Suppress("KDocMissingDocumentation") +class NativeEnclaveTests { + + @Test + fun `cannot create enclave without valid executable`() { + val enclave = NativeAttestationEnclave("/somewhere/over/the/rainbow") + assertTrue(enclave.isFresh()) + assertEquals(SgxStatus.ERROR_ENCLAVE_FILE_ACCESS, enclave.create()) + } + + @Test + fun `can create enclave without platform services`() { + val enclave = NativeAttestationEnclave(EnclaveConfiguration.path, false) + assertTrue(enclave.isFresh()) + assertEquals(SgxStatus.SUCCESS, enclave.create()) + assertFalse(enclave.isFresh()) + assertTrue(enclave.destroy()) + } + + @Test + fun `can create enclave with platform services`() { + val enclave = NativeAttestationEnclave(EnclaveConfiguration.path, true) + assertTrue(enclave.isFresh()) + assertEquals(SgxStatus.SUCCESS, enclave.create()) + assertFalse(enclave.isFresh()) + assertTrue(enclave.destroy()) + } + + @Test + fun `can destroy existing enclave`() { + val enclave = NativeAttestationEnclave(EnclaveConfiguration.path) + assertEquals(SgxStatus.SUCCESS, enclave.create()) + assertFalse(enclave.isFresh()) + assertTrue(enclave.destroy()) + assertTrue(enclave.isFresh()) + } + + @Test + fun `can detect destruction of non-existent enclave`() { + val enclave = NativeAttestationEnclave(EnclaveConfiguration.path) + assertTrue(enclave.isFresh()) + assertTrue(enclave.destroy()) + } + + @Test + fun `can enter enclave that has already been created`() { + val enclave = NativeAttestationEnclave(EnclaveConfiguration.path) + assertTrue(enclave.isFresh()) + + assertEquals(SgxStatus.SUCCESS, enclave.create()) + assertFalse(enclave.isFresh()) + + assertEquals(SgxStatus.SUCCESS, enclave.create()) + assertFalse(enclave.isFresh()) + + assertTrue(enclave.destroy()) + } + + @Test + fun `can create same enclave twice`() { + val enclaveA = NativeAttestationEnclave(EnclaveConfiguration.path) + assertTrue(enclaveA.isFresh()) + assertEquals(SgxStatus.SUCCESS, enclaveA.create()) + assertFalse(enclaveA.isFresh()) + val identifierA = enclaveA.identifier + + val enclaveB = NativeAttestationEnclave(EnclaveConfiguration.path) + assertTrue(enclaveB.isFresh()) + assertEquals(SgxStatus.SUCCESS, enclaveB.create()) + assertFalse(enclaveB.isFresh()) + val identifierB = enclaveB.identifier + + assertNotEquals(identifierA, identifierB) + assertTrue(enclaveA.destroy()) + assertTrue(enclaveB.destroy()) + } + + @Test + fun `can activate enclave`() { + val enclave = NativeAttestationEnclave(EnclaveConfiguration.path) + enclave.activate() // throws on failure + } + +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/system/NativeSgxSystemTests.kt b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/system/NativeSgxSystemTests.kt new file mode 100644 index 0000000000..2b26b3dda2 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/bridge/system/NativeSgxSystemTests.kt @@ -0,0 +1,24 @@ +package net.corda.sgx.bridge.system + +import net.corda.sgx.system.ExtendedGroupIdentifier +import net.corda.sgx.system.SgxDeviceStatus +import org.junit.Assert.assertEquals +import org.junit.Test + +@Suppress("KDocMissingDocumentation") +class NativeSgxSystemTests { + + @Test + fun `can retrieve device status`() { + val system = NativeSgxSystem() + assertEquals(SgxDeviceStatus.ENABLED, system.getDeviceStatus()) + } + + @Test + fun `can retrieve extended group identifier`() { + val system = NativeSgxSystem() + val identifier = system.getExtendedGroupIdentifier() + assertEquals(ExtendedGroupIdentifier.INTEL, identifier) + } + +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/enclave/ECKeyTests.kt b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/enclave/ECKeyTests.kt new file mode 100644 index 0000000000..5cbb26eab1 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/enclave/ECKeyTests.kt @@ -0,0 +1,24 @@ +package net.corda.sgx.enclave + +import org.junit.Assert.assertArrayEquals +import org.junit.Test + +@Suppress("KDocMissingDocumentation") +class ECKeyTests { + + @Test + fun `can get bytes from EC key instantiated from two empty components`() { + val ecKey = ECKey(byteArrayOf(), byteArrayOf()) + assertArrayEquals(byteArrayOf(), ecKey.bytes) + } + + @Test + fun `can get bytes from EC key instantiated from two components`() { + val ecKey1 = ECKey(byteArrayOf(1), byteArrayOf(2)) + assertArrayEquals(byteArrayOf(1, 2), ecKey1.bytes) + + val ecKey2 = ECKey(byteArrayOf(1, 2, 3, 4), byteArrayOf(5, 6, 7, 8)) + assertArrayEquals(byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8), ecKey2.bytes) + } + +} \ No newline at end of file diff --git a/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/enclave/SgxStatusTests.kt b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/enclave/SgxStatusTests.kt new file mode 100644 index 0000000000..5a5268866b --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/enclave/SgxStatusTests.kt @@ -0,0 +1,26 @@ +package net.corda.sgx.enclave + +import net.corda.sgx.system.SgxSystem +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import kotlin.test.assertFails + +@Suppress("KDocMissingDocumentation") +class SgxStatusTests { + + @Test + fun `can retrieve status from valid integer`() { + assertEquals(SgxStatus.SUCCESS, SgxSystem.statusFromCode(0)) + assertEquals(SgxStatus.ERROR_BUSY, SgxSystem.statusFromCode(0x400a)) + assertEquals(SgxStatus.ERROR_FILE_CLOSE_FAILED, SgxSystem.statusFromCode(0x7009)) + assertNotEquals(SgxStatus.ERROR_AE_INVALID_EPIDBLOB, SgxSystem.statusFromCode(0x4003 - 1)) + } + + @Test + fun `cannot retrieve status from invalid integer`() { + assertFails { SgxSystem.statusFromCode(-1) } + assertFails { SgxSystem.statusFromCode(0xfedc) } + } + +} diff --git a/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/system/ExtendedGroupIdentifierTests.kt b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/system/ExtendedGroupIdentifierTests.kt new file mode 100644 index 0000000000..246452520d --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/system/ExtendedGroupIdentifierTests.kt @@ -0,0 +1,26 @@ +package net.corda.sgx.system + +import org.junit.Assert.assertEquals +import org.junit.Test +import kotlin.test.assertNull + +@Suppress("KDocMissingDocumentation") +class ExtendedGroupIdentifierTests { + + @Test + fun `can retrieve extended group identifier for Intel`() { + assertEquals( + ExtendedGroupIdentifier.INTEL, + SgxSystem.extendedGroupIdentifier(0) + ) + } + + @Test + fun `cannot retrieve other extended group identifiers`() { + assertNull(SgxSystem.extendedGroupIdentifier(-1)) + assertNull(SgxSystem.extendedGroupIdentifier(1)) + assertNull(SgxSystem.extendedGroupIdentifier(2)) + assertNull(SgxSystem.extendedGroupIdentifier(10)) + } + +} diff --git a/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/system/SgxDeviceStatusTests.kt b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/system/SgxDeviceStatusTests.kt new file mode 100644 index 0000000000..8e8a8a4f66 --- /dev/null +++ b/sgx-jvm/remote-attestation/host/src/test/kotlin/net/corda/sgx/system/SgxDeviceStatusTests.kt @@ -0,0 +1,22 @@ +package net.corda.sgx.system + +import org.junit.Assert.assertEquals +import org.junit.Test +import kotlin.test.assertFails + +@Suppress("KDocMissingDocumentation") +class SgxDeviceStatusTests { + + @Test + fun `can retrieve status from valid integer`() { + assertEquals(SgxDeviceStatus.ENABLED, SgxSystem.deviceStatusFromCode(0)) + assertEquals(SgxDeviceStatus.DISABLED, SgxSystem.deviceStatusFromCode(3)) + } + + @Test + fun `cannot retrieve status from invalid integer`() { + assertFails { SgxSystem.deviceStatusFromCode(-1) } + assertFails { SgxSystem.deviceStatusFromCode(0xfedc) } + } + +} diff --git a/sgx-jvm/remote-attestation/settings.gradle b/sgx-jvm/remote-attestation/settings.gradle index 45be9e2c58..ebd2861e7a 100644 --- a/sgx-jvm/remote-attestation/settings.gradle +++ b/sgx-jvm/remote-attestation/settings.gradle @@ -1,2 +1,3 @@ rootProject.name = 'remote-attestation' include 'attestation-server' +include 'host'