src/connection.rs
// Copyright 2019 Trevor Bentley
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
use crate::*;
use rand::RngCore;
use rand::rngs::OsRng;
impl OssuaryConnection {
fn generate_session_material() -> Result<SessionKeyMaterial, OssuaryError> {
let mut rng = OsRng::new().expect("RNG not available.");
let sec_key = EphemeralSecret::new(&mut rng);
let pub_key = EphemeralPublic::from(&sec_key);
let mut nonce: [u8; NONCE_LEN] = [0; NONCE_LEN];
rng.fill_bytes(&mut nonce);
Ok(SessionKeyMaterial {
secret: Some(sec_key),
public: *pub_key.as_bytes(),
nonce: nonce,
session: None,
})
}
/// Allocate a new OssuaryConnection.
///
/// `conn_type` is a [`ConnectionType`] indicating whether this instance
/// is for a client or server.
///
/// `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. This key
/// can be changed with [`OssuaryConnection::set_secret_key`].
///
pub fn new(conn_type: ConnectionType, auth_secret_key: Option<&[u8]>) -> Result<OssuaryConnection, OssuaryError> {
let mut rng = OsRng::new().expect("RNG not available.");
let mut challenge: [u8; CHALLENGE_LEN] = [0; CHALLENGE_LEN];
rng.fill_bytes(&mut challenge);
let key = OssuaryConnection::generate_session_material()?;
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,
secret_key: auth_sec,
public_key: auth_pub,
};
Ok(OssuaryConnection {
state: match conn_type {
ConnectionType::Client => ConnectionState::ClientSendHandshake,
_ => ConnectionState::ServerWaitHandshake(std::time::SystemTime::now()),
},
conn_type: conn_type,
local_key: key,
local_auth: auth,
..Default::default()
})
}
/// Terminate a connection, or an on-going connection attempt.
///
/// Calling this immediately closes the local end of Ossuary's connection,
/// and queues a disconnect packet to be sent to the remote host to inform
/// it to close its end.
///
/// After calling disconnect(), the application should continue calling
/// Ossuary's functions (or at least its handshake functions) in a loop
/// until [`OssuaryConnection::handshake_done`] returns the matching error.
/// This allows Ossuary to generate the final disconnect packet.
///
/// The handshake will return [`OssuaryError::ConnectionFailed`] if 'error'
/// is true, or [`OssuaryError::ConnectionClosed`] otherwise.
///
/// 'error' - Indicates the reason for termination. True means the channel
/// is being closed because of some error, False means it is being
/// closed due to completion or a clean shutdown.
///
pub fn disconnect(&mut self, error: bool) {
match error {
true => self.reset_state(Some(OssuaryError::ConnectionFailed)),
false => self.reset_state(Some(OssuaryError::ConnectionClosed)),
}
}
/// Get the initial state machine state of this connection
pub(crate) fn initial_state(&self) -> ConnectionState {
match self.conn_type {
ConnectionType::Client => ConnectionState::ClientSendHandshake,
_ => ConnectionState::ServerWaitHandshake(std::time::SystemTime::now()),
}
}
/// Reset the context back to its default state.
///
/// If `permanent_err` is None, this connection can be re-established by
/// calling the connection handshake functions. This indicates that
/// something unexpected went wrong with the connection, such as an invalid
/// state or corrupt data, such that reestablishing the connection may fix
/// it.
///
/// If `permanent_err` is set to some error, it indicates a permanent
/// failure on this connection, and attempting to reestablish it will likely
/// not work. This includes situations where the server has rejected the
/// connection, such as when the client's key is not authorized.
///
pub(crate) fn reset_state(&mut self, permanent_err: Option<OssuaryError>) {
self.local_key = OssuaryConnection::generate_session_material().unwrap_or_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.reset_count += 1;
let perm_error = match self.reset_count {
c if c < MAX_RESET_COUNT => permanent_err,
_ => Some(OssuaryError::ConnectionFailed),
};
self.state = match perm_error {
None => ConnectionState::Resetting(true),
Some(e) => ConnectionState::Failing(e),
};
}
/// Whether this context represents a server (as opposed to a client).
pub fn is_server(&self) -> bool {
match self.conn_type {
ConnectionType::Client => false,
_ => true,
}
}
/// Add key received from a remote connection and generate session key
pub(crate) fn add_remote_key(&mut self, public: &[u8; 32], nonce: &[u8; 12]) {
let key = SessionKeyMaterial {
secret: None,
public: public.to_owned(),
nonce: nonce.to_owned(),
session: None,
};
self.remote_key = Some(key);
let secret = self.local_key.secret.take();
if let Some(secret) = secret {
self.local_key.session = Some(secret.diffie_hellman(&EphemeralPublic::from(*public)));
}
}
/// 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.
///
/// 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 {
let _ = self.add_authorized_key(key)?;
count += 1;
}
Ok(count)
}
/// Set authentication secret signing key
///
/// Changes the secret authentication key of this side of the connection,
/// which was previously set by [`OssuaryConnection::new`]
///
/// `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.
///
/// This key is used to authenticate during the handshake if the remote
/// server requires authentication. During the handshake, the server will
/// send a challenge (a buffer of random bytes) which the client signs
/// with this secret key. The client returns its public key and the
/// signature of the challenge data to identify which key it is using for
/// authentication, and to prove possession of the secret key.
///
pub fn set_secret_key(&mut self, key: &[u8]) -> Result<(), OssuaryError> {
if key.len() != 32 {
return Err(OssuaryError::KeySize(32, key.len()));
}
let secret = SecretKey::from_bytes(key)?;
let public = PublicKey::from(&secret);
self.local_auth.secret_key = Some(secret);
self.local_auth.public_key = Some(public);
Ok(())
}
/// 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 local_public_key(&self) -> Result<&[u8], OssuaryError> {
match self.local_auth.public_key {
None => Err(OssuaryError::InvalidKey),
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()),
}
}
}