0
0
mirror of https://github.com/OpenVPN/openvpn3.git synced 2024-09-19 19:52:15 +02:00

Port the psid cookie defense from ovpn2

The psid cookie defense is designed to thwart resource exhaustion and
amplification attacks wherein a malicious client sends the server a
flood of CONTROL_HARD_RESET_CLIENT_V2 packets with spooofed source
addresses.  This patch allows the server to defer client tracking
state creation until the client responds to the server's
CONTROL_HARD_RESET_SERVER_V2 message.

Signed-off-by: Mark Deric <jmark@openvpn.net>
This commit is contained in:
Mark Deric 2023-10-03 12:42:29 -07:00
parent f9c878b90b
commit 989dd7ead5
12 changed files with 689 additions and 60 deletions

View File

@ -52,10 +52,10 @@ namespace openvpn {
*
* This data structure is always sent
* over the net in network byte order,
* by calling htonpid, ntohpid,
* htontime, and ntohtime on the
* data elements to change them
* to and from standard sizes.
* by calling htonl, ntohl, on the
* 32-bit data elements, id_t and
* net_time_t, to change them to and
* from network order.
*
* In addition, time is converted to
* a PacketID::net_time_t before sending,
@ -81,7 +81,7 @@ struct PacketID
id_t id; // legal values are 1 through 2^32-1
time_t time; // converted to PacketID::net_time_t before transmission
static size_t size(const int form)
static constexpr size_t size(const int form)
{
if (form == PacketID::LONG_FORM)
return longidsize;
@ -103,7 +103,8 @@ struct PacketID
time = time_t(0);
}
void read(Buffer &buf, const int form)
template <typename BufType> // so it can take a Buffer or a ConstBuffer
void read(BufType &buf, const int form)
{
id_t net_id;
net_time_t net_time;
@ -165,30 +166,25 @@ class PacketIDSend
PacketIDSend()
{
init(PacketID::SHORT_FORM, 0);
init(PacketID::SHORT_FORM);
}
PacketIDSend(const int form, PacketID::id_t startid)
explicit PacketIDSend(int form, PacketID::id_t start_at = PacketID::id_t(0))
{
init(PacketID::SHORT_FORM, startid);
init(form, start_at);
}
/**
* @param form PacketID::LONG_FORM or PacketID::SHORT_FORM
* @param initial id for the sending
* @param start_at initial id for the sending
*/
void init(const int form, PacketID::id_t startid)
void init(const int form, PacketID::id_t start_at = 0)
{
pid_.id = PacketID::id_t(startid);
pid_.id = start_at;
pid_.time = PacketID::time_t(0);
form_ = form;
}
void init(const int form)
{
init(form, 0);
}
PacketID next(const PacketID::time_t now)
{
PacketID ret;

View File

@ -47,14 +47,14 @@ class ReliableRecvTemplate
ReliableRecvTemplate()
{
}
ReliableRecvTemplate(const id_t span)
ReliableRecvTemplate(const id_t span, id_t start_at = 0)
{
init(span);
init(span, start_at);
}
void init(const id_t span)
void init(const id_t span, id_t start_at = 0)
{
window_.init(0, span);
window_.init(start_at, span);
}
// Call with unsequenced packet off of the wire.
@ -66,15 +66,17 @@ class ReliableRecvTemplate
};
unsigned int receive(const PACKET &packet, const id_t id)
{
unsigned int rflags;
if (window_.in_window(id))
{
Message &m = window_.ref_by_id(id);
m.id_ = id;
m.packet = packet;
return ACK_TO_SENDER | IN_WINDOW;
rflags = ACK_TO_SENDER | IN_WINDOW;
}
else
return window_.pre_window(id) ? ACK_TO_SENDER : 0;
rflags = window_.pre_window(id) ? ACK_TO_SENDER : 0;
return rflags;
}
// Return true if next_sequenced() is ready to return next message

View File

@ -70,14 +70,14 @@ class ReliableSendTemplate
: next(0)
{
}
ReliableSendTemplate(const id_t span)
ReliableSendTemplate(const id_t span, id_t start_at = 0)
{
init(span);
init(span, start_at);
}
void init(const id_t span)
void init(const id_t span, id_t start_at = 0)
{
next = 0;
next = start_at;
window_.init(next, span);
}

View File

@ -110,7 +110,7 @@ class ServerProto
};
// This is the main server-side client instance object
class Session : Base, // OpenVPN protocol implementation
class Session : ProtoContext, // OpenVPN protocol impl (aka, Base per typedef above)
public TransportLink, // Transport layer
public TunLink, // Tun/routing layer
public ManLink // Management layer
@ -138,16 +138,17 @@ class ServerProto
virtual void start(const TransportClientInstance::Send::Ptr &parent,
const PeerAddr::Ptr &addr,
const int local_peer_id) override
const int local_peer_id,
const ProtoSessionID cookie_psid = ProtoSessionID()) override
{
TransportLink::send = parent;
peer_addr = addr;
// init OpenVPN protocol handshake
Base::update_now();
Base::reset();
Base::reset(cookie_psid);
Base::set_local_peer_id(local_peer_id);
Base::start();
Base::start(cookie_psid);
Base::flush(true);
// coarse wakeup range

View File

@ -1635,12 +1635,13 @@ class ProtoContext
}
}
KeyContext(ProtoContext &p, const bool initiator)
KeyContext(ProtoContext &p, const bool initiator, bool psid_cookie_mode = false)
: Base(*p.config->ssl_factory,
p.config->now,
p.config->tls_timeout,
p.config->frame,
p.stats),
p.stats,
psid_cookie_mode),
proto(p),
state(STATE_UNDEF),
crypto_flags(0),
@ -1677,9 +1678,20 @@ class ProtoContext
return Base::get_tls_warnings();
}
// need to call only on the initiator side of the connection
void start()
/**
* @brief Initialize the state machine and start protocol negotiation
*
* Called by ProtoContext::start()
*
* @param cookie_psid see comment in ProtoContext::reset()
*/
void start(const ProtoSessionID cookie_psid = ProtoSessionID())
{
if (cookie_psid.defined())
{
set_state(S_WAIT_RESET_ACK);
dirty = true;
}
if (state == C_INITIAL || state == S_INITIAL)
{
send_reset();
@ -3365,6 +3377,34 @@ class ProtoContext
};
public:
class PsidCookieHelper
{
public:
PsidCookieHelper(unsigned int op_field)
: op_code_(opcode_extract(op_field)), key_id_(key_id_extract(op_field))
{
}
bool is_clients_initial_reset() const
{
return key_id_ == 0 && op_code_ == CONTROL_HARD_RESET_CLIENT_V2;
}
bool is_clients_server_reset_ack() const
{
return key_id_ == 0 && (op_code_ == CONTROL_V1 || op_code_ == ACK_V1);
}
static unsigned int get_server_hard_reset_opfield()
{
return op_compose(CONTROL_HARD_RESET_SERVER_V2, 0);
}
private:
const unsigned int op_code_;
const unsigned int key_id_;
};
class TLSWrapPreValidate : public RC<thread_unsafe_refcount>
{
public:
@ -3637,7 +3677,18 @@ class ProtoContext
tls_crypt_metadata = c.tls_crypt_metadata_factory->new_obj();
}
void reset()
/**
* @brief Resets ProtoContext *this to it's initial state
*
* @param cookie_psid the ProtoSessionID parameter that allows a server
* implementation using the psid cookie mechanism to pass in the verified hmac
* server session cookie. In the client implementation, the parameter is
* meaningless and defaults to an empty ProtoSessionID which is created at compile
* time since the default ProtoSessionID ctor is constexpr. For the default
* cookie_psid, defined() returns false (vs true for the verified session cookie)
* so the absence of a parameter selects the correct code path.
*/
void reset(const ProtoSessionID cookie_psid = ProtoSessionID())
{
const ProtoConfig &c = *config;
@ -3697,8 +3748,16 @@ class ProtoContext
ta_hmac_recv->init(c.tls_key.slice(OpenVPNStaticKey::HMAC));
}
// init tls_auth packet ID
ta_pid_send.init(PacketID::LONG_FORM);
/**
* @brief Initialize tls_auth packet ID for the send case
*
* The second argument sets the expected packet id. If the server
* implementation is using the psid cookie mechanism, the state creation is
* deferred until the client's second packet, id 1, is received; otherwise we
* expect to handle the 1st packet, id 0.
*
*/
ta_pid_send.init(PacketID::LONG_FORM, cookie_psid.defined() ? 1 : 0);
ta_pid_recv.init(c.pid_mode, PacketID::LONG_FORM, "SSL-CC", 0, stats);
break;
case TLS_PLAIN:
@ -3706,11 +3765,14 @@ class ProtoContext
}
// initialize proto session ID
psid_self.randomize(*c.prng);
if (cookie_psid.defined())
psid_self = cookie_psid;
else
psid_self.randomize(*c.prng);
psid_peer.reset();
// initialize key contexts
primary.reset(new KeyContext(*this, is_client()));
primary.reset(new KeyContext(*this, is_client(), cookie_psid.defined()));
OPENVPN_LOG_PROTO_VERBOSE(debug_prefix() << " New KeyContext PRIMARY id=" << primary->key_id());
// initialize keepalive timers
@ -3750,12 +3812,19 @@ class ProtoContext
return PacketType(buf, *this);
}
// start protocol negotiation
void start()
/**
* @brief Initialize the state machine and start protocol negotiation
*
* Called by both derived client and server protocol classes, this function hands
* off to the implementation in KeyContext::start()
*
* @param cookie_psid see ProtoContext::reset()
*/
void start(const ProtoSessionID cookie_psid = ProtoSessionID())
{
if (!primary)
throw proto_error("start: no primary key");
primary->start();
primary->start(cookie_psid);
update_last_received(); // set an upper bound on when we expect a response
}

View File

@ -100,12 +100,16 @@ class ProtoStackBase
TimePtr now_arg, // pointer to current time
const Time::Duration &tls_timeout_arg, // packet retransmit timeout
const Frame::Ptr &frame, // contains info on how to allocate and align buffers
const SessionStats::Ptr &stats_arg) // error statistics
const SessionStats::Ptr &stats_arg, // error statistics
bool psid_cookie_mode) // start the reliability layer at packet id 1, not 0
: tls_timeout(tls_timeout_arg),
ssl_(ssl_factory.ssl()),
frame_(frame),
stats(stats_arg),
now(now_arg)
now(now_arg),
rel_recv(ovpn_receiving_window, psid_cookie_mode ? 1 : 0),
rel_send(ovpn_sending_window, psid_cookie_mode ? 1 : 0)
{
}
@ -510,8 +514,8 @@ class ProtoStackBase
protected:
TimePtr now;
ReliableRecv rel_recv{ovpn_receiving_window};
ReliableSend rel_send{ovpn_sending_window};
ReliableRecv rel_recv;
ReliableSend rel_send;
ReliableAck xmit_acks{};
};

View File

@ -41,9 +41,9 @@ class ProtoSessionID
SIZE = 8
};
ProtoSessionID()
constexpr ProtoSessionID()
: defined_(false), id_{}
{
reset();
}
void reset()
@ -52,7 +52,8 @@ class ProtoSessionID
std::memset(id_, 0, SIZE);
}
explicit ProtoSessionID(Buffer &buf)
template <typename BufType> // so it can take a Buffer or a ConstBuffer
explicit ProtoSessionID(BufType &buf)
{
buf.read(id_, SIZE);
defined_ = true;
@ -66,7 +67,8 @@ class ProtoSessionID
defined_ = true;
}
void read(Buffer &buf)
template <typename BufType> // so it can take a Buffer or a ConstBuffer
void read(BufType &buf)
{
buf.read(id_, SIZE);
defined_ = true;
@ -82,7 +84,17 @@ class ProtoSessionID
buf.prepend(id_, SIZE);
}
bool defined() const
// returned buffer is only valid for *this lifetime
const Buffer get_buf() const
{
if (defined_)
{
return PsidBuf(const_cast<Buffer::type>(id_));
}
return Buffer();
}
constexpr bool defined() const
{
return defined_;
}
@ -97,13 +109,17 @@ class ProtoSessionID
return render_hex(id_, SIZE);
}
protected:
ProtoSessionID(const unsigned char *data)
{
std::memcpy(id_, data, SIZE);
}
private:
// access protected ctor to use Buffer w/o memcpy
struct PsidBuf : Buffer
{
// T* data, const size_t offset, const size_t size, const size_t capacity
PsidBuf(typename Buffer::type id)
: Buffer(id, 0, SIZE, SIZE)
{
}
};
bool defined_;
unsigned char id_[SIZE];
};

157
openvpn/ssl/psid_cookie.hpp Normal file
View File

@ -0,0 +1,157 @@
// 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/>.
/**
* @brief Support deferred server-side state creation when client connects
*
* Creating OpenVPN protocol tracking state upon receipt of an initial client HARD_RESET
* packet invites the bad actor to flood the server with connection requests maintaining
* anonymity by spoofing the client's source address. Not only does this invite
* resource exhaustion, but, because of reliability layer retries, it creates an
* amplification attack as the server retries its un-acknowledged HARD_RESET replies to
* the spoofed address.
*
* This solution treats the server's 64-bit protocol session ID ("Psid or psid") as a
* cookie that allows the server to defer state creation. It is ported here to openvpn3
* from original work in OpenVPN. Unlike the randomly created server psid generated in
* psid.hpp for the server's HARD_RESET reply, this approach derives the server psid via
* an HMAC of information from the incoming client OpenVPN HARD_RESET control message
* (i.e., the psid cookie). This allows the server to verify the client as it returns
* the server psid in it's second packet, only then creating protocol state.
*
* Not only does this prevent the resource exhaustion, but it has the happy consequence
* of avoiding the amplification attack. Since no state is created on the first packet,
* there is no reliability layer; and, hence, no retries of the server's HARD_RESET
* reply.
*/
#pragma once
#include <openvpn/buffer/buffer.hpp> // includes rc.hpp
#include <openvpn/ssl/psid.hpp>
namespace openvpn {
/**
* @brief Interface to communicate the server's address semantics
*
* The server implementation must derive a concrete class from this abstract one.
* This encapsulates the server implementation's knowledge of the address semantics it
* needs to return the HARD_RESET packet to the client. Further, in support of the
* psid calculation, this class also needs to supply this component with a
* reproducably hashable memory slab that represents the client address.
*/
class PsidCookieAddrInfoBase
{
public:
virtual const unsigned char *get_abstract_cli_addrport(size_t &slab_size) const = 0;
virtual const void *get_impl_info() const = 0;
virtual ~PsidCookieAddrInfoBase() = default;
};
/**
* @brief Interface to provide access to the server's transport capability
*
* The server implementation must derive a concrete class from this abstract one. The
* server implementation is presumed to own the transport and must implement the
* member function to send the
*/
class PsidCookieTransportBase : public RC<thread_unsafe_refcount>
{
public:
typedef RCPtr<PsidCookieTransportBase> Ptr;
virtual bool psid_cookie_send_const(Buffer &send_buf, const PsidCookieAddrInfoBase &pcaib) = 0;
virtual ~PsidCookieTransportBase() = default;
};
/**
* @brief Interface to integrate this component into the server implementation
*/
class PsidCookie : public RC<thread_unsafe_refcount>
{
public:
typedef RCPtr<PsidCookie> Ptr;
/**
* @brief Values returned by the intercept() function
*
* These are status values depending upon the action that intercept() took in
* handling client's 1st and 2nd packets. Early drop indicates that the packet was
* dropped before determining whether the packet was client's 1st or 2nd.
*/
enum class Intercept
{
DECLINE_HANDLING,
EARLY_DROP,
DROP_1ST,
HANDLE_1ST,
DROP_2ND,
HANDLE_2ND,
};
/**
* @brief Called when a potential new client session packet is received
*
* Called by the server implementation when it recieves a packet for which it has no
* state information. Such a packet is potentially a client HARD_RESET or a 2nd
* client packet returning the psid cookie.
*
* @param pkt_buf The packet received by the server implementation.
* @param pcaib The address information as contained in an instance of the class
* that the server implementation derived from the PsidCookieAddrInfoBase class
* @return Intercept Status of the packet handling
*/
virtual Intercept intercept(ConstBuffer &pkt_buf, const PsidCookieAddrInfoBase &pcaib) = 0;
/**
* @brief Get the cookie psid from client's 2nd packet
*
* This provides the server's psid (a.k.a, the cookie_psid) as returned by the
* client in it's 2nd packet. It may only be called after intercept() returns
* HANDLE_2ND, indicating a valid psid cookie. Further, it may only be called once
* as it invalidates the internal data source after it sets the return value.
*
* @return ProtoSessionID
*/
virtual ProtoSessionID get_cookie_psid() = 0;
// The PsidCookie server implementation owns the transport detail for sending the psid cookie packet that the class implementing this interface creates. The intercept() method will call the derived class' psid_cookie_send_const() function above.
/**
* @brief Give this component the transport needed to send the server's HARD_RESET
*
* The server implementation must call this method before the intercept() function
* is asked to handle a packet
*
* @param pctb The transport capability as provided by the server implementation's
* object derived from the PsidCookieTransportBase class
*/
virtual void provide_psid_cookie_transport(PsidCookieTransportBase::Ptr pctb) = 0;
virtual ~PsidCookie() = default;
};
} // namespace openvpn

View File

@ -0,0 +1,356 @@
// 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;
}
private:
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 int 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] [cli_pktid]
= 1 + SID_SIZE + hmac_size + long_pktid_size_ + 5 + SID_SIZE + short_pktid_size_;
// 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()
{
RandomAPI::Ptr rng(new SSLLib::RandomAPI(false));
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
*
* @param cli_psid Client's protocol session id, ProtoSessionID
* @param pcaib Client's address information, reproducibly hashable
* @param offset moves the time quantisation window
* @return ProtoSessionID the resulting psid cookie
*/
ProtoSessionID calculate_session_id_hmac(const ProtoSessionID &cli_psid,
const PsidCookieAddrInfoBase &pcaib,
int offset)
{
hmac_ctx_.reset();
// Get the valid time quantisation for our hmac, we divide time by handwindow/2
// and allow the previous and future session time if specified by offset
uint32_t session_id_time = now_->raw() / ((handwindow_.raw() + 1) / 2) + offset;
// no endian concerns; hmac is created and checked by the same host
hmac_ctx_.update(reinterpret_cast<const unsigned char *>(&session_id_time),
sizeof(session_id_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 adjacent timestamps too */
for (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

View File

@ -35,11 +35,21 @@
#include <openvpn/buffer/buffer.hpp>
#include <openvpn/addr/route.hpp>
#include <openvpn/crypto/cryptodc.hpp>
#include <openvpn/tun/server/tunbase.hpp>
#include <openvpn/server/servhalt.hpp>
#include <openvpn/server/peerstats.hpp>
#include <openvpn/server/peeraddr.hpp>
#include <openvpn/ssl/datalimit.hpp>
#include <openvpn/ssl/psid.hpp>
// TunClientInstance fwd decl replaces
//#include <openvpn/tun/server/tunbase.hpp>
namespace openvpn {
class PsidCookie;
namespace TunClientInstance {
struct Recv;
struct Send;
} // namespace TunClientInstance
} // namespace openvpn
// used by ipma_notify()
struct ovpn_tun_head_ipma;
@ -100,7 +110,8 @@ struct Recv : public virtual RC<thread_unsafe_refcount>
virtual void start(const Send::Ptr &parent,
const PeerAddr::Ptr &addr,
const int local_peer_id) = 0;
const int local_peer_id,
const ProtoSessionID cookie_psid = ProtoSessionID()) = 0;
// Called with OpenVPN-encapsulated packets from transport layer.
// Returns true if packet successfully validated.

View File

@ -119,6 +119,9 @@ if (UNIX)
# Uses Unix Pipe semantics
test_pipe.cpp
# for now, only for ovpn3 servers (i.e., pgserv)
test_psid_cookie.cpp
)
endif ()

View File

@ -0,0 +1,14 @@
#include "test_common.h"
#include <openvpn/ssl/psid_cookie_impl.hpp>
using namespace openvpn;
// TEST(psid_cookie, create) {
TEST(psid_cookie, setup)
{
PsidCookieImpl::pre_threading_setup();
ASSERT_TRUE(true);
}