OpenVPN 3 Core Library
Loading...
Searching...
No Matches
test_oob_probe_wrap.cpp
Go to the documentation of this file.
1// OpenVPN -- An application to securely tunnel IP networks
2// over a single port, with support for SSL/TLS-based
3// session authentication and key exchange,
4// packet encryption, packet authentication, and
5// packet compression.
6//
7// Copyright (C) 2026- OpenVPN Inc.
8//
9// SPDX-License-Identifier: MPL-2.0 OR AGPL-3.0-only WITH openvpn3-openssl-exception
10//
11
12// Tests for ProtoContext::ProbeWrap -- wrapping an out-of-band SERVER_PROBE and
13// unwrapping the matching PROBE_REPLY with each control-channel protection mode
14// (tls-auth, tls-crypt, tls-crypt-v2, plaintext), built standalone from a
15// ProtoConfig without an established session. Each test wraps as the client, then
16// plays the server with the low-level static helpers (ProtoContext::build_* /
17// unwrap_*), verifying the framing round-trips and the OOB payload survives.
18
19#include "test_common.hpp"
20
23#include <openvpn/ssl/proto.hpp>
28
29using namespace openvpn;
30
31namespace {
32
33// key/cert material lives next to the unit tests, in ../ssl
34#define OOB_KEYDIR UNITTEST_SOURCE_DIR "../ssl/"
35
36enum class WrapMode
37{
38 PLAIN,
39 AUTH,
40 CRYPT,
41 CRYPT_V2,
42};
43
44// Build a minimal client ProtoConfig carrying just enough to wrap a control
45// packet in the requested mode. tls-auth uses bidirectional key-direction so the
46// same HMAC key wraps and unwraps within a single config; tls-crypt derives its
47// direction from the client/server role, matched by the server side below.
48ProtoContext::ProtoConfig::Ptr make_config(WrapMode m, Frame::Ptr frame, StrongRandomAPI::Ptr rng, Time &time)
49{
51 cp->frame = frame;
52 cp->now = &time;
53 cp->rng = rng;
54 cp->prng = rng;
55
56 const std::string tls_auth_key = read_text(OOB_KEYDIR "tls-auth.key");
57
58 switch (m)
59 {
60 case WrapMode::AUTH:
61 cp->tls_auth_factory.reset(new CryptoOvpnHMACFactory<SSLLib::CryptoAPI>());
62 cp->tls_auth_key.parse(tls_auth_key);
63 cp->set_tls_auth_digest(CryptoAlgs::lookup("SHA256"));
64 cp->key_direction = -1; // bidirectional
65 break;
66 case WrapMode::CRYPT:
67 case WrapMode::CRYPT_V2:
68 {
69 // tls-crypt needs an SSL library context (for its cipher), obtained from
70 // an ssl_factory -- build one exactly as the proto tests do
71 auto cc = SSLLib::SSLAPI::Config::Ptr(new SSLLib::SSLAPI::Config());
72 cc->set_mode(Mode(Mode::CLIENT));
73 cc->set_frame(frame);
74 cc->set_rng(rng);
75 cc->load_ca(read_text(OOB_KEYDIR "ca.crt"), true);
76 cc->load_cert(read_text(OOB_KEYDIR "client.crt"));
77 cc->load_private_key(read_text(OOB_KEYDIR "client.key"));
78 cp->ssl_factory = cc->new_factory();
79
80 cp->tls_crypt_factory.reset(new CryptoTLSCryptFactory<SSLLib::CryptoAPI>());
81 cp->set_tls_crypt_algs();
82 if (m == WrapMode::CRYPT)
83 {
84 cp->tls_crypt_key.parse(tls_auth_key);
85 cp->tls_crypt_ = ProtoContext::ProtoConfig::TLSCrypt::V1;
86 }
87 else
88 {
89 TLSCryptV2ClientKey k(cp->tls_crypt_context);
90 k.parse(read_text(OOB_KEYDIR "tls-crypt-v2-client.key"));
91 k.extract_key(cp->tls_crypt_key);
92 k.extract_wkc(cp->wkc);
93 cp->tls_crypt_ = ProtoContext::ProtoConfig::TLSCrypt::V2;
94 }
95 break;
96 }
97 case WrapMode::PLAIN:
98 break;
99 }
100 return cp;
101}
102
103// Stateless server end: unwraps a client probe and wraps a reply, using the
104// control-channel protection built with the *server* role (so the encrypt/decrypt
105// slices are the mirror of the client's). For tls-crypt-v2 the server key is the
106// Kc the client sent wrapped as WKc; here both sides share it via the config.
107class ServerSim
108{
109 public:
110 ServerSim(const ProtoContext::ProtoConfig::Ptr &cp, WrapMode m, const SessionStats::Ptr &stats)
111 : config(cp), mode(m)
112 {
113 pid_recv.init("OOB-SRV", 0, stats);
114 psid_self.randomize(*config->rng);
115
116 switch (mode)
117 {
118 case WrapMode::AUTH:
119 hmac_size = config->tls_auth_context->size();
120 ProtoContext::build_tls_auth(*config, ta_send, ta_recv);
121 break;
122 case WrapMode::CRYPT:
123 case WrapMode::CRYPT_V2:
124 hmac_size = config->tls_crypt_context->digest_size();
125 ProtoContext::build_tls_crypt(*config, true, config->tls_crypt_key, tc_send, tc_recv);
126 break;
127 case WrapMode::PLAIN:
128 break;
129 }
130 }
131
134 ProtoSessionID &src,
135 PacketIDControl &pid)
136 {
137 switch (mode)
138 {
139 case WrapMode::AUTH:
140 return ProtoContext::unwrap_tls_auth(buf, hmac_size, *ta_recv, pid_recv, src, pid);
141 case WrapMode::CRYPT:
142 case WrapMode::CRYPT_V2:
143 return ProtoContext::unwrap_tls_crypt(buf, work, *config->frame, hmac_size, *tc_recv, pid_recv, src, pid);
144 case WrapMode::PLAIN:
145 buf.advance(1);
146 src.read(buf);
147 return ProtoContext::UnwrapStatus::OK;
148 }
149 return ProtoContext::UnwrapStatus::DROP;
150 }
151
152 // wrap a server->client reply; plain tls-crypt for v2 (the reply carries no WKc)
153 void wrap_reply(BufferAllocated &buf, BufferAllocated &work)
154 {
155 const PacketIDControl::time_t now_secs = config->now->seconds_since_epoch();
156 switch (mode)
157 {
158 case WrapMode::AUTH:
159 ProtoContext::wrap_tls_auth(ProtoContext::CONTROL_OOB_V1, buf, pid_send, now_secs, hmac_size, psid_self, 0, *ta_send);
160 break;
161 case WrapMode::CRYPT:
162 case WrapMode::CRYPT_V2:
163 ProtoContext::wrap_tls_crypt(ProtoContext::CONTROL_OOB_V1, buf, work, *config->frame, pid_send, now_secs, hmac_size, psid_self, 0, *tc_send, nullptr);
164 break;
165 case WrapMode::PLAIN:
167 break;
168 }
169 }
170
171 ProtoSessionID psid_self;
172
173 private:
175 WrapMode mode;
176 size_t hmac_size = 0;
177 OvpnHMACInstance::Ptr ta_send, ta_recv;
178 TLSCryptInstance::Ptr tc_send, tc_recv;
179 PacketIDControlSend pid_send;
180 PacketIDControlReceive pid_recv;
181};
182
183BufferAllocated make_msg_buffer()
184{
185 BufferAllocated buf;
186 buf.reset(512 /*headroom*/, 2048 /*capacity*/, BufAllocFlags::NO_FLAGS);
187 return buf;
188}
189
190// client SERVER_PROBE -> server unwrap -> server PROBE_REPLY -> client unwrap
191void run_roundtrip(WrapMode m)
192{
193 Frame::Ptr frame(new Frame(Frame::Context(512, 2048, 512, 0, 16, BufAllocFlags::NO_FLAGS)));
194 // Use the SSL library's own RNG: mbedTLS requires an MbedTLSRandom for the
195 // SSL context built below (tls-crypt), and a real RNG is fine here since the
196 // tests assert round-trip correctness, not specific random bytes.
197 StrongRandomAPI::Ptr rng(new SSLLib::RandomAPI());
198 Time time = Time::now();
199 auto cp = make_config(m, frame, rng, time);
200 SessionStats::Ptr stats(new SessionStats());
201
202 ProtoContext::ProbeWrap client(cp, stats);
203 ServerSim server(cp, m, stats);
204
205 // ---- client wraps a SERVER_PROBE ----
206 const oob::ProbeParameter param{.timestamp = 0x1122334455667788ULL, .flags = 0};
207
208 BufferAllocated probe = make_msg_buffer();
209 ASSERT_TRUE(oob::server_probe_write(probe, param));
210 BufferAllocated cwork;
211 client.wrap(probe, cwork);
212
213 // ---- server unwraps it ----
214 BufferAllocated swork;
215 ProtoSessionID src;
216 PacketIDControl pid;
217 ASSERT_EQ(server.unwrap(probe, swork, src, pid), ProtoContext::UnwrapStatus::OK);
218 EXPECT_TRUE(src.match(client.self_psid()));
219
220 const auto got_param = oob::server_probe_read(probe);
221 ASSERT_TRUE(got_param);
222 EXPECT_EQ(got_param->timestamp, param.timestamp);
223 EXPECT_EQ(got_param->flags, param.flags);
224
225 // ---- server wraps a PROBE_REPLY ----
226 const oob::ProbeReply reply{.peer_session_id = client.self_psid(),
227 .priority = 3,
228 .weight = 7,
229 .max_latency_diff = 0,
230 .connect_lifetime = 60,
231 .flags = 0};
232
233 BufferAllocated rbuf = make_msg_buffer();
234 ASSERT_TRUE(oob::client_reply_write(rbuf, reply));
235 BufferAllocated rwork;
236 server.wrap_reply(rbuf, rwork);
237
238 // ---- client unwraps the reply ----
239 BufferAllocated cwork2;
240 ProtoSessionID rsrc;
241 PacketIDControl rpid;
242 ASSERT_EQ(client.unwrap(rbuf, cwork2, rsrc, rpid), ProtoContext::UnwrapStatus::OK);
243 EXPECT_TRUE(rsrc.match(server.psid_self));
244
245 const auto got_reply = oob::client_reply_read(rbuf);
246 ASSERT_TRUE(got_reply);
247 EXPECT_TRUE(got_reply->peer_session_id.match(client.self_psid()));
248 EXPECT_EQ(got_reply->priority, reply.priority);
249 EXPECT_EQ(got_reply->weight, reply.weight);
250 EXPECT_EQ(got_reply->connect_lifetime, reply.connect_lifetime);
251}
252
253} // namespace
254
255TEST(OobProbeWrap, plaintext_roundtrip)
256{
257 run_roundtrip(WrapMode::PLAIN);
258}
259
260TEST(OobProbeWrap, tls_auth_roundtrip)
261{
262 run_roundtrip(WrapMode::AUTH);
263}
264
265TEST(OobProbeWrap, tls_crypt_roundtrip)
266{
267 run_roundtrip(WrapMode::CRYPT);
268}
269
270// tls-crypt-v2: the probe must use the WKc-bearing opcode and carry the wrapped
271// client key as a trailer; stripping it, the server (holding the same Kc) unwraps
272// the probe body. The reply direction is covered by tls_crypt_roundtrip.
273TEST(OobProbeWrap, tls_crypt_v2_probe_appends_wkc)
274{
275 Frame::Ptr frame(new Frame(Frame::Context(512, 2048, 512, 0, 16, BufAllocFlags::NO_FLAGS)));
276 StrongRandomAPI::Ptr rng(new SSLLib::RandomAPI());
277 Time time = Time::now();
278 auto cp = make_config(WrapMode::CRYPT_V2, frame, rng, time);
279 SessionStats::Ptr stats(new SessionStats());
280
281 ASSERT_TRUE(cp->wkc.defined());
282 const size_t wkc_size = cp->wkc.size();
283
284 ProtoContext::ProbeWrap client(cp, stats);
285
286 const oob::ProbeParameter param{.timestamp = 0xdeadbeefcafef00dULL, .flags = 0};
287
288 BufferAllocated probe = make_msg_buffer();
289 ASSERT_TRUE(oob::server_probe_write(probe, param));
290 BufferAllocated cwork;
291 client.wrap(probe, cwork);
292
293 // opcode is CONTROL_OOB_WKC_V1 with key-id 0
294 const unsigned char expected_op =
296 ASSERT_GE(probe.size(), wkc_size);
297 EXPECT_EQ(probe.c_data()[0], expected_op);
298
299 // the last wkc_size bytes are exactly the wrapped client key
300 EXPECT_EQ(std::memcmp(probe.c_data() + probe.size() - wkc_size, cp->wkc.c_data(), wkc_size), 0);
301
302 // strip the WKc trailer; the server (same Kc) unwraps the remaining probe
303 BufferAllocated stripped;
304 stripped.reset(0, probe.size(), BufAllocFlags::NO_FLAGS);
305 stripped.write(probe.c_data(), probe.size() - wkc_size);
306
307 ServerSim server(cp, WrapMode::CRYPT_V2, stats);
308 BufferAllocated swork;
309 ProtoSessionID src;
310 PacketIDControl pid;
311 ASSERT_EQ(server.unwrap(stripped, swork, src, pid), ProtoContext::UnwrapStatus::OK);
312 EXPECT_TRUE(src.match(client.self_psid()));
313
314 const auto got_param = oob::server_probe_read(stripped);
315 ASSERT_TRUE(got_param);
316 EXPECT_EQ(got_param->timestamp, param.timestamp);
317}
318
319namespace {
320
321// A reply too short to hold the fixed [op][psid][pid][hmac] header must be
322// dropped, not parsed: ProbeWrap::unwrap() runs in the prober's asio receive
323// handler, where the advance()/read() underflow exception would escape into the
324// event loop. Feed every wrap mode a range of truncated datagrams.
325void run_truncated_reply_is_dropped(WrapMode m)
326{
327 Frame::Ptr frame(new Frame(Frame::Context(512, 2048, 512, 0, 16, BufAllocFlags::NO_FLAGS)));
328 StrongRandomAPI::Ptr rng(new SSLLib::RandomAPI());
329 Time time = Time::now();
330 auto cp = make_config(m, frame, rng, time);
331 SessionStats::Ptr stats(new SessionStats());
332
333 ProtoContext::ProbeWrap client(cp, stats);
334
335 // 1 byte (opcode only) up to a partial session id -- all below any header size
336 const unsigned char junk[ProtoSessionID::SIZE] = {};
337 for (size_t len = 1; len <= sizeof(junk); ++len)
338 {
339 BufferAllocated recv = make_msg_buffer();
340 recv.write(junk, len);
341
343 ProtoSessionID src;
344 PacketIDControl pid;
345 EXPECT_EQ(client.unwrap(recv, work, src, pid), ProtoContext::UnwrapStatus::DROP) << "len=" << len;
346 }
347}
348
349} // namespace
350
351TEST(OobProbeWrap, plaintext_truncated_reply_is_dropped)
352{
353 run_truncated_reply_is_dropped(WrapMode::PLAIN);
354}
355
356TEST(OobProbeWrap, tls_auth_truncated_reply_is_dropped)
357{
358 run_truncated_reply_is_dropped(WrapMode::AUTH);
359}
360
361TEST(OobProbeWrap, tls_crypt_truncated_reply_is_dropped)
362{
363 run_truncated_reply_is_dropped(WrapMode::CRYPT);
364}
void reset(const size_t min_capacity, const BufferFlags flags=BufAllocFlags::NO_FLAGS)
Resets the buffer with the specified minimum capacity and flags.
Definition buffer.hpp:1773
const T * c_data() const
Returns a const pointer to the start of the buffer.
Definition buffer.hpp:1193
size_t size() const
Returns the size of the buffer in T objects.
Definition buffer.hpp:1241
void advance(const size_t delta)
Advances the buffer by the specified delta.
Definition buffer.hpp:1276
void write(const T *data, const size_t size)
Write data to the buffer.
Definition buffer.hpp:1561
Wrap an out-of-band SERVER_PROBE / unwrap the matching PROBE_REPLY.
Definition proto.hpp:1996
const ProtoSessionID & self_psid() const
the client session id carried in the probe (echoed back in the reply)
Definition proto.hpp:2029
void wrap(BufferAllocated &buf, BufferAllocated &work)
Wrap an already-encoded SERVER_PROBE payload in place.
Definition proto.hpp:2040
RCPtr< ProtoConfig > Ptr
Definition proto.hpp:365
static void wrap_tls_plain(const unsigned int opcode, Buffer &buf, const ProtoSessionID &psid_self, const unsigned int key_id)
plaintext control channel: [op][psid][payload]
Definition proto.hpp:1805
static UnwrapStatus unwrap_tls_auth(Buffer &recv, const size_t hmac_size, OvpnHMACInstance &ta_hmac_recv, const PacketIDControlReceive &pid_recv, ProtoSessionID &src_psid, PacketIDControl &pid)
Definition proto.hpp:1830
static void build_tls_auth(const ProtoConfig &c, OvpnHMACInstance::Ptr &send, OvpnHMACInstance::Ptr &recv)
Definition proto.hpp:1908
static void wrap_tls_auth(const unsigned int opcode, Buffer &buf, PacketIDControlSend &pid_send, const PacketIDControl::time_t now_secs, const size_t hmac_size, const ProtoSessionID &psid_self, const unsigned int key_id, OvpnHMACInstance &ta_hmac_send)
tls-auth: [op][psid][hmac][pid][payload]
Definition proto.hpp:1730
static void wrap_tls_crypt(const unsigned int opcode, BufferAllocated &buf, BufferAllocated &work, Frame &frame, PacketIDControlSend &pid_send, const PacketIDControl::time_t now_secs, const size_t hmac_size, const ProtoSessionID &psid_self, const unsigned int key_id, TLSCryptInstance &tls_crypt_send, const Buffer *wkc)
Definition proto.hpp:1756
static UnwrapStatus unwrap_tls_crypt(BufferAllocated &recv, BufferAllocated &work, Frame &frame, const size_t hmac_size, TLSCryptInstance &tls_crypt_recv, const PacketIDControlReceive &pid_recv, ProtoSessionID &src_psid, PacketIDControl &pid)
Definition proto.hpp:1859
static void build_tls_crypt(const ProtoConfig &c, const bool server, const OpenVPNStaticKey &key, TLSCryptInstance::Ptr &send, TLSCryptInstance::Ptr &recv)
Definition proto.hpp:1933
bool match(const ProtoSessionID &other) const
Definition psid.hpp:90
void read(BufType &buf)
Definition psid.hpp:59
static TimeType now()
Definition time.hpp:302
void work(openvpn_io::io_context &io_context, ThreadCommon &tc, MyRunContext &runctx, const unsigned int unit)
constexpr BufferFlags NO_FLAGS(0U)
no flags set
Mode mode(const Type type)
Type lookup(const std::string &name)
bool server_probe_write(Buffer &buf, const ProbeParameter &param)
Write a complete SERVER_PROBE (message header + probe_parameter TLV). Client.
bool client_reply_write(Buffer &buf, const ProbeReply &reply)
Write a complete PROBE_REPLY (message header + probe_reply TLV). Server.
std::optional< ProbeReply > client_reply_read(Buffer &buf)
std::optional< ProbeParameter > server_probe_read(Buffer &buf)
std::string read_text(const std::string &filename, const std::uint64_t max_size=0)
Definition file.hpp:127
probe parameter TLV (sent by the client in a SERVER_PROBE).
std::uint64_t timestamp
client clock as a UNIX timestamp
probe reply TLV (sent by the server in a PROBE_REPLY).
ProtoSessionID peer_session_id
echoes the session id of the request
#define OOB_KEYDIR
TEST(OobProbeWrap, plaintext_roundtrip)