use wirefeather::crypto::{self, Key, TAG_LEN};
use x25519_dalek::{PublicKey, StaticSecret};
use zeroize::Zeroizing;
use crate::noise::{CAPABILITY_VERSION, Error};
const PROTOCOL_NAME: &[u8] = b"Noise_IK_25519_ChaChaPoly_BLAKE2s";
const PROLOGUE_PREFIX: &[u8] = b"Tailscale Control Protocol v";
const PROLOGUE_MAX: usize = 28 + 5;
pub const TYPE_INITIATION: u8 = 1;
pub const TYPE_RESPONSE: u8 = 2;
pub const INITIATION_LEN: usize = 5 + 32 + (32 + TAG_LEN) + TAG_LEN;
pub const RESPONSE_LEN: usize = 3 + 32 + TAG_LEN;
const INITIATION_BODY_LEN: u16 = (INITIATION_LEN - 5) as u16;
const RESPONSE_BODY_LEN: u16 = (RESPONSE_LEN - 3) as u16;
const HANDSHAKE_NONCE: crypto::Nonce = [0u8; crypto::NONCE_LEN];
fn prologue(version: u16, out: &mut [u8; PROLOGUE_MAX]) -> &[u8] {
out[..PROLOGUE_PREFIX.len()].copy_from_slice(PROLOGUE_PREFIX);
let mut len = PROLOGUE_PREFIX.len();
let mut digits = [0u8; 5];
let mut count = 0;
let mut value = version;
loop {
digits[count] = b'0' + (value % 10) as u8;
count += 1;
value /= 10;
if value == 0 {
break;
}
}
for i in 0..count {
out[len] = digits[count - 1 - i];
len += 1;
}
&out[..len]
}
pub struct Handshake {
machine_private: StaticSecret,
ephemeral: StaticSecret,
chaining_key: Zeroizing<Key>,
hash: Key,
}
pub fn initiate(
machine_private: &StaticSecret,
server_public: &PublicKey,
ephemeral_private: &StaticSecret,
out: &mut [u8],
) -> Result<(usize, Handshake), Error> {
let out = out.get_mut(..INITIATION_LEN).ok_or(Error::BufferTooSmall)?;
let mut chaining_key = Zeroizing::new(crypto::hash(&[PROTOCOL_NAME]));
let mut hash = *chaining_key;
let mut prologue_buffer = [0u8; PROLOGUE_MAX];
hash = crypto::hash(&[&hash, prologue(CAPABILITY_VERSION, &mut prologue_buffer)]);
hash = crypto::hash(&[&hash, server_public.as_bytes()]);
out[..2].copy_from_slice(&CAPABILITY_VERSION.to_be_bytes());
out[2] = TYPE_INITIATION;
out[3..5].copy_from_slice(&INITIATION_BODY_LEN.to_be_bytes());
let ephemeral_public = PublicKey::from(ephemeral_private);
out[5..37].copy_from_slice(ephemeral_public.as_bytes());
hash = crypto::hash(&[&hash, ephemeral_public.as_bytes()]);
let (ck, key) = crypto::kdf2(
&chaining_key,
&crypto::dh(ephemeral_private, server_public).map_err(|_| Error::InvalidPublicKey)?,
);
*chaining_key = ck;
let machine_public = PublicKey::from(machine_private);
out[37..69].copy_from_slice(machine_public.as_bytes());
let tag = crypto::aead_seal_nonce(&key, &HANDSHAKE_NONCE, &hash, &mut out[37..69]);
out[69..85].copy_from_slice(&tag);
hash = crypto::hash(&[&hash, &out[37..85]]);
let (ck, key) = crypto::kdf2(
&chaining_key,
&crypto::dh(machine_private, server_public).map_err(|_| Error::InvalidPublicKey)?,
);
*chaining_key = ck;
let tag = crypto::aead_seal_nonce(&key, &HANDSHAKE_NONCE, &hash, &mut []);
out[85..101].copy_from_slice(&tag);
hash = crypto::hash(&[&hash, &out[85..101]]);
Ok((
INITIATION_LEN,
Handshake {
machine_private: machine_private.clone(),
ephemeral: ephemeral_private.clone(),
chaining_key,
hash,
},
))
}
impl Handshake {
pub fn consume_response(self, msg: &[u8]) -> Result<crate::noise::Session, Error> {
if msg.len() != RESPONSE_LEN || msg[0] != TYPE_RESPONSE {
return Err(Error::Malformed);
}
if u16::from_be_bytes([msg[1], msg[2]]) != RESPONSE_BODY_LEN {
return Err(Error::Malformed);
}
let mut chaining_key = self.chaining_key;
let mut hash = self.hash;
let mut peer_ephemeral = [0u8; 32];
peer_ephemeral.copy_from_slice(&msg[3..35]);
let peer_ephemeral = PublicKey::from(peer_ephemeral);
hash = crypto::hash(&[&hash, peer_ephemeral.as_bytes()]);
let (ck, _) = crypto::kdf2(
&chaining_key,
&crypto::dh(&self.ephemeral, &peer_ephemeral).map_err(|_| Error::InvalidPublicKey)?,
);
*chaining_key = ck;
let (ck, key) = crypto::kdf2(
&chaining_key,
&crypto::dh(&self.machine_private, &peer_ephemeral)
.map_err(|_| Error::InvalidPublicKey)?,
);
*chaining_key = ck;
crypto::aead_open_nonce(&key, &HANDSHAKE_NONCE, &hash, &mut [], &msg[35..51])
.map_err(|_| Error::Decryption)?;
let (tx, rx) = crypto::kdf2(&chaining_key, &[]);
Ok(crate::noise::Session::new(tx, rx))
}
}
#[cfg(test)]
pub(crate) fn respond_for_test(
server_private: &StaticSecret,
server_ephemeral: &StaticSecret,
initiation: &[u8],
) -> ([u8; RESPONSE_LEN], crate::noise::Session) {
assert_eq!(initiation.len(), INITIATION_LEN);
let server_public = PublicKey::from(server_private);
let mut chaining_key = crypto::hash(&[PROTOCOL_NAME]);
let mut hash = chaining_key;
let mut prologue_buffer = [0u8; PROLOGUE_MAX];
hash = crypto::hash(&[&hash, prologue(CAPABILITY_VERSION, &mut prologue_buffer)]);
hash = crypto::hash(&[&hash, server_public.as_bytes()]);
let mut initiator_ephemeral = [0u8; 32];
initiator_ephemeral.copy_from_slice(&initiation[5..37]);
let initiator_ephemeral = PublicKey::from(initiator_ephemeral);
hash = crypto::hash(&[&hash, initiator_ephemeral.as_bytes()]);
let (ck, key) = crypto::kdf2(
&chaining_key,
&crypto::dh(server_private, &initiator_ephemeral).unwrap(),
);
chaining_key = ck;
let mut initiator_static = [0u8; 32];
initiator_static.copy_from_slice(&initiation[37..69]);
crypto::aead_open_nonce(
&key,
&HANDSHAKE_NONCE,
&hash,
&mut initiator_static,
&initiation[69..85],
)
.unwrap();
hash = crypto::hash(&[&hash, &initiation[37..85]]);
let initiator_static = PublicKey::from(initiator_static);
let (ck, key) = crypto::kdf2(
&chaining_key,
&crypto::dh(server_private, &initiator_static).unwrap(),
);
chaining_key = ck;
crypto::aead_open_nonce(&key, &HANDSHAKE_NONCE, &hash, &mut [], &initiation[85..101]).unwrap();
hash = crypto::hash(&[&hash, &initiation[85..101]]);
let mut response = [0u8; RESPONSE_LEN];
response[0] = TYPE_RESPONSE;
response[1..3].copy_from_slice(&RESPONSE_BODY_LEN.to_be_bytes());
let ephemeral_public = PublicKey::from(server_ephemeral);
response[3..35].copy_from_slice(ephemeral_public.as_bytes());
hash = crypto::hash(&[&hash, ephemeral_public.as_bytes()]);
let (ck, _) = crypto::kdf2(
&chaining_key,
&crypto::dh(server_ephemeral, &initiator_ephemeral).unwrap(),
);
chaining_key = ck;
let (ck, key) = crypto::kdf2(
&chaining_key,
&crypto::dh(server_ephemeral, &initiator_static).unwrap(),
);
chaining_key = ck;
let tag = crypto::aead_seal_nonce(&key, &HANDSHAKE_NONCE, &hash, &mut []);
response[35..51].copy_from_slice(&tag);
let (initiator_tx, initiator_rx) = crypto::kdf2(&chaining_key, &[]);
(
response,
crate::noise::Session::new(initiator_rx, initiator_tx),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_prologue_matches_what_the_capture_shows() {
let mut buffer = [0u8; PROLOGUE_MAX];
assert_eq!(
prologue(131, &mut buffer),
b"Tailscale Control Protocol v131"
);
assert_eq!(prologue(1, &mut buffer), b"Tailscale Control Protocol v1");
assert_eq!(
prologue(65535, &mut buffer),
b"Tailscale Control Protocol v65535"
);
}
#[test]
fn the_protocol_name_is_the_length_that_forces_hashing() {
assert_eq!(PROTOCOL_NAME.len(), 33);
}
#[test]
fn the_message_lengths_match_the_capture() {
assert_eq!(INITIATION_LEN, 101);
assert_eq!(RESPONSE_LEN, 51);
assert_eq!(INITIATION_BODY_LEN, 96);
assert_eq!(RESPONSE_BODY_LEN, 48);
}
#[test]
fn an_initiation_has_the_header_the_capture_shows() {
let machine = StaticSecret::from([0x11; 32]);
let server = PublicKey::from([0x22; 32]);
let ephemeral = StaticSecret::from([0x33; 32]);
let mut out = [0u8; 256];
let (len, _) = initiate(&machine, &server, &ephemeral, &mut out).unwrap();
assert_eq!(len, 101);
assert_eq!(u16::from_be_bytes([out[0], out[1]]), 131);
assert_eq!(out[2], TYPE_INITIATION);
assert_eq!(u16::from_be_bytes([out[3], out[4]]), 96);
assert_eq!(&out[5..37], PublicKey::from(&ephemeral).as_bytes());
assert_ne!(&out[37..69], PublicKey::from(&machine).as_bytes());
}
#[test]
fn a_response_of_the_wrong_shape_is_rejected_before_any_crypto() {
let machine = StaticSecret::from([0x11; 32]);
let server = PublicKey::from([0x22; 32]);
let ephemeral = StaticSecret::from([0x33; 32]);
let mut out = [0u8; 256];
let (_, handshake) = initiate(&machine, &server, &ephemeral, &mut out).unwrap();
assert_eq!(
handshake.consume_response(&[2, 0, 48]).err(),
Some(Error::Malformed)
);
let (_, handshake) = initiate(&machine, &server, &ephemeral, &mut out).unwrap();
let mut response = [0u8; RESPONSE_LEN];
response[0] = TYPE_INITIATION; response[1..3].copy_from_slice(&RESPONSE_BODY_LEN.to_be_bytes());
assert_eq!(
handshake.consume_response(&response).err(),
Some(Error::Malformed)
);
let (_, handshake) = initiate(&machine, &server, &ephemeral, &mut out).unwrap();
response[0] = TYPE_RESPONSE;
assert_eq!(
handshake.consume_response(&response).err(),
Some(Error::InvalidPublicKey)
);
let (_, handshake) = initiate(&machine, &server, &ephemeral, &mut out).unwrap();
response[3..35]
.copy_from_slice(PublicKey::from(&StaticSecret::from([0x44; 32])).as_bytes());
assert_eq!(
handshake.consume_response(&response).err(),
Some(Error::Decryption)
);
}
}