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"));