mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-28 19:53:46 +00:00
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:
+2
-2
@@ -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)$'
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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/`.
|
||||
|
||||
@@ -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
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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;
|
||||
}
|
||||
Vendored
+5
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user