mirror of
https://github.com/OpenVPN/openvpn3.git
synced 2024-09-20 04:02: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:
parent
f9c878b90b
commit
989dd7ead5
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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{};
|
||||
};
|
||||
|
||||
|
@ -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
157
openvpn/ssl/psid_cookie.hpp
Normal 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
|
356
openvpn/ssl/psid_cookie_impl.hpp
Normal file
356
openvpn/ssl/psid_cookie_impl.hpp
Normal 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
|
@ -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.
|
||||
|
@ -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 ()
|
||||
|
||||
|
14
test/unittests/test_psid_cookie.cpp
Normal file
14
test/unittests/test_psid_cookie.cpp
Normal 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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user