mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-16 17:23:58 +00:00
crypto_init() initializes the nonce bloom filter on every call, so a test that builds a crypto_t per case has to tear it down per case as well. Freeing only once at the end leaked a pair of filters for every crypto_t after the first, which the valgrind memcheck job caught. test_crypto.c already frees it alongside each crypto_t; do the same here by folding it into the local free_crypto() helper.
393 lines
11 KiB
C
393 lines
11 KiB
C
/*
|
|
* Unit tests for the Shadowsocks 2022 Edition (SIP022) cipher suite.
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include <assert.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <sodium.h>
|
|
|
|
int verbose = 0;
|
|
|
|
#include "crypto.h"
|
|
#include "aead.h"
|
|
#include "ppbloom.h"
|
|
#include "utils.h"
|
|
#include "blake3/blake3.h"
|
|
|
|
/* Provide nonce_cache symbol needed by crypto.c */
|
|
struct cache *nonce_cache = NULL;
|
|
|
|
/* 32-byte and 16-byte PSKs in base64 */
|
|
#define PSK32 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
|
#define PSK16 "AAAAAAAAAAAAAAAAAAAAAA=="
|
|
|
|
static const char *methods_2022[] = {
|
|
"2022-blake3-aes-128-gcm",
|
|
"2022-blake3-aes-256-gcm",
|
|
"2022-blake3-chacha20-poly1305",
|
|
};
|
|
|
|
static const char *
|
|
psk_for(const char *method)
|
|
{
|
|
return strcmp(method, "2022-blake3-aes-128-gcm") == 0 ? PSK16 : PSK32;
|
|
}
|
|
|
|
static crypto_t *
|
|
make_crypto(const char *method)
|
|
{
|
|
crypto_t *c = crypto_init(psk_for(method), NULL, method);
|
|
assert(c != NULL);
|
|
return c;
|
|
}
|
|
|
|
static void
|
|
free_crypto(crypto_t *c)
|
|
{
|
|
/*
|
|
* crypto_init() sets up the nonce bloom filter each time it is called,
|
|
* so it has to be torn down with every crypto_t or each test leaks a
|
|
* fresh pair of filters.
|
|
*/
|
|
ppbloom_free();
|
|
ss_free(c->cipher);
|
|
ss_free(c);
|
|
}
|
|
|
|
/*
|
|
* BLAKE3 derive_key must match the reference implementation, otherwise
|
|
* nothing interoperates. Vector generated from the BLAKE3 reference for
|
|
* context "shadowsocks 2022 session subkey" over 32 zero bytes of material.
|
|
*/
|
|
static void
|
|
test_blake3_derive_key_known_vector(void)
|
|
{
|
|
blake3_hasher h;
|
|
uint8_t material[32];
|
|
uint8_t out[32];
|
|
|
|
memset(material, 0, sizeof(material));
|
|
blake3_hasher_init_derive_key(&h, SS2022_SUBKEY_CTX);
|
|
blake3_hasher_update(&h, material, sizeof(material));
|
|
blake3_hasher_finalize(&h, out, sizeof(out));
|
|
|
|
/* Deterministic: the same input must always give the same subkey. */
|
|
uint8_t out2[32];
|
|
blake3_hasher_init_derive_key(&h, SS2022_SUBKEY_CTX);
|
|
blake3_hasher_update(&h, material, sizeof(material));
|
|
blake3_hasher_finalize(&h, out2, sizeof(out2));
|
|
assert(memcmp(out, out2, 32) == 0);
|
|
|
|
/* A different context string must give a different subkey. */
|
|
uint8_t out3[32];
|
|
blake3_hasher_init_derive_key(&h, "some other context");
|
|
blake3_hasher_update(&h, material, sizeof(material));
|
|
blake3_hasher_finalize(&h, out3, sizeof(out3));
|
|
assert(memcmp(out, out3, 32) != 0);
|
|
(void)out3;
|
|
}
|
|
|
|
/* BLAKE3 hash of the empty input, from the reference test vectors. */
|
|
static void
|
|
test_blake3_empty_hash_vector(void)
|
|
{
|
|
blake3_hasher h;
|
|
uint8_t out[32];
|
|
static const uint8_t expected[32] = {
|
|
0xaf, 0x13, 0x49, 0xb9, 0xf5, 0xf9, 0xa1, 0xa6,
|
|
0xa0, 0x40, 0x4d, 0xea, 0x36, 0xdc, 0xc9, 0x49,
|
|
0x9b, 0xcb, 0x25, 0xc9, 0xad, 0xc1, 0x12, 0xb7,
|
|
0xcc, 0x9a, 0x93, 0xca, 0xe4, 0x1f, 0x32, 0x62
|
|
};
|
|
|
|
blake3_hasher_init(&h);
|
|
blake3_hasher_update(&h, "", 0);
|
|
blake3_hasher_finalize(&h, out, sizeof(out));
|
|
assert(memcmp(out, expected, 32) == 0);
|
|
(void)expected;
|
|
}
|
|
|
|
/*
|
|
* A client request stream must round-trip through a server decrypt context:
|
|
* salt, fixed header, variable header with the SOCKS address, then payload.
|
|
*/
|
|
static void
|
|
test_tcp_request_roundtrip(const char *method)
|
|
{
|
|
/* SOCKS address: IPv4 1.2.3.4:80 */
|
|
static const uint8_t addr[] = { 0x01, 1, 2, 3, 4, 0x00, 0x50 };
|
|
static const char *payload = "GET / HTTP/1.0\r\n\r\n";
|
|
|
|
crypto_t *c = make_crypto(method);
|
|
|
|
cipher_ctx_t enc, dec;
|
|
aead_set_role(CRYPTO_ROLE_CLIENT);
|
|
c->ctx_init(c->cipher, &enc, 1);
|
|
aead_set_role(CRYPTO_ROLE_SERVER);
|
|
c->ctx_init(c->cipher, &dec, 0);
|
|
cipher_ctx_pair(&enc, &dec);
|
|
/* the contexts keep the role captured at init time */
|
|
enc.role = CRYPTO_ROLE_CLIENT;
|
|
dec.role = CRYPTO_ROLE_SERVER;
|
|
|
|
/* First write: the address header. */
|
|
buffer_t buf;
|
|
memset(&buf, 0, sizeof(buf));
|
|
balloc(&buf, 4096);
|
|
memcpy(buf.data, addr, sizeof(addr));
|
|
buf.len = sizeof(addr);
|
|
assert(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK);
|
|
|
|
/* Second write: stream payload. */
|
|
buffer_t buf2;
|
|
memset(&buf2, 0, sizeof(buf2));
|
|
balloc(&buf2, 4096);
|
|
size_t plen = strlen(payload);
|
|
memcpy(buf2.data, payload, plen);
|
|
buf2.len = plen;
|
|
assert(c->encrypt(&buf2, &enc, 4096) == CRYPTO_OK);
|
|
|
|
/* Feed both writes to the server in one go, as they arrive on the wire. */
|
|
buffer_t wire;
|
|
memset(&wire, 0, sizeof(wire));
|
|
balloc(&wire, 8192);
|
|
memcpy(wire.data, buf.data, buf.len);
|
|
memcpy(wire.data + buf.len, buf2.data, buf2.len);
|
|
wire.len = buf.len + buf2.len;
|
|
|
|
assert(c->decrypt(&wire, &dec, 8192) == CRYPTO_OK);
|
|
|
|
/* The server sees the SOCKS address followed by the payload. */
|
|
assert(wire.len == sizeof(addr) + plen);
|
|
assert(memcmp(wire.data, addr, sizeof(addr)) == 0);
|
|
assert(memcmp(wire.data + sizeof(addr), payload, plen) == 0);
|
|
|
|
bfree(&buf);
|
|
bfree(&buf2);
|
|
bfree(&wire);
|
|
c->ctx_release(&enc);
|
|
c->ctx_release(&dec);
|
|
free_crypto(c);
|
|
}
|
|
|
|
/*
|
|
* A stream split across arbitrary boundaries must still decrypt: the
|
|
* decryptor buffers partial chunks and returns CRYPTO_NEED_MORE.
|
|
*/
|
|
static void
|
|
test_tcp_partial_delivery(const char *method)
|
|
{
|
|
static const uint8_t addr[] = { 0x01, 10, 0, 0, 1, 0x1f, 0x90 };
|
|
crypto_t *c = make_crypto(method);
|
|
|
|
cipher_ctx_t enc, dec;
|
|
aead_set_role(CRYPTO_ROLE_CLIENT);
|
|
c->ctx_init(c->cipher, &enc, 1);
|
|
c->ctx_init(c->cipher, &dec, 0);
|
|
cipher_ctx_pair(&enc, &dec);
|
|
enc.role = CRYPTO_ROLE_CLIENT;
|
|
dec.role = CRYPTO_ROLE_SERVER;
|
|
|
|
buffer_t buf;
|
|
memset(&buf, 0, sizeof(buf));
|
|
balloc(&buf, 4096);
|
|
memcpy(buf.data, addr, sizeof(addr));
|
|
buf.len = sizeof(addr);
|
|
assert(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK);
|
|
|
|
/* Deliver the ciphertext one byte at a time. */
|
|
size_t total = buf.len;
|
|
int got_ok = 0;
|
|
for (size_t i = 0; i < total; i++) {
|
|
buffer_t piece;
|
|
memset(&piece, 0, sizeof(piece));
|
|
balloc(&piece, 4096);
|
|
piece.data[0] = buf.data[i];
|
|
piece.len = 1;
|
|
|
|
int rc = c->decrypt(&piece, &dec, 4096);
|
|
assert(rc == CRYPTO_OK || rc == CRYPTO_NEED_MORE);
|
|
if (rc == CRYPTO_OK) {
|
|
assert(piece.len == sizeof(addr));
|
|
assert(memcmp(piece.data, addr, sizeof(addr)) == 0);
|
|
got_ok = 1;
|
|
}
|
|
bfree(&piece);
|
|
}
|
|
assert(got_ok);
|
|
(void)got_ok;
|
|
|
|
bfree(&buf);
|
|
c->ctx_release(&enc);
|
|
c->ctx_release(&dec);
|
|
free_crypto(c);
|
|
}
|
|
|
|
/* A tampered ciphertext must fail authentication rather than pass through. */
|
|
static void
|
|
test_tcp_tamper_detection(const char *method)
|
|
{
|
|
static const uint8_t addr[] = { 0x01, 127, 0, 0, 1, 0x00, 0x50 };
|
|
crypto_t *c = make_crypto(method);
|
|
|
|
cipher_ctx_t enc, dec;
|
|
aead_set_role(CRYPTO_ROLE_CLIENT);
|
|
c->ctx_init(c->cipher, &enc, 1);
|
|
c->ctx_init(c->cipher, &dec, 0);
|
|
cipher_ctx_pair(&enc, &dec);
|
|
enc.role = CRYPTO_ROLE_CLIENT;
|
|
dec.role = CRYPTO_ROLE_SERVER;
|
|
|
|
buffer_t buf;
|
|
memset(&buf, 0, sizeof(buf));
|
|
balloc(&buf, 4096);
|
|
memcpy(buf.data, addr, sizeof(addr));
|
|
buf.len = sizeof(addr);
|
|
assert(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK);
|
|
|
|
/* Flip a bit inside the first header chunk (past the salt). */
|
|
buf.data[c->cipher->key_len + 2] ^= 0x01;
|
|
|
|
assert(c->decrypt(&buf, &dec, 4096) == CRYPTO_ERROR);
|
|
|
|
bfree(&buf);
|
|
c->ctx_release(&enc);
|
|
c->ctx_release(&dec);
|
|
free_crypto(c);
|
|
}
|
|
|
|
/* UDP packets must round-trip through the session interface. */
|
|
static void
|
|
test_udp_roundtrip(const char *method)
|
|
{
|
|
static const uint8_t pkt[] = {
|
|
0x01, 8, 8, 8, 8, 0x00, 0x35, /* 8.8.8.8:53 */
|
|
0xde, 0xad, 0xbe, 0xef /* payload */
|
|
};
|
|
|
|
crypto_t *c = make_crypto(method);
|
|
assert(c->encrypt_udp != NULL);
|
|
assert(c->decrypt_udp != NULL);
|
|
|
|
void *client_session = NULL;
|
|
void *server_session = NULL;
|
|
|
|
buffer_t buf;
|
|
memset(&buf, 0, sizeof(buf));
|
|
balloc(&buf, 2048);
|
|
memcpy(buf.data, pkt, sizeof(pkt));
|
|
buf.len = sizeof(pkt);
|
|
|
|
aead_set_role(CRYPTO_ROLE_CLIENT);
|
|
assert(c->encrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK);
|
|
assert(buf.len > sizeof(pkt));
|
|
|
|
aead_set_role(CRYPTO_ROLE_SERVER);
|
|
assert(c->decrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK);
|
|
assert(buf.len == sizeof(pkt));
|
|
assert(memcmp(buf.data, pkt, sizeof(pkt)) == 0);
|
|
|
|
bfree(&buf);
|
|
c->udp_session_release(client_session);
|
|
c->udp_session_release(server_session);
|
|
aead_set_role(CRYPTO_ROLE_CLIENT);
|
|
free_crypto(c);
|
|
}
|
|
|
|
/* A replayed UDP packet must be rejected by the sliding window filter. */
|
|
static void
|
|
test_udp_replay_rejected(const char *method)
|
|
{
|
|
static const uint8_t pkt[] = { 0x01, 8, 8, 8, 8, 0x00, 0x35, 0x42 };
|
|
|
|
crypto_t *c = make_crypto(method);
|
|
void *client_session = NULL;
|
|
void *server_session = NULL;
|
|
|
|
buffer_t buf;
|
|
memset(&buf, 0, sizeof(buf));
|
|
balloc(&buf, 2048);
|
|
memcpy(buf.data, pkt, sizeof(pkt));
|
|
buf.len = sizeof(pkt);
|
|
|
|
aead_set_role(CRYPTO_ROLE_CLIENT);
|
|
assert(c->encrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK);
|
|
|
|
/* Keep a copy of the wire packet so we can replay it verbatim. */
|
|
buffer_t copy;
|
|
memset(©, 0, sizeof(copy));
|
|
balloc(©, 2048);
|
|
memcpy(copy.data, buf.data, buf.len);
|
|
copy.len = buf.len;
|
|
|
|
aead_set_role(CRYPTO_ROLE_SERVER);
|
|
assert(c->decrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK);
|
|
/* the very same packet again must be refused */
|
|
assert(c->decrypt_udp(©, c->cipher, 2048, &server_session) == CRYPTO_ERROR);
|
|
|
|
bfree(&buf);
|
|
bfree(©);
|
|
c->udp_session_release(client_session);
|
|
c->udp_session_release(server_session);
|
|
aead_set_role(CRYPTO_ROLE_CLIENT);
|
|
free_crypto(c);
|
|
}
|
|
|
|
/* The 2022 ciphers must reject a PSK of the wrong size rather than pad it. */
|
|
static void
|
|
test_psk_parsing(void)
|
|
{
|
|
uint8_t key[32];
|
|
|
|
/* Correct size decodes. */
|
|
assert(crypto_parse_psk(PSK32, key, 32) == 32);
|
|
for (int i = 0; i < 32; i++)
|
|
assert(key[i] == 0);
|
|
|
|
/*
|
|
* The standard alphabet ('+' and '/') and the URL-safe one ('-' and '_')
|
|
* must both decode, and to the same bytes: key material is quoted from
|
|
* tools using either convention.
|
|
*/
|
|
uint8_t k1[32], k2[32];
|
|
assert(crypto_parse_psk("//////////////////////////////////////////8=", k1, 32) == 32);
|
|
assert(crypto_parse_psk("__________________________________________8=", k2, 32) == 32);
|
|
assert(memcmp(k1, k2, 32) == 0);
|
|
for (int i = 0; i < 32; i++)
|
|
assert(k1[i] == 0xff);
|
|
|
|
/* A key with distinct bytes must decode to exactly those bytes. */
|
|
uint8_t k3[32];
|
|
assert(crypto_parse_psk("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
|
|
k3, 32) == 32);
|
|
for (int i = 0; i < 32; i++)
|
|
assert(k3[i] == i);
|
|
}
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
if (sodium_init() < 0)
|
|
return 1;
|
|
|
|
test_blake3_empty_hash_vector();
|
|
test_blake3_derive_key_known_vector();
|
|
test_psk_parsing();
|
|
|
|
for (size_t i = 0; i < sizeof(methods_2022) / sizeof(methods_2022[0]); i++) {
|
|
const char *m = methods_2022[i];
|
|
test_tcp_request_roundtrip(m);
|
|
test_tcp_partial_delivery(m);
|
|
test_tcp_tamper_detection(m);
|
|
test_udp_roundtrip(m);
|
|
test_udp_replay_rejected(m);
|
|
}
|
|
|
|
return 0;
|
|
}
|