aboutsummaryrefslogtreecommitdiffstats
path: root/src/wireguard/handshake/macs.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/wireguard/handshake/macs.rs')
-rw-r--r--src/wireguard/handshake/macs.rs327
1 files changed, 327 insertions, 0 deletions
diff --git a/src/wireguard/handshake/macs.rs b/src/wireguard/handshake/macs.rs
new file mode 100644
index 0000000..689826b
--- /dev/null
+++ b/src/wireguard/handshake/macs.rs
@@ -0,0 +1,327 @@
+use generic_array::GenericArray;
+use rand::{CryptoRng, RngCore};
+use spin::RwLock;
+use std::time::{Duration, Instant};
+
+// types to coalesce into bytes
+use std::net::SocketAddr;
+use x25519_dalek::PublicKey;
+
+// AEAD
+use aead::{Aead, NewAead, Payload};
+use chacha20poly1305::XChaCha20Poly1305;
+
+// MAC
+use blake2::Blake2s;
+use subtle::ConstantTimeEq;
+
+use super::messages::{CookieReply, MacsFooter, TYPE_COOKIE_REPLY};
+use super::types::HandshakeError;
+
+const LABEL_MAC1: &[u8] = b"mac1----";
+const LABEL_COOKIE: &[u8] = b"cookie--";
+
+const SIZE_COOKIE: usize = 16;
+const SIZE_SECRET: usize = 32;
+const SIZE_MAC: usize = 16; // blake2s-mac128
+const SIZE_TAG: usize = 16; // xchacha20poly1305 tag
+
+const COOKIE_UPDATE_INTERVAL: Duration = Duration::from_secs(120);
+
+macro_rules! HASH {
+ ( $($input:expr),* ) => {{
+ use blake2::Digest;
+ let mut hsh = Blake2s::new();
+ $(
+ hsh.input($input);
+ )*
+ hsh.result()
+ }};
+}
+
+macro_rules! MAC {
+ ( $key:expr, $($input:expr),* ) => {{
+ use blake2::VarBlake2s;
+ use digest::Input;
+ use digest::VariableOutput;
+ let mut tag = [0u8; SIZE_MAC];
+ let mut mac = VarBlake2s::new_keyed($key, SIZE_MAC);
+ $(
+ mac.input($input);
+ )*
+ mac.variable_result(|buf| tag.copy_from_slice(buf));
+ tag
+ }};
+}
+
+macro_rules! XSEAL {
+ ($key:expr, $nonce:expr, $ad:expr, $pt:expr, $ct:expr) => {{
+ let ct = XChaCha20Poly1305::new(*GenericArray::from_slice($key))
+ .encrypt(
+ GenericArray::from_slice($nonce),
+ Payload { msg: $pt, aad: $ad },
+ )
+ .unwrap();
+ debug_assert_eq!(ct.len(), $pt.len() + SIZE_TAG);
+ $ct.copy_from_slice(&ct);
+ }};
+}
+
+macro_rules! XOPEN {
+ ($key:expr, $nonce:expr, $ad:expr, $pt:expr, $ct:expr) => {{
+ debug_assert_eq!($ct.len(), $pt.len() + SIZE_TAG);
+ XChaCha20Poly1305::new(*GenericArray::from_slice($key))
+ .decrypt(
+ GenericArray::from_slice($nonce),
+ Payload { msg: $ct, aad: $ad },
+ )
+ .map_err(|_| HandshakeError::DecryptionFailure)
+ .map(|pt| $pt.copy_from_slice(&pt))
+ }};
+}
+
+struct Cookie {
+ value: [u8; 16],
+ birth: Instant,
+}
+
+pub struct Generator {
+ mac1_key: [u8; 32],
+ cookie_key: [u8; 32], // xchacha20poly key for opening cookie response
+ last_mac1: Option<[u8; 16]>,
+ cookie: Option<Cookie>,
+}
+
+fn addr_to_mac_bytes(addr: &SocketAddr) -> Vec<u8> {
+ match addr {
+ SocketAddr::V4(addr) => {
+ let mut res = Vec::with_capacity(4 + 2);
+ res.extend(&addr.ip().octets());
+ res.extend(&addr.port().to_le_bytes());
+ res
+ }
+ SocketAddr::V6(addr) => {
+ let mut res = Vec::with_capacity(16 + 2);
+ res.extend(&addr.ip().octets());
+ res.extend(&addr.port().to_le_bytes());
+ res
+ }
+ }
+}
+
+impl Generator {
+ /// Initalize a new mac field generator
+ ///
+ /// # Arguments
+ ///
+ /// - pk: The public key of the peer to which the generator is associated
+ ///
+ /// # Returns
+ ///
+ /// A freshly initated generator
+ pub fn new(pk: PublicKey) -> Generator {
+ Generator {
+ mac1_key: HASH!(LABEL_MAC1, pk.as_bytes()).into(),
+ cookie_key: HASH!(LABEL_COOKIE, pk.as_bytes()).into(),
+ last_mac1: None,
+ cookie: None,
+ }
+ }
+
+ /// Process a CookieReply message
+ ///
+ /// # Arguments
+ ///
+ /// - reply: CookieReply to process
+ ///
+ /// # Returns
+ ///
+ /// Can fail if the cookie reply fails to validate
+ /// (either indicating that it is outdated or malformed)
+ pub fn process(&mut self, reply: &CookieReply) -> Result<(), HandshakeError> {
+ let mac1 = self.last_mac1.ok_or(HandshakeError::InvalidState)?;
+ let mut tau = [0u8; SIZE_COOKIE];
+ XOPEN!(
+ &self.cookie_key, // key
+ &reply.f_nonce, // nonce
+ &mac1, // ad
+ &mut tau, // pt
+ &reply.f_cookie // ct || tag
+ )?;
+ self.cookie = Some(Cookie {
+ birth: Instant::now(),
+ value: tau,
+ });
+ Ok(())
+ }
+
+ /// Generate both mac fields for an inner message
+ ///
+ /// # Arguments
+ ///
+ /// - inner: A byteslice representing the inner message to be covered
+ /// - macs: The destination mac footer for the resulting macs
+ pub fn generate(&mut self, inner: &[u8], macs: &mut MacsFooter) {
+ macs.f_mac1 = MAC!(&self.mac1_key, inner);
+ macs.f_mac2 = match &self.cookie {
+ Some(cookie) => {
+ if cookie.birth.elapsed() > COOKIE_UPDATE_INTERVAL {
+ self.cookie = None;
+ [0u8; SIZE_MAC]
+ } else {
+ MAC!(&cookie.value, inner, macs.f_mac1)
+ }
+ }
+ None => [0u8; SIZE_MAC],
+ };
+ self.last_mac1 = Some(macs.f_mac1);
+ }
+}
+
+struct Secret {
+ value: [u8; 32],
+ birth: Instant,
+}
+
+pub struct Validator {
+ mac1_key: [u8; 32], // mac1 key, derived from device public key
+ cookie_key: [u8; 32], // xchacha20poly key for sealing cookie response
+ secret: RwLock<Secret>,
+}
+
+impl Validator {
+ pub fn new(pk: PublicKey) -> Validator {
+ Validator {
+ mac1_key: HASH!(LABEL_MAC1, pk.as_bytes()).into(),
+ cookie_key: HASH!(LABEL_COOKIE, pk.as_bytes()).into(),
+ secret: RwLock::new(Secret {
+ value: [0u8; SIZE_SECRET],
+ birth: Instant::now() - Duration::new(86400, 0),
+ }),
+ }
+ }
+
+ fn get_tau(&self, src: &[u8]) -> Option<[u8; SIZE_COOKIE]> {
+ let secret = self.secret.read();
+ if secret.birth.elapsed() < COOKIE_UPDATE_INTERVAL {
+ Some(MAC!(&secret.value, src))
+ } else {
+ None
+ }
+ }
+
+ fn get_set_tau<R: RngCore + CryptoRng>(&self, rng: &mut R, src: &[u8]) -> [u8; SIZE_COOKIE] {
+ // check if current value is still valid
+ {
+ let secret = self.secret.read();
+ if secret.birth.elapsed() < COOKIE_UPDATE_INTERVAL {
+ return MAC!(&secret.value, src);
+ };
+ }
+
+ // take write lock, check again
+ {
+ let mut secret = self.secret.write();
+ if secret.birth.elapsed() < COOKIE_UPDATE_INTERVAL {
+ return MAC!(&secret.value, src);
+ };
+
+ // set new random cookie secret
+ rng.fill_bytes(&mut secret.value);
+ secret.birth = Instant::now();
+ MAC!(&secret.value, src)
+ }
+ }
+
+ pub fn create_cookie_reply<R: RngCore + CryptoRng>(
+ &self,
+ rng: &mut R,
+ receiver: u32, // receiver id of incoming message
+ src: &SocketAddr, // source address of incoming message
+ macs: &MacsFooter, // footer of incoming message
+ msg: &mut CookieReply, // resulting cookie reply
+ ) {
+ let src = addr_to_mac_bytes(src);
+ msg.f_type.set(TYPE_COOKIE_REPLY as u32);
+ msg.f_receiver.set(receiver);
+ rng.fill_bytes(&mut msg.f_nonce);
+ XSEAL!(
+ &self.cookie_key, // key
+ &msg.f_nonce, // nonce
+ &macs.f_mac1, // ad
+ &self.get_set_tau(rng, &src), // pt
+ &mut msg.f_cookie // ct || tag
+ );
+ }
+
+ /// Check the mac1 field against the inner message
+ ///
+ /// # Arguments
+ ///
+ /// - inner: The inner message covered by the mac1 field
+ /// - macs: The mac footer
+ pub fn check_mac1(&self, inner: &[u8], macs: &MacsFooter) -> Result<(), HandshakeError> {
+ let valid_mac1: bool = MAC!(&self.mac1_key, inner).ct_eq(&macs.f_mac1).into();
+ if !valid_mac1 {
+ Err(HandshakeError::InvalidMac1)
+ } else {
+ Ok(())
+ }
+ }
+
+ pub fn check_mac2(&self, inner: &[u8], src: &SocketAddr, macs: &MacsFooter) -> bool {
+ let src = addr_to_mac_bytes(src);
+ match self.get_tau(&src) {
+ Some(tau) => MAC!(&tau, inner, macs.f_mac1).ct_eq(&macs.f_mac2).into(),
+ None => false,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use proptest::prelude::*;
+ use rand::rngs::OsRng;
+ use x25519_dalek::StaticSecret;
+
+ fn new_validator_generator() -> (Validator, Generator) {
+ let mut rng = OsRng::new().unwrap();
+ let sk = StaticSecret::new(&mut rng);
+ let pk = PublicKey::from(&sk);
+ (Validator::new(pk), Generator::new(pk))
+ }
+
+ proptest! {
+ #[test]
+ fn test_cookie_reply(inner1 : Vec<u8>, inner2 : Vec<u8>, receiver : u32) {
+ let mut msg = CookieReply::default();
+ let mut rng = OsRng::new().expect("failed to create rng");
+ let mut macs = MacsFooter::default();
+ let src = "192.0.2.16:8080".parse().unwrap();
+ let (validator, mut generator) = new_validator_generator();
+
+ // generate mac1 for first message
+ generator.generate(&inner1[..], &mut macs);
+ assert_ne!(macs.f_mac1, [0u8; SIZE_MAC], "mac1 should be set");
+ assert_eq!(macs.f_mac2, [0u8; SIZE_MAC], "mac2 should not be set");
+
+ // check validity of mac1
+ validator.check_mac1(&inner1[..], &macs).expect("mac1 of inner1 did not validate");
+ assert_eq!(validator.check_mac2(&inner1[..], &src, &macs), false, "mac2 of inner2 did not validate");
+ validator.create_cookie_reply(&mut rng, receiver, &src, &macs, &mut msg);
+
+ // consume cookie reply
+ generator.process(&msg).expect("failed to process CookieReply");
+
+ // generate mac2 & mac2 for second message
+ generator.generate(&inner2[..], &mut macs);
+ assert_ne!(macs.f_mac1, [0u8; SIZE_MAC], "mac1 should be set");
+ assert_ne!(macs.f_mac2, [0u8; SIZE_MAC], "mac2 should be set");
+
+ // check validity of mac1 and mac2
+ validator.check_mac1(&inner2[..], &macs).expect("mac1 of inner2 did not validate");
+ assert!(validator.check_mac2(&inner2[..], &src, &macs), "mac2 of inner2 did not validate");
+ }
+ }
+}