From ca91f3e91cd0e66a37444f5ff7eaf6e896ee5099 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Mon, 29 Jan 2024 09:34:06 +0100 Subject: [PATCH] Implement data v3 features for AEAD tag at the end and 64 bit packet counter Split the implementation of the packet counter for normal packet ID that includes the "weird" long format for long 64 bit packet ids used in tls-auth and tls-crypt and a simplified implementation for AEAD that only does 32 bit and 64 bit flat counters. Signed-off-by: Arne Schwabe --- openvpn/client/cliproto.hpp | 5 + openvpn/crypto/crypto_aead.hpp | 141 +++++--- openvpn/crypto/cryptodc.hpp | 40 +++ openvpn/crypto/packet_id_aead.hpp | 413 ++++++++++++++++++++++++ openvpn/dco/dcocli.hpp | 7 + openvpn/server/servproto.hpp | 6 + openvpn/ssl/proto.hpp | 28 +- openvpn/tun/builder/client.hpp | 5 + openvpn/tun/client/tunbase.hpp | 10 + openvpn/tun/client/tunnull.hpp | 7 +- openvpn/tun/linux/client/tuncli.hpp | 6 + openvpn/tun/mac/client/tuncli.hpp | 5 + openvpn/tun/win/client/clientconfig.hpp | 5 + test/unittests/test_crypto.cpp | 83 ++++- test/unittests/test_proto.cpp | 10 +- 15 files changed, 704 insertions(+), 67 deletions(-) create mode 100644 openvpn/crypto/packet_id_aead.hpp diff --git a/openvpn/client/cliproto.hpp b/openvpn/client/cliproto.hpp index caa30989..f509a195 100644 --- a/openvpn/client/cliproto.hpp +++ b/openvpn/client/cliproto.hpp @@ -1359,6 +1359,11 @@ class Session : ProtoContextCallbackInterface, notify_callback->client_proto_renegotiated(); } + bool supports_proto_v3() override + { + return tun_factory->supports_proto_v3(); + } + void housekeeping_callback(const openvpn_io::error_code &e) { try diff --git a/openvpn/crypto/crypto_aead.hpp b/openvpn/crypto/crypto_aead.hpp index 5931000e..ecb343b8 100644 --- a/openvpn/crypto/crypto_aead.hpp +++ b/openvpn/crypto/crypto_aead.hpp @@ -32,7 +32,7 @@ #include #include #include -#include +#include #include #include @@ -62,86 +62,113 @@ class Crypto : public CryptoDCInstance std::memset(data, 0, sizeof(data)); } - // setup - void set_tail(const StaticKey &sk) + /** + * Sets the IV tail for AEAD operations + * + * The IV for AEAD ciphers (both AES-GCM and Chacha20-Poly1305) consists of 96 bits/12 bytes + * (It then gets concatenated with internal 32 bits for block counter to form a 128 bit counter for the + * encryption). + * + * Since we only use 4 bytes (32 bit packet ID) or 8 bytes (64 bit packet ID) on the wire, we + * fill out the rest of the IV with pseudorandom bytes that come from of the negotiated key for the + * HMAC key (this key is not used by AEAD ciphers, so we reuse it for this purpose in AEAD mode). + */ + void set_tail(const StaticKey &sk, bool use64bitcounter) { - if (sk.size() < 8) + size_t implicit_iv_len = use64bitcounter ? 4 : 8; + if (sk.size() < implicit_iv_len) throw aead_error("insufficient key material for nonce tail"); - std::memcpy(data + 8, sk.data(), 8); + + /* 4 bytes opcode + 4-8 bytes on wire IV */ + size_t implicit_iv_offset = data_offset_pkt_id + (12 - implicit_iv_len); + std::memcpy(data + implicit_iv_offset, sk.data(), implicit_iv_len); } // for encrypt - Nonce(const Nonce &ref, PacketIDSend &pid_send, const PacketID::time_t now, const unsigned char *op32) + Nonce(const Nonce &ref, PacketIDAEADSend &pid_send, const unsigned char *op32) { + /** Copy op code and tail of packet ID */ std::memcpy(data, ref.data, sizeof(data)); - Buffer buf(data + 4, 4, false); - pid_send.write_next(buf, false, now); + + Buffer buf(data + data_offset_pkt_id, PacketIDAEAD::long_id_size, false); + pid_send.write_next(buf); if (op32) { ad_op32 = true; - std::memcpy(data, op32, 4); + std::memcpy(data, op32, op32_size); } else ad_op32 = false; } // for encrypt - void prepend_ad(Buffer &buf) const + void prepend_ad(Buffer &buf, const PacketIDAEADSend &pid_send) const { - buf.prepend(data + 4, 4); + buf.prepend(data + data_offset_pkt_id, pid_send.length()); } // for decrypt - Nonce(const Nonce &ref, Buffer &buf, const unsigned char *op32) + Nonce(const Nonce &ref, const PacketIDAEADReceive &recv_pid, Buffer &buf, const unsigned char *op32) { + /* Copy opcode and tail of packet ID */ std::memcpy(data, ref.data, sizeof(data)); - buf.read(data + 4, 4); + + /* copy dynamic packet of IV into */ + buf.read(data + data_offset_pkt_id, recv_pid.length()); if (op32) { ad_op32 = true; - std::memcpy(data, op32, 4); + std::memcpy(data, op32, op32_size); } else ad_op32 = false; } // for decrypt - bool verify_packet_id(PacketIDReceive &pid_recv, const PacketID::time_t now) + bool verify_packet_id(PacketIDAEADReceive &pid_recv, const PacketID::time_t now) { - Buffer buf(data + 4, 4, true); - const PacketID pid = pid_recv.read_next(buf); - return pid_recv.test_add(pid, now, true); // verify packet ID + Buffer buf(data + data_offset_pkt_id, PacketIDAEAD::long_id_size, true); + const PacketIDAEAD pid = pid_recv.read_next(buf); + return pid_recv.test_add(pid, now); // verify packet ID } const unsigned char *iv() const { - return data + 4; + return data + data_offset_pkt_id; } const unsigned char *ad() const { - return ad_op32 ? data : data + 4; + return ad_op32 ? data : data + data_offset_pkt_id; } - size_t ad_len() const + size_t ad_len(const PacketIDAEADSend &pid_send) const { - return ad_op32 ? 8 : 4; + return (ad_op32 ? op32_size : 0) + pid_send.length(); } + size_t ad_len(const PacketIDAEADReceive &pid_recv) const + { + return (ad_op32 ? op32_size : 0) + pid_recv.length(); + } + + private: - bool ad_op32; // true if AD includes op32 opcode + bool ad_op32; // true if AD (authenticated data) includes op32 opcode // Sample data: // [ OP32 (optional) ] [ pkt ID ] [ nonce tail ] // [ 48 00 00 01 ] [ 00 00 00 05 ] [ 7f 45 64 db 33 5b 6c 29 ] unsigned char data[16]; + static constexpr std::size_t data_offset_pkt_id = 4; + static constexpr std::size_t op32_size = 4; }; struct Encrypt { typename CRYPTO_API::CipherContextAEAD impl; Nonce nonce; - PacketIDSend pid_send; + PacketIDAEADSend pid_send{false}; BufferAllocated work; }; @@ -149,7 +176,7 @@ class Crypto : public CryptoDCInstance { typename CRYPTO_API::CipherContextAEAD impl; Nonce nonce; - PacketIDReceive pid_recv; + PacketIDAEADReceive pid_recv{}; BufferAllocated work; }; @@ -176,32 +203,42 @@ class Crypto : public CryptoDCInstance if (buf.size()) { // build nonce/IV/AD - Nonce nonce(e.nonce, e.pid_send, now, op32); + Nonce nonce(e.nonce, e.pid_send, op32); // encrypt to work buf frame->prepare(Frame::ENCRYPT_WORK, e.work); if (e.work.max_size() < buf.size()) throw aead_error("encrypt work buffer too small"); - // alloc auth tag in buffer - unsigned char *auth_tag = e.work.prepend_alloc(CRYPTO_API::CipherContextAEAD::AUTH_TAG_LEN); - unsigned char *auth_tag_end; - - // prepare output buffer unsigned char *work_data = e.work.write_alloc(buf.size()); - if (e.impl.requires_authtag_at_end()) + + + unsigned char *auth_tag; + unsigned char *auth_tag_tmp = nullptr; + + // alloc auth tag in buffer where it needs to be + // Create a temporary auth tag at the end if the implementation and mode require it + if (dc_settings.aeadTagAtTheEnd()) + { + auth_tag = e.work.write_alloc(CRYPTO_API::CipherContextAEAD::AUTH_TAG_LEN); + } + else { - auth_tag_end = e.work.write_alloc(CRYPTO_API::CipherContextAEAD::AUTH_TAG_LEN); + auth_tag = e.work.prepend_alloc(CRYPTO_API::CipherContextAEAD::AUTH_TAG_LEN); + if (e.impl.requires_authtag_at_end()) + { + auth_tag_tmp = e.work.write_alloc(CRYPTO_API::CipherContextAEAD::AUTH_TAG_LEN); + } } // encrypt - e.impl.encrypt(buf.data(), work_data, buf.size(), nonce.iv(), auth_tag, nonce.ad(), nonce.ad_len()); + e.impl.encrypt(buf.data(), work_data, buf.size(), nonce.iv(), auth_tag, nonce.ad(), nonce.ad_len(e.pid_send)); - if (e.impl.requires_authtag_at_end()) + if (auth_tag_tmp) { /* move the auth tag to the front */ - std::memcpy(auth_tag, auth_tag_end, CRYPTO_API::CipherContextAEAD::AUTH_TAG_LEN); + std::memcpy(auth_tag, auth_tag_tmp, CRYPTO_API::CipherContextAEAD::AUTH_TAG_LEN); /* Ignore the auth tag at the end */ e.work.inc_size(-CRYPTO_API::CipherContextAEAD::AUTH_TAG_LEN); } @@ -209,7 +246,7 @@ class Crypto : public CryptoDCInstance buf.swap(e.work); // prepend additional data - nonce.prepend_ad(buf); + nonce.prepend_ad(buf, e.pid_send); } return e.pid_send.wrap_warning(); } @@ -220,29 +257,37 @@ class Crypto : public CryptoDCInstance if (buf.size()) { // get nonce/IV/AD - Nonce nonce(d.nonce, buf, op32); + Nonce nonce(d.nonce, d.pid_recv, buf, op32); - // get auth tag - unsigned char *auth_tag = buf.read_alloc(CRYPTO_API::CipherContextAEAD::AUTH_TAG_LEN); + // get auth tag if it is at the front. If the auth tag is at the end + // the decrypt function will just treat it as part of the input + unsigned char *auth_tag = nullptr; - // initialize work buffer + if (!dc_settings.aeadTagAtTheEnd()) + { + auth_tag = buf.read_alloc(CRYPTO_API::CipherContextAEAD::AUTH_TAG_LEN); + } + + // initialize work buffer. frame->prepare(Frame::DECRYPT_WORK, d.work); if (d.work.max_size() < buf.size()) throw aead_error("decrypt work buffer too small"); - if (e.impl.requires_authtag_at_end()) + if (auth_tag && e.impl.requires_authtag_at_end()) { unsigned char *auth_tag_end = buf.write_alloc(CRYPTO_API::CipherContextAEAD::AUTH_TAG_LEN); std::memcpy(auth_tag_end, auth_tag, CRYPTO_API::CipherContextAEAD::AUTH_TAG_LEN); + auth_tag = nullptr; } // decrypt from buf -> work - if (!d.impl.decrypt(buf.c_data(), d.work.data(), buf.size(), nonce.iv(), auth_tag, nonce.ad(), nonce.ad_len())) + if (!d.impl.decrypt(buf.c_data(), d.work.data(), buf.size(), nonce.iv(), auth_tag, nonce.ad(), nonce.ad_len(d.pid_recv))) { buf.reset_size(); return Error::DECRYPT_ERROR; } - if (e.impl.requires_authtag_at_end()) + + if (dc_settings.aeadTagAtTheEnd() || e.impl.requires_authtag_at_end()) { d.work.set_size(buf.size() - CRYPTO_API::CipherContextAEAD::AUTH_TAG_LEN); } @@ -284,8 +329,8 @@ class Crypto : public CryptoDCInstance void init_hmac(StaticKey &&encrypt_key, StaticKey &&decrypt_key) override { - e.nonce.set_tail(encrypt_key); - d.nonce.set_tail(decrypt_key); + e.nonce.set_tail(encrypt_key, dc_settings.use64bitPktCounter()); + d.nonce.set_tail(decrypt_key, dc_settings.use64bitPktCounter()); } void init_pid(const int recv_mode, @@ -293,8 +338,8 @@ class Crypto : public CryptoDCInstance const int recv_unit, const SessionStats::Ptr &recv_stats_arg) override { - e.pid_send.init(PacketID::SHORT_FORM); - d.pid_recv.init(recv_mode, PacketID::SHORT_FORM, recv_name, recv_unit, recv_stats_arg); + e.pid_send = PacketIDAEADSend{dc_settings.use64bitPktCounter()}; + d.pid_recv.init(recv_name, recv_unit, dc_settings.use64bitPktCounter(), recv_stats_arg); } // Indicate whether or not cipher/digest is defined diff --git a/openvpn/crypto/cryptodc.hpp b/openvpn/crypto/cryptodc.hpp index 84a23f53..394101f1 100644 --- a/openvpn/crypto/cryptodc.hpp +++ b/openvpn/crypto/cryptodc.hpp @@ -122,6 +122,16 @@ class CryptoDCSettingsData digest_ = digest; } + void set_aead_tag_end(bool at_the_end) + { + aead_tag_at_the_end = at_the_end; + } + + void set_64_bit_packet_id(bool use_64bit_packet_id) + { + pktcounter_64bit = use_64bit_packet_id; + } + CryptoAlgs::Type cipher() const { return cipher_; @@ -139,6 +149,16 @@ class CryptoDCSettingsData return (CryptoAlgs::use_cipher_digest(cipher_) ? digest_ : CryptoAlgs::NONE); } + bool use64bitPktCounter() const + { + return pktcounter_64bit; + } + + bool aeadTagAtTheEnd() const + { + return aead_tag_at_the_end; + } + void set_key_derivation(CryptoAlgs::KeyDerivation method) { key_derivation_ = method; @@ -154,6 +174,8 @@ class CryptoDCSettingsData CryptoAlgs::Type cipher_ = CryptoAlgs::NONE; CryptoAlgs::Type digest_ = CryptoAlgs::NONE; CryptoAlgs::KeyDerivation key_derivation_ = CryptoAlgs::KeyDerivation::OPENVPN_PRF; + bool pktcounter_64bit = false; + bool aead_tag_at_the_end = false; }; // Factory for CryptoDCInstance objects @@ -221,6 +243,24 @@ class CryptoDCSettings : public CryptoDCSettingsData } } + void set_aead_tag_end(bool at_the_end) + { + if (at_the_end != aeadTagAtTheEnd()) + { + CryptoDCSettingsData::set_aead_tag_end(at_the_end); + dirty = true; + } + } + + void set_64_bit_packet_id(bool use_64bit_packet_id) + { + if (use_64bit_packet_id != use64bitPktCounter()) + { + CryptoDCSettingsData::set_64_bit_packet_id(use_64bit_packet_id); + dirty = true; + } + } + CryptoDCContext &context() { if (!context_ || dirty) diff --git a/openvpn/crypto/packet_id_aead.hpp b/openvpn/crypto/packet_id_aead.hpp new file mode 100644 index 00000000..06eca020 --- /dev/null +++ b/openvpn/crypto/packet_id_aead.hpp @@ -0,0 +1,413 @@ +// OpenVPN -- An application to securely tunnel IP networks +// over a single port, with support for SSL/TLS-based +// session authentication and key exchange, +// packet encryption, packet authentication, and +// packet compression. +// +// Copyright (C) 2012- OpenVPN Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License Version 3 +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program in the COPYING file. +// If not, see . + +// Manage OpenVPN protocol Packet IDs for packet replay detection +#pragma once + +#include +#include +#include +#include +#include // for std::uint32_t + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace openvpn { +/** + * Communicate packet-id over the wire for AEAD + * A short packet-id is just a 32 bit sequence number. A long packet-id is a + * 64 bit sequence number. This sequence number is reused for AEAD IV. + * + * This data structure is always sent over the net in network byte order, + * + * This class is different from PacketID in the way that it always uses + * a "flat" packet id that is either 32 or 64 bit while PacketID has a long + * packet id that is 32bit + 32bit but follow different rules and includes + * a timestamp. Merging PacketIDAEAD and PacketID would result in a much + * more convoluted and hard to understand class than keeping them seperate + * + */ +struct PacketIDAEAD +{ + typedef std::uint64_t aead_id_t; + + aead_id_t id = 0; // legal values are 1 through 2^64-1 + bool wide = false; + + /** + * Returns the size of the packet id. This is either 4 or 8 depending on the mode in use + * @return 4 or 8 + */ + [[nodiscard]] constexpr std::size_t size() const + { + return size(wide); + } + + static constexpr size_t size(bool wide) + { + if (wide) + return long_id_size; + else + return short_id_size; + } + + + explicit PacketIDAEAD(bool wide_arg) + : wide(wide_arg) + { + } + + explicit PacketIDAEAD(bool wide_arg, aead_id_t id_arg) + : id(id_arg), wide(wide_arg) + { + } + + constexpr static std::size_t short_id_size = sizeof(std::uint32_t); + constexpr static std::size_t long_id_size = sizeof(std::uint64_t); + + bool is_valid() const + { + return id != 0; + } + + void reset() + { + id = aead_id_t(0); + } + + /** + * Reads the packet id from the specified buffer. + * @param buf the buffer to read the packet id from + */ + void read(ConstBuffer &buf) + { + if (wide) + { + std::uint64_t net_id; + buf.read(reinterpret_cast(&net_id), sizeof(net_id)); + id = Endian::rev64(net_id); + } + else + { + std::uint32_t net_id; + buf.read(reinterpret_cast(&net_id), sizeof(net_id)); + id = ntohl(net_id); + } + } + + /** Writes the packet id to a buffer */ + void write(Buffer &buf) const + { + if (wide) + { + const std::uint64_t net_id = Endian::rev64(id); + buf.write(reinterpret_cast(&net_id), sizeof(net_id)); + } + else + { + const std::uint32_t net_id = htonl(static_cast(id)); + buf.write(reinterpret_cast(&net_id), sizeof(net_id)); + } + } + + std::string str() const + { + std::ostringstream os; + os << std::hex << "[0x" << id << "]"; + return os.str(); + } +}; + +class PacketIDAEADSend +{ + public: + OPENVPN_SIMPLE_EXCEPTION(packet_id_wrap); + + PacketIDAEADSend(bool wide_arg) + : pid_(wide_arg) + { + } + + + /** + * Increment the packet ID and return the next packet id to use. + * @throws packet_id_wrap if the packet id space is exhausted + * @return packet id to use next. + */ + PacketIDAEAD next() + { + ++pid_.id; + PacketIDAEAD ret{pid_.wide, pid_.id}; + if (!pid_.wide && unlikely(pid_.id == std::numeric_limits::max())) // wraparound + { + throw packet_id_wrap(); + } + else if (unlikely(pid_.id == std::numeric_limits::max())) + { + throw packet_id_wrap(); + } + return ret; + } + + /** + * increases the packet id and writes it to a buffer + * @param buf buffer to write to + */ + void write_next(Buffer &buf) + { + const PacketIDAEAD pid = next(); + pid.write(buf); + } + + /** + * When a VPN runs in TLS mode (the only mode that OpenVPN supports, + * there is no --secret mode anymore), it needs to be warned about wrapping to + * start thinking about triggering a new SSL/TLS handshake. + * This method can be called to see if that level has been reached. + */ + bool wrap_warning() const + { + if (pid_.wide) + return false; + + const PacketIDAEAD::aead_id_t wrap_at = 0xFF000000; + return pid_.id >= wrap_at; + } + + std::string str() const + { + std::string ret; + ret = pid_.str(); + if (pid_.wide) + ret += 'L'; + return ret; + } + + /** + * Returns the size of the packet id. This is either 4 or 8 depending on the mode in use + * @return 4 or 8 + */ + [[nodiscard]] constexpr std::size_t length() const + { + return pid_.size(); + } + + private: + PacketIDAEAD pid_; +}; + +/* + * This is the data structure we keep on the receiving side, + * to check that no packet-id is accepted more than once. + * + * Replay window sizing in bytes = 2^REPLAY_WINDOW_ORDER. + * PKTID_RECV_EXPIRE is backtrack expire in seconds. + */ +template +class PacketIDAEADReceiveType +{ + public: + static constexpr unsigned int REPLAY_WINDOW_BYTES = 1u << REPLAY_WINDOW_ORDER; + static constexpr unsigned int REPLAY_WINDOW_SIZE = REPLAY_WINDOW_BYTES * 8; + + void init(const char *name_arg, + const int unit_arg, + bool wide_arg, + const SessionStats::Ptr &stats_arg) + { + wide = wide_arg; + base = 0; + extent = 0; + expire = 0; + id_high = 0; + id_floor = 0; + unit = unit_arg; + name = name_arg; + stats = stats_arg; + std::memset(history, 0, sizeof(history)); + } + + + /** + * Checks if a packet ID is allowed and modifies the history of seen packets ids and + * adds any errors to the internal stats. + * + * It returns the verdict of the packet id if it is fine or not + * + * @param pin packet ID to check + * @param now Current time to check that reordered packets are in the allowed time + * @return true if the packet id is okay and has been accepted + */ + [[nodiscard]] bool test_add(const PacketIDAEAD &pin, + const Time::base_type now) + { + const Error::Type err = do_test_add(pin, now); + if (unlikely(err != Error::SUCCESS)) + { + stats->error(err); + return false; + } + else + return true; + } + + /** + * Checks if a packet ID is allowed and modifies the history of seen packets ids. + * + * It returns the verdict of the packet id if it is fine or not + * + * @param pin packet ID to check + * @param now Current time to check that reordered packets are in the allowed time + * @return Error::SUCCESS if successful, otherwise PKTID_EXPIRE, PKTID_BACKTRACK or PKTID_REPLAY + */ + [[nodiscard]] Error::Type do_test_add(const PacketIDAEAD &pin, + const Time::base_type now) + { + // expire backtracks at or below id_floor after PKTID_RECV_EXPIRE time + if (unlikely(now >= expire)) + id_floor = id_high; + expire = now + PKTID_RECV_EXPIRE; + + // ID must not be zero + if (unlikely(!pin.is_valid())) + return Error::PKTID_INVALID; + + + if (likely(pin.id == id_high + 1)) + { + // well-formed ID sequence (incremented by 1) + base = replay_index(-1); + history[base / 8] |= static_cast(1 << (base % 8)); + if (extent < REPLAY_WINDOW_SIZE) + ++extent; + id_high = pin.id; + } + else if (pin.id > id_high) + { + // ID jumped forward by more than one + + const auto delta = pin.id - id_high; + if (delta < REPLAY_WINDOW_SIZE) + { + base = replay_index(-delta); + history[base / 8] |= static_cast(1u << (base % 8)); + extent += static_cast(delta); + if (extent > REPLAY_WINDOW_SIZE) + extent = REPLAY_WINDOW_SIZE; + for (unsigned i = 1; i < delta; ++i) + { + const auto newbase = replay_index(i); + history[newbase / 8] &= static_cast(~(1u << (newbase % 8))); + } + } + else + { + base = 0; + extent = REPLAY_WINDOW_SIZE; + std::memset(history, 0, sizeof(history)); + history[0] = 1; + } + id_high = pin.id; + } + else + { + // ID backtrack + const auto delta = id_high - pin.id; + if (delta < extent) + { + if (pin.id > id_floor) + { + const auto ri = replay_index(delta); + std::uint8_t *p = &history[ri / 8]; + const std::uint8_t mask = static_cast(1u << (ri % 8)); + if (*p & mask) + return Error::PKTID_REPLAY; + *p |= mask; + } + else + return Error::PKTID_EXPIRE; + } + else + return Error::PKTID_BACKTRACK; + } + + return Error::SUCCESS; + } + + PacketIDAEAD read_next(Buffer &buf) const + { + PacketIDAEAD pid{wide}; + pid.read(buf); + return pid; + } + + [[nodiscard]] std::string str() const + { + std::ostringstream os; + os << "[e=" << extent << " f=" << id_floor << id_high << ']'; + return os.str(); + } + + [[nodiscard]] std::size_t constexpr length() const + { + return PacketIDAEAD::size(wide); + } + + private: + [[nodiscard]] constexpr std::size_t replay_index(PacketIDAEAD::aead_id_t i) const + { + return (base + i) & (REPLAY_WINDOW_SIZE - 1); + } + + std::size_t base; // bit position of deque base in history + std::size_t extent; // extent (in bits) of deque in history + Time::base_type expire; // expiration of history + PacketIDAEAD::aead_id_t id_high; // highest sequence number received + PacketIDAEAD::aead_id_t id_floor; // we will only accept backtrack IDs > id_floor + + //!< 32 or 64 bit packet counter + bool wide; + int unit; // unit number of this object (for debugging) + std::string name; // name of this object (for debugging) + + SessionStats::Ptr stats; + + //! "sliding window" bitmask of recent packet IDs received */ + std::uint8_t history[REPLAY_WINDOW_BYTES]; +}; + +// Our standard packet ID window with order=8 (window size=2048). +// and recv expire=30 seconds. +typedef PacketIDAEADReceiveType<8, 30> PacketIDAEADReceive; + +} // namespace openvpn diff --git a/openvpn/dco/dcocli.hpp b/openvpn/dco/dcocli.hpp index cec15750..1552e6ca 100644 --- a/openvpn/dco/dcocli.hpp +++ b/openvpn/dco/dcocli.hpp @@ -142,6 +142,13 @@ class ClientConfig : public DCO, return ctrl; } + bool supports_proto_v3() override + { + /* Currently, there is no version of ovpn-dco for Linux or Windows that supports + * the new features, so we always return false here */ + return false; + } + protected: ClientConfig() = default; }; diff --git a/openvpn/server/servproto.hpp b/openvpn/server/servproto.hpp index 87f20a71..fdac66bd 100644 --- a/openvpn/server/servproto.hpp +++ b/openvpn/server/servproto.hpp @@ -307,6 +307,12 @@ class ServerProto { } + bool supports_proto_v3() override + { + /* TODO: currently all server implementations do not implement this feature in their data channel */ + return false; + } + bool defined_() const { return !halt && TransportLink::send; diff --git a/openvpn/ssl/proto.hpp b/openvpn/ssl/proto.hpp index e43a28fa..f5a9680f 100644 --- a/openvpn/ssl/proto.hpp +++ b/openvpn/ssl/proto.hpp @@ -203,6 +203,11 @@ class ProtoContextCallbackInterface buf.write(&empty, 2); } + /** the protocol context needs to know if the parent and its tun/transport layer are able to + * support 64bit and AEAD tag at the end in order to properly handshake this protocol feature + */ + virtual bool supports_proto_v3() = 0; + //! Called when KeyContext transitions to ACTIVE state virtual void active(bool primary) = 0; }; @@ -279,6 +284,7 @@ class ProtoContext : public logging::LoggingMixinpeer_info_string(); + const std::string peer_info = proto.config->peer_info_string(proto.proto_callback->supports_proto_v3()); write_auth_string(peer_info, *buf); } app_send_validate(std::move(buf)); diff --git a/openvpn/tun/builder/client.hpp b/openvpn/tun/builder/client.hpp index dd3b42bc..7f392f35 100644 --- a/openvpn/tun/builder/client.hpp +++ b/openvpn/tun/builder/client.hpp @@ -109,6 +109,11 @@ class ClientConfig : public TunClientFactory tun_persist.reset(); } + bool supports_proto_v3() override + { + return true; + } + private: ClientConfig() : n_parallel(8), retain_sd(false), tun_prefix(false), builder(nullptr) diff --git a/openvpn/tun/client/tunbase.hpp b/openvpn/tun/client/tunbase.hpp index e3c29757..540999d6 100644 --- a/openvpn/tun/client/tunbase.hpp +++ b/openvpn/tun/client/tunbase.hpp @@ -101,6 +101,16 @@ struct TunClientFactory : public virtual RC return false; } + /** + * Return whether this tun implementation will support data v3 features + * (AEAD tag at the end and 64 bit packet counters). + * + * This is more a property of the data encryption layer than of the tun device + * but since all of our DCO encryptions are setup with the tun setup, we also + * make it the responsibility of the tun client to signal v3 data layer support. + */ + virtual bool supports_proto_v3() = 0; + // Called on TunClient close, after TunClient::stop has been called. // disconnected -> // true: this is the final disconnect, or diff --git a/openvpn/tun/client/tunnull.hpp b/openvpn/tun/client/tunnull.hpp index 7db8ccf5..57c3f9f0 100644 --- a/openvpn/tun/client/tunnull.hpp +++ b/openvpn/tun/client/tunnull.hpp @@ -45,10 +45,13 @@ class ClientConfig : public TunClientFactory TunClientParent &parent, TransportClient *transcli) override; - private: - ClientConfig() + bool supports_proto_v3() override { + return true; } + + private: + ClientConfig() = default; }; class Client : public TunClient diff --git a/openvpn/tun/linux/client/tuncli.hpp b/openvpn/tun/linux/client/tuncli.hpp index 71b6e83c..b811c564 100644 --- a/openvpn/tun/linux/client/tuncli.hpp +++ b/openvpn/tun/linux/client/tuncli.hpp @@ -121,6 +121,12 @@ class ClientConfig : public TunClientFactory return new TunLinuxSetup::Setup(); } + bool supports_proto_v3() override + { + /* The normal tun implementation that uses the internal data channel */ + return true; + } + private: ClientConfig() { diff --git a/openvpn/tun/mac/client/tuncli.hpp b/openvpn/tun/mac/client/tuncli.hpp index 8b4c2476..5a0358b0 100644 --- a/openvpn/tun/mac/client/tuncli.hpp +++ b/openvpn/tun/mac/client/tuncli.hpp @@ -116,6 +116,11 @@ class ClientConfig : public TunClientFactory return new ClientConfig; } + bool supports_proto_v3() override + { + return true; + } + TunClient::Ptr new_tun_client_obj(openvpn_io::io_context &io_context, TunClientParent &parent, TransportClient *transcli) override; diff --git a/openvpn/tun/win/client/clientconfig.hpp b/openvpn/tun/win/client/clientconfig.hpp index 84456cbf..419a8c65 100644 --- a/openvpn/tun/win/client/clientconfig.hpp +++ b/openvpn/tun/win/client/clientconfig.hpp @@ -87,6 +87,11 @@ class ClientConfig : public TunClientFactory TunClientParent &parent, TransportClient *transcli) override; + bool supports_proto_v3() override + { + return tun_type != TunWin::OvpnDco; + } + void finalize(const bool disconnected) override { if (disconnected) diff --git a/test/unittests/test_crypto.cpp b/test/unittests/test_crypto.cpp index a6a090df..0818a107 100644 --- a/test/unittests/test_crypto.cpp +++ b/test/unittests/test_crypto.cpp @@ -96,7 +96,7 @@ static openvpn::Frame::Context frame_ctx() } -TEST(crypto, dcaead) +void test_datachannel_crypto(bool tag_at_the_end, bool longpktcounter = false) { auto frameptr = openvpn::Frame::Ptr{new openvpn::Frame{frame_ctx()}}; @@ -104,6 +104,8 @@ TEST(crypto, dcaead) openvpn::CryptoDCSettingsData dc; dc.set_cipher(openvpn::CryptoAlgs::AES_256_GCM); + dc.set_aead_tag_end(tag_at_the_end); + dc.set_64_bit_packet_id(longpktcounter); openvpn::AEAD::Crypto cryptodc{nullptr, dc, frameptr, statsptr}; @@ -155,20 +157,57 @@ TEST(crypto, dcaead) bool const wrapwarn = cryptodc.encrypt(work, now, op32); ASSERT_FALSE(wrapwarn); - /* 16 for tag, 4 for IV */ - EXPECT_EQ(work.size(), std::strlen(plaintext) + 4 + 16); + size_t pkt_counter_len = longpktcounter ? 8 : 4; + size_t tag_len = 16; + + /* 16 for tag, 4 or 8 for packet counter */ + EXPECT_EQ(work.size(), std::strlen(plaintext) + pkt_counter_len + tag_len); + + const uint8_t exp_tag_short[16]{0x1f, 0xdd, 0x90, 0x8f, 0x0e, 0x9d, 0xc2, 0x5e, 0x79, 0xd8, 0x32, 0x02, 0x0d, 0x58, 0xe7, 0x3f}; + const uint8_t exp_tag_long[16]{0x52, 0xee, 0xef, 0xdb, 0x34, 0xb7, 0xbd, 0x79, 0xfe, 0xbf, 0x69, 0xd0, 0x4e, 0x92, 0xfe, 0x4b}; + + const uint8_t *expected_tag; + + if (longpktcounter) + expected_tag = exp_tag_long; + else + expected_tag = exp_tag_short; - const uint8_t expected_tag[16]{0x1f, 0xdd, 0x90, 0x8f, 0x0e, 0x9d, 0xc2, 0x5e, 0x79, 0xd8, 0x32, 0x02, 0x0d, 0x58, 0xe7, 0x3f}; // Packet id/IV should 1 - uint8_t packetid1[]{0, 0, 0, 1}; - EXPECT_TRUE(std::memcmp(work.data(), packetid1, 4) == 0); + if (longpktcounter) + { + uint8_t packetid1[]{0, 0, 0, 0, 0, 0, 0, 1}; + EXPECT_EQ(std::memcmp(work.data(), packetid1, 8), 0); + } + else + { + uint8_t packetid1[]{0, 0, 0, 1}; + EXPECT_EQ(std::memcmp(work.data(), packetid1, 4), 0); + } + // Tag is in the front after packet id - EXPECT_TRUE(std::memcmp(work.data() + 4, expected_tag, 16) == 0); + if (tag_at_the_end) + { + EXPECT_EQ(std::memcmp(work.data() + 56 + pkt_counter_len, expected_tag, 16), 0); + } + else + { + EXPECT_EQ(std::memcmp(work.data() + pkt_counter_len, expected_tag, 16), 0); + } - // Check a few random bytes of the encrypted output - const uint8_t bytesat30[6]{0xa8, 0x2e, 0x6b, 0x2e, 0x6b, 0x17}; - EXPECT_TRUE(std::memcmp(work.data() + 30, bytesat30, 6) == 0); + // Check a few random bytes of the encrypted output. Different IVs lead to different output here. + ptrdiff_t tagoffset = tag_at_the_end ? 0 : 16; + if (longpktcounter) + { + const uint8_t bytesat14[6]{0xc7, 0x40, 0x47, 0x81, 0xac, 0x8c}; + EXPECT_EQ(std::memcmp(work.data() + tagoffset + 14, bytesat14, 6), 0); + } + else + { + const uint8_t bytesat14[6]{0xa8, 0x2e, 0x6b, 0x17, 0x06, 0xd9}; + EXPECT_EQ(std::memcmp(work.data() + tagoffset + 14, bytesat14, 6), 0); + } /* Check now if decrypting also works */ auto ret = cryptodc.decrypt(work, now, op32); @@ -176,5 +215,27 @@ TEST(crypto, dcaead) EXPECT_EQ(ret, openvpn::Error::SUCCESS); EXPECT_EQ(work.size(), std::strlen(plaintext)); - EXPECT_TRUE(std::memcmp(work.data(), plaintext, std::strlen(plaintext)) == 0); + EXPECT_EQ(std::memcmp(work.data(), plaintext, std::strlen(plaintext)), 0); +} + + +TEST(crypto, dcaead_tag_at_the_front) +{ + test_datachannel_crypto(false); +} + +TEST(crypto, dcaead_tag_at_the_end) +{ + test_datachannel_crypto(true); +} + + +TEST(crypto, dcaead_tag_at_the_front_long_pktcntr) +{ + test_datachannel_crypto(false, true); +} + +TEST(crypto, dcaead_tag_at_the_end_long_pktcntr) +{ + test_datachannel_crypto(true, true); } diff --git a/test/unittests/test_proto.cpp b/test/unittests/test_proto.cpp index a5a4ad9d..f5ce2f4f 100644 --- a/test/unittests/test_proto.cpp +++ b/test/unittests/test_proto.cpp @@ -351,6 +351,10 @@ class TestProto : public ProtoContextCallbackInterface { } + bool supports_proto_v3() override + { + return true; + } public: OPENVPN_EXCEPTION(session_invalidated); @@ -1258,7 +1262,7 @@ TEST(proto, iv_ciphers_aead) auto protoConf = openvpn::ProtoContext::ProtoConfig(); - auto infostring = protoConf.peer_info_string(); + auto infostring = protoConf.peer_info_string(false); auto ivciphers = infostring.substr(infostring.find("IV_CIPHERS=")); ivciphers = ivciphers.substr(0, ivciphers.find("\n")); @@ -1277,7 +1281,7 @@ TEST(proto, iv_ciphers_non_preferred) auto protoConf = openvpn::ProtoContext::ProtoConfig(); - auto infostring = protoConf.peer_info_string(); + auto infostring = protoConf.peer_info_string(true); auto ivciphers = infostring.substr(infostring.find("IV_CIPHERS=")); ivciphers = ivciphers.substr(0, ivciphers.find("\n")); @@ -1316,7 +1320,7 @@ TEST(proto, iv_ciphers_legacy) auto protoConf = openvpn::ProtoContext::ProtoConfig(); - auto infostring = protoConf.peer_info_string(); + auto infostring = protoConf.peer_info_string(false); auto ivciphers = infostring.substr(infostring.find("IV_CIPHERS=")); ivciphers = ivciphers.substr(0, ivciphers.find("\n"));