Generated WebSocket masks use a secure PRNG by default:

This resolves a medium vulnerability described in the
Beast Hybrid Assessment Report by Bishop Fox, where masks generated
for use with outgoing WebSocket client frames use an insufficient
source of entropy and a non-cryptographically secure pseudo-random
number generator.

By default, all newly constructed WebSocket streams will use a
uniquely seeded secure PRNG (ChaCha20 in counter mode). As this may
result in increased CPU resource consumption, the function
websocket::stream::secure_prng() may be used to select a faster but
less secure PRNG, for the case where the caller knows that the secure
generator is not necessary.

On some systems, std::random_device may produce insufficient entropy
to securely seed the PRNG. As this condition cannot be detected by
Beast, callers may use the function websocket::seed_prng() called
once at startup to provide at least 256 bits of entropy which will
be used to uniquely seed all subsequent PRNGs.
This commit is contained in:
Vinnie Falco
2018-07-05 16:47:07 -07:00
parent 68727b3cfb
commit 749e54f31b
16 changed files with 495 additions and 139 deletions

View File

@@ -129,6 +129,7 @@ install:
- export PATH="`pwd`":$PATH
- git submodule update --init tools/build
- git submodule update --init tools/boostdep
- git submodule update --init libs/align
- git submodule update --init libs/asio
- git submodule update --init libs/assert
- git submodule update --init libs/config

View File

@@ -2,6 +2,7 @@ Version 176:
* Tidy up Quick Reference
* Fix array end calculation in utf8 assertion
* WebSocket masks use secure PRNG by default
--------------------------------------------------------------------------------

View File

@@ -23,6 +23,7 @@ install:
- cd boost-root
- git submodule update --init tools/build
- git submodule update --init tools/boostdep
- git submodule update --init libs/align
- git submodule update --init libs/asio
- git submodule update --init libs/assert
- git submodule update --init libs/config

View File

@@ -10,8 +10,11 @@
[section Release Notes]
This version fixes a missing executor work guard in all composed operations
used in the implementatio. Users who are experiencing crashes related to
asynchronous completion handlers are encouraged to upgrade.
used in the implementation. Users who are experiencing crashes related to
asynchronous completion handlers are encouraged to upgrade. Also included
is an improved mechanism for generating random numbers used to mask outgoing
websocket frames when operating in the client mode. This resolves a
vulnerability described in the Beast Hybrid Assessment Report from Bishop Fox.
[heading Boost 1.68]
@@ -35,14 +38,20 @@ in future versions.
* New [link beast.ref.boost__beast__http__is_mutable_body_writer `http::is_mutable_body_writer`] metafunction
* New [link beast.ref.boost__beast__websocket__seed_prng `websocket::seed_prng`] for manually providing entropy to the PRNG
* New [link beast.ref.boost__beast__websocket__stream.secure_prng `websocket::stream::secure_prng`] to control whether the connection uses a secure PRNG
[*Improvements]
* Generated WebSocket masks use a secure PRNG by default
* Improvements to [link beast.ref.boost__beast__buffers_adapter `buffers_adapter`]
* ([issue 1109]) Use a shared string for example HTTP server doc roots
* ([issue 1079]) Add [link beast.ref.boost__beast__handler_ptr.has_value `handler_ptr::has_value`]
* Improvements to [link beast.ref.boost__beast__buffers_adapter `buffers_adapter`]
[*Fixes]
* ([issue 1073]) Fix race in advanced server examples

View File

@@ -137,6 +137,7 @@
<simplelist type="vert" columns="1">
<member><link linkend="beast.ref.boost__beast__websocket__async_teardown">async_teardown</link></member>
<member><link linkend="beast.ref.boost__beast__websocket__is_upgrade">is_upgrade</link></member>
<member><link linkend="beast.ref.boost__beast__websocket__seed_prng">seed_prng</link></member>
<member><link linkend="beast.ref.boost__beast__websocket__teardown">teardown</link></member>
</simplelist>
<bridgehead renderas="sect3">Options</bridgehead>

View File

@@ -0,0 +1,196 @@
//
// Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/boostorg/beast
//
//
// This is a derivative work, original copyright follows:
//
/*
Copyright (c) 2015 Orson Peters <orsonpeters@gmail.com>
This software is provided 'as-is', without any express or implied warranty. In no event will the
authors be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose, including commercial
applications, and to alter it and redistribute it freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim that you wrote the
original software. If you use this software in a product, an acknowledgment in the product
documentation would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be misrepresented as
being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
#ifndef BOOST_BEAST_CORE_DETAIL_CHACHA_HPP
#define BOOST_BEAST_CORE_DETAIL_CHACHA_HPP
#include <cstdint>
#include <limits>
#include <iosfwd>
namespace boost {
namespace beast {
namespace detail {
template<std::size_t R>
class chacha
{
void generate_block();
void chacha_core();
alignas(16) std::uint32_t block_[16];
std::uint32_t keysetup_[8];
std::uint64_t ctr_ = 0;
int idx_ = 16;
public:
static constexpr std::size_t state_size = sizeof(chacha::keysetup_);
using result_type = std::uint32_t;
chacha(std::uint32_t const* v, std::uint64_t stream);
std::uint32_t
operator()();
#if 0
template<std::size_t R_>
friend
bool
operator==(chacha<R_> const& lhs, chacha<R_> const& rhs);
template<std::size_t R_>
friend
bool
operator!=(chacha<R_> const& lhs, chacha<R_> const& rhs);
static
constexpr
std::uint32_t
min()
{
return (std::numeric_limits<std::uint32_t>::min)();
}
static
constexpr
std::uint32_t
max()
{
return (std::numeric_limits<std::uint32_t>::max)();
}
#endif
};
template<std::size_t R>
chacha<R>::
chacha(std::uint32_t const* v, std::uint64_t stream)
{
for (int i = 0; i < 6; ++i)
keysetup_[i] = v[i];
keysetup_[6] = v[6] + (stream & 0xffffffff);
keysetup_[7] = v[7] + ((stream >> 32) & 0xffffffff);
}
template<std::size_t R>
std::uint32_t
chacha<R>::
operator()()
{
if(idx_ == 16)
{
idx_ = 0;
++ctr_;
generate_block();
}
return block_[idx_++];
}
template<std::size_t R>
void
chacha<R>::
generate_block()
{
std::uint32_t constexpr constants[4] = {
0x61707865, 0x3320646e, 0x79622d32, 0x6b206574 };
std::uint32_t input[16];
for (int i = 0; i < 4; ++i)
input[i] = constants[i];
for (int i = 0; i < 8; ++i)
input[4 + i] = keysetup_[i];
input[12] = (ctr_ / 16) & 0xffffffffu;
input[13] = (ctr_ / 16) >> 32;
input[14] = input[15] = 0xdeadbeef; // Could use 128-bit counter.
for (int i = 0; i < 16; ++i)
block_[i] = input[i];
chacha_core();
for (int i = 0; i < 16; ++i)
block_[i] += input[i];
}
template<std::size_t R>
void
chacha<R>::
chacha_core()
{
#define BOOST_BEAST_CHACHA_ROTL32(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
#define BOOST_BEAST_CHACHA_QUARTERROUND(x, a, b, c, d) \
x[a] = x[a] + x[b]; x[d] ^= x[a]; x[d] = BOOST_BEAST_CHACHA_ROTL32(x[d], 16); \
x[c] = x[c] + x[d]; x[b] ^= x[c]; x[b] = BOOST_BEAST_CHACHA_ROTL32(x[b], 12); \
x[a] = x[a] + x[b]; x[d] ^= x[a]; x[d] = BOOST_BEAST_CHACHA_ROTL32(x[d], 8); \
x[c] = x[c] + x[d]; x[b] ^= x[c]; x[b] = BOOST_BEAST_CHACHA_ROTL32(x[b], 7)
for (unsigned i = 0; i < R; i += 2)
{
BOOST_BEAST_CHACHA_QUARTERROUND(block_, 0, 4, 8, 12);
BOOST_BEAST_CHACHA_QUARTERROUND(block_, 1, 5, 9, 13);
BOOST_BEAST_CHACHA_QUARTERROUND(block_, 2, 6, 10, 14);
BOOST_BEAST_CHACHA_QUARTERROUND(block_, 3, 7, 11, 15);
BOOST_BEAST_CHACHA_QUARTERROUND(block_, 0, 5, 10, 15);
BOOST_BEAST_CHACHA_QUARTERROUND(block_, 1, 6, 11, 12);
BOOST_BEAST_CHACHA_QUARTERROUND(block_, 2, 7, 8, 13);
BOOST_BEAST_CHACHA_QUARTERROUND(block_, 3, 4, 9, 14);
}
#undef BOOST_BEAST_CHACHA_QUARTERROUND
#undef BOOST_BEAST_CHACHA_ROTL32
}
//#endif
#if 0
// Implement <random> interface.
template<std::size_t R>
bool
operator==(chacha<R> const& lhs, chacha<R> const& rhs)
{
for (int i = 0; i < 8; ++i)
if (lhs.keysetup_[i] != rhs.keysetup_[i])
return false;
return lhs.ctr_ == rhs.ctr_;
}
template<std::size_t R>
bool
operator!=(chacha<R> const& lhs, chacha<R> const& rhs)
{
return !(lhs == rhs);
}
#endif
} // detail
} // beast
} // boost
#endif

View File

@@ -14,6 +14,7 @@
#include <boost/beast/core/string.hpp>
#include <boost/beast/core/detail/base64.hpp>
#include <boost/beast/core/detail/sha1.hpp>
#include <boost/beast/websocket/detail/stream_base.hpp>
#include <boost/assert.hpp>
#include <array>
#include <cstdint>
@@ -31,14 +32,15 @@ using sec_ws_key_type = static_string<
using sec_ws_accept_type = static_string<
beast::detail::base64::encoded_size(20)>;
template<class Gen>
inline
void
make_sec_ws_key(sec_ws_key_type& key, Gen& g)
make_sec_ws_key(sec_ws_key_type& key)
{
auto p = stream_prng::prng();
char a[16];
for(int i = 0; i < 16; i += 4)
{
auto const v = g();
auto const v = p->secure();
a[i ] = v & 0xff;
a[i+1] = (v >> 8) & 0xff;
a[i+2] = (v >> 16) & 0xff;

View File

@@ -24,66 +24,6 @@ namespace beast {
namespace websocket {
namespace detail {
// Pseudo-random source of mask keys
//
template<class Generator>
class maskgen_t
{
Generator g_;
public:
using result_type =
typename Generator::result_type;
maskgen_t();
result_type
operator()() noexcept;
void
rekey();
};
template<class Generator>
maskgen_t<Generator>::maskgen_t()
{
rekey();
}
template<class Generator>
auto
maskgen_t<Generator>::operator()() noexcept ->
result_type
{
for(;;)
if(auto key = g_())
return key;
}
template<class _>
void
maskgen_t<_>::rekey()
{
std::random_device rng;
#if 0
std::array<std::uint32_t, 32> e;
for(auto& i : e)
i = rng();
// VFALCO This constructor causes
// address sanitizer to fail, no idea why.
std::seed_seq ss(e.begin(), e.end());
g_.seed(ss);
#else
g_.seed(rng());
#endif
}
// VFALCO NOTE This generator has 5KB of state!
//using maskgen = maskgen_t<std::mt19937>;
using maskgen = maskgen_t<std::minstd_rand>;
//------------------------------------------------------------------------------
using prepared_key = std::array<unsigned char, 4>;
inline

View File

@@ -16,9 +16,23 @@
#include <boost/beast/zlib/inflate_stream.hpp>
#include <boost/beast/core/buffers_suffix.hpp>
#include <boost/beast/core/error.hpp>
#include <boost/beast/core/detail/chacha.hpp>
#include <boost/beast/core/detail/integer_sequence.hpp>
#include <boost/align/aligned_alloc.hpp>
#include <boost/asio/buffer.hpp>
#include <atomic>
#include <cstdint>
#include <memory>
#include <new>
#include <random>
// Turn this on to avoid using thread_local
//#define BOOST_BEAST_NO_THREAD_LOCAL 1
#ifdef BOOST_BEAST_NO_THREAD_LOCAL
#include <atomic>
#include <mutex>
#endif
namespace boost {
namespace beast {
@@ -105,8 +119,191 @@ public:
}
};
//------------------------------------------------------------------------------
struct stream_prng
{
bool secure_prng_ = true;
struct prng_type
{
std::minstd_rand fast;
beast::detail::chacha<20> secure;
#if BOOST_BEAST_NO_THREAD_LOCAL
prng_type* next = nullptr;
#endif
prng_type(std::uint32_t const* v, std::uint64_t stream)
: fast(static_cast<typename decltype(fast)::result_type>(
v[0] + v[1] + v[2] + v[3] + v[4] + v[5] + v[6] + v[7] + stream))
, secure(v, stream)
{
}
};
class prng_ref
{
prng_type* p_;
public:
prng_ref& operator=(prng_ref&&) = delete;
explicit
prng_ref(prng_type& p)
: p_(&p)
{
}
prng_ref(prng_ref&& other)
: p_([&other]
{
auto p = other.p_;
other.p_ = nullptr;
return p;
}())
{
}
#ifdef BOOST_BEAST_NO_THREAD_LOCAL
~prng_ref()
{
if(p_)
pool::impl().release(*p_);
}
#endif
prng_type*
operator->() const
{
return p_;
}
};
#ifndef BOOST_BEAST_NO_THREAD_LOCAL
static
prng_ref
prng()
{
static std::atomic<std::uint64_t> stream{0};
thread_local prng_type p{seed(), stream++};
return prng_ref(p);
}
#else
static
prng_ref
prng()
{
return prng_ref(pool::impl().acquire());
}
#endif
static
std::uint32_t const*
seed(std::seed_seq* ss = nullptr)
{
static seed_data d(ss);
return d.v;
}
std::uint32_t
create_mask()
{
auto p = prng();
if(secure_prng_)
for(;;)
if(auto key = p->secure())
return key;
for(;;)
if(auto key = p->fast())
return key;
}
private:
struct seed_data
{
std::uint32_t v[8];
explicit
seed_data(std::seed_seq* pss)
{
if(! pss)
{
std::random_device g;
std::seed_seq ss{
g(), g(), g(), g(), g(), g(), g(), g()};
ss.generate(v, v+8);
}
else
{
pss->generate(v, v+8);
}
}
};
#ifdef BOOST_BEAST_NO_THREAD_LOCAL
class pool
{
prng_type* head_ = nullptr;
std::atomic<std::uint64_t> n_{0};
std::mutex m_;
public:
~pool()
{
for(auto p = head_; p;)
{
auto next = p->next;
p->~prng_type();
boost::alignment::aligned_free(p);
p = next;
}
}
prng_type&
acquire()
{
for(;;)
{
std::lock_guard<std::mutex> lock(m_);
if(! head_)
break;
auto p = head_;
head_ = head_->next;
return *p;
}
auto p = boost::alignment::aligned_alloc(
16, sizeof(prng_type));
if(! p)
BOOST_THROW_EXCEPTION(std::bad_alloc{});
return *(new(p) prng_type(seed(), n_++));
}
void
release(prng_type& p)
{
std::lock_guard<std::mutex> lock(m_);
p.next = head_;
head_ = &p;
}
static
pool&
impl()
{
static pool instance;
return instance;
}
};
#endif
};
//------------------------------------------------------------------------------
template<bool deflateSupported>
struct stream_base
struct stream_base : stream_prng
{
// State information for the permessage-deflate extension
struct pmd_type
@@ -165,7 +362,7 @@ struct stream_base
};
template<>
struct stream_base<false>
struct stream_base<false> : stream_prng
{
// These stubs are for avoiding linking in the zlib
// code when permessage-deflate is not enabled.

View File

@@ -556,7 +556,7 @@ write_close(DynamicBuffer& db, close_reason const& cr)
if(role_ == role_type::client)
{
fh.mask = true;
fh.key = wr_gen_();
fh.key = this->create_mask();
}
else
{
@@ -608,7 +608,7 @@ write_ping(DynamicBuffer& db,
fh.len = data.size();
fh.mask = role_ == role_type::client;
if(fh.mask)
fh.key = wr_gen_();
fh.key = this->create_mask();
detail::write(db, fh);
if(data.empty())
return;
@@ -641,7 +641,7 @@ build_request(detail::sec_ws_key_type& key,
req.set(http::field::host, host);
req.set(http::field::upgrade, "websocket");
req.set(http::field::connection, "upgrade");
detail::make_sec_ws_key(key, wr_gen_);
detail::make_sec_ws_key(key);
req.set(http::field::sec_websocket_key, key);
req.set(http::field::sec_websocket_version, "13");
build_request_pmd(req, is_deflate_supported{});

View File

@@ -406,7 +406,7 @@ operator()(
remain_ = buffer_size(cb_);
fh_.fin = fin_;
fh_.len = remain_;
fh_.key = ws_.wr_gen_();
fh_.key = ws_.create_mask();
detail::prepare_key(key_, fh_.key);
ws_.wr_fb_.reset();
detail::write<flat_static_buffer_base>(
@@ -458,7 +458,7 @@ operator()(
n = clamp(remain_, ws_.wr_buf_size_);
remain_ -= n;
fh_.len = n;
fh_.key = ws_.wr_gen_();
fh_.key = ws_.create_mask();
fh_.fin = fin_ ? remain_ == 0 : false;
detail::prepare_key(key_, fh_.key);
buffer_copy(buffer(
@@ -521,7 +521,7 @@ operator()(
}
if(fh_.mask)
{
fh_.key = ws_.wr_gen_();
fh_.key = ws_.create_mask();
detail::prepared_key key;
detail::prepare_key(key, fh_.key);
detail::mask_inplace(b, key);
@@ -666,7 +666,7 @@ write_some(bool fin,
}
if(fh.mask)
{
fh.key = wr_gen_();
fh.key = this->create_mask();
detail::prepared_key key;
detail::prepare_key(key, fh.key);
detail::mask_inplace(b, key);
@@ -740,7 +740,7 @@ write_some(bool fin,
// mask, no autofrag
fh.fin = fin;
fh.len = remain;
fh.key = wr_gen_();
fh.key = this->create_mask();
detail::prepared_key key;
detail::prepare_key(key, fh.key);
detail::fh_buffer fh_buf;
@@ -784,7 +784,7 @@ write_some(bool fin,
ConstBufferSequence> cb{buffers};
for(;;)
{
fh.key = wr_gen_();
fh.key = this->create_mask();
detail::prepared_key key;
detail::prepare_key(key, fh.key);
auto const n = clamp(remain, wr_buf_size_);

View File

@@ -206,7 +206,6 @@ class stream
std::size_t wr_buf_opt_ // write buffer size option setting
= 4096;
detail::fh_buffer wr_fb_; // header buffer used for writes
detail::maskgen wr_gen_; // source of mask keys
detail::pausation paused_rd_; // paused read op
detail::pausation paused_wr_; // paused write op
@@ -633,6 +632,37 @@ public:
return rd_msg_max_;
}
/** Set whether the PRNG is cryptographically secure
This controls whether or not the source of pseudo-random
numbers used to produce the masks required by the WebSocket
protocol are of cryptographic quality. When the setting is
`true`, a strong algorithm is used which cannot be guessed
by observing outputs. When the setting is `false`, a much
faster algorithm is used.
Masking is only performed by streams operating in the client
mode. For streams operating in the server mode, this setting
has no effect.
By default, newly constructed streams use a secure PRNG.
If the WebSocket stream is used with an encrypted SSL or TLS
next layer, if it is known to the application that intermediate
proxies are not vulnerable to cache poisoning, or if the
application is designed such that an attacker cannot send
arbitrary inputs to the stream interface, then the faster
algorithm may be used.
For more information please consult the WebSocket protocol RFC.
@param value `true` if the PRNG algorithm should be
cryptographically secure.
*/
void
secure_prng(bool value)
{
this->secure_prng_ = value;
}
/** Set the write buffer size option.
Sets the size of the write buffer used by the implementation to
@@ -3563,6 +3593,36 @@ private:
error_code& ec);
};
/** Manually provide a one-time seed to initialize the PRNG
This function invokes the specified seed sequence to produce a seed
suitable for use with the pseudo-random number generator used to
create masks and perform WebSocket protocol handshakes.
If a seed is not manually provided, the implementation will
perform a one-time seed generation using `std::random_device`. This
function may be used when the application runs in an environment
where the random device is unreliable or does not provide sufficient
entropy.
@par Preconditions
This function may not be called after any websocket @ref stream objects
have been constructed.
@param ss A reference to a `std::seed_seq` which will be used to seed
the pseudo-random number generator. The seed sequence should have at
least 256 bits of entropy.
@see stream::secure_prng
*/
inline
void
seed_prng(std::seed_seq& ss)
{
detail::stream_prng::seed(&ss);
}
} // websocket
} // beast
} // boost

View File

@@ -23,7 +23,6 @@ add_executable (tests-beast-websocket
error.cpp
frame.cpp
handshake.cpp
mask.cpp
option.cpp
ping.cpp
read1.cpp

View File

@@ -13,7 +13,6 @@ local SOURCES =
error.cpp
frame.cpp
handshake.cpp
mask.cpp
option.cpp
ping.cpp
read1.cpp

View File

@@ -1,58 +0,0 @@
//
// Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/boostorg/beast
//
// Test that header file is self-contained.
#include <boost/beast/websocket/detail/mask.hpp>
#include <boost/beast/unit_test/suite.hpp>
namespace boost {
namespace beast {
namespace websocket {
namespace detail {
class mask_test : public beast::unit_test::suite
{
public:
struct test_generator
{
using result_type = std::uint32_t;
result_type n = 0;
void
seed(std::seed_seq const&)
{
}
void
seed(result_type const&)
{
}
std::uint32_t
operator()()
{
return n++;
}
};
void run() override
{
maskgen_t<test_generator> mg;
BEAST_EXPECT(mg() != 0);
}
};
BEAST_DEFINE_TESTSUITE(beast,websocket,mask);
} // detail
} // websocket
} // beast
} // boost

View File

@@ -22,6 +22,11 @@ public:
void
testOptions()
{
{
std::seed_seq ss{42};
seed_prng(ss);
}
stream<test::stream> ws{ioc_};
ws.auto_fragment(true);
ws.write_buffer_size(2048);
@@ -37,6 +42,9 @@ public:
pass();
}
ws.secure_prng(true);
ws.secure_prng(false);
auto const bad =
[&](permessage_deflate const& pmd)
{