diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index d2812429f..685bccde7 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -3,6 +3,7 @@ pub mod aes; pub mod aes_gmac_siv; pub mod hash; +pub mod mimcvdf; pub mod p384; pub mod poly1305; pub mod random; diff --git a/crypto/src/mimcvdf.rs b/crypto/src/mimcvdf.rs new file mode 100644 index 000000000..7e77fd114 --- /dev/null +++ b/crypto/src/mimcvdf.rs @@ -0,0 +1,148 @@ +/* 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/ + */ + +/* + * https://eprint.iacr.org/2016/492.pdf + * https://vitalik.ca/general/2018/07/21/starks_part_3.html + */ + +// 2^127 - 39 +const PRIME: u128 = 170141183460469231731687303715884105689; +// 2p-1/3 +const PRIME_2P_MINUS_1_DIV_3: u128 = 113427455640312821154458202477256070459; + +const K_COUNT_MASK: usize = 63; +const K: [u64; 64] = [ + 0x921cdfd99022340f, + 0xe7c65f78c70afaa8, + 0x72793744494c4fda, + 0x67759e2688bc9c0a, + 0x7681a224661f0ac0, + 0xa7b81b099925a2bf, + 0x16d43792e66b030a, + 0x841bd90742d26ee9, + 0xb1346ec08db97053, + 0xd044229c1173d972, + 0xf4813498dfdead0e, + 0xe46dca4c237d2c28, + 0xac64872778089599, + 0x67be75af74416e74, + 0xb9dec3aefd3ae012, + 0xf0497147953c4276, + 0xf6ac07fd3944177d, + 0xccf1c28813eb589b, + 0x49abb5e2b0bff5bd, + 0xd5c15eeb39587d69, + 0x9c6ff50ee6898649, + 0x763f3b25524a0fbf, + 0xa6029c37f715c02c, + 0xe458a5902b2b5629, + 0x8e4d6be6a1ba32c5, + 0x052aba0b61738f20, + 0xc18a6901fa026b12, + 0x137df11cf1dbe811, + 0x5da0310e419be602, + 0xc66ddec578f52891, + 0xe4eae4efc0f0d54f, + 0xf9d488269f118012, + 0xcf9b5108f66e77d1, + 0x443ba29939f5a657, + 0xa4e4b7d28c51e5c2, + 0xe030d1772f112c01, + 0xe136f0cf8da5e172, + 0x3e9ee638f9663dc2, + 0xbc5c1db73e639dfd, + 0xa9fbbaa873fedf73, + 0xffb2a5247d10ab8f, + 0x06e6f3b5ae4b67ac, + 0x475e7d427d331282, + 0xcac6237c40a9d653, + 0xe9a15c1d177beefa, + 0xa14ef2111c2175a3, + 0x8427d4b68982fc21, + 0x12171e2a55d43343, + 0x37715fdea87a0a60, + 0x24bc5d28cff8ecad, + 0x92276e4118304e62, + 0x824b66792f58dd45, + 0xe43973cf253b6947, + 0xd0db2c5a2a4f064d, + 0x734cdb241520ad04, + 0xcec4f2ce5013069e, + 0x2741c83c07bbf9e0, + 0x284be707dcbda1a4, + 0xd602f3d8545799b2, + 0xea3977f56573b4d2, + 0x0723fda64d57d0c6, + 0x04dc344d0dde863a, + 0x7584143462914be4, + 0x111307f7823dfcc6, +]; + +fn mulmod(mut a: u128, mut b: u128) -> u128 { + let mut res: u128 = 0; + a %= M; + loop { + if (b & 1) != 0 { + res = res.wrapping_add(a) % M; + } + b = b.wrapping_shr(1); + if b != 0 { + a = a.wrapping_shl(1) % M; + } else { + return res; + } + } +} + +#[inline(always)] +fn powmod(mut base: u128, mut exp: u128) -> u128 { + let mut res: u128 = 1; + loop { + if (exp & 1) != 0 { + res = mulmod::(base, res); + } + exp = exp.wrapping_shr(1); + if exp != 0 { + base = mulmod::(base, base); + } else { + return res; + } + } +} + +pub fn delay(mut input: u128, rounds: usize) -> u128 { + debug_assert!(rounds > 0); + input %= PRIME; + for r in 1..(rounds + 1) { + input = powmod::(input ^ (K[(rounds - r) & K_COUNT_MASK] as u128), PRIME_2P_MINUS_1_DIV_3); + } + input +} + +pub fn verify(mut proof: u128, expected: u128, rounds: usize) -> bool { + debug_assert!(rounds > 0); + for r in 0..rounds { + proof = mulmod::(proof, mulmod::(proof, proof)) ^ (K[r & K_COUNT_MASK] as u128); + } + proof == (expected % PRIME) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn delay_and_verify() { + for i in 1..5 { + let input = (crate::random::xorshift64_random() as u128).wrapping_mul(crate::random::xorshift64_random() as u128); + let proof = delay(input, i * 3); + assert!(verify(proof, input, i * 3)); + } + } +} diff --git a/zssp/src/proto.rs b/zssp/src/proto.rs index e01c60acd..b52ee778b 100644 --- a/zssp/src/proto.rs +++ b/zssp/src/proto.rs @@ -22,8 +22,6 @@ pub(crate) const SESSION_PROTOCOL_VERSION: u8 = 0x00; pub(crate) const COUNTER_WINDOW_MAX_OOO: usize = 16; pub(crate) const COUNTER_WINDOW_MAX_SKIP_AHEAD: u64 = 16777216; -pub(crate) const NOISE_MAX_HANDSHAKE_PACKET_SIZE: usize = 2048; - pub(crate) const PACKET_TYPE_DATA: u8 = 0; pub(crate) const PACKET_TYPE_ALICE_NOISE_XK_INIT: u8 = 1; pub(crate) const PACKET_TYPE_BOB_NOISE_XK_ACK: u8 = 2; @@ -41,7 +39,8 @@ pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB: u8 = b'A'; // AES-G pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE: u8 = b'B'; // AES-GCM in B->A direction pub(crate) const MAX_FRAGMENTS: usize = 48; // hard protocol max: 63 -pub(crate) const KEY_EXCHANGE_MAX_FRAGMENTS: usize = 8; // enough room for p384 + ZT identity + kyber1024 + tag/hmac/etc. +pub(crate) const MAX_NOISE_HANDSHAKE_FRAGMENTS: usize = 16; // enough room for p384 + ZT identity + kyber1024 + tag/hmac/etc. +pub(crate) const MAX_NOISE_HANDSHAKE_SIZE: usize = MAX_NOISE_HANDSHAKE_FRAGMENTS * MIN_TRANSPORT_MTU; pub(crate) const AES_KEY_SIZE: usize = 32; pub(crate) const AES_HEADER_CHECK_KEY_SIZE: usize = 16; @@ -80,13 +79,12 @@ pub(crate) struct BobNoiseXKAck { pub bob_hk_ciphertext: [u8; KYBER_CIPHERTEXTBYTES], // -- end encrypted sectiion pub hmac_es_ee: [u8; HMAC_SHA384_SIZE], - pub hmac_es_ee_se_hk_psk: [u8; HMAC_SHA384_SIZE], } impl BobNoiseXKAck { pub const ENC_START: usize = HEADER_SIZE + 1 + P384_PUBLIC_KEY_SIZE; pub const AUTH_START: usize = Self::ENC_START + SessionId::SIZE + KYBER_CIPHERTEXTBYTES; - pub const SIZE: usize = Self::AUTH_START + HMAC_SHA384_SIZE + HMAC_SHA384_SIZE; + pub const SIZE: usize = Self::AUTH_START + HMAC_SHA384_SIZE; } /* diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index bea51742e..e182f83d0 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -23,7 +23,6 @@ use zerotier_crypto::{random, secure_eq}; use zerotier_utils::gatherarray::GatherArray; use zerotier_utils::memory; use zerotier_utils::ringbuffermap::RingBufferMap; -use zerotier_utils::unlikely_branch; use pqc_kyber::{KYBER_SECRETKEYBYTES, KYBER_SSBYTES}; @@ -38,7 +37,8 @@ use crate::sessionid::SessionId; /// Each application using ZSSP must create an instance of this to own sessions and /// defragment incoming packets that are not yet associated with a session. pub struct Context { - initial_offer_defrag: Mutex, 1024, 1024>>, + initial_offer_defrag: + Mutex, 1024, 1024>>, sessions: RwLock>, } @@ -87,6 +87,7 @@ struct SessionMaps { incomplete: HashMap>, } +/// State for an incoming incomplete Noise_XK session that isn't fully negotiated yet. struct NoiseXKIncoming { timestamp: i64, alice_session_id: SessionId, @@ -308,7 +309,6 @@ impl Context { ) -> Result, Error> { let incoming_packet: &mut [u8] = incoming_packet_buf.as_mut(); if incoming_packet.len() < MIN_PACKET_SIZE { - unlikely_branch(); return Err(Error::InvalidPacket); } @@ -345,7 +345,6 @@ impl Context { return Ok(ReceiveResult::Ok); } } else { - unlikely_branch(); return Err(Error::InvalidPacket); } } else { @@ -365,11 +364,9 @@ impl Context { ); } } else { - unlikely_branch(); return Ok(ReceiveResult::Ignored); } } else { - unlikely_branch(); if let Some(p) = self.sessions.read().unwrap().incomplete.get(&local_session_id).cloned() { Aes::new(p.header_check_cipher_key.as_bytes()) .decrypt_block_in_place(&mut incoming_packet[HEADER_CHECK_ENCRYPT_START..HEADER_CHECK_ENCRYPT_END]); @@ -378,12 +375,12 @@ impl Context { return Err(Error::UnknownLocalSessionId(local_session_id)); } } - } else { - unlikely_branch(); } - let (key_index, packet_type, fragment_count, fragment_no, counter) = parse_packet_header(&incoming_packet); + // If we make it here the packet is not associated with a session or is associated with an + // incomplete session (Noise_XK mid-negotiation). + let (key_index, packet_type, fragment_count, fragment_no, counter) = parse_packet_header(&incoming_packet); if fragment_count > 1 { let mut defrag = self.initial_offer_defrag.lock().unwrap(); let fragment_gather_array = defrag.get_or_create_mut(&counter, || GatherArray::new(fragment_count)); @@ -424,10 +421,6 @@ impl Context { return Ok(ReceiveResult::Ok); } - /// Called internally when all fragments of a packet are received. - /// - /// NOTE: header check codes will already have been validated on receipt of each fragment. AEAD authentication - /// and decryption has NOT yet been performed, and is done here. fn receive_complete< 'b, SendFunction: FnMut(Option<&Arc>>, &mut [u8]), @@ -466,7 +459,6 @@ impl Context { let current_frag_data_start = data_len; data_len += f.len() - HEADER_SIZE; if data_len > data_buf.len() { - unlikely_branch(); session_key.return_receive_cipher(c); return Err(Error::DataBufferTooSmall); } @@ -477,12 +469,10 @@ impl Context { let current_frag_data_start = data_len; let last_fragment = fragments.last().unwrap().as_ref(); if last_fragment.len() < (HEADER_SIZE + AES_GCM_TAG_SIZE) { - unlikely_branch(); return Err(Error::InvalidPacket); } data_len += last_fragment.len() - (HEADER_SIZE + AES_GCM_TAG_SIZE); if data_len > data_buf.len() { - unlikely_branch(); session_key.return_receive_cipher(c); return Err(Error::DataBufferTooSmall); } @@ -492,8 +482,7 @@ impl Context { &mut data_buf[current_frag_data_start..data_len], ); - let gcm_tag = &last_fragment[payload_end..]; - let aead_authentication_ok = c.finish_decrypt(gcm_tag); + let aead_authentication_ok = c.finish_decrypt(&last_fragment[payload_end..]); session_key.return_receive_cipher(c); if aead_authentication_ok { @@ -504,7 +493,6 @@ impl Context { if session_key.confirmed { drop(state); } else { - unlikely_branch(); let key_created_at_counter = session_key.created_at_counter; drop(state); @@ -523,21 +511,18 @@ impl Context { return Ok(ReceiveResult::OkData(session, &mut data_buf[..data_len])); } else { - unlikely_branch(); return Ok(ReceiveResult::Ignored); } } } + return Err(Error::FailedAuthentication); } else { - unlikely_branch(); return Err(Error::SessionNotEstablished); } } else { - unlikely_branch(); - - // For KEX packets go ahead and pre-assemble all fragments to simplify the code below. - let mut pkt_assembly_buffer = [0u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE]; + // For Noise setup/KEX packets go ahead and pre-assemble all fragments to simplify the code below. + let mut pkt_assembly_buffer = [0u8; MAX_NOISE_HANDSHAKE_SIZE]; let pkt_assembled_size = assemble_fragments_into::(fragments, &mut pkt_assembly_buffer)?; if pkt_assembled_size < MIN_PACKET_SIZE { return Err(Error::InvalidPacket); @@ -572,7 +557,6 @@ impl Context { } let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?; - let alice_noise_e = P384PublicKey::from_bytes(&pkt.alice_noise_e).ok_or(Error::FailedAuthentication)?; let noise_es = app.get_local_s_keypair().agree(&alice_noise_e).ok_or(Error::FailedAuthentication)?; @@ -596,8 +580,8 @@ impl Context { let mut ctr = AesCtr::new(kbkdf::(noise_es.as_bytes()).as_bytes()); ctr.reset_set_iv(&SHA384::hash(&pkt.alice_noise_e)[..AES_CTR_NONCE_SIZE]); ctr.crypt_in_place(&mut pkt_assembled[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START]); - let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?; + let pkt: &AliceNoiseXKInit = byte_array_as_proto_buffer(pkt_assembled)?; let alice_session_id = SessionId::new_from_bytes(&pkt.alice_session_id).ok_or(Error::InvalidPacket)?; // Create Bob's ephemeral keys and derive noise_es_ee by agreeing with Alice's. Also create @@ -639,7 +623,7 @@ impl Context { newest_id = Some(*id); } } - sessions.incomplete.remove(newest_id.as_ref().unwrap()); + let _ = sessions.incomplete.remove(newest_id.as_ref().unwrap()); } sessions.incomplete.insert( @@ -672,7 +656,7 @@ impl Context { ctr.reset_set_iv(&bob_noise_e[P384_PUBLIC_KEY_SIZE - AES_CTR_NONCE_SIZE..]); ctr.crypt_in_place(&mut reply_buffer[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START]); - // Add HMAC-SHA384 to reply packet, allowing Alice to derive noise_es_ee and authenticate. + // Add HMAC-SHA384 to reply packet. let reply_hmac = hmac_sha384_2( kbkdf::(noise_es_ee.as_bytes()).as_bytes(), &create_message_nonce(PACKET_TYPE_BOB_NOISE_XK_ACK, 1), @@ -763,28 +747,17 @@ impl Context { noise_es_ee_se_hk_psk.as_bytes(), ); - // Authenticate entire key exchange. - if !secure_eq( - &pkt.hmac_es_ee_se_hk_psk, - &hmac_sha384_2( - noise_es_ee_se_hk_psk_hmac_key.as_bytes(), - &message_nonce, - &pkt_assembled[HEADER_SIZE..BobNoiseXKAck::AUTH_START + HMAC_SHA384_SIZE], - ), - ) { - return Err(Error::FailedAuthentication); - } - let reply_counter = session.get_next_outgoing_counter().ok_or(Error::MaxKeyLifetimeExceeded)?; let reply_message_nonce = create_message_nonce(PACKET_TYPE_ALICE_NOISE_XK_ACK, reply_counter.get()); // Create reply informing Bob of our static identity now that we've verified Bob and set // up forward secrecy. Also return Bob's opaque note. - let mut reply_buffer = [0u8; NOISE_MAX_HANDSHAKE_PACKET_SIZE]; + let mut reply_buffer = [0u8; MAX_NOISE_HANDSHAKE_SIZE]; reply_buffer[HEADER_SIZE] = SESSION_PROTOCOL_VERSION; let mut reply_len = HEADER_SIZE + 1; let mut reply_buffer_append = |b: &[u8]| { let reply_len_new = reply_len + b.len(); + debug_assert!(reply_len_new <= MAX_NOISE_HANDSHAKE_SIZE); reply_buffer[reply_len..reply_len_new].copy_from_slice(b); reply_len = reply_len_new; }; @@ -1040,7 +1013,7 @@ impl Session { u64::from(remote_session_id), state.current_key, counter, - )?; + ); c.crypt(&data[..chunk_size], &mut mtu_sized_buffer[HEADER_SIZE..fragment_size]); data = &data[chunk_size..]; if fragment_no == last_fragment_no { @@ -1058,8 +1031,6 @@ impl Session { session_key.return_send_cipher(c); return Ok(()); - } else { - unlikely_branch(); } } return Err(Error::SessionNotEstablished); @@ -1101,34 +1072,30 @@ fn set_packet_header( recipient_session_id: u64, key_index: usize, counter: u64, -) -> Result<(), Error> { +) { debug_assert!(packet.len() >= MIN_PACKET_SIZE); debug_assert!(fragment_count > 0); + debug_assert!(fragment_count <= MAX_FRAGMENTS); debug_assert!(fragment_no < MAX_FRAGMENTS); debug_assert!(packet_type <= 0x0f); // packet type is 4 bits - if fragment_count <= MAX_FRAGMENTS { - // [0-47] recipient session ID - // -- start of header check cipher single block encrypt -- - // [48-48] key index (least significant bit) - // [49-51] packet type (0-15) - // [52-57] fragment count (1..64 - 1, so 0 means 1 fragment) - // [58-63] fragment number (0..63) - // [64-127] 64-bit counter - memory::store_raw( - (u64::from(recipient_session_id) - | ((key_index & 1) as u64).wrapping_shl(48) - | (packet_type as u64).wrapping_shl(49) - | ((fragment_count - 1) as u64).wrapping_shl(52) - | (fragment_no as u64).wrapping_shl(58)) - .to_le(), - packet, - ); - memory::store_raw(counter.to_le(), &mut packet[8..]); - Ok(()) - } else { - unlikely_branch(); - Err(Error::DataTooLarge) - } + + // [0-47] recipient session ID + // -- start of header check cipher single block encrypt -- + // [48-48] key index (least significant bit) + // [49-51] packet type (0-15) + // [52-57] fragment count (1..64 - 1, so 0 means 1 fragment) + // [58-63] fragment number (0..63) + // [64-127] 64-bit counter + memory::store_raw( + (u64::from(recipient_session_id) + | ((key_index & 1) as u64).wrapping_shl(48) + | (packet_type as u64).wrapping_shl(49) + | ((fragment_count - 1) as u64).wrapping_shl(52) + | (fragment_no as u64).wrapping_shl(58)) + .to_le(), + packet, + ); + memory::store_raw(counter.to_le(), &mut packet[8..]); } #[inline(always)] @@ -1187,7 +1154,7 @@ fn send_with_fragmentation( recipient_session_id, key_index, counter, - )?; + ); if let Some(hcc) = header_check_cipher { hcc.encrypt_block_in_place(&mut fragment[6..22]); } @@ -1232,8 +1199,6 @@ impl SessionKey { .pop() .unwrap_or_else(|| Box::new(AesGcm::new(self.send_key.as_bytes(), true)))) } else { - unlikely_branch(); - // Not only do we return an error, but we also destroy the key. let mut scp = self.send_cipher_pool.lock().unwrap(); scp.clear();