Almost ready to test...

This commit is contained in:
Adam Ierymenko 2023-02-27 13:36:35 -05:00
commit 6bc69d1465
8 changed files with 531 additions and 318 deletions

View file

@ -5,6 +5,22 @@ license = "MPL-2.0"
name = "zssp"
version = "0.1.0"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = 'abort'
[lib]
name = "zssp"
path = "src/lib.rs"
doc = true
[[bin]]
name = "zssp_test"
path = "src/main.rs"
doc = false
[dependencies]
zerotier-utils = { path = "../utils" }
zerotier-crypto = { path = "../crypto" }

View file

@ -6,32 +6,59 @@
* https://www.zerotier.com/
*/
use zerotier_crypto::p384::{P384KeyPair, P384PublicKey};
use zerotier_crypto::p384::P384KeyPair;
/// Trait to implement to integrate the session into an application.
///
/// Templating the session on this trait lets the code here be almost entirely transport, OS,
/// and use case independent.
///
/// The constants exposed in this trait can be redefined from their defaults to change rekey
/// and negotiation timeout behavior. This is discouraged except for testing purposes when low
/// key lifetime values may be desirable to test rekeying. Also note that each side takes turns
/// initiating rekey, so if both sides don't have the same values you'll get asymmetric timing
/// behavior. This will still work as long as the key usage counter doesn't exceed the
/// EXPIRE_AFTER_USES limit.
pub trait ApplicationLayer: Sized {
/// Arbitrary opaque object associated with a session, such as a connection state object.
/// Rekey after this many key uses.
///
/// The default is 1/4 the recommended NIST limit for AES-GCM. Unless you are transferring
/// a massive amount of data REKEY_AFTER_TIME_MS is probably going to kick in first.
const REKEY_AFTER_USES: u64 = 536870912;
/// Hard expiration after this many uses.
///
/// Attempting to encrypt more than this many messages with a key will cause a hard error
/// and the internal erasure of ephemeral key material. You'll only ever hit this if something
/// goes wrong and rekeying fails.
const EXPIRE_AFTER_USES: u64 = 2147483648;
/// Start attempting to rekey after a key has been in use for this many milliseconds.
///
/// Default is two hours.
const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60 * 2;
/// Maximum random jitter to add to rekey-after time.
///
/// Default is ten minutes.
const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10;
/// Timeout for Noise_XK session negotiation in milliseconds.
///
/// Default is two seconds, which should be enough for even extremely slow links or links
/// over very long distances.
const SESSION_NEGOTIATION_TIMEOUT_MS: i64 = 2000;
/// Type for arbitrary opaque object for use by the application that is attached to each session.
type Data;
/// A buffer containing data read from the network that can be cached.
/// Data type for incoming packet buffers.
///
/// This can be e.g. a pooled buffer that automatically returns itself to the pool when dropped.
/// It can also just be a Vec<u8> or Box<[u8]> or something like that.
/// This can be something like Vec<u8> or Box<[u8]> or it can be something like a pooled reusable
/// buffer that automatically returns to its pool when ZSSP is done with it. ZSSP may hold these
/// for a short period of time when assembling fragmented packets on the receive path.
type IncomingPacketBuffer: AsRef<[u8]> + AsMut<[u8]>;
/// Rate limit for attempts to rekey existing sessions in milliseconds (default: 2000).
const REKEY_RATE_LIMIT_MS: i64 = 2000;
/// Extract the NIST P-384 ECC public key component from a static public key blob or return None on failure.
///
/// This is called to parse the static public key blob from the other end and extract its NIST P-384 public
/// key. SECURITY NOTE: the information supplied here is from the wire so care must be taken to parse it
/// safely and fail on any error or corruption.
fn extract_s_public_from_static_public_blob(static_public: &[u8]) -> Option<P384PublicKey>;
/// Get a reference to this host's static public key blob.
///
/// This must contain a NIST P-384 public key but can contain other information. In ZeroTier this

View file

@ -1,38 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* (c) ZeroTier, Inc.
* https://www.zerotier.com/
*/
use crate::proto::{AES_GCM_TAG_SIZE, HEADER_SIZE};
/// Minimum size of a valid physical ZSSP packet of any type. Anything smaller is discarded.
pub const MIN_PACKET_SIZE: usize = HEADER_SIZE + AES_GCM_TAG_SIZE;
/// Minimum physical MTU for ZSSP to function.
pub const MIN_TRANSPORT_MTU: usize = 128;
/// Maximum size of init meta-data objects.
pub const MAX_METADATA_SIZE: usize = 256;
/// Start attempting to rekey after a key has been used to send packets this many times.
/// This is 1/4 the recommended NIST limit for AES-GCM key lifetimes under most conditions.
pub(crate) const REKEY_AFTER_USES: u64 = 536870912;
/// Hard expiration after this many uses.
///
/// Use of the key beyond this point is prohibited. If we reach this number of key uses
/// the key will be destroyed in memory and the session will cease to function. A hard
/// error is also generated.
pub(crate) const EXPIRE_AFTER_USES: u64 = REKEY_AFTER_USES * 2;
/// Start attempting to rekey after a key has been in use for this many milliseconds.
pub(crate) const REKEY_AFTER_TIME_MS: i64 = 1000 * 60 * 60; // 1 hour
/// Maximum random jitter to add to rekey-after time.
pub(crate) const REKEY_AFTER_TIME_MS_MAX_JITTER: u32 = 1000 * 60 * 10; // 10 minutes
/// Timeout for incoming sessions in incomplete state in milliseconds.
pub(crate) const INCOMPLETE_SESSION_TIMEOUT: i64 = 1000;

View file

@ -6,11 +6,9 @@
* https://www.zerotier.com/
*/
use crate::sessionid::SessionId;
pub enum Error {
/// The packet was addressed to an unrecognized local session (should usually be ignored)
UnknownLocalSessionId(SessionId),
UnknownLocalSessionId,
/// Packet was not well formed
InvalidPacket,
@ -29,12 +27,6 @@ pub enum Error {
/// Attempt to send using session without established key
SessionNotEstablished,
/// Packet ignored by rate limiter.
RateLimited,
/// Packet counter is too far outside window.
OutOfCounterWindow,
/// The other peer specified an unrecognized protocol version
UnknownProtocolVersion,
@ -44,6 +36,9 @@ pub enum Error {
/// Data object is too large to send, even with fragmentation
DataTooLarge,
/// Packet counter was outside window or packet arrived with session in an unexpected state.
OutOfSequence,
/// An unexpected buffer overrun occured while attempting to encode or decode a packet.
///
/// This can only ever happen if exceptionally large key blobs or metadata are being used,
@ -61,20 +56,19 @@ impl From<std::io::Error> for Error {
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownLocalSessionId(id) => f.write_str(format!("UnknownLocalSessionId({})", id).as_str()),
Self::InvalidPacket => f.write_str("InvalidPacket"),
Self::InvalidParameter => f.write_str("InvalidParameter"),
Self::FailedAuthentication => f.write_str("FailedAuthentication"),
Self::MaxKeyLifetimeExceeded => f.write_str("MaxKeyLifetimeExceeded"),
Self::SessionNotEstablished => f.write_str("SessionNotEstablished"),
Self::RateLimited => f.write_str("RateLimited"),
Self::OutOfCounterWindow => f.write_str("OutOfCounterWindow"),
Self::UnknownProtocolVersion => f.write_str("UnknownProtocolVersion"),
Self::DataBufferTooSmall => f.write_str("DataBufferTooSmall"),
Self::DataTooLarge => f.write_str("DataTooLarge"),
Self::UnexpectedBufferOverrun => f.write_str("UnexpectedBufferOverrun"),
}
f.write_str(match self {
Self::UnknownLocalSessionId => "UnknownLocalSessionId",
Self::InvalidPacket => "InvalidPacket",
Self::InvalidParameter => "InvalidParameter",
Self::FailedAuthentication => "FailedAuthentication",
Self::MaxKeyLifetimeExceeded => "MaxKeyLifetimeExceeded",
Self::SessionNotEstablished => "SessionNotEstablished",
Self::UnknownProtocolVersion => "UnknownProtocolVersion",
Self::DataBufferTooSmall => "DataBufferTooSmall",
Self::DataTooLarge => "DataTooLarge",
Self::OutOfSequence => "OutOfSequence",
Self::UnexpectedBufferOverrun => "UnexpectedBufferOverrun",
})
}
}

View file

@ -13,9 +13,8 @@ mod sessionid;
mod tests;
mod zssp;
pub mod constants;
pub use crate::applicationlayer::ApplicationLayer;
pub use crate::error::Error;
pub use crate::proto::{MAX_METADATA_SIZE, MIN_PACKET_SIZE, MIN_TRANSPORT_MTU};
pub use crate::sessionid::SessionId;
pub use crate::zssp::{Context, ReceiveResult, Session};

3
zssp/src/main.rs Normal file
View file

@ -0,0 +1,3 @@
use zssp::*;
fn main() {}

View file

@ -13,10 +13,18 @@ use zerotier_crypto::hash::{HMAC_SHA384_SIZE, SHA384_HASH_SIZE};
use zerotier_crypto::p384::P384_PUBLIC_KEY_SIZE;
use crate::applicationlayer::ApplicationLayer;
use crate::constants::*;
use crate::error::Error;
use crate::sessionid::SessionId;
/// Minimum size of a valid physical ZSSP packet of any type. Anything smaller is discarded.
pub const MIN_PACKET_SIZE: usize = HEADER_SIZE + AES_GCM_TAG_SIZE;
/// Minimum physical MTU for ZSSP to function.
pub const MIN_TRANSPORT_MTU: usize = 128;
/// Maximum size of init meta-data objects.
pub const MAX_METADATA_SIZE: usize = 256;
pub(crate) const SESSION_PROTOCOL_VERSION: u8 = 0x00;
pub(crate) const COUNTER_WINDOW_MAX_OOO: usize = 16;
@ -30,8 +38,8 @@ pub(crate) const PACKET_TYPE_ALICE_REKEY_INIT: u8 = 4;
pub(crate) const PACKET_TYPE_BOB_REKEY_ACK: u8 = 5;
pub(crate) const HEADER_SIZE: usize = 16;
pub(crate) const HEADER_CHECK_ENCRYPT_START: usize = 6;
pub(crate) const HEADER_CHECK_ENCRYPT_END: usize = 22;
pub(crate) const HEADER_PROTECT_ENCRYPT_START: usize = 6;
pub(crate) const HEADER_PROTECT_ENCRYPT_END: usize = 22;
pub(crate) const KBKDF_KEY_USAGE_LABEL_KEX_ENCRYPTION: u8 = b'X'; // intermediate keys used in key exchanges
pub(crate) const KBKDF_KEY_USAGE_LABEL_KEX_AUTHENTICATION: u8 = b'x'; // intermediate keys used in key exchanges
@ -46,7 +54,7 @@ pub(crate) const MAX_NOISE_HANDSHAKE_SIZE: usize = MAX_NOISE_HANDSHAKE_FRAGMENTS
pub(crate) const BASE_KEY_SIZE: usize = 64;
pub(crate) const AES_KEY_SIZE: usize = 32;
pub(crate) const AES_HEADER_CHECK_KEY_SIZE: usize = 16;
pub(crate) const AES_HEADER_PROTECTION_KEY_SIZE: usize = 16;
pub(crate) const AES_GCM_TAG_SIZE: usize = 16;
pub(crate) const AES_GCM_NONCE_SIZE: usize = 12;
pub(crate) const AES_CTR_NONCE_SIZE: usize = 12;
@ -62,14 +70,14 @@ pub(crate) struct AliceNoiseXKInit {
// -- start AES-CTR(es) encrypted section (IV is last 12 bytes of alice_noise_e))
pub alice_session_id: [u8; SessionId::SIZE],
pub alice_hk_public: [u8; KYBER_PUBLICKEYBYTES],
pub header_check_cipher_key: [u8; AES_HEADER_CHECK_KEY_SIZE],
pub header_protection_key: [u8; AES_HEADER_PROTECTION_KEY_SIZE],
// -- end encrypted section
pub hmac_es: [u8; HMAC_SHA384_SIZE],
}
impl AliceNoiseXKInit {
pub const ENC_START: usize = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE;
pub const AUTH_START: usize = Self::ENC_START + SessionId::SIZE + KYBER_PUBLICKEYBYTES + AES_HEADER_CHECK_KEY_SIZE;
pub const AUTH_START: usize = Self::ENC_START + SessionId::SIZE + KYBER_PUBLICKEYBYTES + AES_HEADER_PROTECTION_KEY_SIZE;
pub const SIZE: usize = Self::AUTH_START + HMAC_SHA384_SIZE;
}
@ -100,7 +108,7 @@ impl BobNoiseXKAck {
pub(crate) struct AliceNoiseXKAck {
pub header: [u8; HEADER_SIZE],
pub session_protocol_version: u8,
// -- start AES-CTR(es_ee) encrypted section (IV is first 12 bytes of SHA384(hk))
// -- start AES-CTR(es_ee) encrypted section (IV is first 12 bytes of hk)
pub alice_static_blob_length: [u8; 2],
pub alice_static_blob: [u8; ???],
pub alice_metadata_length: [u8; 2],
@ -111,27 +119,43 @@ pub(crate) struct AliceNoiseXKAck {
}
*/
pub(crate) const ALICE_NOISE_XK_ACK_ENC_START: usize = HEADER_SIZE + 1;
pub(crate) const ALICE_NOISE_XK_ACK_AUTH_SIZE: usize = HMAC_SHA384_SIZE + HMAC_SHA384_SIZE;
pub(crate) const ALICE_NOISE_XK_ACK_MIN_SIZE: usize = ALICE_NOISE_XK_ACK_ENC_START + 2 + 2 + ALICE_NOISE_XK_ACK_AUTH_SIZE;
#[allow(unused)]
#[repr(C, packed)]
pub(crate) struct AliceRekeyInit {
pub header: [u8; HEADER_SIZE],
// -- start AES-GCM encrypted portion (using current key)
pub alice_noise_e: [u8; P384_PUBLIC_KEY_SIZE],
pub alice_e: [u8; P384_PUBLIC_KEY_SIZE],
// -- end AES-GCM encrypted portion
pub gcm_mac: [u8; AES_GCM_TAG_SIZE],
}
impl AliceRekeyInit {
pub const ENC_START: usize = HEADER_SIZE;
pub const AUTH_START: usize = Self::ENC_START + P384_PUBLIC_KEY_SIZE;
pub const SIZE: usize = Self::AUTH_START + AES_GCM_TAG_SIZE;
}
#[allow(unused)]
#[repr(C, packed)]
pub(crate) struct BobRekeyAck {
pub header: [u8; HEADER_SIZE],
// -- start AES-GCM encrypted portion (using current key)
pub bob_noise_e: [u8; P384_PUBLIC_KEY_SIZE],
pub ee_fingerprint: [u8; SHA384_HASH_SIZE],
pub bob_e: [u8; P384_PUBLIC_KEY_SIZE],
pub next_key_fingerprint: [u8; SHA384_HASH_SIZE],
// -- end AES-GCM encrypted portion
pub gcm_mac: [u8; AES_GCM_TAG_SIZE],
}
impl BobRekeyAck {
pub const ENC_START: usize = HEADER_SIZE;
pub const AUTH_START: usize = Self::ENC_START + P384_PUBLIC_KEY_SIZE + SHA384_HASH_SIZE;
pub const SIZE: usize = Self::AUTH_START + AES_GCM_TAG_SIZE;
}
/// Assemble a series of fragments into a buffer and return the length of the assembled packet in bytes.
pub(crate) fn assemble_fragments_into<A: ApplicationLayer>(fragments: &[A::IncomingPacketBuffer], d: &mut [u8]) -> Result<usize, Error> {
let mut l = 0;

File diff suppressed because it is too large Load diff