commit: | 4577ac87b11fe20fc8b932836d23c8fe9b807e16 |
author: | Trevor Bentley |
committer: | Trevor Bentley |
date: | Sun May 19 21:46:05 2019 +0200 |
parents: | 7e295717f41604aa6d81618db517ae65cbacb404 |
diff --git a/benches/basic.rs b/benches/basic.rs line changes: +24/-5 index f1f363d..3626615 --- a/benches/basic.rs +++ b/benches/basic.rs
@@ -13,7 +13,14 @@ fn bench_test(b: &mut Bencher) { let server_thread = thread::spawn(move || { let listener = TcpListener::bind("127.0.0.1:9987").unwrap(); let mut server_stream = listener.incoming().next().unwrap().unwrap(); - let mut server_conn = OssuaryConnection::new(ConnectionType::UnauthenticatedServer); + let auth_secret_key = &[ + 0x50, 0x29, 0x04, 0x97, 0x62, 0xbd, 0xa6, 0x07, + 0x71, 0xca, 0x29, 0x14, 0xe3, 0x83, 0x19, 0x0e, + 0xa0, 0x9e, 0xd4, 0xb7, 0x1a, 0xf9, 0xc9, 0x59, + 0x3e, 0xa3, 0x1c, 0x85, 0x0f, 0xc4, 0xfa, 0xa2, + ]; + let mut server_conn = OssuaryConnection::new(ConnectionType::UnauthenticatedServer, + Some(auth_secret_key)).unwrap(); while server_conn.handshake_done().unwrap() == false { if server_conn.send_handshake(&mut server_stream).is_ok() { loop {
@@ -36,7 +43,7 @@ fn bench_test(b: &mut Bencher) { Ok((read, _written)) => bytes += read as u64, Err(OssuaryError::WouldBlock(_)) => continue, Err(e) => { - println!("err: {:?}", e); + println!("server recv_data err: {:?}", e); panic!("Recv failed") }, }
@@ -46,7 +53,9 @@ fn bench_test(b: &mut Bencher) { let t = dur.as_secs() as f64 + dur.subsec_nanos() as f64 * 1e-9; println!("Benchmark done (recv): {} bytes in {:.2} s", bytes, t); - println!("{:.2} MB/s", bytes as f64 / 1024.0 / 1024.0 / t); + println!("{:.2} MB/s [{:.2} Mbps]", + bytes as f64 / 1024.0 / 1024.0 / t, + bytes as f64 * 8.0 / 1024.0 / 1024.0 / t); } break; }
@@ -57,7 +66,15 @@ fn bench_test(b: &mut Bencher) { std::thread::sleep(std::time::Duration::from_millis(500)); let mut client_stream = TcpStream::connect("127.0.0.1:9987").unwrap(); client_stream.set_nonblocking(true).unwrap(); - let mut client_conn = OssuaryConnection::new(ConnectionType::Client); + let mut client_conn = OssuaryConnection::new(ConnectionType::Client, None).unwrap(); + let auth_public_key = &[ + 0x20, 0x88, 0x55, 0x8e, 0xbd, 0x9b, 0x46, 0x1d, + 0xd0, 0x9d, 0xf0, 0x00, 0xda, 0xf4, 0x0f, 0x87, + 0xf7, 0x38, 0x40, 0xc5, 0x54, 0x18, 0x57, 0x60, + 0x74, 0x39, 0x3b, 0xb9, 0x70, 0xe1, 0x46, 0x98, + ]; + let keys: Vec<&[u8]> = vec![auth_public_key]; + let _ = client_conn.add_authorized_keys(keys).unwrap(); while client_conn.handshake_done().unwrap() == false { if client_conn.send_handshake(&mut client_stream).is_ok() { loop {
@@ -89,7 +106,9 @@ fn bench_test(b: &mut Bencher) { let t = dur.as_secs() as f64 + dur.subsec_nanos() as f64 * 1e-9; println!("Benchmark done (xmit): {} bytes in {:.2} s", bytes, t); - println!("{:.2} MB/s", bytes as f64 / 1024.0 / 1024.0 / t); + println!("{:.2} MB/s [{:.2} Mbps]", + bytes as f64 / 1024.0 / 1024.0 / t, + bytes as f64 * 8.0 / 1024.0 / 1024.0 / t); } let mut plaintext: &[u8] = &[0xde, 0xde, 0xbe, 0xbe]; loop {
diff --git a/examples/ffi.c b/examples/ffi.c line changes: +10/-3 index 3f84eff..782ba01 --- a/examples/ffi.c +++ b/examples/ffi.c
@@ -30,12 +30,13 @@ int main(int argc, char **argv) { uint16_t client_bytes, server_bytes, bytes, out_len; OssuaryConnection *client_conn = NULL; OssuaryConnection *server_conn = NULL; + uint8_t remote_key[32]; - client_conn = ossuary_create_connection(CONN_TYPE_CLIENT); + client_conn = ossuary_create_connection(OSSUARY_CONN_TYPE_CLIENT, NULL); ossuary_set_secret_key(client_conn, secret_key); - server_conn = ossuary_create_connection(CONN_TYPE_AUTHENTICATED_SERVER); - ossuary_set_authorized_keys(server_conn, authorized_keys, 1); + server_conn = ossuary_create_connection(OSSUARY_CONN_TYPE_AUTHENTICATED_SERVER, NULL); + ossuary_add_authorized_keys(server_conn, authorized_keys, 1); memset(client_buf, 0, sizeof(client_buf)); memset(server_buf, 0, sizeof(server_buf));
@@ -47,6 +48,12 @@ int main(int argc, char **argv) { server_done = ossuary_handshake_done(server_conn); printf("done: %d %d\n", client_done, server_done); + // Trust-On-First-Use + if (client_done == OSSUARY_ERR_UNTRUSTED_SERVER) { + ossuary_remote_public_key(client_conn, remote_key, sizeof(remote_key)); + ossuary_add_authorized_key(client_conn, remote_key); + } + if (!client_done) { client_bytes = sizeof(client_buf); ossuary_send_handshake(client_conn, client_buf, &client_bytes);
diff --git a/examples/generate_keypair.rs b/examples/generate_keypair.rs line changes: +21/-0 index 0000000..ebe6633 --- /dev/null +++ b/examples/generate_keypair.rs
@@ -0,0 +1,21 @@ +use ossuary; + +fn main() { + let (secret, public) = ossuary::generate_auth_keypair().unwrap(); + print!("let auth_secret_key = &["); + for (idx,byte) in secret.iter().enumerate() { + if idx % 8 == 0 { + print!("\n "); + } + print!("0x{:02x}, ", byte); + } + println!("\n];"); + print!("let auth_public_key = &["); + for (idx,byte) in public.iter().enumerate() { + if idx % 8 == 0 { + print!("\n "); + } + print!("0x{:02x}, ", byte); + } + println!("\n];"); +}
diff --git a/ffi/ossuary.h b/ffi/ossuary.h line changes: +16/-7 index 06a489c..f1ee180 --- a/ffi/ossuary.h +++ b/ffi/ossuary.h
@@ -5,15 +5,24 @@ typedef struct OssuaryConnection OssuaryConnection; typedef enum { - CONN_TYPE_CLIENT = 0x00, - CONN_TYPE_AUTHENTICATED_SERVER = 0x01, - CONN_TYPE_UNAUTHENTICATED_SERVER = 0x02, -} connection_type_t; + OSSUARY_CONN_TYPE_CLIENT = 0x00, + OSSUARY_CONN_TYPE_AUTHENTICATED_SERVER = 0x01, + OSSUARY_CONN_TYPE_UNAUTHENTICATED_SERVER = 0x02, +} ossuary_conn_type_t; -OssuaryConnection *ossuary_create_connection(connection_type_t type); +typedef enum { + OSSUARY_ERR_OTHER = -1, + OSSUARY_ERR_WOULDBLOCK = -64, + OSSUARY_ERR_UNTRUSTED_SERVER = -65, +} ossuary_error_t; + +OssuaryConnection *ossuary_create_connection(ossuary_conn_type_t type, const uint8_t auth_key[32]); int32_t ossuary_destroy_connection(OssuaryConnection **conn); -int32_t ossuary_set_secret_key(OssuaryConnection *conn, uint8_t key[32]); -int32_t ossuary_set_authorized_keys(OssuaryConnection *conn, uint8_t *key[], uint8_t count); +int32_t ossuary_set_secret_key(OssuaryConnection *conn, const uint8_t key[32]); +int32_t ossuary_add_authorized_key(OssuaryConnection *conn, const uint8_t key[32]); +int32_t ossuary_add_authorized_keys(OssuaryConnection *conn, uint8_t *key[], uint8_t count); +int32_t ossuary_remote_public_key(OssuaryConnection *conn, + uint8_t *key_buf, uint16_t key_buf_len); int32_t ossuary_recv_handshake(OssuaryConnection *conn, uint8_t *in_buf, uint16_t *in_buf_len); int32_t ossuary_send_handshake(OssuaryConnection *conn,
diff --git a/src/clib.rs b/src/clib.rs line changes: +62/-13 index b72b3d9..787e76f --- a/src/clib.rs +++ b/src/clib.rs
@@ -1,16 +1,25 @@ -use crate::{OssuaryConnection, ConnectionType, OssuaryError}; +use crate::{OssuaryConnection, ConnectionType, OssuaryError, KEY_LEN}; -const ERROR_WOULD_BLOCK: i32 = -64; +pub const OSSUARY_ERR_WOULD_BLOCK: i32 = -64; +pub const OSSUARY_ERR_UNTRUSTED_SERVER: i32 = -65; #[no_mangle] -pub extern "C" fn ossuary_create_connection(conn_type: u8) -> *mut OssuaryConnection { +pub extern "C" fn ossuary_create_connection(conn_type: u8, auth_key: *const u8) -> *mut OssuaryConnection { let conn_type: ConnectionType = match conn_type { 0 => ConnectionType::Client, 1 => ConnectionType::AuthenticatedServer, 2 => ConnectionType::UnauthenticatedServer, _ => { return ::std::ptr::null_mut(); } }; - let mut conn = Box::new(OssuaryConnection::new(conn_type)); + let key: Option<&[u8]> = match auth_key.is_null() { + false => unsafe { Some(std::slice::from_raw_parts(auth_key, 32)) }, + true => None, + }; + let conn = match OssuaryConnection::new(conn_type, key) { + Ok(c) => c, + Err(_e) => return ::std::ptr::null_mut(), + }; + let mut conn = Box::new(conn); let ptr: *mut _ = &mut *conn; ::std::mem::forget(conn); ptr
@@ -27,7 +36,25 @@ pub extern "C" fn ossuary_destroy_connection(conn: &mut *mut OssuaryConnection) } #[no_mangle] -pub extern "C" fn ossuary_set_authorized_keys(conn: *mut OssuaryConnection, +pub extern "C" fn ossuary_add_authorized_key(conn: *mut OssuaryConnection, + key_buf: *const u8) -> i32 { + if conn.is_null() || key_buf.is_null() { + return -1i32; + } + let conn = unsafe { &mut *conn }; + let r_key_buf: &[u8] = unsafe { + std::slice::from_raw_parts(key_buf, KEY_LEN) + }; + let added = match conn.add_authorized_key(r_key_buf) { + Ok(_) => true, + Err(_) => false, + }; + ::std::mem::forget(conn); + added as i32 +} + +#[no_mangle] +pub extern "C" fn ossuary_add_authorized_keys(conn: *mut OssuaryConnection, keys: *const *const u8, key_count: u8) -> i32 { if conn.is_null() || keys.is_null() {
@@ -42,7 +69,7 @@ pub extern "C" fn ossuary_set_authorized_keys(conn: *mut OssuaryConnection, r_keys.push(key); } } - let written = match conn.set_authorized_keys(r_keys) { + let written = match conn.add_authorized_keys(r_keys) { Ok(c) => c as i32, Err(_) => -1i32, };
@@ -83,7 +110,7 @@ pub extern "C" fn ossuary_recv_handshake(conn: *mut OssuaryConnection, }, Err(OssuaryError::WouldBlock(b)) => { unsafe { *in_buf_len = b as u16; } - ERROR_WOULD_BLOCK + OSSUARY_ERR_WOULD_BLOCK }, _ => -1i32, };
@@ -108,7 +135,7 @@ pub extern "C" fn ossuary_send_handshake(conn: *mut OssuaryConnection, }, Err(OssuaryError::WouldBlock(w)) => { unsafe { *out_buf_len = w as u16 }; - ERROR_WOULD_BLOCK + OSSUARY_ERR_WOULD_BLOCK }, Err(_) => -1, };
@@ -117,15 +144,16 @@ pub extern "C" fn ossuary_send_handshake(conn: *mut OssuaryConnection, } #[no_mangle] -pub extern "C" fn ossuary_handshake_done(conn: *const OssuaryConnection) -> i32 { +pub extern "C" fn ossuary_handshake_done(conn: *mut OssuaryConnection) -> i32 { if conn.is_null() { return -1i32; } - let conn = unsafe { &*conn }; + let conn = unsafe { &mut *conn }; let done = conn.handshake_done(); ::std::mem::forget(conn); match done { Ok(done) => done as i32, + Err(OssuaryError::UntrustedServer(_)) => OSSUARY_ERR_UNTRUSTED_SERVER, Err(_) => -1i32, } }
@@ -152,7 +180,7 @@ pub extern "C" fn ossuary_send_data(conn: *mut OssuaryConnection, }, Err(OssuaryError::WouldBlock(w)) => { unsafe { *out_buf_len = w as u16; } - ERROR_WOULD_BLOCK + OSSUARY_ERR_WOULD_BLOCK }, Err(_) => -1i32, };
@@ -187,7 +215,7 @@ pub extern "C" fn ossuary_recv_data(conn: *mut OssuaryConnection, unsafe { *out_buf_len = w as u16; }; - ERROR_WOULD_BLOCK + OSSUARY_ERR_WOULD_BLOCK }, Err(_) => -1i32, };
@@ -208,9 +236,30 @@ pub extern "C" fn ossuary_flush(conn: *mut OssuaryConnection, let mut out_slice = r_out_buf; let bytes_written = match conn.flush(&mut out_slice) { Ok(x) => x as i32, - Err(OssuaryError::WouldBlock(_)) => ERROR_WOULD_BLOCK, + Err(OssuaryError::WouldBlock(_)) => OSSUARY_ERR_WOULD_BLOCK, Err(_) => -1i32, }; ::std::mem::forget(conn); bytes_written } + +#[no_mangle] +pub extern "C" fn ossuary_remote_public_key(conn: *mut OssuaryConnection, + key_buf: *mut u8, key_buf_len: u16) -> i32 { + if conn.is_null() || key_buf.is_null() || key_buf_len < KEY_LEN as u16 { + return -1i32; + } + let conn = unsafe { &mut *conn }; + let r_key_buf: &mut [u8] = unsafe { + std::slice::from_raw_parts_mut(key_buf, KEY_LEN) + }; + let found = match conn.remote_public_key() { + Ok(key) => { + r_key_buf.copy_from_slice(key); + true + }, + Err(_) => false, + }; + ::std::mem::forget(conn); + found as i32 +}
diff --git a/src/comm.rs b/src/comm.rs line changes: +1/-1 index 6d49b1a..ec8186f --- a/src/comm.rs +++ b/src/comm.rs
@@ -147,6 +147,7 @@ impl OssuaryConnection { return Err(OssuaryError::InvalidKey);; } }; + increment_nonce(&mut self.local_key.nonce); let pkt: EncryptedPacket = EncryptedPacket { tag_len: tag.len() as u16,
@@ -158,7 +159,6 @@ impl OssuaryConnection { buf.extend(&tag); let written = write_packet(self, &mut out_buf, &buf, PacketType::EncryptedData)?; - increment_nonce(&mut self.local_key.nonce); Ok(written) }
diff --git a/src/connection.rs b/src/connection.rs line changes: +108/-33 index 210de68..7e4b929 --- a/src/connection.rs +++ b/src/connection.rs
@@ -9,7 +9,11 @@ impl OssuaryConnection { /// /// `conn_type` is a [`ConnectionType`] indicating whether this instance /// is for a client or server. - pub fn new(conn_type: ConnectionType) -> OssuaryConnection { + /// + /// `auth_secret_key` is the secret portion of the long-term Ed25519 key + /// used for host authentication. If `None` is provided, a keypair will + /// be generated for the lifetime of this connection object. + pub fn new(conn_type: ConnectionType, auth_secret_key: Option<&[u8]>) -> Result<OssuaryConnection, OssuaryError> { //let mut rng = thread_rng(); let mut rng = OsRng::new().expect("RNG not available."); let sec_key = EphemeralSecret::new(&mut rng);
@@ -26,13 +30,38 @@ impl OssuaryConnection { nonce: nonce, session: None, }; + let (auth_sec, auth_pub) = match auth_secret_key { + Some(s) => { + // Use the given secret key + if s.len() != 32 { + return Err(OssuaryError::KeySize(32, s.len())); + } + let secret = SecretKey::from_bytes(s)?; + let public = PublicKey::from(&secret); + (Some(secret), Some(public)) + } + None => { + match conn_type { + // Allow no auth key for clients + ConnectionType::Client => (None, None), + // Generate a random auth key for servers, if not provided + _ => { + let mut sec: [u8; KEY_LEN] = [0u8; KEY_LEN]; + rng.fill_bytes(&mut sec); + let secret = SecretKey::from_bytes(&sec)?; + let public = PublicKey::from(&secret); + (Some(secret), Some(public)) + } + } + }, + }; let auth = AuthKeyMaterial { challenge: Some(challenge), signature: None, - public_key: None, - secret_key: None, + secret_key: auth_sec, + public_key: auth_pub, }; - OssuaryConnection { + Ok(OssuaryConnection { state: match conn_type { ConnectionType::Client => ConnectionState::ClientSendHandshake, _ => ConnectionState::ServerWaitHandshake(std::time::SystemTime::now()),
@@ -41,7 +70,7 @@ impl OssuaryConnection { local_key: key, local_auth: auth, ..Default::default() - } + }) } /// Reset the context back to its default state.
@@ -58,8 +87,15 @@ impl OssuaryConnection { /// connection, such as when the client's key is not authorized. /// pub(crate) fn reset_state(&mut self, permanent_err: Option<OssuaryError>) { - let default = OssuaryConnection::new(self.conn_type.clone()); - *self = default; + self.local_key = Default::default(); + self.remote_key = None; + self.local_msg_id = 0; + self.remote_msg_id = 0; + self.remote_auth = Default::default(); + self.read_buf = [0u8; PACKET_BUF_SIZE]; + self.read_buf_used = 0; + self.write_buf = [0u8; PACKET_BUF_SIZE]; + self.write_buf_used = 0; self.state = match permanent_err { None => { match self.conn_type {
@@ -93,37 +129,67 @@ impl OssuaryConnection { self.local_key.session = Some(secret.diffie_hellman(&EphemeralPublic::from(*public))); } } - /// Add public keys of clients permitted to connect to this server. + + /// Add public key of permitted remote hosts + /// + /// During the handshake, both hosts will be required to sign a challenge + /// with their secret authentication key. The host sends both the signature + /// and the public key it signed with. The other side validates the + /// signature, and verifies that the public key is in the list of authorized + /// keys. + /// + /// Unauthenticated servers do not verify the public key. Authenticated + /// servers do verify the public key, and reject the connection if the key + /// is unknown. Clients verify the public key, and raise + /// [`OssuaryError::UntrustedServer`] if the key is unknown, permitting a + /// Trust-On-First-Use scheme if desired. + /// + /// If a key is rejected, permanent connection failures are raised on both sides. + /// + pub fn add_authorized_key(&mut self, key: &[u8]) -> Result<(), OssuaryError> { + if key.len() != 32 { + return Err(OssuaryError::KeySize(32, key.len())); + } + let mut key_owned = [0u8; 32]; + key_owned.copy_from_slice(key); + self.authorized_keys.push(key_owned); + + // If handshake is waiting for key approval, check if this is the key. + match self.state { + ConnectionState::ClientWaitServerApproval => { + match self.remote_auth.public_key { + Some(remote_key) => { + if remote_key.as_bytes() == key { + self.state = ConnectionState::ClientSendAuthentication + } + }, + _ => {}, + } + }, + _ => {}, + }; + Ok(()) + } + + /// Add public keys of permitted remote hosts /// /// `keys` must be an iterable of `&[u8]` slices containing valid 32-byte - /// ed25519 public keys. During the handshake, a client will be required - /// to sign a challenge with its secret signing key. The client sends the - /// public key it signed with and the resulting signature, and the server - /// validates that the public key is in this provided list of keys prior - /// to validating the signature. - /// - /// If a client attempts to connect with a key not matching one of these - /// provided keys, a permanent connection failure is raised on both ends. - /// - /// **NOTE:** keys are only checked if the context was created with - /// [`ConnectionType::AuthenticatedServer`]. - pub fn set_authorized_keys<'a,T>(&mut self, keys: T) -> Result<usize, OssuaryError> + /// ed25519 public keys. + /// + /// See [`OssuaryConnection::add_authorized_key`] for documentation. + /// + pub fn add_authorized_keys<'a,T>(&mut self, keys: T) -> Result<usize, OssuaryError> where T: std::iter::IntoIterator<Item = &'a [u8]> { let mut count: usize = 0; for key in keys { - if key.len() != 32 { - return Err(OssuaryError::KeySize(32, key.len())); - } - let mut key_owned = [0u8; 32]; - key_owned.copy_from_slice(key); - self.authorized_keys.push(key_owned); + let _ = self.add_authorized_key(key)?; count += 1; } Ok(count) } /// Add authentication secret signing key /// - /// ´key` must be a `&[u8]` slice containing a valid 32-byte ed25519 + /// `key` must be a `&[u8]` slice containing a valid 32-byte ed25519 /// signing key. Signing keys should be kept secret and should be stored /// securely. ///
@@ -144,18 +210,27 @@ impl OssuaryConnection { self.local_auth.public_key = Some(public); Ok(()) } - /// Get the client's authentication public verification key + /// Get the local host's authentication public key /// /// When a secret key is set with [`OssuaryConnection::set_secret_key`], the /// matching public key is calculated. This function returns that public /// key, which can be shared with a remote server for future authentication. - /// - pub fn public_key(&self) -> Result<&[u8], OssuaryError> { + pub fn local_public_key(&self) -> Result<&[u8], OssuaryError> { match self.local_auth.public_key { None => Err(OssuaryError::InvalidKey), - Some(ref p) => { - Ok(p.as_bytes()) - } + Some(ref p) => Ok(p.as_bytes()), + } + } + /// Get the remote host's authentication public key + /// + /// When a connection is established, or during the initial handshake after + /// reeiving an [`OssuaryError::UntrustedServer`] response, this returns the + /// remote side's authentication public key. This is typically needed by a + /// client to get the remote server's key for a Trust-On-First-Use scheme. + pub fn remote_public_key(&self) -> Result<&[u8], OssuaryError> { + match self.remote_auth.public_key { + None => Err(OssuaryError::InvalidKey), + Some(ref p) => Ok(p.as_bytes()), } } }
diff --git a/src/error.rs b/src/error.rs line changes: +36/-3 index bfc014e..5646ad8 --- a/src/error.rs +++ b/src/error.rs
@@ -31,16 +31,38 @@ pub enum OssuaryError { /// be processed immediately. WouldBlock(usize), // bytes consumed + /// Connection accepted by an unknown/untrusted server. + /// + /// Returned when, during the handshake process, the server correctly + /// verifies itself, but its public key is not specified as an authorized + /// key. The caller may use this to implement a Trust-On-First-Use (TOFU) + /// policy. + /// + /// This error contains the public key of the remote server. + /// + /// After this is returned, the handshake process is paused until the caller + /// verifies that the key is to be trusted by calling + /// [`OssuaryConnection::add_authorized_keys`]] with the returned public + /// key. This should only be done if the user has opted for a TOFU policy, + /// and this is the first time connecting to this remote host. + /// + /// It is the caller's responsibility to save a database of (host, key) + /// pairs when implementing TOFU. + UntrustedServer(Vec<u8>), // bytes consumed, public key + /// Error casting received bytes to a primitive type. /// /// This error likely indicates a sync or corruption error in the data /// stream, and will trigger a connection reset. Unpack(core::array::TryFromSliceError), + /// Error reading from random number generator + NoRandomSource, + /// An invalid sized encryption key was encountered. /// /// This error is most likely caused by an attempt to register an invalid - /// secret or public key in [`OssuaryConnection::set_authorized_keys`] or + /// secret or public key in [`OssuaryConnection::add_authorized_keys`] or /// [`OssuaryConnection::set_secret_key`]. Both should be 32 bytes. KeySize(usize, usize), // (expected, actual)
@@ -54,6 +76,9 @@ pub enum OssuaryError { /// connection to reset. InvalidKey, + /// Encrypted data failed to be decrypted + DecryptionFailed, + /// The channel received an unexpected or malformed packet /// /// The associated string may describe the problem that went wrong. This
@@ -103,6 +128,7 @@ impl std::fmt::Debug for OssuaryError { OssuaryError::Io(e) => write!(f, "OssuaryError::Io {}", e), OssuaryError::WouldBlock(_) => write!(f, "OssuaryError::WouldBlock"), OssuaryError::Unpack(_) => write!(f, "OssuaryError::Unpack"), + OssuaryError::NoRandomSource => write!(f, "OssuaryError::NoRandomSource"), OssuaryError::KeySize(_,_) => write!(f, "OssuaryError::KeySize"), OssuaryError::InvalidKey => write!(f, "OssuaryError::InvalidKey"), OssuaryError::InvalidPacket(_) => write!(f, "OssuaryError::InvalidPacket"),
@@ -110,6 +136,8 @@ impl std::fmt::Debug for OssuaryError { OssuaryError::InvalidSignature => write!(f, "OssuaryError::InvalidSignature"), OssuaryError::ConnectionReset => write!(f, "OssuaryError::ConnectionReset"), OssuaryError::ConnectionFailed => write!(f, "OssuaryError::ConnectionFailed"), + OssuaryError::UntrustedServer(_) => write!(f, "OssuaryError::UntrustedServer"), + OssuaryError::DecryptionFailed => write!(f, "OssuaryError::DecryptionFailed"), } } }
@@ -128,11 +156,16 @@ impl From<core::array::TryFromSliceError> for OssuaryError { } impl From<ed25519_dalek::SignatureError> for OssuaryError { fn from(_error: ed25519_dalek::SignatureError) -> Self { - OssuaryError::InvalidKey + OssuaryError::InvalidSignature } } impl From<chacha20_poly1305_aead::DecryptError> for OssuaryError { fn from(_error: chacha20_poly1305_aead::DecryptError) -> Self { - OssuaryError::InvalidKey + OssuaryError::DecryptionFailed + } +} +impl From<rand::Error> for OssuaryError { + fn from(_error: rand::Error) -> Self { + OssuaryError::NoRandomSource } }
diff --git a/src/handshake.rs b/src/handshake.rs line changes: +68/-26 index 79753b1..e2eb470 --- a/src/handshake.rs +++ b/src/handshake.rs
@@ -188,7 +188,9 @@ impl OssuaryConnection { } let written = match self.state { // No-op states - ConnectionState::Encrypted => {0}, + ConnectionState::Encrypted | + ConnectionState::ClientRaiseUntrustedServer | + ConnectionState::ClientWaitServerApproval => {0}, // Timeout wait states ConnectionState::ServerWaitHandshake(t) |
@@ -387,8 +389,11 @@ impl OssuaryConnection { } self.remote_msg_id = pkt.header.msg_id + 1; - println!("Recv packet: ({}) {:?} <- {:?}", self.is_server(), self.state, pkt.kind()); match self.state { + // no-op states + ConnectionState::ClientRaiseUntrustedServer | + ConnectionState::ClientWaitServerApproval => {}, + // Non-receive states. Receiving handshake data is an error. ConnectionState::ClientSendHandshake | ConnectionState::ClientSendAuthentication |
@@ -472,28 +477,31 @@ impl OssuaryConnection { return Err(OssuaryError::InvalidSignature); } }; - // TODO: support trust on first use - if self.authorized_keys.len() > 0 { - if chal.iter().all(|x| *x == 0) || - sig.iter().all(|x| *x == 0) || - enc_pkt.public_key.iter().all(|x| *x == 0) { - // Parameters must be non-zero - self.reset_state(None); - return Err(OssuaryError::InvalidSignature); - } - // This is the first encrypted message, so the nonce has not changed yet - let mut sign_data = [0u8; KEY_LEN + NONCE_LEN + CHALLENGE_LEN]; - sign_data[0..KEY_LEN].copy_from_slice(self.remote_key.as_ref().map(|k| &k.public).unwrap_or(&[0u8; KEY_LEN])); - sign_data[KEY_LEN..KEY_LEN+NONCE_LEN].copy_from_slice(self.remote_key.as_ref().map(|k| &k.nonce).unwrap_or(&[0u8; NONCE_LEN])); - sign_data[KEY_LEN+NONCE_LEN..].copy_from_slice(&self.local_auth.challenge.unwrap_or([0u8; CHALLENGE_LEN])); - match pubkey.verify(&sign_data, &signature) { - Ok(_) => {}, - Err(_) => { - self.reset_state(None); - return Err(OssuaryError::InvalidSignature); - }, + + // All servers should have an auth key set, so + // these parameters should be non-zero and the + // signature should verify. + if chal.iter().all(|x| *x == 0) || + sig.iter().all(|x| *x == 0) || + enc_pkt.public_key.iter().all(|x| *x == 0) { + // Parameters must be non-zero + self.reset_state(None); + return Err(OssuaryError::InvalidSignature); } + + // This is the first encrypted message, so the nonce has not changed yet + let mut sign_data = [0u8; KEY_LEN + NONCE_LEN + CHALLENGE_LEN]; + sign_data[0..KEY_LEN].copy_from_slice(self.remote_key.as_ref().map(|k| &k.public).unwrap_or(&[0u8; KEY_LEN])); + sign_data[KEY_LEN..KEY_LEN+NONCE_LEN].copy_from_slice(self.remote_key.as_ref().map(|k| &k.nonce).unwrap_or(&[0u8; NONCE_LEN])); + sign_data[KEY_LEN+NONCE_LEN..].copy_from_slice(&self.local_auth.challenge.unwrap_or([0u8; CHALLENGE_LEN])); + match pubkey.verify(&sign_data, &signature) { + Ok(_) => {}, + Err(_) => { + self.reset_state(None); + return Err(OssuaryError::InvalidSignature); + }, } + self.remote_auth = AuthKeyMaterial { challenge: Some(chal), public_key: Some(pubkey),
@@ -501,7 +509,11 @@ impl OssuaryConnection { secret_key: None, }; let _ = self.remote_key.as_mut().map(|k| increment_nonce(&mut k.nonce)); - self.state = ConnectionState::ClientSendAuthentication; + + match self.authorized_keys.contains(&enc_pkt.public_key) { + true => self.state = ConnectionState::ClientSendAuthentication, + false => self.state = ConnectionState::ClientRaiseUntrustedServer, + } } } },
@@ -556,7 +568,6 @@ impl OssuaryConnection { } }; match self.conn_type { - // TODO: only permit known pubkeys ConnectionType::AuthenticatedServer => { if challenge.iter().all(|x| *x == 0) || sig.iter().all(|x| *x == 0) ||
@@ -565,6 +576,7 @@ impl OssuaryConnection { self.reset_state(None); return Err(OssuaryError::InvalidSignature); } + // This is the first encrypted message, so the nonce has not changed yet let mut sign_data = [0u8; KEY_LEN + NONCE_LEN + CHALLENGE_LEN]; sign_data[0..KEY_LEN].copy_from_slice(self.remote_key.as_ref().map(|k| &k.public).unwrap_or(&[0u8; KEY_LEN]));
@@ -577,6 +589,15 @@ impl OssuaryConnection { return Err(OssuaryError::InvalidSignature); }, } + + // Ensure this key is permitted to connect + match self.authorized_keys.contains(&enc_pkt.public_key) { + true => {}, + false => { + self.reset_state(None); + return Err(OssuaryError::InvalidKey); + }, + } } _ => {}, }
@@ -607,10 +628,31 @@ impl OssuaryConnection { /// Returns whether the handshake process is complete. /// /// - pub fn handshake_done(&self) -> Result<bool, &OssuaryError> { + pub fn handshake_done(&mut self) -> Result<bool, OssuaryError> { + let failed = match self.state { + ConnectionState::Failed(_) => true, + _ => false, + }; + if failed { + let mut new_state = ConnectionState::Failed(OssuaryError::ConnectionFailed); + std::mem::swap(&mut self.state, &mut new_state); + match new_state { + ConnectionState::Failed(e) => return Err(e), + _ => return Err(OssuaryError::ConnectionFailed), + } + } match self.state { ConnectionState::Encrypted => Ok(true), - ConnectionState::Failed(ref e) => Err(e), + ConnectionState::Failed(ref _e) => Err(OssuaryError::ConnectionFailed), + ConnectionState::ClientRaiseUntrustedServer => { + self.state = ConnectionState::ClientWaitServerApproval; + let mut key: Vec<u8> = Vec::new(); + match self.remote_auth.public_key { + Some(ref p) => key.extend_from_slice(p.as_bytes()), + None => key.extend_from_slice(&[0; KEY_LEN]), + }; + Err(OssuaryError::UntrustedServer(key)) + }, _ => Ok(false), } }
diff --git a/src/lib.rs b/src/lib.rs line changes: +35/-10 index 8a171f6..c7d5bf4 --- a/src/lib.rs +++ b/src/lib.rs
@@ -25,7 +25,7 @@ //! are encrypted with the established session key. //! //! In the following diagram, fields in [single brackets] are sent in the clear, -//! and those in [[double brackets]] are encrypted: +//! and those in [[double brackets]] are encrypted with the shared session key: //! //! ```text //! <client> --> [ session x25519 public key,
@@ -124,14 +124,13 @@ // // TODO -// - server certificate (TOFU) // - consider all unexpected packet types to be errors // - ensure that a reset on one end always sends a reset to the other // - limit connection retries // - tests should check their received strings // - rustdoc everything +// - expose generate_auth_keypair in FFI // -// TODO: raise OssuaryError::UntrustedServer() when trust-on-first-use pub mod clib; mod connection;
@@ -150,6 +149,8 @@ use chacha20_poly1305_aead::{encrypt,decrypt}; use x25519_dalek::{EphemeralSecret, EphemeralPublic, SharedSecret}; use ed25519_dalek::{Signature, Keypair, SecretKey, PublicKey}; +use rand::rngs::OsRng; + const PROTOCOL_VERSION: u8 = 1u8; // Maximum time to wait (in seconds) for a handshake response
@@ -298,6 +299,23 @@ enum ConnectionState { /// Next client state: Encrypted ClientSendAuthentication, + /// Client is about to raise an UntrustedServer error to the caller + /// + /// The client has established and verified a connection with a remote + /// server, but the server's authentication key is unknown. The + /// [`OssuaryError::UntrustedServer`] will be raised on the next call to + /// [`OssuaryConnection::handhake_done`]. + ClientRaiseUntrustedServer, + + /// Client is waiting for the caller to approve an unknown remote server + /// + /// After raising [`OssuaryError::UntrustedServer`], the client waits in + /// this state until the server's public key is added to the list of + /// authorized keys with [`OssuaryConnection::add_authorized_key`], or + /// the connection is killed. This permits callers to implement a + /// Trust-On-First-Use policy. + ClientWaitServerApproval, + /// Connection is established, encrypted, and optionally authenticated. Encrypted,
@@ -332,7 +350,7 @@ pub enum ConnectionType { /// Authenticated servers only allow connections from clients with secret /// keys set using [`OssuaryConnection::set_secret_key`], and with the /// matching public key registered with the server using - /// [`OssuaryConnection::set_authorized_keys`]. + /// [`OssuaryConnection::add_authorized_keys`]. AuthenticatedServer, /// This context is a server that does not support authentication.
@@ -354,7 +372,7 @@ pub enum ConnectionType { /// [`ConnectionType`] identifying whether it is to act as a client or server. /// Server contexts can optionally require authentication, verified by providing /// a list of public keys of permitted clients with -/// [`OssuaryConnection::set_authorized_keys`]. Clients, on the other hand, +/// [`OssuaryConnection::add_authorized_keys`]. Clients, on the other hand, /// authenticate by setting their secret key with /// [`OssuaryConnection::set_secret_key`]. ///
@@ -403,6 +421,13 @@ impl Default for OssuaryConnection { } } +/// Generate secret/public Ed25519 keypair for host authentication +pub fn generate_auth_keypair() -> Result<([u8; KEY_LEN],[u8; KEY_LEN]), OssuaryError> { + let mut rng = OsRng::new()?; + let keypair: Keypair = Keypair::generate(&mut rng); + Ok((keypair.secret.to_bytes(), keypair.public.to_bytes())) +} + /// Cast the data bytes in a NetworkPacket into a struct fn interpret_packet<'a, T>(pkt: &'a NetworkPacket) -> Result<&'a T, OssuaryError> { let s: &T = slice_as_struct(&pkt.data)?;
@@ -448,8 +473,8 @@ mod tests { use crate::*; #[test] - fn test_set_authorized_keys() { - let mut conn = OssuaryConnection::new(ConnectionType::AuthenticatedServer); + fn test_add_authorized_keys() { + let mut conn = OssuaryConnection::new(ConnectionType::AuthenticatedServer, None).unwrap(); // Vec of slices let keys: Vec<&[u8]> = vec![
@@ -458,7 +483,7 @@ mod tests { 0xbb, 0x9e, 0x86, 0x62, 0x28, 0x7c, 0x33, 0x89, 0xa2, 0xe1, 0x63, 0xdc, 0x55, 0xde, 0x28, 0x1f] ]; - let _ = conn.set_authorized_keys(keys).unwrap(); + let _ = conn.add_authorized_keys(keys).unwrap(); // Vec of owned arrays let keys: Vec<[u8; 32]> = vec![
@@ -467,7 +492,7 @@ mod tests { 0xbb, 0x9e, 0x86, 0x62, 0x28, 0x7c, 0x33, 0x89, 0xa2, 0xe1, 0x63, 0xdc, 0x55, 0xde, 0x28, 0x1f] ]; - let _ = conn.set_authorized_keys(keys.iter().map(|x| x.as_ref()).collect::<Vec<&[u8]>>()).unwrap(); + let _ = conn.add_authorized_keys(keys.iter().map(|x| x.as_ref()).collect::<Vec<&[u8]>>()).unwrap(); // Vec of vecs let keys: Vec<Vec<u8>> = vec![
@@ -476,6 +501,6 @@ mod tests { 0xbb, 0x9e, 0x86, 0x62, 0x28, 0x7c, 0x33, 0x89, 0xa2, 0xe1, 0x63, 0xdc, 0x55, 0xde, 0x28, 0x1f] ]; - let _ = conn.set_authorized_keys(keys.iter().map(|x| x.as_slice())).unwrap(); + let _ = conn.add_authorized_keys(keys.iter().map(|x| x.as_slice())).unwrap(); } }
diff --git a/tests/basic.rs b/tests/basic.rs line changes: +15/-3 index 8a0815f..ac26f63 --- a/tests/basic.rs +++ b/tests/basic.rs
@@ -9,7 +9,18 @@ fn event_loop<T>(mut conn: OssuaryConnection, is_server: bool) -> Result<(), std::io::Error> where T: std::io::Read + std::io::Write { // Run the opaque handshake until the connection is established - while conn.handshake_done().unwrap() == false { + loop { + match conn.handshake_done() { + Ok(true) => break, + Ok(false) => {}, + Err(OssuaryError::UntrustedServer(pubkey)) => { + // Trust-On-First-Use would be implemented here. This + // client trusts all servers. + let keys: Vec<&[u8]> = vec![&pubkey]; + let _ = conn.add_authorized_keys(keys).unwrap(); + } + Err(e) => panic!("Handshake failed with error: {:?}", e), + } if conn.send_handshake(&mut stream).is_ok() { loop { match conn.recv_handshake(&mut stream) {
@@ -50,7 +61,8 @@ fn server() -> Result<(), std::io::Error> { let listener = TcpListener::bind("127.0.0.1:9988").unwrap(); let stream: TcpStream = listener.incoming().next().unwrap().unwrap(); let _ = stream.set_read_timeout(Some(std::time::Duration::from_millis(100u64))); - let conn = OssuaryConnection::new(ConnectionType::UnauthenticatedServer); + // This server lets any client connect + let conn = OssuaryConnection::new(ConnectionType::UnauthenticatedServer, None).unwrap(); let _ = event_loop(conn, stream, true); Ok(()) }
@@ -58,7 +70,7 @@ fn server() -> Result<(), std::io::Error> { fn client() -> Result<(), std::io::Error> { let stream = TcpStream::connect("127.0.0.1:9988").unwrap(); let _ = stream.set_read_timeout(Some(std::time::Duration::from_millis(100u64))); - let conn = OssuaryConnection::new(ConnectionType::Client); + let conn = OssuaryConnection::new(ConnectionType::Client, None).unwrap(); let _ = event_loop(conn, stream, false); Ok(()) }
diff --git a/tests/basic_auth.rs b/tests/basic_auth.rs line changes: +33/-10 index 25c946a..ffcb9db --- a/tests/basic_auth.rs +++ b/tests/basic_auth.rs
@@ -9,7 +9,15 @@ fn event_loop<T>(mut conn: OssuaryConnection, is_server: bool) -> Result<(), std::io::Error> where T: std::io::Read + std::io::Write { // Run the opaque handshake until the connection is established - while conn.handshake_done().unwrap() == false { + loop { + match conn.handshake_done() { + Ok(true) => break, + Ok(false) => {}, + Err(OssuaryError::UntrustedServer(_)) => { + panic!("Untrusted server, authentication failed!") + } + Err(e) => panic!("Handshake failed with error: {:?}", e), + } if conn.send_handshake(&mut stream).is_ok() { loop { match conn.recv_handshake(&mut stream) {
@@ -48,26 +56,41 @@ where T: std::io::Read + std::io::Write { fn server() -> Result<(), std::io::Error> { let listener = TcpListener::bind("127.0.0.1:9988").unwrap(); let stream: TcpStream = listener.incoming().next().unwrap().unwrap(); - let mut conn = OssuaryConnection::new(ConnectionType::AuthenticatedServer); - let keys: Vec<&[u8]> = vec![ + let auth_secret_key = &[ + 0x50, 0x29, 0x04, 0x97, 0x62, 0xbd, 0xa6, 0x07, + 0x71, 0xca, 0x29, 0x14, 0xe3, 0x83, 0x19, 0x0e, + 0xa0, 0x9e, 0xd4, 0xb7, 0x1a, 0xf9, 0xc9, 0x59, + 0x3e, 0xa3, 0x1c, 0x85, 0x0f, 0xc4, 0xfa, 0xa2, + ]; + let client_keys: Vec<&[u8]> = vec![ &[0xbe, 0x1c, 0xa0, 0x74, 0xf4, 0xa5, 0x8b, 0xbb, 0xd2, 0x62, 0xa7, 0xf9, 0x52, 0x3b, 0x6f, 0xb0, 0xbb, 0x9e, 0x86, 0x62, 0x28, 0x7c, 0x33, 0x89, 0xa2, 0xe1, 0x63, 0xdc, 0x55, 0xde, 0x28, 0x1f] ]; - let _ = conn.set_authorized_keys(keys).unwrap(); + // This server only accepts connections from a single known client + let mut conn = OssuaryConnection::new(ConnectionType::AuthenticatedServer, Some(auth_secret_key)).unwrap(); + let _ = conn.add_authorized_keys(client_keys).unwrap(); let _ = event_loop(conn, stream, true); Ok(()) } fn client() -> Result<(), std::io::Error> { let stream = TcpStream::connect("127.0.0.1:9988").unwrap(); - let mut conn = OssuaryConnection::new(ConnectionType::Client); - let _ = conn.set_secret_key( - &[0x10, 0x86, 0x6e, 0xc4, 0x8a, 0x11, 0xf3, 0xc5, - 0x6d, 0x77, 0xa6, 0x4b, 0x2f, 0x54, 0xaa, 0x06, - 0x6c, 0x0c, 0xb4, 0x75, 0xd8, 0xc8, 0x7d, 0x35, - 0xb4, 0x91, 0xee, 0xd6, 0xac, 0x0b, 0xde, 0xbc]).unwrap(); + let sec_key = &[0x10, 0x86, 0x6e, 0xc4, 0x8a, 0x11, 0xf3, 0xc5, + 0x6d, 0x77, 0xa6, 0x4b, 0x2f, 0x54, 0xaa, 0x06, + 0x6c, 0x0c, 0xb4, 0x75, 0xd8, 0xc8, 0x7d, 0x35, + 0xb4, 0x91, 0xee, 0xd6, 0xac, 0x0b, 0xde, 0xbc]; + let server_public_key = &[ + 0x20, 0x88, 0x55, 0x8e, 0xbd, 0x9b, 0x46, 0x1d, + 0xd0, 0x9d, 0xf0, 0x00, 0xda, 0xf4, 0x0f, 0x87, + 0xf7, 0x38, 0x40, 0xc5, 0x54, 0x18, 0x57, 0x60, + 0x74, 0x39, 0x3b, 0xb9, 0x70, 0xe1, 0x46, 0x98, + ]; + let keys: Vec<&[u8]> = vec![server_public_key]; + // This client only accepts connections to a single known server + let mut conn = OssuaryConnection::new(ConnectionType::Client, Some(sec_key)).unwrap(); + let _ = conn.add_authorized_keys(keys).unwrap(); let _ = event_loop(conn, stream, false); Ok(()) }
diff --git a/tests/clib_ffi.rs b/tests/clib_ffi.rs line changes: +19/-5 index 8f48ef4..dc1fb3a --- a/tests/clib_ffi.rs +++ b/tests/clib_ffi.rs
@@ -2,12 +2,15 @@ use ossuary::clib::{ ossuary_create_connection, ossuary_destroy_connection, ossuary_set_secret_key, - ossuary_set_authorized_keys, + ossuary_add_authorized_keys, ossuary_send_handshake, ossuary_recv_handshake, ossuary_handshake_done, ossuary_send_data, ossuary_recv_data, + ossuary_remote_public_key, + ossuary_add_authorized_key, + OSSUARY_ERR_UNTRUSTED_SERVER, }; use std::thread;
@@ -21,13 +24,13 @@ fn server() -> Result<(), std::io::Error> { for stream in listener.incoming() { let mut stream: TcpStream = stream.unwrap(); let mut reader = std::io::BufReader::new(stream.try_clone().unwrap()); - let mut conn = ossuary_create_connection(1); + let mut conn = ossuary_create_connection(1, ::std::ptr::null_mut()); let key: &[u8; 32] = &[0xbe, 0x1c, 0xa0, 0x74, 0xf4, 0xa5, 0x8b, 0xbb, 0xd2, 0x62, 0xa7, 0xf9, 0x52, 0x3b, 0x6f, 0xb0, 0xbb, 0x9e, 0x86, 0x62, 0x28, 0x7c, 0x33, 0x89, 0xa2, 0xe1, 0x63, 0xdc, 0x55, 0xde, 0x28, 0x1f]; let keys: &[*const u8; 1] = &[key as *const u8]; - ossuary_set_authorized_keys(conn, keys as *const *const u8, keys.len() as u8); + ossuary_add_authorized_keys(conn, keys as *const *const u8, keys.len() as u8); let out_buf: [u8; 512] = [0; 512];
@@ -89,7 +92,7 @@ fn server() -> Result<(), std::io::Error> { fn client() -> Result<(), std::io::Error> { let mut stream = TcpStream::connect("127.0.0.1:9989").unwrap(); - let mut conn = ossuary_create_connection(0); + let mut conn = ossuary_create_connection(0, ::std::ptr::null_mut()); let key: &[u8; 32] = &[0x10, 0x86, 0x6e, 0xc4, 0x8a, 0x11, 0xf3, 0xc5, 0x6d, 0x77, 0xa6, 0x4b, 0x2f, 0x54, 0xaa, 0x06, 0x6c, 0x0c, 0xb4, 0x75, 0xd8, 0xc8, 0x7d, 0x35,
@@ -99,7 +102,18 @@ fn client() -> Result<(), std::io::Error> { let out_buf: [u8; 512] = [0; 512]; let mut reader = std::io::BufReader::new(stream.try_clone().unwrap()); - while ossuary_handshake_done(conn) == 0 { + loop { + match ossuary_handshake_done(conn) { + 0 => {}, + x if x > 0 => break, + OSSUARY_ERR_UNTRUSTED_SERVER => { + let key = [0u8; 32]; + ossuary_remote_public_key(conn, &key as *const u8 as *mut u8, key.len() as u16); + ossuary_add_authorized_key(conn, &key as *const u8); + continue; + }, + x => panic!("handshake failed: {}", x), + } let mut out_len = out_buf.len() as u16; let wrote = ossuary_send_handshake(conn, (&out_buf) as *const u8 as *mut u8,