summary history branches tags files
commit:4577ac87b11fe20fc8b932836d23c8fe9b807e16
author:Trevor Bentley
committer:Trevor Bentley
date:Sun May 19 21:46:05 2019 +0200
parents:7e295717f41604aa6d81618db517ae65cbacb404
Implement host verification and support TOFU
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,