C++20 port 6/14: allocation-free AEAD codecs (#3070)

* Add allocation-free C++ AEAD codecs

src/crypto/aead.hpp implements the SIP004/SIP007 AEAD ciphers as value
types: AeadEngine holds the libsodium or Mbed TLS GCM state by value,
AeadTcpEncryptor/AeadTcpDecryptor frame the chunk stream with a fixed
reassembly buffer, and aead_udp_seal/open handle packets. HKDF-SHA1 runs
on stack SHA-1 state instead of Mbed TLS's allocating HMAC API.

The bundled Mbed TLS config now enables MBEDTLS_BLOCK_CIPHER_C, which
keeps the AES context inside mbedtls_gcm_context, so GCM key setup no
longer allocates. That alone saves the C programs two allocations per
connection.

test_crypto_aead_cpp checks HKDF against RFC 5869 and crypto_hkdf, runs
TCP streams and UDP packets between the C and C++ implementations in
both directions for all five AEAD methods, and covers tampering,
framing and salt replay. alloc_steady_crypto_aead runs it under the
malloc counter and requires zero allocations across 600 new
connections and 600 packets (tests/alloc_audit/check_steady.py).

ss_strict_warnings() now applies -Wextra per source file so targets
mixing new C++ with converted C modules keep the latter at -Wall, and
Result gains move assignment.

* Include <algorithm> for std::min in test_crypto_aead_cpp.cpp

* Build the C++ AEAD codecs against Mbed TLS 2.x

Mbed TLS 2 returns void from mbedtls_sha1_starts/update/finish (the int
versions are the _ret variants), which the system-library build on
Ubuntu (2.28) rejects. crypto/mbedtls_compat.hpp gives one int-returning
spelling for both.

* Include C++ headers before crypto.h in test_crypto_aead_cpp.cpp

crypto.h defines min/max macros for the C sources; libstdc++ headers
included after it fail to parse.

* Disable the zero-allocation checks with a system Mbed TLS

They hold for the bundled Mbed TLS (MBEDTLS_BLOCK_CIPHER_C); a system
one may allocate in GCM key setup, as the Debian package build shows.
This commit is contained in:
Max Lv
2026-10-15 09:56:12 +08:00
committed by GitHub
parent 8c3390887e
commit 8d39cd7569
13 changed files with 1294 additions and 12 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
# clang-tidy configuration for shadowsocks-c project sources (src/ only).
# Bundled sources under third_party/ and src/blake3/ are third-party code and
# are excluded by the file regex used in CI:
# run-clang-tidy -p build '/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|event_cpp)\.cpp)$'
# run-clang-tidy -p build '/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|[a-z_]+_cpp)\.cpp)$'
#
# The header-only C++ core (src/core/*.hpp) is analysed through the
# test_core_* translation units that include it.
@@ -39,4 +39,4 @@ Checks: >
-clang-analyzer-security.insecureAPI.strcpy,
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling
WarningsAsErrors: ''
HeaderFilterRegex: '.*/src/([^/]+\.h|(core|event)/[^/]+\.hpp)$'
HeaderFilterRegex: '.*/src/([^/]+\.h|(core|event|crypto)/[^/]+\.hpp)$'
+1 -1
View File
@@ -125,7 +125,7 @@ jobs:
run: |
set -o pipefail
tidy_status=0
run-clang-tidy-18 -quiet -p build -extra-arg=-Wno-unknown-warning-option "$PWD/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|event_cpp)\.cpp)$" 2>&1 | tee tidy.log || tidy_status=$?
run-clang-tidy-18 -quiet -p build -extra-arg=-Wno-unknown-warning-option "$PWD/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|[a-z_]+_cpp)\.cpp)$" 2>&1 | tee tidy.log || tidy_status=$?
grep 'warning:' tidy.log | sort -u > warnings.txt || true
count="$(wc -l < warnings.txt)"
echo "clang-tidy warnings: $count (max allowed: $MAX_WARNINGS)"
+1 -1
View File
@@ -76,7 +76,7 @@ Uses **uncrustify** with the config at `.uncrustify.cfg`. Key settings: 4-space
- **clang-tidy**: config in `.clang-tidy`. The build exports `compile_commands.json` automatically. Run locally:
```bash
run-clang-tidy -quiet -p build '/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|event_cpp)\.cpp)$'
run-clang-tidy -quiet -p build '/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|[a-z_]+_cpp)\.cpp)$'
```
On macOS with Homebrew LLVM, add `-extra-arg="-isysroot$(xcrun --show-sdk-path)"`. CI (`clang-tidy` job in `tests.yml`) enforces a warning-count ratchet via `MAX_WARNINGS` — lower it when fixing findings; never raise it without justification.
- **No-heap lint**: `scripts/check_no_heap.py --self-test` (also a ctest) rejects heap allocation in C++ sources under `src/`.
+7 -5
View File
@@ -62,11 +62,13 @@ function(ss_project_options target)
endif()
endfunction()
# Extra warnings for code written as C++ from the start (src/core and its
# tests).
function(ss_strict_warnings target)
# Extra warnings for source files written as C++ from the start (src/core,
# src/event, src/crypto and their tests). Per file, so targets that also
# compile converted C modules keep those at -Wall. Source properties are
# directory-scoped: call this from the directory that defines the targets.
function(ss_strict_warnings)
if(NOT MSVC)
target_compile_options(${target} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-Wextra>
$<$<COMPILE_LANG_AND_ID:CXX,GNU>:-Wsign-compare -Wclass-memaccess>)
set_source_files_properties(${ARGN} PROPERTIES COMPILE_OPTIONS
"-Wextra;$<$<CXX_COMPILER_ID:GNU>:-Wsign-compare>;$<$<CXX_COMPILER_ID:GNU>:-Wclass-memaccess>")
endif()
endfunction()
+9 -1
View File
@@ -104,7 +104,15 @@ public:
}
Result &operator=(const Result &) = delete;
Result &operator=(Result &&) = delete;
Result &operator=(Result &&other) noexcept(std::is_nothrow_move_constructible_v<T>)
{
if (this != &other) {
this->~Result();
std::construct_at(this, std::move(other));
}
return *this;
}
~Result()
{
+514
View File
@@ -0,0 +1,514 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#include "crypto/aead.hpp"
#include "crypto/mbedtls_compat.hpp"
#include <algorithm>
#include <cstring>
#include <mbedtls/sha1.h>
#include "crypto.h"
#include "ppbloom.h"
// crypto.h defines min/max macros for the C sources.
#undef min
#undef max
namespace ss::crypto {
namespace {
constexpr AeadSpec kSpecs[] = {
{ "aes-128-gcm", AeadKind::aes_128_gcm, 16, 12, 16 },
{ "aes-192-gcm", AeadKind::aes_192_gcm, 24, 12, 16 },
{ "aes-256-gcm", AeadKind::aes_256_gcm, 32, 12, 16 },
{ "chacha20-ietf-poly1305", AeadKind::chacha20_poly1305, 32, 12, 16 },
#ifdef crypto_aead_xchacha20poly1305_ietf_ABYTES
{ "xchacha20-ietf-poly1305", AeadKind::xchacha20_poly1305, 32, 24, 16 },
#endif
};
constexpr std::string_view kSubkeyInfo = "ss-subkey";
bool is_gcm(AeadKind kind) noexcept
{
return kind == AeadKind::aes_128_gcm || kind == AeadKind::aes_192_gcm || kind == AeadKind::aes_256_gcm;
}
// RFC 2104 HMAC-SHA1 with all state on the stack.
class HmacSha1 {
public:
static constexpr std::size_t kSize = 20;
static constexpr std::size_t kBlock = 64;
explicit HmacSha1(std::span<const std::uint8_t> key) noexcept
{
std::array<std::uint8_t, kBlock> block{};
if (key.size() > kBlock) {
(void)mbed::sha1(key.data(), key.size(), block.data());
} else if (!key.empty()) {
std::memcpy(block.data(), key.data(), key.size());
}
for (std::size_t i = 0; i < kBlock; ++i) {
ipad_[i] = block[i] ^ 0x36;
opad_[i] = block[i] ^ 0x5c;
}
sodium_memzero(block.data(), block.size());
mbedtls_sha1_init(&inner_);
ok_ = mbed::sha1_starts(&inner_) == 0 && mbed::sha1_update(&inner_, ipad_.data(), kBlock) == 0;
}
~HmacSha1()
{
mbedtls_sha1_free(&inner_);
sodium_memzero(ipad_.data(), ipad_.size());
sodium_memzero(opad_.data(), opad_.size());
}
HmacSha1(const HmacSha1 &) = delete;
HmacSha1 &operator=(const HmacSha1 &) = delete;
void update(std::span<const std::uint8_t> data) noexcept
{
ok_ = ok_ && mbed::sha1_update(&inner_, data.data(), data.size()) == 0;
}
bool finish(std::uint8_t (&mac)[kSize]) noexcept
{
std::uint8_t inner_hash[kSize];
ok_ = ok_ && mbed::sha1_finish(&inner_, inner_hash) == 0;
mbedtls_sha1_context outer;
mbedtls_sha1_init(&outer);
ok_ = ok_ && mbed::sha1_starts(&outer) == 0 && mbed::sha1_update(&outer, opad_.data(), kBlock) == 0
&& mbed::sha1_update(&outer, inner_hash, kSize) == 0 && mbed::sha1_finish(&outer, mac) == 0;
mbedtls_sha1_free(&outer);
sodium_memzero(inner_hash, sizeof(inner_hash));
return ok_;
}
private:
mbedtls_sha1_context inner_;
std::array<std::uint8_t, kBlock> ipad_;
std::array<std::uint8_t, kBlock> opad_;
bool ok_ = false;
};
bool ppbloom_seen(void *, std::span<const std::uint8_t> salt) noexcept
{
return ppbloom_check(salt.data(), static_cast<int>(salt.size())) == 1;
}
void ppbloom_remember(void *, std::span<const std::uint8_t> salt) noexcept
{
(void)ppbloom_add(salt.data(), static_cast<int>(salt.size()));
}
void store16_be(std::uint8_t *p, std::uint16_t v) noexcept
{
p[0] = static_cast<std::uint8_t>(v >> 8);
p[1] = static_cast<std::uint8_t>(v);
}
std::uint16_t load16_be(const std::uint8_t *p) noexcept
{
return static_cast<std::uint16_t>((p[0] << 8) | p[1]);
}
} // namespace
std::optional<AeadSpec> find_aead(std::string_view name) noexcept
{
for (const AeadSpec &spec : kSpecs) {
if (spec.name == name) {
return spec;
}
}
return std::nullopt;
}
Result<MasterKey> MasterKey::from_password(const AeadSpec &spec, const char *password) noexcept
{
if (password == nullptr) {
return fail(Errc::config);
}
MasterKey master{ spec, {} };
if (crypto_derive_key(password, master.bytes.data(), spec.key_len) != spec.key_len) {
return fail(Errc::config);
}
return master;
}
Result<MasterKey> MasterKey::from_base64(const AeadSpec &spec, const char *key) noexcept
{
if (key == nullptr) {
return fail(Errc::config);
}
MasterKey master{ spec, {} };
if (crypto_parse_key(key, master.bytes.data(), spec.key_len) != spec.key_len) {
return fail(Errc::config);
}
return master;
}
ReplayFilter ReplayFilter::ppbloom() noexcept
{
return ReplayFilter{ &ppbloom_seen, &ppbloom_remember, nullptr };
}
Status hkdf_sha1(std::span<const std::uint8_t> salt, std::span<const std::uint8_t> ikm,
std::span<const std::uint8_t> info, std::span<std::uint8_t> okm) noexcept
{
constexpr std::size_t kHash = HmacSha1::kSize;
if (okm.size() > 255 * kHash) {
return fail(Errc::invalid);
}
std::uint8_t zero_salt[kHash] = {};
std::uint8_t prk[kHash];
{
HmacSha1 extract(salt.empty() ? std::span<const std::uint8_t>(zero_salt) : salt);
extract.update(ikm);
if (!extract.finish(prk)) {
return fail(Errc::crypto);
}
}
std::uint8_t t[kHash];
std::size_t t_len = 0;
std::size_t done = 0;
for (std::uint8_t counter = 1; done < okm.size(); ++counter) {
HmacSha1 expand(prk);
expand.update({ t, t_len });
expand.update(info);
expand.update({ &counter, 1 });
if (!expand.finish(t)) {
sodium_memzero(prk, sizeof(prk));
return fail(Errc::crypto);
}
t_len = kHash;
std::size_t n = std::min(kHash, okm.size() - done);
std::memcpy(okm.data() + done, t, n);
done += n;
}
sodium_memzero(prk, sizeof(prk));
sodium_memzero(t, sizeof(t));
return {};
}
Status derive_subkey(const MasterKey &master, std::span<const std::uint8_t> salt,
std::span<std::uint8_t> subkey) noexcept
{
auto info = std::span<const std::uint8_t>(reinterpret_cast<const std::uint8_t *>(kSubkeyInfo.data()),
kSubkeyInfo.size());
return hkdf_sha1(salt, master.key(), info, subkey);
}
Status AeadEngine::init(const AeadSpec &spec, std::span<const std::uint8_t> subkey) noexcept
{
if (subkey.size() != spec.key_len) {
return fail(Errc::invalid);
}
reset();
spec_ = spec;
if (spec.kind == AeadKind::aes_256_gcm && crypto_aead_aes256gcm_is_available()) {
auto &aes = impl_.emplace<SodiumAes256Gcm>();
if (crypto_aead_aes256gcm_beforenm(&aes.state, subkey.data()) != 0) {
return fail(Errc::crypto);
}
} else if (is_gcm(spec.kind)) {
auto &gcm = impl_.emplace<MbedGcm>();
if (mbedtls_gcm_setkey(&gcm.ctx, MBEDTLS_CIPHER_ID_AES, subkey.data(),
static_cast<unsigned>(subkey.size() * 8)) != 0) {
return fail(Errc::crypto);
}
} else {
auto &chacha = impl_.emplace<SodiumChacha>();
std::memcpy(chacha.key.data(), subkey.data(), subkey.size());
}
return {};
}
void AeadEngine::reset() noexcept
{
impl_.emplace<std::monostate>();
}
Status AeadEngine::seal(std::uint8_t *out, std::span<const std::uint8_t> plaintext, const std::uint8_t *nonce) noexcept
{
int rc = -1;
if (auto *aes = std::get_if<SodiumAes256Gcm>(&impl_)) {
unsigned long long clen = 0;
rc = crypto_aead_aes256gcm_encrypt_afternm(out, &clen, plaintext.data(), plaintext.size(), nullptr, 0,
nullptr, nonce, &aes->state);
} else if (auto *gcm = std::get_if<MbedGcm>(&impl_)) {
rc = mbedtls_gcm_crypt_and_tag(&gcm->ctx, MBEDTLS_GCM_ENCRYPT, plaintext.size(), nonce, spec_.nonce_len,
nullptr, 0, plaintext.data(), out, spec_.tag_len, out + plaintext.size());
} else if (auto *chacha = std::get_if<SodiumChacha>(&impl_)) {
unsigned long long clen = 0;
if (spec_.kind == AeadKind::xchacha20_poly1305) {
rc = crypto_aead_xchacha20poly1305_ietf_encrypt(out, &clen, plaintext.data(), plaintext.size(), nullptr,
0, nullptr, nonce, chacha->key.data());
} else {
rc = crypto_aead_chacha20poly1305_ietf_encrypt(out, &clen, plaintext.data(), plaintext.size(), nullptr,
0, nullptr, nonce, chacha->key.data());
}
}
if (rc != 0) {
return fail(Errc::crypto);
}
return {};
}
Status AeadEngine::open(std::uint8_t *out, std::span<const std::uint8_t> ciphertext, const std::uint8_t *nonce) noexcept
{
if (ciphertext.size() < spec_.tag_len) {
return fail(Errc::crypto);
}
std::size_t mlen = ciphertext.size() - spec_.tag_len;
int rc = -1;
if (auto *aes = std::get_if<SodiumAes256Gcm>(&impl_)) {
unsigned long long plen = 0;
rc = crypto_aead_aes256gcm_decrypt_afternm(out, &plen, nullptr, ciphertext.data(), ciphertext.size(), nullptr,
0, nonce, &aes->state);
} else if (auto *gcm = std::get_if<MbedGcm>(&impl_)) {
rc = mbedtls_gcm_auth_decrypt(&gcm->ctx, mlen, nonce, spec_.nonce_len, nullptr, 0,
ciphertext.data() + mlen, spec_.tag_len, ciphertext.data(), out);
} else if (auto *chacha = std::get_if<SodiumChacha>(&impl_)) {
unsigned long long plen = 0;
if (spec_.kind == AeadKind::xchacha20_poly1305) {
rc = crypto_aead_xchacha20poly1305_ietf_decrypt(out, &plen, nullptr, ciphertext.data(), ciphertext.size(),
nullptr, 0, nonce, chacha->key.data());
} else {
rc = crypto_aead_chacha20poly1305_ietf_decrypt(out, &plen, nullptr, ciphertext.data(), ciphertext.size(),
nullptr, 0, nonce, chacha->key.data());
}
}
if (rc != 0) {
return fail(Errc::crypto);
}
return {};
}
Status AeadTcpEncryptor::init(const MasterKey &master, ReplayFilter filter, bool printable_salt) noexcept
{
master_ = master;
filter_ = filter;
salt_sent_ = false;
nonce_.fill(0);
randombytes_buf(salt_.data(), master.spec.key_len);
if (printable_salt) {
// Vary the constrained span without adding a delimiter or marker.
std::size_t prefix = 6 + randombytes_uniform(7);
for (std::size_t i = 0; i < prefix; ++i) {
salt_[i] = static_cast<std::uint8_t>(0x20 + randombytes_uniform(95));
}
}
std::array<std::uint8_t, kMaxKey> subkey{};
Status st = derive_subkey(master_, salt(), { subkey.data(), master.spec.key_len });
if (st) {
st = engine_.init(master.spec, { subkey.data(), master.spec.key_len });
}
sodium_memzero(subkey.data(), subkey.size());
return st;
}
std::size_t AeadTcpEncryptor::sealed_size(std::size_t plaintext_len) const noexcept
{
std::size_t chunks = (plaintext_len + kMaxChunk - 1) / kMaxChunk;
std::size_t per = kChunkLenSize + 2 * master_.spec.tag_len;
return (salt_sent_ ? 0 : master_.spec.key_len) + plaintext_len + chunks * per;
}
Result<std::size_t> AeadTcpEncryptor::seal(std::span<const std::uint8_t> plaintext, std::span<std::uint8_t> out) noexcept
{
if (plaintext.empty()) {
return std::size_t{ 0 };
}
if (out.size() < sealed_size(plaintext.size())) {
return fail(Errc::invalid);
}
std::size_t at = 0;
if (!salt_sent_) {
std::memcpy(out.data(), salt_.data(), master_.spec.key_len);
at += master_.spec.key_len;
filter_.add(salt());
salt_sent_ = true;
}
const std::size_t tag = master_.spec.tag_len;
while (!plaintext.empty()) {
std::size_t n = std::min(plaintext.size(), kMaxChunk);
std::uint8_t len[kChunkLenSize];
store16_be(len, static_cast<std::uint16_t>(n));
SS_TRY(engine_.seal(out.data() + at, { len, kChunkLenSize }, nonce_.data()));
sodium_increment(nonce_.data(), master_.spec.nonce_len);
at += kChunkLenSize + tag;
SS_TRY(engine_.seal(out.data() + at, plaintext.first(n), nonce_.data()));
sodium_increment(nonce_.data(), master_.spec.nonce_len);
at += n + tag;
plaintext = plaintext.subspan(n);
}
return at;
}
Status AeadTcpDecryptor::init(const MasterKey &master, ReplayFilter filter) noexcept
{
master_ = master;
filter_ = filter;
engine_ready_ = false;
salt_remembered_ = false;
failed_ = false;
pending_payload_ = 0;
nonce_.fill(0);
buffer_.clear();
engine_.reset();
return {};
}
Result<AeadTcpDecryptor::Step> AeadTcpDecryptor::decrypt(std::span<const std::uint8_t> in,
std::span<std::uint8_t> out) noexcept
{
if (failed_) {
return fail(Errc::closed);
}
Step step{ 0, 0 };
if (buffer_.writable().size() < in.size()) {
buffer_.compact();
}
std::size_t take = std::min(in.size(), buffer_.writable().size());
if (take != 0) {
std::memcpy(buffer_.writable().data(), in.data(), take);
(void)buffer_.commit(take);
step.consumed = take;
}
auto bytes = [this]() {
auto r = buffer_.readable();
return std::span<const std::uint8_t>(reinterpret_cast<const std::uint8_t *>(r.data()), r.size());
};
const std::size_t salt_len = master_.spec.key_len;
const std::size_t tag = master_.spec.tag_len;
if (!engine_ready_) {
if (buffer_.size() <= salt_len) {
return step; // the salt alone is not enough to make progress
}
std::memcpy(salt_.data(), bytes().data(), salt_len);
std::span<const std::uint8_t> salt(salt_.data(), salt_len);
if (filter_.check(salt)) {
failed_ = true;
return fail(Errc::crypto);
}
std::array<std::uint8_t, kMaxKey> subkey{};
Status st = derive_subkey(master_, salt, { subkey.data(), salt_len });
if (st) {
st = engine_.init(master_.spec, { subkey.data(), salt_len });
}
sodium_memzero(subkey.data(), subkey.size());
if (!st) {
failed_ = true;
return fail(st.error());
}
(void)buffer_.consume(salt_len);
engine_ready_ = true;
}
for (;;) {
auto data = bytes();
if (pending_payload_ == 0) {
// Same threshold as the C decoder: wait for more than an empty
// chunk's worth of bytes before decoding the length.
if (data.size() <= kChunkLenSize + 2 * tag) {
break;
}
std::uint8_t len[kChunkLenSize];
if (!engine_.open(len, data.first(kChunkLenSize + tag), nonce_.data())) {
failed_ = true;
return fail(Errc::crypto);
}
std::size_t payload = load16_be(len) & kMaxChunk;
if (payload == 0) {
failed_ = true;
return fail(Errc::protocol);
}
sodium_increment(nonce_.data(), master_.spec.nonce_len);
pending_payload_ = payload;
(void)buffer_.consume(kChunkLenSize + tag);
data = bytes();
}
if (data.size() < pending_payload_ + tag || out.size() - step.produced < pending_payload_) {
break;
}
if (!engine_.open(out.data() + step.produced, data.first(pending_payload_ + tag), nonce_.data())) {
failed_ = true;
return fail(Errc::crypto);
}
sodium_increment(nonce_.data(), master_.spec.nonce_len);
step.produced += pending_payload_;
(void)buffer_.consume(pending_payload_ + tag);
pending_payload_ = 0;
}
if (step.produced != 0 && !salt_remembered_) {
// Like the C decoder: remember the salt once the stream has proven
// authentic, rechecking in case a concurrent stream used it.
std::span<const std::uint8_t> salt(salt_.data(), salt_len);
if (filter_.check(salt)) {
failed_ = true;
return fail(Errc::crypto);
}
filter_.add(salt);
salt_remembered_ = true;
}
return step;
}
Result<std::size_t> aead_udp_seal(const MasterKey &master, std::span<const std::uint8_t> plaintext,
std::span<std::uint8_t> out, ReplayFilter filter) noexcept
{
const std::size_t salt_len = master.spec.key_len;
const std::size_t total = salt_len + plaintext.size() + master.spec.tag_len;
if (out.size() < total) {
return fail(Errc::invalid);
}
randombytes_buf(out.data(), salt_len);
std::span<const std::uint8_t> salt(out.data(), salt_len);
filter.add(salt);
std::array<std::uint8_t, kMaxKey> subkey{};
std::array<std::uint8_t, kMaxNonce> nonce{};
AeadEngine engine;
Status st = derive_subkey(master, salt, { subkey.data(), salt_len });
if (st) {
st = engine.init(master.spec, { subkey.data(), salt_len });
}
sodium_memzero(subkey.data(), subkey.size());
SS_TRY(st);
SS_TRY(engine.seal(out.data() + salt_len, plaintext, nonce.data()));
return total;
}
Result<std::size_t> aead_udp_open(const MasterKey &master, std::span<const std::uint8_t> packet,
std::span<std::uint8_t> out, ReplayFilter filter) noexcept
{
const std::size_t salt_len = master.spec.key_len;
const std::size_t tag = master.spec.tag_len;
if (packet.size() <= salt_len + tag) {
return fail(Errc::protocol);
}
std::size_t plaintext_len = packet.size() - salt_len - tag;
if (out.size() < plaintext_len) {
return fail(Errc::invalid);
}
std::span<const std::uint8_t> salt = packet.first(salt_len);
if (filter.check(salt)) {
return fail(Errc::crypto);
}
std::array<std::uint8_t, kMaxKey> subkey{};
std::array<std::uint8_t, kMaxNonce> nonce{};
AeadEngine engine;
Status st = derive_subkey(master, salt, { subkey.data(), salt_len });
if (st) {
st = engine.init(master.spec, { subkey.data(), salt_len });
}
sodium_memzero(subkey.data(), subkey.size());
SS_TRY(st);
SS_TRY(engine.open(out.data(), packet.subspan(salt_len), nonce.data()));
filter.add(salt);
return plaintext_len;
}
} // namespace ss::crypto
+215
View File
@@ -0,0 +1,215 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef SS_CRYPTO_AEAD_HPP
#define SS_CRYPTO_AEAD_HPP
// SIP004/SIP007 AEAD ciphers as value types.
//
// Replaces the per-connection heap state of aead.cpp (an mbedtls cipher
// context, an aligned libsodium AES-GCM state and a lazily allocated chunk
// buffer, plus two HMAC contexts allocated by every HKDF call). Everything
// here lives inside the object that uses it:
//
// AeadEngine one subkey, ready to seal/open; holds the libsodium
// or Mbed TLS GCM state by value
// AeadTcpEncryptor salt + length/payload chunk stream
// AeadTcpDecryptor the reverse, with a fixed reassembly buffer for one
// partial chunk
// aead_udp_seal/open one-shot packets
//
// With the bundled Mbed TLS (MBEDTLS_BLOCK_CIPHER_C) GCM key setup does not
// allocate either. System Mbed TLS builds without it may still allocate
// inside mbedtls_gcm_setkey.
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <span>
#include <string_view>
#include <variant>
#include <mbedtls/gcm.h>
#include <sodium.h>
#include "core/buffer.hpp"
#include "core/result.hpp"
namespace ss::crypto {
inline constexpr std::size_t kMaxKey = 32;
inline constexpr std::size_t kMaxNonce = 24;
inline constexpr std::size_t kTag = 16;
inline constexpr std::size_t kChunkLenSize = 2;
inline constexpr std::size_t kMaxChunk = 0x3FFF; // payload bytes per TCP chunk
// Largest encrypted chunk on the wire.
inline constexpr std::size_t kMaxChunkWire = kChunkLenSize + kTag + kMaxChunk + kTag;
enum class AeadKind : std::uint8_t {
aes_128_gcm,
aes_192_gcm,
aes_256_gcm,
chacha20_poly1305,
xchacha20_poly1305,
};
struct AeadSpec {
std::string_view name;
AeadKind kind;
std::uint8_t key_len;
std::uint8_t nonce_len;
std::uint8_t tag_len;
};
// The classic (non-2022) AEAD method called `name`, if supported.
std::optional<AeadSpec> find_aead(std::string_view name) noexcept;
// The master key shared by both peers.
struct MasterKey {
AeadSpec spec;
std::array<std::uint8_t, kMaxKey> bytes{};
std::span<const std::uint8_t> key() const noexcept { return { bytes.data(), spec.key_len }; }
// EVP_BytesToKey(MD5) from a password, as the C implementation does.
static Result<MasterKey> from_password(const AeadSpec &spec, const char *password) noexcept;
// A base64 (URL-safe) encoded key of exactly spec.key_len bytes.
static Result<MasterKey> from_base64(const AeadSpec &spec, const char *key) noexcept;
};
// HKDF-SHA1(salt, ikm, info) -> okm (RFC 5869), using SHA-1 state on the
// stack; okm may be up to 255 * 20 bytes.
Status hkdf_sha1(std::span<const std::uint8_t> salt, std::span<const std::uint8_t> ikm,
std::span<const std::uint8_t> info, std::span<std::uint8_t> okm) noexcept;
// Session subkey for a salt: HKDF-SHA1(salt, master, "ss-subkey").
Status derive_subkey(const MasterKey &master, std::span<const std::uint8_t> salt,
std::span<std::uint8_t> subkey) noexcept;
// Salt replay protection hook (the ping-pong bloom filter in production).
struct ReplayFilter {
bool (*seen)(void *ctx, std::span<const std::uint8_t> salt) noexcept = nullptr;
void (*remember)(void *ctx, std::span<const std::uint8_t> salt) noexcept = nullptr;
void *ctx = nullptr;
bool check(std::span<const std::uint8_t> salt) const noexcept { return seen != nullptr && seen(ctx, salt); }
void add(std::span<const std::uint8_t> salt) const noexcept
{
if (remember != nullptr) {
remember(ctx, salt);
}
}
// The process-wide ppbloom filter used by the C implementation.
static ReplayFilter ppbloom() noexcept;
};
class AeadEngine {
public:
AeadEngine() noexcept = default;
AeadEngine(const AeadEngine &) = delete;
AeadEngine &operator=(const AeadEngine &) = delete;
~AeadEngine() { reset(); }
Status init(const AeadSpec &spec, std::span<const std::uint8_t> subkey) noexcept;
void reset() noexcept;
// out must hold plaintext.size() + tag bytes.
Status seal(std::uint8_t *out, std::span<const std::uint8_t> plaintext, const std::uint8_t *nonce) noexcept;
// out must hold ciphertext.size() - tag bytes; fails on authentication errors.
Status open(std::uint8_t *out, std::span<const std::uint8_t> ciphertext, const std::uint8_t *nonce) noexcept;
const AeadSpec &spec() const noexcept { return spec_; }
private:
struct MbedGcm {
MbedGcm() noexcept { mbedtls_gcm_init(&ctx); }
~MbedGcm() { mbedtls_gcm_free(&ctx); }
MbedGcm(const MbedGcm &) = delete;
MbedGcm &operator=(const MbedGcm &) = delete;
mbedtls_gcm_context ctx;
};
struct SodiumAes256Gcm {
~SodiumAes256Gcm() { sodium_memzero(&state, sizeof(state)); }
alignas(16) crypto_aead_aes256gcm_state state;
};
struct SodiumChacha {
~SodiumChacha() { sodium_memzero(key.data(), key.size()); }
std::array<std::uint8_t, kMaxKey> key{};
};
AeadSpec spec_{};
std::variant<std::monostate, MbedGcm, SodiumAes256Gcm, SodiumChacha> impl_;
};
class AeadTcpEncryptor {
public:
// A fresh random salt; with `printable_salt` the first 6-12 bytes are
// printable ASCII (the ss-local experiment, TCP only).
Status init(const MasterKey &master, ReplayFilter filter, bool printable_salt = false) noexcept;
// Bytes seal() writes for `plaintext_len` input.
std::size_t sealed_size(std::size_t plaintext_len) const noexcept;
// Encrypt `plaintext` (any length; split into chunks of at most
// kMaxChunk) into `out`, which must hold sealed_size() bytes. The first
// call also emits the salt. Returns the bytes written.
Result<std::size_t> seal(std::span<const std::uint8_t> plaintext, std::span<std::uint8_t> out) noexcept;
std::span<const std::uint8_t> salt() const noexcept { return { salt_.data(), master_.spec.key_len }; }
private:
MasterKey master_{};
ReplayFilter filter_{};
AeadEngine engine_;
std::array<std::uint8_t, kMaxKey> salt_{};
std::array<std::uint8_t, kMaxNonce> nonce_{};
bool salt_sent_ = false;
};
class AeadTcpDecryptor {
public:
Status init(const MasterKey &master, ReplayFilter filter) noexcept;
struct Step {
std::size_t consumed; // input bytes taken into the reassembly buffer
std::size_t produced; // plaintext bytes written to `out`
};
// Take as much of `in` as fits, then decrypt every complete chunk whose
// payload fits in `out`. Call again with the rest of the input, or with
// more output space, until both consumed and produced are 0. Fails with
// Errc::crypto on authentication errors or a replayed salt and with
// Errc::protocol on a malformed chunk; the stream is dead after that.
Result<Step> decrypt(std::span<const std::uint8_t> in, std::span<std::uint8_t> out) noexcept;
// Bytes received but not yet decrypted.
std::size_t pending() const noexcept { return buffer_.size(); }
bool salt_received() const noexcept { return engine_ready_; }
private:
MasterKey master_{};
ReplayFilter filter_{};
AeadEngine engine_;
std::array<std::uint8_t, kMaxKey> salt_{};
std::array<std::uint8_t, kMaxNonce> nonce_{};
// One salt plus one complete chunk always fits, so a stalled peer cannot
// make the buffer grow and a full buffer always holds decryptable data.
FixedBuffer<kMaxKey + kMaxChunkWire> buffer_;
std::size_t pending_payload_ = 0; // decoded length of the next chunk, 0 if unknown
bool engine_ready_ = false;
bool salt_remembered_ = false;
bool failed_ = false;
};
// One-shot UDP packet: [salt][ciphertext][tag]. `out` must hold
// key_len + plaintext.size() + tag bytes. Returns the bytes written.
Result<std::size_t> aead_udp_seal(const MasterKey &master, std::span<const std::uint8_t> plaintext,
std::span<std::uint8_t> out, ReplayFilter filter) noexcept;
// Returns the plaintext length written to `out` (at least packet size -
// salt - tag bytes).
Result<std::size_t> aead_udp_open(const MasterKey &master, std::span<const std::uint8_t> packet,
std::span<std::uint8_t> out, ReplayFilter filter) noexcept;
} // namespace ss::crypto
#endif
+67
View File
@@ -0,0 +1,67 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef SS_CRYPTO_MBEDTLS_COMPAT_HPP
#define SS_CRYPTO_MBEDTLS_COMPAT_HPP
// Mbed TLS 2.x returns void from the plain SHA-1/MD5 functions and int
// from their _ret variants; 3.x keeps only the int-returning plain names.
// These wrappers always return 0 on success.
#include <cstddef>
#include <mbedtls/md5.h>
#include <mbedtls/sha1.h>
#include <mbedtls/version.h>
namespace ss::crypto::mbed {
#if MBEDTLS_VERSION_NUMBER < 0x03000000
inline int sha1(const unsigned char *in, std::size_t n, unsigned char out[20]) noexcept
{
return mbedtls_sha1_ret(in, n, out);
}
inline int sha1_starts(mbedtls_sha1_context *c) noexcept { return mbedtls_sha1_starts_ret(c); }
inline int sha1_update(mbedtls_sha1_context *c, const unsigned char *in, std::size_t n) noexcept
{
return mbedtls_sha1_update_ret(c, in, n);
}
inline int sha1_finish(mbedtls_sha1_context *c, unsigned char out[20]) noexcept
{
return mbedtls_sha1_finish_ret(c, out);
}
inline int md5_starts(mbedtls_md5_context *c) noexcept { return mbedtls_md5_starts_ret(c); }
inline int md5_update(mbedtls_md5_context *c, const unsigned char *in, std::size_t n) noexcept
{
return mbedtls_md5_update_ret(c, in, n);
}
inline int md5_finish(mbedtls_md5_context *c, unsigned char out[16]) noexcept
{
return mbedtls_md5_finish_ret(c, out);
}
#else
inline int sha1(const unsigned char *in, std::size_t n, unsigned char out[20]) noexcept
{
return mbedtls_sha1(in, n, out);
}
inline int sha1_starts(mbedtls_sha1_context *c) noexcept { return mbedtls_sha1_starts(c); }
inline int sha1_update(mbedtls_sha1_context *c, const unsigned char *in, std::size_t n) noexcept
{
return mbedtls_sha1_update(c, in, n);
}
inline int sha1_finish(mbedtls_sha1_context *c, unsigned char out[20]) noexcept
{
return mbedtls_sha1_finish(c, out);
}
inline int md5_starts(mbedtls_md5_context *c) noexcept { return mbedtls_md5_starts(c); }
inline int md5_update(mbedtls_md5_context *c, const unsigned char *in, std::size_t n) noexcept
{
return mbedtls_md5_update(c, in, n);
}
inline int md5_finish(mbedtls_md5_context *c, unsigned char out[16]) noexcept
{
return mbedtls_md5_finish(c, out);
}
#endif
} // namespace ss::crypto::mbed
#endif
+25 -2
View File
@@ -60,12 +60,12 @@ endfunction()
# C++ core (src/core): header-only ownership and allocation primitives.
foreach(core_test result pool buffer arena fixed_map ownership)
ss_add_test(test_core_${core_test} "test_core_${core_test}.cpp" "")
ss_strict_warnings(test_core_${core_test})
ss_strict_warnings(test_core_${core_test}.cpp)
endforeach()
# C++ event layer (src/event) over libuv.
ss_add_test(test_event_cpp "test_event_cpp.cpp" "ss_uv")
ss_strict_warnings(test_event_cpp)
ss_strict_warnings(test_event_cpp.cpp)
set_tests_properties(test_event_cpp PROPERTIES TIMEOUT 30)
# C++ sources under src/ must not allocate on the heap (see the script).
@@ -207,6 +207,12 @@ ss_add_test(test_aead2022
"test_aead2022.c;${SS_CRYPTO_TEST_SOURCES}"
"${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}")
# test_crypto_aead_cpp - C++ AEAD codecs, differentially against aead.cpp
ss_add_test(test_crypto_aead_cpp
"test_crypto_aead_cpp.cpp;${PROJECT_SOURCE_DIR}/src/crypto/aead.cpp;${SS_CRYPTO_TEST_SOURCES}"
"${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}")
ss_strict_warnings(test_crypto_aead_cpp.cpp ${PROJECT_SOURCE_DIR}/src/crypto/aead.cpp)
# test_interop_rust - interoperability against shadowsocks-rust.
# Skips itself when shadowsocks-rust or the built binaries are absent.
add_test(NAME test_interop_rust
@@ -283,6 +289,11 @@ if(SS_BUILD_EXECUTABLES AND NOT CMAKE_CROSSCOMPILING)
# Steady-state allocation audit (report-only until the C++ relay
# lands; see tests/alloc_audit/measure_alloc.py).
if(TARGET ss_alloc_audit)
# The C++ codecs must not allocate once initialised.
add_test(NAME alloc_steady_crypto_aead
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/alloc_audit/check_steady.py
--shim $<TARGET_FILE:ss_alloc_audit> -- $<TARGET_FILE:test_crypto_aead_cpp>)
set_tests_properties(alloc_steady_crypto_aead PROPERTIES LABELS "unit" TIMEOUT 60)
add_test(NAME alloc_audit_report
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/alloc_audit/measure_alloc.py
--bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
@@ -296,3 +307,15 @@ if(SS_BUILD_EXECUTABLES AND NOT CMAKE_CROSSCOMPILING)
LABELS "integration" TIMEOUT 90)
endif()
endif()
# The zero-allocation checks hold for the bundled Mbed TLS, which is built
# with MBEDTLS_BLOCK_CIPHER_C; a system Mbed TLS may allocate during GCM key
# setup. Keep those tests visible but disabled in system-dependency builds.
if(NOT SS_DEPENDENCY_MODE STREQUAL "bundled")
get_property(ss_all_tests DIRECTORY PROPERTY TESTS)
foreach(ss_test IN LISTS ss_all_tests)
if(ss_test MATCHES "^alloc_steady_(crypto|programs)")
set_tests_properties(${ss_test} PROPERTIES DISABLED TRUE)
endif()
endforeach()
endif()
+33
View File
@@ -0,0 +1,33 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef SS_ALLOC_PROBE_HPP
#define SS_ALLOC_PROBE_HPP
// Mark points in a test program for tests/alloc_audit/check_steady.py.
//
// When the alloc_counter shim is preloaded (it sets SS_ALLOC_AUDIT_OUT and
// handles SIGUSR2), each mark appends the running allocation count to the
// audit file. Without the shim the marks do nothing, so the same test binary
// also runs normally.
#include <csignal>
#include <cstdlib>
namespace ss::test {
inline bool alloc_audit_enabled()
{
return std::getenv("SS_ALLOC_AUDIT_OUT") != nullptr;
}
inline void alloc_mark()
{
#ifdef SIGUSR2
if (alloc_audit_enabled()) {
std::raise(SIGUSR2);
}
#endif
}
} // namespace ss::test
#endif
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Run a test program under the alloc_counter shim and check a steady phase.
The program calls ss::test::alloc_mark() (tests/alloc_audit/alloc_probe.hpp)
before and after the code that must not allocate. This script preloads the
shim, runs the program, and fails unless exactly two marks were recorded and
no allocation happened between them.
Usage: check_steady.py --shim LIB [--max N] -- PROGRAM [ARGS...]
"""
import argparse
import os
from pathlib import Path
import subprocess
import sys
import tempfile
def main():
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--shim", required=True)
parser.add_argument("--max", type=int, default=0, help="allocations allowed between the marks")
parser.add_argument("command", nargs=argparse.REMAINDER)
args = parser.parse_args()
command = args.command[1:] if args.command[:1] == ["--"] else args.command
if not command:
parser.error("missing program to run")
with tempfile.TemporaryDirectory(prefix="ss-steady-") as temp:
out = Path(temp) / "counts"
env = os.environ.copy()
env["DYLD_INSERT_LIBRARIES" if sys.platform == "darwin" else "LD_PRELOAD"] = str(Path(args.shim).resolve())
env["SS_ALLOC_AUDIT_OUT"] = str(out)
result = subprocess.run(command, env=env)
if result.returncode != 0:
print(f"{command[0]} exited with {result.returncode}")
return 1
lines = out.read_text().splitlines() if out.exists() else []
if len(lines) != 2:
print(f"expected 2 allocation marks, got {len(lines)}")
return 1
before, after = ([int(v) for v in line.split()] for line in lines)
allocations = after[1] - before[1]
print(f"steady phase: {allocations} allocation(s), {after[3] - before[3]} bytes (allowed: {args.max})")
return 0 if allocations <= args.max else 1
if __name__ == "__main__":
sys.exit(main())
+365
View File
@@ -0,0 +1,365 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
// The C++ AEAD codecs (src/crypto/aead.hpp) against RFC 5869 vectors and,
// differentially, against the C implementation in aead.cpp: every stream
// and packet produced by one side must be accepted by the other.
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <vector>
#include <sodium.h>
#include "crypto/aead.hpp"
#include "alloc_audit/alloc_probe.hpp"
#include "test_helpers.hpp"
#include "crypto.h"
#include "netutils.h"
#include "ppbloom.h"
#include "utils.h"
#undef min
#undef max
extern "C" {
int verbose = 0;
struct cache *nonce_cache = nullptr;
}
namespace sc = ss::crypto;
namespace {
std::vector<std::uint8_t> unhex(const char *hex)
{
std::vector<std::uint8_t> out;
for (; hex[0] && hex[1]; hex += 2) {
char pair[3] = { hex[0], hex[1], '\0' };
char *end = nullptr;
unsigned long v = std::strtoul(pair, &end, 16);
SS_CHECK(end == pair + 2);
out.push_back(static_cast<std::uint8_t>(v));
}
return out;
}
std::vector<std::uint8_t> random_bytes(std::size_t n)
{
std::vector<std::uint8_t> v(n);
randombytes_buf(v.data(), n);
return v;
}
// A replay filter backed by a small set, independent of ppbloom.
struct SaltSet {
std::vector<std::vector<std::uint8_t> > salts;
static bool seen(void *ctx, std::span<const std::uint8_t> salt) noexcept
{
for (auto &s : static_cast<SaltSet *>(ctx)->salts) {
if (s.size() == salt.size() && std::memcmp(s.data(), salt.data(), salt.size()) == 0) {
return true;
}
}
return false;
}
static void remember(void *ctx, std::span<const std::uint8_t> salt) noexcept
{
static_cast<SaltSet *>(ctx)->salts.emplace_back(salt.begin(), salt.end());
}
sc::ReplayFilter filter() { return { &seen, &remember, this }; }
};
void test_hkdf_rfc5869()
{
// RFC 5869 test case 4 (SHA-1).
auto ikm = unhex("0b0b0b0b0b0b0b0b0b0b0b");
auto salt = unhex("000102030405060708090a0b0c");
auto info = unhex("f0f1f2f3f4f5f6f7f8f9");
std::uint8_t okm[42];
SS_CHECK(sc::hkdf_sha1(salt, ikm, info, okm));
auto expect = unhex("085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896");
SS_CHECK(std::memcmp(okm, expect.data(), 42) == 0);
// Test case 7: no salt (treated as 20 zero bytes), empty info.
auto ikm7 = unhex("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c");
SS_CHECK(sc::hkdf_sha1({}, ikm7, {}, okm));
auto expect7 = unhex("2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5673a081d70cce7acfc48");
SS_CHECK(std::memcmp(okm, expect7.data(), 42) == 0);
}
void test_hkdf_matches_c()
{
const mbedtls_md_info_t *md = mbedtls_md_info_from_type(MBEDTLS_MD_SHA1);
for (int i = 0; i < 64; ++i) {
auto salt = random_bytes(16 + (i % 17));
auto ikm = random_bytes(16 + (i % 23));
const char *info = "ss-subkey";
std::uint8_t mine[32], theirs[32];
SS_CHECK(sc::hkdf_sha1(salt, ikm, { reinterpret_cast<const std::uint8_t *>(info), 9 }, mine));
SS_CHECK(crypto_hkdf(md, salt.data(), (int)salt.size(), ikm.data(), (int)ikm.size(),
reinterpret_cast<const unsigned char *>(info), 9, theirs, 32) == 0);
SS_CHECK(std::memcmp(mine, theirs, 32) == 0);
}
}
struct CMethod {
crypto_t *crypto = nullptr;
explicit CMethod(const char *method)
{
crypto = crypto_init("password", nullptr, method);
SS_CHECK(crypto != nullptr);
}
~CMethod()
{
ppbloom_free();
ss_free(crypto->cipher);
ss_free(crypto);
}
};
// C encrypts a stream, C++ decrypts it from irregular fragments.
void tcp_c_to_cpp(const char *method, const sc::MasterKey &master)
{
CMethod c(method);
cipher_ctx_t enc;
c.crypto->ctx_init(c.crypto->cipher, &enc, 1);
auto plaintext = random_bytes(200000);
std::vector<std::uint8_t> wire;
buffer_t buf{};
SS_CHECK(balloc(&buf, SOCKET_BUF_SIZE * 2) > 0);
for (std::size_t off = 0; off < plaintext.size();) {
std::size_t n = std::min<std::size_t>(1 + randombytes_uniform(16383), plaintext.size() - off);
std::memcpy(buf.data, plaintext.data() + off, n);
buf.len = n;
SS_CHECK(c.crypto->encrypt(&buf, &enc, SOCKET_BUF_SIZE * 2) == CRYPTO_OK);
wire.insert(wire.end(), buf.data, buf.data + buf.len);
off += n;
}
bfree(&buf);
c.crypto->ctx_release(&enc);
SaltSet seen;
sc::AeadTcpDecryptor dec;
SS_CHECK(dec.init(master, seen.filter()));
std::vector<std::uint8_t> got;
std::vector<std::uint8_t> out(sc::kMaxChunk);
for (std::size_t off = 0; off < wire.size();) {
std::size_t n = std::min<std::size_t>(1 + randombytes_uniform(40000), wire.size() - off);
std::span<const std::uint8_t> in(wire.data() + off, n);
for (;;) {
auto step = dec.decrypt(in, out);
SS_CHECK(step);
got.insert(got.end(), out.begin(), out.begin() + step->produced);
in = in.subspan(step->consumed);
if (step->consumed == 0 && step->produced == 0) {
break;
}
}
SS_CHECK(in.empty());
off += n;
}
SS_CHECK(got == plaintext);
SS_CHECK(dec.pending() == 0);
SS_CHECK(seen.salts.size() == 1);
}
// C++ encrypts, C decrypts.
void tcp_cpp_to_c(const char *method, const sc::MasterKey &master)
{
CMethod c(method);
cipher_ctx_t dec;
c.crypto->ctx_init(c.crypto->cipher, &dec, 0);
SaltSet seen;
sc::AeadTcpEncryptor enc;
SS_CHECK(enc.init(master, seen.filter()));
auto plaintext = random_bytes(100000);
std::vector<std::uint8_t> got;
buffer_t buf{};
const std::size_t cap = 4 * SOCKET_BUF_SIZE;
SS_CHECK(balloc(&buf, cap) > 0);
for (std::size_t off = 0; off < plaintext.size();) {
// Larger than one chunk on purpose: the C++ side splits.
std::size_t n = std::min<std::size_t>(1 + randombytes_uniform(30000), plaintext.size() - off);
std::vector<std::uint8_t> wire(enc.sealed_size(n));
auto written = enc.seal({ plaintext.data() + off, n }, wire);
SS_CHECK(written && *written == wire.size());
std::memcpy(buf.data, wire.data(), wire.size());
buf.len = wire.size();
SS_CHECK(c.crypto->decrypt(&buf, &dec, cap) == CRYPTO_OK);
got.insert(got.end(), buf.data, buf.data + buf.len);
off += n;
}
bfree(&buf);
c.crypto->ctx_release(&dec);
SS_CHECK(got == plaintext);
SS_CHECK(seen.salts.size() == 1);
}
void udp_both_ways(const char *method, const sc::MasterKey &master)
{
CMethod c(method);
const std::size_t cap = 65536;
for (std::size_t size : { 1u, 100u, 1400u, 9000u }) {
auto payload = random_bytes(size);
// C -> C++
buffer_t buf{};
SS_CHECK(balloc(&buf, cap) > 0);
std::memcpy(buf.data, payload.data(), size);
buf.len = size;
SS_CHECK(c.crypto->encrypt_all(&buf, c.crypto->cipher, cap) == CRYPTO_OK);
std::vector<std::uint8_t> out(size);
SaltSet seen;
auto n = sc::aead_udp_open(master, { reinterpret_cast<std::uint8_t *>(buf.data), buf.len }, out,
seen.filter());
SS_CHECK(n && *n == size && std::memcmp(out.data(), payload.data(), size) == 0);
// A second delivery of the same packet is a replay.
auto again = sc::aead_udp_open(master, { reinterpret_cast<std::uint8_t *>(buf.data), buf.len }, out,
seen.filter());
SS_CHECK(!again && again.error().code == ss::Errc::crypto);
// C++ -> C
std::vector<std::uint8_t> packet(master.spec.key_len + size + master.spec.tag_len);
auto w = sc::aead_udp_seal(master, payload, packet, {});
SS_CHECK(w && *w == packet.size());
std::memcpy(buf.data, packet.data(), packet.size());
buf.len = packet.size();
SS_CHECK(c.crypto->decrypt_all(&buf, c.crypto->cipher, cap) == CRYPTO_OK);
SS_CHECK(buf.len == size && std::memcmp(buf.data, payload.data(), size) == 0);
bfree(&buf);
}
}
void tampering_and_framing(const sc::MasterKey &master)
{
sc::AeadTcpEncryptor enc;
SS_CHECK(enc.init(master, {}));
auto plaintext = random_bytes(1000);
std::vector<std::uint8_t> wire(enc.sealed_size(plaintext.size()));
SS_CHECK(enc.seal(plaintext, wire));
std::vector<std::uint8_t> out(sc::kMaxChunk);
// Any flipped byte after the salt is rejected, and the stream stays dead.
for (std::size_t pos : { (std::size_t)master.spec.key_len, wire.size() - 1 }) {
auto bad = wire;
bad[pos] ^= 0x01;
sc::AeadTcpDecryptor dec;
SS_CHECK(dec.init(master, {}));
auto r = dec.decrypt(bad, out);
SS_CHECK(!r && r.error().code == ss::Errc::crypto);
auto again = dec.decrypt({}, out);
SS_CHECK(!again && again.error().code == ss::Errc::closed);
}
// A salt alone, or a partial header, yields nothing yet.
sc::AeadTcpDecryptor dec;
SS_CHECK(dec.init(master, {}));
auto step = dec.decrypt(std::span<const std::uint8_t>(wire.data(), master.spec.key_len), out);
SS_CHECK(step && step->produced == 0 && !dec.salt_received());
step = dec.decrypt(std::span<const std::uint8_t>(wire.data() + master.spec.key_len, 10), out);
SS_CHECK(step && step->produced == 0 && dec.salt_received());
// An output buffer too small for the next chunk stalls instead of
// truncating.
sc::AeadTcpDecryptor small;
SS_CHECK(small.init(master, {}));
std::uint8_t tiny[10];
step = small.decrypt(wire, tiny);
SS_CHECK(step && step->consumed == wire.size() && step->produced == 0);
step = small.decrypt({}, out);
SS_CHECK(step && step->produced == plaintext.size());
SS_CHECK(std::memcmp(out.data(), plaintext.data(), plaintext.size()) == 0);
// A replayed salt is refused before any key work.
SaltSet seen;
sc::AeadTcpDecryptor first, second;
SS_CHECK(first.init(master, seen.filter()) && second.init(master, seen.filter()));
SS_CHECK(first.decrypt(wire, out));
auto replay = second.decrypt(wire, out);
SS_CHECK(!replay && replay.error().code == ss::Errc::crypto);
}
void test_method(const char *name)
{
auto spec = sc::find_aead(name);
SS_CHECK(spec);
auto master = sc::MasterKey::from_password(*spec, "password");
SS_CHECK(master);
tcp_c_to_cpp(name, *master);
tcp_cpp_to_c(name, *master);
udp_both_ways(name, *master);
tampering_and_framing(*master);
std::printf("ok %s\n", name);
}
void test_lookup()
{
SS_CHECK(!sc::find_aead("2022-blake3-aes-128-gcm"));
SS_CHECK(!sc::find_aead("aes-256-cfb"));
auto chacha = sc::find_aead("chacha20-ietf-poly1305");
SS_CHECK(chacha && chacha->key_len == 32 && chacha->nonce_len == 12);
}
// New connections and packets through the C++ codecs, with every buffer
// preallocated: under check_steady.py this phase must not touch the heap.
void steady_state(const sc::MasterKey &master)
{
static std::uint8_t plain[sc::kMaxChunk];
static std::uint8_t wire[2 * sc::kMaxChunkWire];
static std::uint8_t out[sc::kMaxChunk];
randombytes_buf(plain, sizeof(plain));
for (int conn = 0; conn < 200; ++conn) {
sc::AeadTcpEncryptor enc;
sc::AeadTcpDecryptor dec;
SS_CHECK(enc.init(master, {}) && dec.init(master, {}));
for (std::size_t n : { (std::size_t)1, (std::size_t)1400, sc::kMaxChunk }) {
auto w = enc.seal({ plain, n }, wire);
SS_CHECK(w);
auto step = dec.decrypt({ wire, *w }, out);
SS_CHECK(step && step->consumed == *w && step->produced == n);
}
auto p = sc::aead_udp_seal(master, { plain, 1200 }, wire, {});
SS_CHECK(p);
auto q = sc::aead_udp_open(master, { wire, *p }, out, {});
SS_CHECK(q && *q == 1200);
}
}
void test_no_heap_in_steady_state()
{
// Deriving the master key from a password is startup work (and uses
// Mbed TLS's allocating MD API); keep it outside the measured phase.
sc::MasterKey keys[3];
int i = 0;
for (const char *name : { "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305" }) {
auto spec = sc::find_aead(name);
SS_CHECK(spec.has_value());
auto master = sc::MasterKey::from_password(spec.value(), "password");
SS_CHECK(master);
keys[i++] = *master;
steady_state(*master); // warm-up: lazy library initialisation
}
ss::test::alloc_mark();
for (const sc::MasterKey &master : keys) {
steady_state(master);
}
ss::test::alloc_mark();
}
} // namespace
int main()
{
SS_CHECK(sodium_init() != -1);
SS_RUN(test_hkdf_rfc5869);
SS_RUN(test_hkdf_matches_c);
SS_RUN(test_lookup);
for (const char *m : { "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305",
"xchacha20-ietf-poly1305" }) {
test_method(m);
}
SS_RUN(test_no_heap_in_steady_state);
return 0;
}
+5
View File
@@ -8,6 +8,11 @@
#define MBEDTLS_AES_C
#define MBEDTLS_CIPHER_C
#define MBEDTLS_GCM_C
/* GCM on the block-cipher layer keeps the AES context inside
* mbedtls_gcm_context, so mbedtls_gcm_setkey() does not allocate. Mbed TLS
* only enables this by itself when MBEDTLS_CIPHER_C is off, and the legacy
* stream ciphers need MBEDTLS_CIPHER_C. */
#define MBEDTLS_BLOCK_CIPHER_C
#define MBEDTLS_MD_C
#define MBEDTLS_MD5_C
#define MBEDTLS_SHA1_C