0
0
mirror of https://github.com/signalapp/libsignal.git synced 2024-09-20 03:52:17 +02:00

JNI for HSM enclave client.

This commit is contained in:
Graeme Connell 2021-09-21 10:32:17 -06:00 committed by gram-signal
parent a7bf226166
commit 9caa6615b9
14 changed files with 361 additions and 7 deletions

1
Cargo.lock generated
View File

@ -809,6 +809,7 @@ dependencies = [
"async-trait",
"device-transfer",
"futures-util",
"hsm-enclave",
"jni",
"libc",
"libsignal-bridge-macros",

View File

@ -136,6 +136,13 @@ public final class Native {
public static native byte[] HKDF_DeriveSecrets(int outputLength, int version, byte[] ikm, byte[] label, byte[] salt);
public static native void HsmEnclaveClient_CompleteHandshake(long cli, byte[] handshakeReceived);
public static native void HsmEnclaveClient_Destroy(long handle);
public static native byte[] HsmEnclaveClient_EstablishedRecv(long cli, byte[] receivedCiphertext);
public static native byte[] HsmEnclaveClient_EstablishedSend(long cli, byte[] plaintextToSend);
public static native byte[] HsmEnclaveClient_InitialRequest(long cli);
public static native long HsmEnclaveClient_New(byte[] trustedPublicKey, byte[] trustedCodeHashes);
public static native long[] IdentityKeyPair_Deserialize(byte[] data);
public static native byte[] IdentityKeyPair_Serialize(long publicKey, long privateKey);

View File

@ -0,0 +1,6 @@
package org.signal.libsignal.hsmenclave;
public class EnclaveCommunicationFailureException extends Exception {
public EnclaveCommunicationFailureException(String msg) { super(msg); }
public EnclaveCommunicationFailureException(Throwable t) { super(t); }
}

View File

@ -0,0 +1,73 @@
//
// Copyright 2021 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.hsmenclave;
import org.signal.client.internal.Native;
import org.whispersystems.libsignal.InvalidKeyException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
/**
* HsmEnclaveClient provides bindings to interact with Signal's HSM-backed enclave.
*
* Interaction with the enclave is done over a websocket, which is handled by the client. Once the websocket
* has been initiated, the client establishes a connection in the following manner:
*
* <ul>
* <li>send HsmEnclaveClient.initialRequest()</li>
* <li>receive a response and pass to HsmEnclaveClient.completeHandshake()</li>
* </ul>
*
* After a connection has been established, a client may send or receive messages. To send a message, they
* formulate the plaintext, then pass it to HsmEnclaveClient.establishedSend() to get the ciphertext message
* to pass along. When a message is received (as ciphertext), it is passed to HsmEnclaveClient.establishedRecv(),
* which decrypts and verifies it, passing the plaintext back to the client for processing.
*/
public class HsmEnclaveClient {
private long handle;
public HsmEnclaveClient(byte[] public_key, List<byte[]> code_hashes) {
ByteArrayOutputStream concatHashes = new ByteArrayOutputStream();
for (byte[] hash : code_hashes) {
if (hash.length != 32) {
throw new IllegalArgumentException("code hash length must be 32");
}
try {
concatHashes.write(hash);
} catch (IOException e) {
throw new AssertionError("writing to ByteArrayOutputStream failed", e);
}
}
this.handle = Native.HsmEnclaveClient_New(public_key, concatHashes.toByteArray());
}
@Override
protected void finalize() {
Native.HsmEnclaveClient_Destroy(this.handle);
}
/** Initial request to send to HSM enclave, to begin handshake. */
public byte[] initialRequest() {
return Native.HsmEnclaveClient_InitialRequest(this.handle);
}
/** Called by client upon receipt of first message from HSM enclave, to complete handshake. */
public void completeHandshake(byte[] handshakeResponse) {
Native.HsmEnclaveClient_CompleteHandshake(this.handle, handshakeResponse);
}
/** Called by client after completeHandshake has succeeded, to encrypt a message to send. */
public byte[] establishedSend(byte[] plaintextToSend) {
return Native.HsmEnclaveClient_EstablishedSend(this.handle, plaintextToSend);
}
/** Called by client after completeHandshake has succeeded, to decrypt a received message. */
public byte[] establishedRecv(byte[] receivedCiphertext) {
return Native.HsmEnclaveClient_EstablishedRecv(this.handle, receivedCiphertext);
}
}

View File

@ -0,0 +1,6 @@
package org.signal.libsignal.hsmenclave;
public class TrustedCodeMismatchException extends Exception {
public TrustedCodeMismatchException(String msg) { super(msg); }
public TrustedCodeMismatchException(Throwable t) { super(t); }
}

View File

@ -0,0 +1,86 @@
//
// Copyright 2021 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.hsmenclave;
import junit.framework.TestCase;
import java.util.ArrayList;
import java.util.List;
public class HsmEnclaveClientTest extends TestCase {
public void testCreateClient() throws Exception {
byte[] validKey = new byte[32];
List<byte[]> hashes = new ArrayList<>();
hashes.add(new byte[]{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0});
hashes.add(new byte[]{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1});
HsmEnclaveClient hsmEnclaveClient = new HsmEnclaveClient(validKey, hashes);
byte[] initialMessage = hsmEnclaveClient.initialRequest();
assertEquals(112, initialMessage.length);
}
public void testCreateClientFailsWithInvalidPublicKey() {
byte[] invalidKey = new byte[31];
List<byte[]> hashes = new ArrayList<>();
hashes.add(new byte[]{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0});
hashes.add(new byte[]{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1});
try {
new HsmEnclaveClient(invalidKey, hashes);
} catch (IllegalArgumentException e) {
return;
}
fail();
}
public void testCreateClientFailsWithInvalidHash() {
byte[] validKey = new byte[32];
List<byte[]> hashes = new ArrayList<>();
hashes.add(new byte[]{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0});
hashes.add(new byte[]{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0});
try {
new HsmEnclaveClient(validKey, hashes);
} catch (IllegalArgumentException e) {
return;
}
fail();
}
public void testCreateClientFailsWithNoHashes() {
byte[] validKey = new byte[32];
List<byte[]> hashes = new ArrayList<>();
try {
new HsmEnclaveClient(validKey, hashes);
} catch (IllegalArgumentException e) {
return;
}
fail();
}
public void testEstablishedSendFailsPriorToEstablishment() {
byte[] validKey = new byte[32];
List<byte[]> hashes = new ArrayList<>();
hashes.add(new byte[]{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0});
HsmEnclaveClient hsmEnclaveClient = new HsmEnclaveClient(validKey, hashes);
try {
hsmEnclaveClient.establishedSend(new byte[]{1, 2, 3});
} catch (IllegalStateException e) {
return;
}
fail();
}
public void testEstablishedRecvFailsPriorToEstablishment() {
byte[] validKey = new byte[32];
List<byte[]> hashes = new ArrayList<>();
hashes.add(new byte[]{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0});
HsmEnclaveClient hsmEnclaveClient = new HsmEnclaveClient(validKey, hashes);
try {
hsmEnclaveClient.establishedRecv(new byte[]{1, 2, 3});
} catch (IllegalStateException e) {
return;
}
fail();
}
}

View File

@ -14,6 +14,7 @@ license = "AGPL-3.0-only"
libsignal-protocol = { path = "../../protocol" }
signal-crypto = { path = "../../crypto" }
device-transfer = { path = "../../device-transfer" }
hsm-enclave = { path = "../../hsm-enclave" }
libsignal-bridge-macros = { path = "macros" }
aes-gcm-siv = "0.10.1"
futures-util = "0.3.7"

View File

@ -0,0 +1,113 @@
//
// Copyright 2021 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use ::hsm_enclave;
use libsignal_bridge_macros::*;
use crate::support::*;
use crate::*;
use hsm_enclave::Result;
pub enum HsmEnclaveClient {
ConnectionEstablishment(hsm_enclave::ClientConnectionEstablishment),
Connection(hsm_enclave::ClientConnection),
InvalidConnectionState,
}
impl HsmEnclaveClient {
pub fn new(trusted_public_key: &[u8], trusted_code_hashes: &[u8]) -> Result<Self> {
if trusted_public_key.len() != hsm_enclave::PUB_KEY_SIZE {
return Err(hsm_enclave::Error::InvalidPublicKeyError);
}
if trusted_code_hashes.is_empty()
|| trusted_code_hashes.len() % hsm_enclave::CODE_HASH_SIZE != 0
{
return Err(hsm_enclave::Error::InvalidCodeHashError);
}
let mut pubkey = [0u8; hsm_enclave::PUB_KEY_SIZE];
pubkey.copy_from_slice(trusted_public_key);
let mut hashes: Vec<[u8; hsm_enclave::CODE_HASH_SIZE]> = Vec::new();
for code_hash in trusted_code_hashes.chunks(hsm_enclave::CODE_HASH_SIZE) {
let mut hash = [0u8; hsm_enclave::CODE_HASH_SIZE];
hash.copy_from_slice(code_hash);
hashes.push(hash);
}
Ok(HsmEnclaveClient::ConnectionEstablishment(
hsm_enclave::ClientConnectionEstablishment::new(pubkey, hashes)?,
))
}
pub fn initial_request(&self) -> Result<&[u8]> {
match self {
HsmEnclaveClient::ConnectionEstablishment(c) => Ok(c.initial_request()),
_ => Err(hsm_enclave::Error::InvalidBridgeStateError),
}
}
pub fn complete_handshake(&mut self, handshake_received: &[u8]) -> Result<()> {
match std::mem::replace(self, HsmEnclaveClient::InvalidConnectionState) {
HsmEnclaveClient::ConnectionEstablishment(c) => {
*self = HsmEnclaveClient::Connection(c.complete(handshake_received)?);
Ok(())
}
_ => Err(hsm_enclave::Error::InvalidBridgeStateError),
}
}
pub fn established_send(&mut self, plaintext_to_send: &[u8]) -> Result<Vec<u8>> {
match self {
HsmEnclaveClient::Connection(c) => c.send(plaintext_to_send),
_ => Err(hsm_enclave::Error::InvalidBridgeStateError),
}
}
pub fn established_recv(&mut self, received_ciphertext: &[u8]) -> Result<Vec<u8>> {
match self {
HsmEnclaveClient::Connection(c) => c.recv(received_ciphertext),
_ => Err(hsm_enclave::Error::InvalidBridgeStateError),
}
}
}
bridge_handle!(HsmEnclaveClient, mut = true, ffi = false, node = false);
#[bridge_fn(node = false, ffi = false)]
fn HsmEnclaveClient_New(
trusted_public_key: &[u8],
trusted_code_hashes: &[u8],
) -> Result<HsmEnclaveClient> {
HsmEnclaveClient::new(trusted_public_key, trusted_code_hashes)
}
#[bridge_fn_buffer(node = false, ffi = false)]
fn HsmEnclaveClient_InitialRequest<T: Env>(env: T, cli: &HsmEnclaveClient) -> Result<T::Buffer> {
Ok(env.buffer(cli.initial_request()?))
}
#[bridge_fn_void(node = false, ffi = false)]
fn HsmEnclaveClient_CompleteHandshake(
cli: &mut HsmEnclaveClient,
handshake_received: &[u8],
) -> Result<()> {
cli.complete_handshake(handshake_received)
}
#[bridge_fn_buffer(node = false, ffi = false)]
fn HsmEnclaveClient_EstablishedSend<T: Env>(
env: T,
cli: &mut HsmEnclaveClient,
plaintext_to_send: &[u8],
) -> Result<T::Buffer> {
Ok(env.buffer(cli.established_send(plaintext_to_send)?))
}
#[bridge_fn_buffer(node = false, ffi = false)]
fn HsmEnclaveClient_EstablishedRecv<T: Env>(
env: T,
cli: &mut HsmEnclaveClient,
received_ciphertext: &[u8],
) -> Result<T::Buffer> {
Ok(env.buffer(cli.established_recv(received_ciphertext)?))
}

View File

@ -567,6 +567,16 @@ impl<T: ResultTypeInfo> ResultTypeInfo for Result<T, device_transfer::Error> {
}
}
impl<T: ResultTypeInfo> ResultTypeInfo for Result<T, hsm_enclave::Error> {
type ResultType = T::ResultType;
fn convert_into(self, env: &JNIEnv) -> SignalJniResult<Self::ResultType> {
T::convert_into(self?, env)
}
fn convert_into_jobject(signal_jni_result: &SignalJniResult<Self::ResultType>) -> JObject {
<T as ResultTypeInfo>::convert_into_jobject(signal_jni_result)
}
}
impl<T: ResultTypeInfo> ResultTypeInfo for Result<T, signal_crypto::Error> {
type ResultType = T::ResultType;
fn convert_into(self, env: &JNIEnv) -> SignalJniResult<Self::ResultType> {

View File

@ -8,6 +8,7 @@ use jni::{JNIEnv, JavaVM};
use std::fmt;
use device_transfer::Error as DeviceTransferError;
use hsm_enclave::Error as HsmEnclaveError;
use libsignal_protocol::*;
use signal_crypto::Error as SignalCryptoError;
@ -24,6 +25,7 @@ pub enum SignalJniError {
UnexpectedJniResultType(&'static str, &'static str),
NullHandle,
IntegerOverflow(String),
HsmEnclave(HsmEnclaveError),
UnexpectedPanic(std::boxed::Box<dyn std::any::Any + std::marker::Send>),
}
@ -42,6 +44,9 @@ impl fmt::Display for SignalJniError {
SignalJniError::IntegerOverflow(m) => {
write!(f, "integer overflow during conversion of {}", m)
}
SignalJniError::HsmEnclave(e) => {
write!(f, "{}", e)
}
SignalJniError::UnexpectedPanic(e) => match e.downcast_ref::<&'static str>() {
Some(s) => write!(f, "unexpected panic: {}", s),
None => write!(f, "unknown unexpected panic"),
@ -62,6 +67,12 @@ impl From<DeviceTransferError> for SignalJniError {
}
}
impl From<HsmEnclaveError> for SignalJniError {
fn from(e: HsmEnclaveError) -> SignalJniError {
SignalJniError::HsmEnclave(e)
}
}
impl From<SignalCryptoError> for SignalJniError {
fn from(e: SignalCryptoError) -> SignalJniError {
SignalJniError::SignalCrypto(e)

View File

@ -9,6 +9,7 @@ use jni::objects::{JThrowable, JValue};
use jni::sys::jobject;
use device_transfer::Error as DeviceTransferError;
use hsm_enclave::Error as HsmEnclaveError;
use libsignal_protocol::*;
use signal_crypto::Error as SignalCryptoError;
use std::convert::{TryFrom, TryInto};
@ -293,6 +294,20 @@ fn throw_error(env: &JNIEnv, error: SignalJniError) {
| SignalJniError::UnexpectedJniResultType(_, _) => {
unreachable!("already handled in prior match")
}
SignalJniError::HsmEnclave(HsmEnclaveError::HSMCommunicationError(_)) => {
"org/signal/libsignal/hsmenclave/EnclaveCommunicationFailureException"
}
SignalJniError::HsmEnclave(HsmEnclaveError::TrustedCodeError) => {
"org/signal/libsignal/hsmenclave/TrustedCodeMismatchException"
}
SignalJniError::HsmEnclave(HsmEnclaveError::InvalidPublicKeyError)
| SignalJniError::HsmEnclave(HsmEnclaveError::InvalidCodeHashError) => {
"java/lang/IllegalArgumentException"
}
SignalJniError::HsmEnclave(HsmEnclaveError::InvalidBridgeStateError) => {
"java/lang/IllegalStateException"
}
};
if let Err(e) = env.throw_new(exception_type, error.to_string()) {

View File

@ -30,3 +30,6 @@ pub mod protocol;
// Desktop does not make use of device transfer certificates
#[cfg(any(feature = "jni", feature = "ffi"))]
pub mod device_transfer;
#[cfg(any(feature = "jni"))]
pub mod hsm_enclave;

View File

@ -13,15 +13,24 @@ use std::fmt;
mod snow_resolver;
/// Error types for device transfer.
/// Error types for HSM enclave.
#[derive(Debug)]
pub enum Error {
/// Failure to connect to a trusted HSM.
HSMCommunicationError(snow::Error),
/// Failure to connect to trusted code on the given HSM.
TrustedCodeError,
/// Invalid public key provided (used in bridging)
InvalidPublicKeyError,
/// Invalid code hash provided (used in bridging)
InvalidCodeHashError,
/// Invalid state of wrapper (used in bridging)
InvalidBridgeStateError,
}
/// Result type for HSM enclave.
pub type Result<T> = std::result::Result<T, Error>;
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
@ -29,6 +38,19 @@ impl fmt::Display for Error {
Error::TrustedCodeError => {
write!(f, "Trusted HSM process does not match trusted code hash")
}
Error::InvalidPublicKeyError => {
write!(f, "Invalid public key, must be {} bytes", PUB_KEY_SIZE)
}
Error::InvalidCodeHashError => {
write!(
f,
"Invalid code hashes, must be >0 hashes, each exactly {} bytes",
CODE_HASH_SIZE
)
}
Error::InvalidBridgeStateError => {
write!(f, "Invalid bridge state")
}
}
}
}
@ -67,7 +89,7 @@ impl ClientConnectionEstablishment {
pub fn new(
trusted_public_key: [u8; PUB_KEY_SIZE],
trusted_code_hashes: Vec<[u8; CODE_HASH_SIZE]>,
) -> Result<Self, Error> {
) -> Result<Self> {
let mut hs = snow::Builder::with_resolver(
NOISE_PATTERN.parse().expect("valid"),
Box::new(snow_resolver::Resolver),
@ -91,7 +113,7 @@ impl ClientConnectionEstablishment {
}
/// Completes client connection initiation, returns a valid client connection.
pub fn complete(mut self, initial_received: &[u8]) -> Result<ClientConnection, Error> {
pub fn complete(mut self, initial_received: &[u8]) -> Result<ClientConnection> {
let mut received_hash = [0u8; CODE_HASH_SIZE];
let size = self.hs.read_message(initial_received, &mut received_hash)?;
if size != received_hash.len() {
@ -130,7 +152,7 @@ const NOISE_TRANSPORT_PER_PAYLOAD_MAX: usize =
impl ClientConnection {
/// Wrap a plaintext message to be sent, returning the ciphertext.
pub fn send(&mut self, plaintext_to_send: &[u8]) -> Result<Vec<u8>, Error> {
pub fn send(&mut self, plaintext_to_send: &[u8]) -> Result<Vec<u8>> {
let max_ciphertext_size = plaintext_to_send.len()
+ (1 + plaintext_to_send.len() / NOISE_TRANSPORT_PER_PAYLOAD_MAX)
* NOISE_HANDSHAKE_OVERHEAD;
@ -146,7 +168,7 @@ impl ClientConnection {
}
/// Unwrap a ciphertext message that's been received, returning the plaintext.
pub fn recv(&mut self, received_ciphertext: &[u8]) -> Result<Vec<u8>, Error> {
pub fn recv(&mut self, received_ciphertext: &[u8]) -> Result<Vec<u8>> {
let mut received_plaintext: Vec<u8> = vec![0u8; received_ciphertext.len()];
let mut total_size = 0;
for chunk in received_ciphertext.chunks(NOISE_TRANSPORT_PER_PACKET_MAX) {

View File

@ -7,7 +7,7 @@ use hsm_enclave::*;
const NOISE_PATTERN: &str = "Noise_NK_25519_AESGCM_SHA256";
#[test]
fn test_hsm_enclave_happy_path() -> Result<(), Error> {
fn test_hsm_enclave_happy_path() -> Result<()> {
// Spin up a handshake for the server-side.
let keypair = snow::Builder::new(NOISE_PATTERN.parse()?).generate_keypair()?;
let mut server_hs = snow::Builder::new(NOISE_PATTERN.parse()?)
@ -53,7 +53,7 @@ fn test_hsm_enclave_happy_path() -> Result<(), Error> {
}
#[test]
fn test_hsm_enclave_codehash_mismatch() -> Result<(), Error> {
fn test_hsm_enclave_codehash_mismatch() -> Result<()> {
// Spin up a handshake for the server-side.
let keypair = snow::Builder::new(NOISE_PATTERN.parse()?).generate_keypair()?;
let mut server_hs = snow::Builder::new(NOISE_PATTERN.parse()?)