forked from OpenVPN/openvpn3
-
Notifications
You must be signed in to change notification settings - Fork 0
/
psid_cookie_impl.hpp
369 lines (319 loc) · 14.5 KB
/
psid_cookie_impl.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
// 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) 2022 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 <http://www.gnu.org/licenses/>.
// A 64-bit protocol session ID, used by ProtoContext. But, unlike being random
// in psid.hpp, the PsidCookieImpl class derives it via an HMAC of information
// on the incoming client's OpenVPN HARD_RESET control message. This creates a
// session id that acts like a syn-cookie on the OpenVPN startup 3-way
// handshake.
#pragma once
#include <openvpn/ssl/psid_cookie.hpp>
#include <openvpn/ssl/sslchoose.hpp>
#include <openvpn/common/rc.hpp>
#include <openvpn/crypto/static_key.hpp>
#include <openvpn/crypto/cryptoalgs.hpp>
#include <openvpn/ssl/psid.hpp>
#include <openvpn/transport/server/transbase.hpp>
#include <openvpn/server/servproto.hpp>
namespace openvpn {
/**
* @brief Implements the PsidCookie interface
*
* This code currently only supports tls-auth. The approach can be applied with
* minimal changes also to tls-crypt/no auth but requires more changes/protocol
* changes and updated clients for the tls-crypt-v2 case.
*
* This class is not thread safe; it expects to be instantiated in each thread of a
* multi-threaded server implementation.
*/
class PsidCookieImpl : public PsidCookie
{
public:
static constexpr int SID_SIZE = ProtoSessionID::SIZE;
// must be called _before_ the server implementation starts threads; it guarantees
// that all per thread instances get the same psid cookie hmac key
static void pre_threading_setup()
{
get_key();
}
PsidCookieImpl(ServerProto::Factory *psfp)
: pcfg_(*psfp->proto_context_config),
not_tls_auth_mode_(!pcfg_.tls_auth_enabled()),
now_(pcfg_.now), handwindow_(pcfg_.handshake_window),
ta_hmac_recv_(pcfg_.tls_auth_context->new_obj()),
ta_hmac_send_(pcfg_.tls_auth_context->new_obj())
{
if (not_tls_auth_mode_)
return;
// init tls_auth hmac (see ProtoContext.reset() case TLS_AUTH; also TLSAuthPreValidate ctor)
if (pcfg_.key_direction >= 0)
{
// key-direction is 0 or 1
const unsigned int key_dir = pcfg_.key_direction ? OpenVPNStaticKey::INVERSE : OpenVPNStaticKey::NORMAL;
ta_hmac_send_->init(pcfg_.tls_key.slice(OpenVPNStaticKey::HMAC
| OpenVPNStaticKey::ENCRYPT | key_dir));
ta_hmac_recv_->init(pcfg_.tls_key.slice(OpenVPNStaticKey::HMAC
| OpenVPNStaticKey::DECRYPT | key_dir));
}
else
{
// key-direction bidirectional mode
ta_hmac_send_->init(pcfg_.tls_key.slice(OpenVPNStaticKey::HMAC));
ta_hmac_recv_->init(pcfg_.tls_key.slice(OpenVPNStaticKey::HMAC));
}
// initialize psid HMAC context with digest type and key
const StaticKey &key = get_key();
hmac_ctx_.init(digest_, key.data(), key.size());
}
virtual ~PsidCookieImpl() = default;
virtual Intercept intercept(ConstBuffer &pkt_buf, const PsidCookieAddrInfoBase &pcaib) override
{
// tls auth enabled is the only config we handle
if (not_tls_auth_mode_)
{ // test discovered in TLSAuthPreValidate
return Intercept::DECLINE_HANDLING; // let existing code handle these cases
}
if (!pkt_buf.size())
{
return Intercept::EARLY_DROP; // packet validation fails, no opcode
}
CookieHelper chelp(pkt_buf[0]);
if (chelp.is_clients_initial_reset())
{
return process_clients_initial_reset(pkt_buf, pcaib);
}
else if (chelp.is_clients_server_reset_ack())
{
return process_clients_server_reset_ack(pkt_buf, pcaib);
}
// JMD_TODO: log failure? Logging DDoS?
return Intercept::EARLY_DROP; // bad op field
}
virtual ProtoSessionID get_cookie_psid() override
{
ProtoSessionID ret_val = cookie_psid_;
cookie_psid_.reset();
return ret_val;
}
virtual void provide_psid_cookie_transport(PsidCookieTransportBase::Ptr pctb) override
{
pctb_ = pctb;
}
#ifndef UNIT_TEST
private:
#endif
using CookieHelper = ProtoContext::PsidCookieHelper;
Intercept process_clients_initial_reset(ConstBuffer &pkt_buf, const PsidCookieAddrInfoBase &pcaib)
{
static const size_t hmac_size = ta_hmac_recv_->output_size();
// ovpn_hmac_cmp checks for adequate pkt_buf.size()
bool pkt_hmac_valid = ta_hmac_recv_->ovpn_hmac_cmp(pkt_buf.c_data(), pkt_buf.size(), 1 + SID_SIZE, hmac_size, long_pktid_size_);
if (!pkt_hmac_valid)
{
// JMD_TODO: log failure? Logging DDoS?
return Intercept::DROP_1ST;
}
// check for adequate packet size to complete this function
static const size_t reqd_packet_size
// clang-format off
// [op_field] [cli_psid] [HMAC] [cli_auth_pktid] [cli_pktid]
= 1 + SID_SIZE + hmac_size + long_pktid_size_ + short_pktid_size_;
// clang-format on
if (pkt_buf.size() < reqd_packet_size)
{
// JMD_TODO: log failure? Logging DDoS?
return Intercept::DROP_1ST;
}
// "buf_copy" here uses the same underlying data, but has it's own offset; skip
// past client's op_field.
ConstBuffer recv_buf_copy(pkt_buf.c_data() + 1, pkt_buf.size() - 1, true);
// decapsulate_tls_auth
const ProtoSessionID cli_psid(recv_buf_copy);
recv_buf_copy.advance(hmac_size);
PacketID cli_auth_pktid; // a.k.a, replay_packet_id in draft RFC
cli_auth_pktid.read(recv_buf_copy, PacketID::LONG_FORM);
PacketID cli_pktid; // a.k.a., packet_id in draft RFC
cli_pktid.read(recv_buf_copy, PacketID::SHORT_FORM);
// start building the server reply HARD_RESET packet
BufferAllocated send_buf;
static const Frame &frame = *pcfg_.frame;
frame.prepare(Frame::WRITE_SSL_INIT, send_buf);
// set server packet id (a.k.a., msg seq no) which would come from the
// reliability layer, if we had one
const reliable::id_t net_id = 0; // no htonl(0) since result is 0
send_buf.prepend(static_cast<const void *>(&net_id), sizeof(net_id));
// prepend_dest_psid_and_acks
cli_psid.prepend(send_buf);
const id_t cli_net_id = htonl(cli_pktid.id);
send_buf.prepend((unsigned char *)&cli_net_id, sizeof(cli_net_id));
send_buf.push_front((unsigned char)1);
// gen head
PacketIDSend svr_auth_pid(PacketID::LONG_FORM);
svr_auth_pid.write_next(send_buf, true, now_->seconds_since_epoch());
// make space for tls-auth HMAC
send_buf.prepend_alloc(ta_hmac_send_->output_size());
// write source PSID
const ProtoSessionID srv_psid = calculate_session_id_hmac(cli_psid, pcaib, 0);
srv_psid.prepend(send_buf);
// write opcode
const unsigned char op_field = CookieHelper::get_server_hard_reset_opfield();
send_buf.push_front(op_field);
// write hmac
ta_hmac_send_->ovpn_hmac_gen(send_buf.data(), send_buf.size(), 1 + SID_SIZE, ta_hmac_send_->output_size(), long_pktid_size_);
// consumer's implementation to send the SERVER_HARD_RESET to the client
bool send_ok = pctb_->psid_cookie_send_const(send_buf, pcaib);
if (send_ok)
{
return Intercept::HANDLE_1ST;
}
return Intercept::DROP_1ST;
}
Intercept process_clients_server_reset_ack(ConstBuffer &pkt_buf, const PsidCookieAddrInfoBase &pcaib)
{
static const size_t hmac_size = ta_hmac_recv_->output_size();
// ovpn_hmac_cmp checks for adequate pkt_buf.size()
bool pkt_hmac_valid = ta_hmac_recv_->ovpn_hmac_cmp(pkt_buf.c_data(), pkt_buf.size(), 1 + SID_SIZE, hmac_size, long_pktid_size_);
if (!pkt_hmac_valid)
{
// JMD_TODO: log failure? Logging DDoS?
return Intercept::DROP_2ND;
}
static const size_t reqd_packet_size
// clang-format off
// [op_field] [cli_psid] [HMAC] [cli_auth_pktid] [acked] [srv_psid]
= 1 + SID_SIZE + hmac_size + long_pktid_size_ + 5 + SID_SIZE;
// the fixed size, 5, of the [acked] field recognizes that the client's first
// response will ack exactly one packet, the server's HARD_RESET
// clang-format on
if (pkt_buf.size() < reqd_packet_size)
{
// JMD_TODO: log failure? Logging DDoS?
return Intercept::DROP_2ND;
}
// "buf_copy" here uses the same underlying data, but has it's own offset; skip
// past client's op_field.
ConstBuffer recv_buf_copy(pkt_buf.c_data() + 1, pkt_buf.size() - 1, true);
// decapsulate_tls_auth
const ProtoSessionID cli_psid(recv_buf_copy);
recv_buf_copy.advance(hmac_size);
PacketID cli_auth_pktid; // a.k.a, replay_packet_id in draft RFC
cli_auth_pktid.read(recv_buf_copy, PacketID::LONG_FORM);
unsigned int ack_count = recv_buf_copy[0];
if (ack_count != 1)
{
return Intercept::DROP_2ND;
}
recv_buf_copy.advance(5);
cookie_psid_.read(recv_buf_copy);
// verify client's Psid Cookie
bool is_cookie_valid = check_session_id_hmac(cookie_psid_, cli_psid, pcaib);
if (is_cookie_valid)
{
return Intercept::HANDLE_2ND;
}
return Intercept::DROP_2ND;
}
// key must be common to all threads
static StaticKey create_key()
{
StrongRandomAPI::Ptr rng(new SSLLib::RandomAPI());
const CryptoAlgs::Alg &alg = CryptoAlgs::get(digest_);
// guarantee that the key is large enough
StaticKey key;
key.init_from_rng(*rng, alg.size());
return key;
}
static const StaticKey &get_key()
{
static const StaticKey key = create_key();
return key;
}
/**
* @brief Calculate the psid cookie, the ProtoSessionID hmac
*
* @param cli_psid Client's protocol session id, ProtoSessionID
* @param pcaib Client's address information, reproducibly hashable
* @param offset moves the time valid time window backward from current
* @return ProtoSessionID the psid cookie
*/
ProtoSessionID calculate_session_id_hmac(const ProtoSessionID &cli_psid,
const PsidCookieAddrInfoBase &pcaib,
unsigned int offset)
{
hmac_ctx_.reset();
// Get the time window for which the ProtoSessionID hmac is valid. The window
// size is an interval given by handwindow/2, one half of the configured
// handshake timeout, typically 30 seconds. The valid_time is the count of
// intervals since the beginning of the epoch. With offset zero, the valid_time
// is the server's current interval; with offsets 1 to n, it is the server's nth
// previous interval.
//
// There is the theoretical issue of valid_time wrapping after 2^32 intervals.
// With 30 second intervals, around the year 4010. Will not spoil my weekend.
uint64_t interval = (handwindow_.raw() + 1) / 2;
uint32_t valid_time = static_cast<uint32_t>(now_->raw() / interval - offset);
// no endian concerns; hmac is created and checked by the same host
hmac_ctx_.update(reinterpret_cast<const unsigned char *>(&valid_time),
sizeof(valid_time));
// the memory slab at cli_addr_port of size cli_addrport_size is a reproducibly
// hashable representation of the client's address and port
size_t cli_addrport_size;
const unsigned char *cli_addr_port = pcaib.get_abstract_cli_addrport(cli_addrport_size);
hmac_ctx_.update(cli_addr_port, cli_addrport_size);
// add session id of client
const Buffer cli_psid_buf = cli_psid.get_buf();
hmac_ctx_.update(cli_psid_buf.c_data(), SID_SIZE);
// finalize the hmac and package it as the server's ProtoSessionID
BufferAllocated hmac_result(SSLLib::CryptoAPI::HMACContext::MAX_HMAC_SIZE, 0);
ProtoSessionID srv_psid;
hmac_ctx_.final(hmac_result.write_alloc(hmac_ctx_.size()));
srv_psid.read(hmac_result);
return srv_psid;
}
bool check_session_id_hmac(const ProtoSessionID &srv_psid,
const ProtoSessionID &cli_psid,
const PsidCookieAddrInfoBase &pcaib)
{
// check the current timestamp and the previous one in case the server's clock
// has moved to the one following that given to the client
for (unsigned int offset = 0; offset <= 1; ++offset)
{
ProtoSessionID calc_psid = calculate_session_id_hmac(cli_psid, pcaib, offset);
if (srv_psid.match(calc_psid))
{
return true;
}
}
return false;
}
static constexpr CryptoAlgs::Type digest_ = CryptoAlgs::Type::SHA256;
static constexpr size_t long_pktid_size_ = PacketID::size(PacketID::LONG_FORM);
static constexpr size_t short_pktid_size_ = PacketID::size(PacketID::SHORT_FORM);
const ProtoContext::ProtoConfig &pcfg_;
bool not_tls_auth_mode_;
TimePtr now_;
const Time::Duration &handwindow_;
OvpnHMACInstance::Ptr ta_hmac_recv_;
OvpnHMACInstance::Ptr ta_hmac_send_;
// the psid cookie specific hmac object
SSLLib::CryptoAPI::HMACContext hmac_ctx_;
PsidCookieTransportBase::Ptr pctb_;
ProtoSessionID cookie_psid_;
};
} // namespace openvpn