mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-29 03:00:36 +01:00
1331 lines
59 KiB
C
1331 lines
59 KiB
C
/*
|
|
MASTER_PROTOCOL.C (test)
|
|
|
|
The game's side of the master servers' protocol (docs/master_protocol.md),
|
|
byte for byte against the master's test vectors (vectors.inc, from its
|
|
testdata/vectors.json): the server browser's check of a signed list, the
|
|
entries it takes from one and its masters.txt (port/linux/src/
|
|
master_list.c); the host's listing read and its signature checked
|
|
(port/linux/src/p2p_lobby.c); a dedicated server's publisher key, instance,
|
|
server id, signed requests, its publishes and what it makes of their
|
|
answers, and its answer to a master's probe and the probes' limits
|
|
(port/linux/src/server_master.c); on the real SHA-256 and Ed25519
|
|
(port/linux/src/p2p_crypto.c, Monocypher) and base64 and JSON
|
|
(server_json.c): test_master_protocol.py takes the code (under_test.inc).
|
|
*/
|
|
|
|
#include "harness.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdint.h>
|
|
|
|
/* ---------- the crypto, base64 and JSON, as whole files */
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Weverything"
|
|
/* (compiled quickly: each negative control builds it again) */
|
|
#pragma clang optimize off
|
|
#include "monocypher.inc"
|
|
/* (both files have a hash_reduce of their own) */
|
|
#define hash_reduce ed25519_hash_reduce
|
|
#include "monocypher_ed25519.inc"
|
|
#undef hash_reduce
|
|
#pragma clang optimize on
|
|
#pragma clang diagnostic pop
|
|
#undef FOR
|
|
#undef COPY
|
|
#undef ZERO
|
|
#undef WIPE_CTX
|
|
#undef WIPE_BUFFER
|
|
#undef MIN
|
|
|
|
#include "p2p_crypto.inc"
|
|
#include "server_json.inc"
|
|
|
|
/* bytes the code asks for at random: the test's */
|
|
static unsigned char random_bytes[64];
|
|
|
|
void posix_random_bytes(void *buffer, posix_ulong size)
|
|
{
|
|
memcpy(buffer, random_bytes, size);
|
|
}
|
|
|
|
#include "master_list.h"
|
|
#include "server_master.h"
|
|
|
|
/* ---------- the fake world */
|
|
|
|
struct vector_listing
|
|
{
|
|
const char *description;
|
|
const char *host_seed;
|
|
const char *host_public_key;
|
|
const char *bytes;
|
|
const char *base64;
|
|
int size;
|
|
int signed_size;
|
|
const char *signed_message;
|
|
const char *signature;
|
|
int valid;
|
|
};
|
|
|
|
#include "vectors.inc"
|
|
|
|
typedef struct SDL_Mutex SDL_Mutex;
|
|
typedef struct SDL_Condition SDL_Condition;
|
|
typedef struct SDL_Thread SDL_Thread;
|
|
#define SDL_CreateMutex() NULL
|
|
#define SDL_CreateCondition() NULL
|
|
#define SDL_CreateThread(function, name, data) NULL
|
|
#define SDL_LockMutex(lock) ((void)0)
|
|
#define SDL_UnlockMutex(lock) ((void)0)
|
|
/* a dedicated server's publisher (server_mode.c) unless a case says it is
|
|
a player's host; which the game's host started as the process exits
|
|
(atexit) never is here: no thread */
|
|
static int server_mode = 1;
|
|
static int halo_server_mode(void) { return server_mode; }
|
|
void server_master_stop(void) {}
|
|
#define SDL_GetBasePath() ((const char *)NULL)
|
|
#define server_log(...) ((void)0)
|
|
static const char *server_mode_build(void) { return "build-206"; }
|
|
|
|
/* the settings and files the code reads */
|
|
static const char *setting_instance = "";
|
|
static const char *setting_name = "";
|
|
static const char *setting_region = "";
|
|
static int setting_publish_address;
|
|
static const char *file_masters = NULL;
|
|
static const char *file_key = NULL;
|
|
static unsigned long now;
|
|
|
|
static const char *config_string(const char *name)
|
|
{
|
|
if (!strcmp(name, "server.instance"))
|
|
return setting_instance;
|
|
if (!strcmp(name, "server.name"))
|
|
return setting_name;
|
|
if (!strcmp(name, "server.region"))
|
|
return setting_region;
|
|
if (!strcmp(name, "server.masters_file") || !strcmp(name, "network.masters_file"))
|
|
return "masters.txt";
|
|
return "";
|
|
}
|
|
|
|
static int config_boolean(const char *name)
|
|
{
|
|
return !strcmp(name, "server.master_publish_address") && setting_publish_address;
|
|
}
|
|
|
|
/* the hosted game (p2p.c, p2p_lobby.c): its listing (none: not hosting for
|
|
the internet), the tunnel's port (network byte order), its public address */
|
|
static unsigned char host_listing[400];
|
|
static int host_listing_size;
|
|
static unsigned short tunnel_port;
|
|
|
|
static int p2p_lobby_host_listing(unsigned char *bytes, int size)
|
|
{
|
|
if (!host_listing_size || size < host_listing_size)
|
|
return 0;
|
|
memcpy(bytes, host_listing, (size_t)host_listing_size);
|
|
return host_listing_size;
|
|
}
|
|
|
|
static unsigned short p2p_tunnel_local_port(void) { return tunnel_port; }
|
|
|
|
static int p2p_public_address(char *text, int size)
|
|
{
|
|
snprintf(text, (size_t)size, "203.0.113.5:2302");
|
|
return 1;
|
|
}
|
|
|
|
/* the events of the master's outcomes (server_events.c) */
|
|
static int events_added;
|
|
|
|
static unsigned long server_event_add(const char *group, const char *object)
|
|
{
|
|
(void)group;
|
|
(void)object;
|
|
return (unsigned long)++events_added;
|
|
}
|
|
|
|
/* a master's entries handed to the server browser's games (p2p_lobby.c):
|
|
the first few kept */
|
|
static struct
|
|
{
|
|
int size;
|
|
char publisher[64];
|
|
char region[64];
|
|
int version;
|
|
int dedicated;
|
|
} entries_taken[4];
|
|
static int entries_count;
|
|
|
|
static int p2p_lobby_take_master_listing(const unsigned char *payload, int size, const char *publisher,
|
|
const char *region, int network_version_shown, int dedicated)
|
|
{
|
|
(void)payload;
|
|
if (entries_count < 4)
|
|
{
|
|
entries_taken[entries_count].size = size;
|
|
snprintf(entries_taken[entries_count].publisher, sizeof(entries_taken[entries_count].publisher), "%s", publisher);
|
|
snprintf(entries_taken[entries_count].region, sizeof(entries_taken[entries_count].region), "%s", region);
|
|
entries_taken[entries_count].version = network_version_shown;
|
|
entries_taken[entries_count].dedicated = dedicated;
|
|
}
|
|
entries_count++;
|
|
return 1;
|
|
}
|
|
|
|
static void config_folder(char *path, size_t size)
|
|
{
|
|
snprintf(path, size, "/config/");
|
|
}
|
|
|
|
static char *config_file_read(const char *path, size_t *size)
|
|
{
|
|
const char *text = !strcmp(path, "/config/masters.txt") ? file_masters :
|
|
!strcmp(path, "/config/publisher.key") ? file_key : NULL;
|
|
char *copy;
|
|
|
|
if (!text)
|
|
return NULL;
|
|
*size = strlen(text);
|
|
copy = malloc(*size + 1);
|
|
memcpy(copy, text, *size + 1);
|
|
return copy;
|
|
}
|
|
|
|
static unsigned long posix_server_unix_seconds(void)
|
|
{
|
|
return now;
|
|
}
|
|
|
|
/* what the code signs, kept */
|
|
static unsigned char signed_message[4096];
|
|
static int signed_size;
|
|
|
|
static void test_sign(const unsigned char *seed, const unsigned char *public_key, const void *message, int size,
|
|
unsigned char *signature)
|
|
{
|
|
memcpy(signed_message, message, (size_t)size < sizeof(signed_message) ? (size_t)size : sizeof(signed_message));
|
|
signed_size = size;
|
|
p2p_ed25519_sign(seed, public_key, message, size, signature);
|
|
}
|
|
#define p2p_ed25519_sign(seed, public_key, message, size, signature) test_sign(seed, public_key, message, size, signature)
|
|
|
|
#include "config.inc"
|
|
|
|
/* the publishes made (update.h's update_https_request), and the master's
|
|
answer to them */
|
|
static struct
|
|
{
|
|
int count;
|
|
char method[8];
|
|
char url[512];
|
|
char headers[1024];
|
|
char body[16384];
|
|
} requested;
|
|
static int answer_status = 200;
|
|
static const char *answer_headers = "";
|
|
static const char *answer_body = "{\"ok\":true,\"listed\":true,\"probe\":\"ok\"}";
|
|
|
|
static int update_https_request(const char *method, const char *url, const char *headers, const void *body,
|
|
unsigned long body_size, unsigned long maximum, struct update_response *response, char *error, int error_size)
|
|
{
|
|
(void)maximum;
|
|
(void)error;
|
|
(void)error_size;
|
|
requested.count++;
|
|
snprintf(requested.method, sizeof(requested.method), "%s", method);
|
|
snprintf(requested.url, sizeof(requested.url), "%s", url);
|
|
snprintf(requested.headers, sizeof(requested.headers), "%s", headers ? headers : "");
|
|
snprintf(requested.body, sizeof(requested.body), "%.*s", (int)body_size, body ? (const char *)body : "");
|
|
memset(response, 0, sizeof(*response));
|
|
response->status = answer_status;
|
|
snprintf(response->headers, sizeof(response->headers), "%s", answer_headers);
|
|
response->body = strdup(answer_body);
|
|
response->body_size = (unsigned long)strlen(answer_body);
|
|
return 1;
|
|
}
|
|
|
|
#include "under_test.inc"
|
|
|
|
/* ---------- the test's own */
|
|
|
|
static int hex_decode(const char *text, unsigned char *bytes, int maximum)
|
|
{
|
|
int count = 0;
|
|
|
|
while (text[0] && text[1] && count < maximum)
|
|
{
|
|
unsigned int value;
|
|
|
|
if (sscanf(text, "%2x", &value) != 1)
|
|
return -1;
|
|
bytes[count++] = (unsigned char)value;
|
|
text += 2;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static const char *hex_of(const void *bytes, int size)
|
|
{
|
|
static char text[2][2048];
|
|
static int which;
|
|
|
|
which ^= 1;
|
|
p2p_hex(bytes, size, text[which]);
|
|
return text[which];
|
|
}
|
|
|
|
static void key_of(const char *hex, unsigned char *key)
|
|
{
|
|
CHECK(hex_decode(hex, key, 32) == 32, "a vector's key is not 32 bytes: %s", hex);
|
|
}
|
|
|
|
/* the publisher started as the vectors have it: its key, instance "main",
|
|
the vectors' master in masters.txt */
|
|
static void start_publisher(void)
|
|
{
|
|
static char masters[256];
|
|
|
|
snprintf(masters, sizeof(masters), "# the vectors' master\nhttps://master.example.org %s\n",
|
|
VECTOR_MASTER_PUBLIC_KEY_TEXT);
|
|
file_masters = masters;
|
|
setenv("HALO_SERVER_PUBLISHER_KEY", VECTOR_PUBLISHER_SEED, 1);
|
|
setting_instance = VECTOR_INSTANCE;
|
|
memset(&master, 0, sizeof(master));
|
|
server_master_start();
|
|
CHECK(master.has_key, "the publisher key was not taken from HALO_SERVER_PUBLISHER_KEY");
|
|
CHECK(master.master_count == 1, "%d masters read from masters.txt", master.master_count);
|
|
}
|
|
|
|
/* a request signed as the vectors' was: the Authorization header and the
|
|
string signed */
|
|
static void check_request(const char *master_key_id, const char *method, const char *path, const char *body,
|
|
unsigned long size, const char *ts, const char *nonce, const char *canonical, const char *authorization)
|
|
{
|
|
char header[512], expected[512];
|
|
unsigned char master_key_id_bytes[8];
|
|
|
|
start_publisher();
|
|
now = strtoul(ts, NULL, 10);
|
|
CHECK(hex_decode(nonce, random_bytes, 16) == 16, "the vector's nonce is not 16 bytes");
|
|
CHECK(hex_decode(master_key_id, master_key_id_bytes, 8) == 8, "the vector's master key id is not 8 bytes");
|
|
signed_size = -1;
|
|
sign_request(master_key_id_bytes, method, path, body, size, header, sizeof(header));
|
|
CHECK(signed_size == (int)strlen(canonical) && !memcmp(signed_message, canonical, strlen(canonical)),
|
|
"signed \"%.*s\", not the vector's \"%s\"", signed_size, (const char *)signed_message, canonical);
|
|
snprintf(expected, sizeof(expected), "Authorization: %s\r\nContent-Type: application/json\r\n", authorization);
|
|
CHECK(!strcmp(header, expected), "the header is\n%s\nnot\n%s", header, expected);
|
|
}
|
|
|
|
static int verify_list(const char *body, unsigned long size, const char *signature_header, const char *key_header,
|
|
const char *key_hex, unsigned long when)
|
|
{
|
|
unsigned char key[32];
|
|
char why[128] = "";
|
|
|
|
key_of(key_hex, key);
|
|
return master_list_verify(body, size, signature_header, key_header, key, when, why, sizeof(why));
|
|
}
|
|
|
|
/* a list body signed by the vectors' master: its Signature header */
|
|
static const char *master_signs(const char *body)
|
|
{
|
|
static char header[128];
|
|
unsigned char seed[32], public_key[32], signature[64];
|
|
|
|
key_of(VECTOR_MASTER_SEED, seed);
|
|
p2p_ed25519_public(seed, public_key, NULL);
|
|
p2p_ed25519_sign(seed, public_key, body, (int)strlen(body), signature);
|
|
memcpy(header, "ed25519=", 8);
|
|
server_base64_encode(signature, 64, header + 8);
|
|
return header;
|
|
}
|
|
|
|
/* a probe from an address (network byte order) answered: 1 */
|
|
static int probe_from(const unsigned char *probe, int size, unsigned long address, unsigned char *answer)
|
|
{
|
|
int answer_size = -1;
|
|
int answered = server_master_probe(probe, size, address, answer, &answer_size);
|
|
|
|
CHECK(!answered || answer_size == PROBE_ANSWER_SIZE, "an answer of %d bytes", answer_size);
|
|
return answered;
|
|
}
|
|
|
|
/* ... from the vectors' master's address */
|
|
static int probe_answered(const unsigned char *probe, int size, unsigned char *answer)
|
|
{
|
|
return probe_from(probe, size, 0x0100A8C0UL, answer);
|
|
}
|
|
|
|
/* a listing as listing_make_token writes one (flags, the names' lengths;
|
|
a password's token sealed), signed by the vectors' host: its size */
|
|
static int make_listing(unsigned char *bytes, int format, int flags, int name_length, int map_length,
|
|
int gametype_length)
|
|
{
|
|
unsigned char seed[32], public_key[32], data[512];
|
|
int lengths[3], size = 0, index, text;
|
|
|
|
lengths[0] = name_length;
|
|
lengths[1] = map_length;
|
|
lengths[2] = gametype_length;
|
|
key_of(VECTOR_HOST_SEED, seed);
|
|
p2p_ed25519_public(seed, public_key, NULL);
|
|
bytes[size++] = 'H';
|
|
bytes[size++] = 'L';
|
|
bytes[size++] = (unsigned char)format;
|
|
bytes[size++] = 0;
|
|
bytes[size++] = 39;
|
|
bytes[size++] = (unsigned char)flags;
|
|
for (index = 0; index < 4; index++)
|
|
bytes[size++] = (unsigned char)(index == 3 ? 9 : 0);
|
|
for (index = 0; index < 4; index++)
|
|
bytes[size++] = (unsigned char)(1760900000UL >> (24 - 8 * index));
|
|
memcpy(bytes + size, public_key, 32);
|
|
size += 32;
|
|
for (index = 0; index < ((flags & 16) ? P2P_SEALED_TOKEN_SIZE : P2P_TOKEN_SIZE); index++)
|
|
bytes[size++] = (unsigned char)(0xB0 + index);
|
|
bytes[size++] = 3;
|
|
bytes[size++] = 16;
|
|
bytes[size++] = 2;
|
|
for (text = 0; text < 3; text++)
|
|
{
|
|
bytes[size++] = (unsigned char)lengths[text];
|
|
for (index = 0; index < lengths[text]; index++)
|
|
bytes[size++] = (unsigned char)('a' + text);
|
|
}
|
|
memset(bytes + size, 0, 8);
|
|
bytes[size] = 3;
|
|
size += 8;
|
|
memcpy(data, "hceu-lobby-1", 12);
|
|
memcpy(data + 12, bytes, (size_t)size);
|
|
p2p_ed25519_sign(seed, public_key, data, 12 + size, bytes + size);
|
|
return size + 64;
|
|
}
|
|
|
|
/* whether listing_read takes the bytes, and listing_signed their
|
|
signature */
|
|
static int listing_good(const unsigned char *bytes, int size)
|
|
{
|
|
struct listing listing;
|
|
|
|
return listing_read(bytes, size, &listing) && listing_signed(bytes, &listing);
|
|
}
|
|
|
|
/* the vectors' first listing as the hosted game's */
|
|
static void host_vector_listing(void)
|
|
{
|
|
host_listing_size = hex_decode(vector_listings[0].bytes, host_listing, sizeof(host_listing));
|
|
CHECK(host_listing_size == vector_listings[0].size, "the vectors' first listing is %d bytes", host_listing_size);
|
|
}
|
|
|
|
/* a JSON body's member, as text: its string's value, or its number or
|
|
boolean as written */
|
|
static const char *body_member(const char *body, const char *name)
|
|
{
|
|
static char value[1024];
|
|
struct server_json_document document;
|
|
int token;
|
|
|
|
value[0] = 0;
|
|
if (!server_json_parse(&document, body, (int)strlen(body), 8))
|
|
return "(not JSON)";
|
|
token = server_json_member(&document, 0, name);
|
|
if (token < 0)
|
|
snprintf(value, sizeof(value), "(none)");
|
|
else if (document.tokens[token].type == _server_json_string_token)
|
|
server_json_get_string(&document, token, value, sizeof(value));
|
|
else
|
|
snprintf(value, sizeof(value), "%.*s", document.tokens[token].end - document.tokens[token].start,
|
|
body + document.tokens[token].start);
|
|
server_json_document_free(&document);
|
|
return value;
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
char const *case_name = argc > 1 ? argv[1] : "";
|
|
|
|
/* the vectors' keys, ids and hashes from the seeds; a small-order key
|
|
refused */
|
|
CASE("crypto")
|
|
{
|
|
const char *seeds[] = { VECTOR_PUBLISHER_SEED, VECTOR_MASTER_SEED, VECTOR_HOST_SEED };
|
|
const char *keys[] = { VECTOR_PUBLISHER_PUBLIC_KEY, VECTOR_MASTER_PUBLIC_KEY, VECTOR_HOST_PUBLIC_KEY };
|
|
const char *ids[] = { VECTOR_PUBLISHER_KEY_ID, VECTOR_MASTER_KEY_ID, VECTOR_HOST_KEY_ID };
|
|
unsigned char seed[32], public_key[32], digest[32], x25519[32], signature[64];
|
|
int index;
|
|
|
|
for (index = 0; index < 3; index++)
|
|
{
|
|
key_of(seeds[index], seed);
|
|
p2p_ed25519_public(seed, public_key, NULL);
|
|
CHECK(!strcmp(hex_of(public_key, 32), keys[index]), "the key of seed %d is %s", index,
|
|
hex_of(public_key, 32));
|
|
p2p_sha256(public_key, 32, digest);
|
|
CHECK(!strcmp(hex_of(digest, 8), ids[index]), "key id %s, not %s", hex_of(digest, 8), ids[index]);
|
|
}
|
|
CHECK(!strcmp(VECTOR_MASTER_KEY_ID_BYTES, VECTOR_MASTER_KEY_ID), "the master's key id bytes are its id");
|
|
p2p_sha256("", 0, digest);
|
|
CHECK(!strcmp(hex_of(digest, 32), VECTOR_ME_BODY_SHA256), "SHA-256 of nothing is %s", hex_of(digest, 32));
|
|
p2p_sha256(VECTOR_PUBLISH_BODY, (int)strlen(VECTOR_PUBLISH_BODY), digest);
|
|
CHECK(!strcmp(hex_of(digest, 32), VECTOR_PUBLISH_BODY_SHA256), "the body's SHA-256 is %s",
|
|
hex_of(digest, 32));
|
|
p2p_sha256(VECTOR_SERVER_ID_INPUT, (int)strlen(VECTOR_SERVER_ID_INPUT), digest);
|
|
CHECK(!strcmp(hex_of(digest, 8), VECTOR_SERVER_ID), "the server id input hashes to %s", hex_of(digest, 8));
|
|
/* (the vectors' signatures are the keys' over the vectors' strings) */
|
|
key_of(VECTOR_PUBLISHER_PUBLIC_KEY, public_key);
|
|
CHECK(server_base64_decode(VECTOR_PUBLISH_SIGNATURE, signature, sizeof(signature)) == 64, "not 64 bytes");
|
|
CHECK(p2p_ed25519_verify(public_key, VECTOR_PUBLISH_CANONICAL, (int)strlen(VECTOR_PUBLISH_CANONICAL),
|
|
signature), "the publish's signature does not verify");
|
|
/* (a key of small order: refused, whatever it signs) */
|
|
key_of(VECTOR_SMALL_ORDER_KEY, public_key);
|
|
CHECK(!p2p_ed25519_to_x25519(public_key, x25519), "a key of small order was taken");
|
|
memset(signature, 0, sizeof(signature));
|
|
CHECK(!p2p_ed25519_verify(public_key, "", 0, signature), "a small-order key's signature verified");
|
|
signature[0] = 1;
|
|
CHECK(!p2p_ed25519_verify(public_key, "", 0, signature), "a small-order key's signature verified");
|
|
return 0;
|
|
}
|
|
/* the listings' bytes from their base64, their host's key, and their
|
|
signatures over "hceu-lobby-1" and their signed bytes */
|
|
CASE("listing-signatures")
|
|
{
|
|
int index;
|
|
|
|
for (index = 0; index < (int)(sizeof(vector_listings) / sizeof(vector_listings[0])); index++)
|
|
{
|
|
struct vector_listing const *vector = &vector_listings[index];
|
|
unsigned char bytes[400], decoded[400], seed[32], public_key[32], message[512], signature[64];
|
|
int size = hex_decode(vector->bytes, bytes, sizeof(bytes));
|
|
|
|
CHECK(size == vector->size, "%s: %d bytes", vector->description, size);
|
|
CHECK(server_base64_decode(vector->base64, decoded, sizeof(decoded)) == size &&
|
|
!memcmp(decoded, bytes, (size_t)size), "%s: its base64 is other bytes", vector->description);
|
|
key_of(vector->host_seed, seed);
|
|
p2p_ed25519_public(seed, public_key, NULL);
|
|
CHECK(!strcmp(hex_of(public_key, 32), vector->host_public_key) && !memcmp(bytes + 14, public_key, 32),
|
|
"%s: another host key", vector->description);
|
|
CHECK(!strcmp(hex_of(bytes + size - 64, 64), vector->signature), "%s: the signature is not its last 64 "
|
|
"bytes", vector->description);
|
|
CHECK(vector->signed_size == size - 64,
|
|
"%s: signed %d of %d bytes", vector->description, vector->signed_size, size);
|
|
memcpy(message, "hceu-lobby-1", 12);
|
|
memcpy(message + 12, bytes, (size_t)vector->signed_size);
|
|
CHECK((strcmp(hex_of(message, 12 + vector->signed_size), vector->signed_message) == 0) == vector->valid,
|
|
"%s: the signed message %s the vector's", vector->description, vector->valid ? "is not" : "is");
|
|
CHECK(p2p_ed25519_verify(public_key, message, 12 + vector->signed_size, bytes + size - 64) ==
|
|
vector->valid, "%s: its signature %s", vector->description, vector->valid ? "fails" : "verifies");
|
|
if (vector->valid)
|
|
{
|
|
p2p_ed25519_sign(seed, public_key, message, 12 + vector->signed_size, signature);
|
|
CHECK(!memcmp(signature, bytes + size - 64, 64), "%s: signed otherwise", vector->description);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
/* --sign: the publisher key's signature of an application's nonce */
|
|
CASE("proof")
|
|
{
|
|
unsigned char signature[64];
|
|
|
|
setenv("HALO_SERVER_PUBLISHER_KEY", VECTOR_PUBLISHER_SEED, 1);
|
|
CHECK(publisher_sign(VECTOR_PROOF_NONCE, (int)strlen(VECTOR_PROOF_NONCE), signature), "no publisher key");
|
|
CHECK(!strcmp(hex_of(signature, 64), VECTOR_PROOF_SIGNATURE), "the proof is %s", hex_of(signature, 64));
|
|
return 0;
|
|
}
|
|
/* the server's key id and server id, and its master's key id */
|
|
CASE("server-id")
|
|
{
|
|
unsigned char master_id[8];
|
|
|
|
start_publisher();
|
|
CHECK(!strcmp(master.key_id, VECTOR_PUBLISHER_KEY_ID), "key id %s", master.key_id);
|
|
CHECK(!strcmp(master.instance, VECTOR_INSTANCE), "instance %s", master.instance);
|
|
CHECK(!strcmp(master.server_id, VECTOR_SERVER_ID), "server id %s, not %s", master.server_id, VECTOR_SERVER_ID);
|
|
CHECK(hex_decode(VECTOR_MASTER_KEY_ID_BYTES, master_id, 8) == 8 &&
|
|
!memcmp(master.masters[0].key_id, master_id, 8), "the master's key id is %s",
|
|
hex_of(master.masters[0].key_id, 8));
|
|
CHECK(!strcmp(master.masters[0].url, "https://master.example.org"), "url %s", master.masters[0].url);
|
|
return 0;
|
|
}
|
|
/* publisher.key: one line of 64 hex digits, comments and blank lines
|
|
skipped; HALO_SERVER_PUBLISHER_KEY first */
|
|
CASE("key-file")
|
|
{
|
|
unsigned char public_key[32];
|
|
char key_id[17], text[256], upper[65];
|
|
int index;
|
|
|
|
unsetenv("HALO_SERVER_PUBLISHER_KEY");
|
|
file_key = NULL;
|
|
CHECK(!server_master_publisher(public_key, key_id), "a key without a file");
|
|
snprintf(text, sizeof(text), "# the operator's key\n\n %s # mine\n", VECTOR_PUBLISHER_SEED);
|
|
file_key = text;
|
|
CHECK(server_master_publisher(public_key, key_id), "the key file was refused");
|
|
CHECK(!strcmp(hex_of(public_key, 32), VECTOR_PUBLISHER_PUBLIC_KEY) && !strcmp(key_id, VECTOR_PUBLISHER_KEY_ID),
|
|
"the file's key is %s (%s)", hex_of(public_key, 32), key_id);
|
|
snprintf(text, sizeof(text), "%s\r\n", VECTOR_PUBLISHER_SEED);
|
|
CHECK(server_master_publisher(public_key, key_id), "a key file with CRLF was refused");
|
|
for (index = 0; index < 64; index++)
|
|
upper[index] = (char)toupper((unsigned char)VECTOR_PUBLISHER_SEED[index]);
|
|
upper[64] = 0;
|
|
snprintf(text, sizeof(text), "%s\n", upper);
|
|
CHECK(server_master_publisher(public_key, key_id) && !strcmp(key_id, VECTOR_PUBLISHER_KEY_ID),
|
|
"a key in capitals was read otherwise");
|
|
snprintf(text, sizeof(text), "%.63s\n", VECTOR_PUBLISHER_SEED);
|
|
CHECK(!server_master_publisher(public_key, key_id), "63 digits were taken");
|
|
snprintf(text, sizeof(text), "%s0\n", VECTOR_PUBLISHER_SEED);
|
|
CHECK(!server_master_publisher(public_key, key_id), "65 digits were taken");
|
|
snprintf(text, sizeof(text), "%.62sxy\n", VECTOR_PUBLISHER_SEED);
|
|
CHECK(!server_master_publisher(public_key, key_id), "a key that is not hex was taken");
|
|
snprintf(text, sizeof(text), "# %s\n", VECTOR_PUBLISHER_SEED);
|
|
CHECK(!server_master_publisher(public_key, key_id), "a commented key was taken");
|
|
/* (the environment's over the file's) */
|
|
snprintf(text, sizeof(text), "%s\n", VECTOR_MASTER_SEED);
|
|
setenv("HALO_SERVER_PUBLISHER_KEY", VECTOR_PUBLISHER_SEED, 1);
|
|
CHECK(server_master_publisher(public_key, key_id) && !strcmp(key_id, VECTOR_PUBLISHER_KEY_ID),
|
|
"the file's key was taken over the environment's");
|
|
setenv("HALO_SERVER_PUBLISHER_KEY", "not a key", 1);
|
|
CHECK(!server_master_publisher(public_key, key_id), "a bad HALO_SERVER_PUBLISHER_KEY was taken");
|
|
return 0;
|
|
}
|
|
/* the instance: server.instance, else a slug of the server's name; 1 to
|
|
32 of a-z 0-9 - _, the first a letter or digit */
|
|
CASE("instance")
|
|
{
|
|
static const struct instance_vector { const char *instance, *name, *expected; } names[] =
|
|
{
|
|
{ "main", "Anything", "main" },
|
|
{ "Main", "", "main" },
|
|
{ "", "OpenCE Dedicated", "opence-dedicated" },
|
|
{ "", " --My.Server__ ", "my-server__" },
|
|
{ "", "EU #1 CTF / Slayer!", "eu-1-ctf-slayer" },
|
|
{ "", "", "main" },
|
|
{ "", "!!! ???", "main" },
|
|
{ "", "_-_", "main" },
|
|
{ "", "\xc3\x9c" "ber Server", "ber-server" },
|
|
{ "", "abcdefghijklmnopqrstuvwxyz0123456789", "abcdefghijklmnopqrstuvwxyz012345" },
|
|
{ "", "a b", "a-b" },
|
|
};
|
|
int index;
|
|
|
|
for (index = 0; index < (int)(sizeof(names) / sizeof(names[0])); index++)
|
|
{
|
|
char instance[33];
|
|
int character;
|
|
|
|
setting_instance = names[index].instance;
|
|
setting_name = names[index].name;
|
|
memset(instance, 'z', sizeof(instance));
|
|
instance_name(instance, sizeof(instance));
|
|
CHECK(!strcmp(instance, names[index].expected), "\"%s\" / \"%s\" made %s, not %s", names[index].instance,
|
|
names[index].name, instance, names[index].expected);
|
|
/* (docs/master_protocol.md's form) */
|
|
CHECK(strlen(instance) >= 1 && strlen(instance) <= 32 && isalnum((unsigned char)instance[0]),
|
|
"%s is no instance", instance);
|
|
for (character = 0; instance[character]; character++)
|
|
{
|
|
CHECK(islower((unsigned char)instance[character]) || isdigit((unsigned char)instance[character]) ||
|
|
instance[character] == '-' || instance[character] == '_', "%s is no instance", instance);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
/* the vectors' signed publish */
|
|
CASE("publish-request")
|
|
{
|
|
CHECK(!strcmp(&VECTOR_PUBLISH_PATH[strlen("/v1/servers/")], VECTOR_SERVER_ID), "the path is another server's");
|
|
check_request(VECTOR_PUBLISH_MASTER_KEY_ID, VECTOR_PUBLISH_METHOD, VECTOR_PUBLISH_PATH, VECTOR_PUBLISH_BODY, strlen(VECTOR_PUBLISH_BODY),
|
|
VECTOR_PUBLISH_TS, VECTOR_PUBLISH_NONCE, VECTOR_PUBLISH_CANONICAL, VECTOR_PUBLISH_AUTHORIZATION);
|
|
return 0;
|
|
}
|
|
/* the vectors' signed GET /v1/me: an empty body ("" or none) */
|
|
CASE("me-request")
|
|
{
|
|
check_request(VECTOR_ME_MASTER_KEY_ID, VECTOR_ME_METHOD, VECTOR_ME_PATH, VECTOR_ME_BODY, 0, VECTOR_ME_TS, VECTOR_ME_NONCE,
|
|
VECTOR_ME_CANONICAL, VECTOR_ME_AUTHORIZATION);
|
|
check_request(VECTOR_ME_MASTER_KEY_ID, VECTOR_ME_METHOD, VECTOR_ME_PATH, NULL, 0, VECTOR_ME_TS, VECTOR_ME_NONCE, VECTOR_ME_CANONICAL,
|
|
VECTOR_ME_AUTHORIZATION);
|
|
return 0;
|
|
}
|
|
/* the vectors' probe answered as they have it */
|
|
CASE("probe")
|
|
{
|
|
unsigned char probe[64], expected[128], answer[128], message[64], public_key[32];
|
|
int size, expected_size, message_size;
|
|
|
|
start_publisher();
|
|
now = 1760900000;
|
|
size = hex_decode(VECTOR_PROBE_PACKET, probe, sizeof(probe));
|
|
expected_size = hex_decode(VECTOR_PROBE_ANSWER, expected, sizeof(expected));
|
|
message_size = hex_decode(VECTOR_PROBE_SIGNED_MESSAGE, message, sizeof(message));
|
|
CHECK(size == 35 && expected_size == 103 && message_size == 29, "the vectors' probe is %d, %d, %d bytes", size,
|
|
expected_size, message_size);
|
|
CHECK(!strcmp(hex_of(probe + 11, 8), VECTOR_PROBE_MASTER_KEY_ID) && !strcmp(hex_of(probe + 19, 16),
|
|
VECTOR_PROBE_NONCE), "the vectors' probe holds another key id or nonce");
|
|
signed_size = -1;
|
|
CHECK(probe_answered(probe, size, answer), "the vectors' probe was not answered");
|
|
CHECK(signed_size == message_size && !memcmp(signed_message, message, (size_t)message_size),
|
|
"signed %s, not the vector's %s", hex_of(signed_message, signed_size > 0 ? signed_size : 0),
|
|
VECTOR_PROBE_SIGNED_MESSAGE);
|
|
CHECK(!memcmp(answer, expected, 39), "answered %s", hex_of(answer, 39));
|
|
CHECK(!memcmp(answer + 39, expected + 39, 64), "signed %s", hex_of(answer + 39, 64));
|
|
key_of(VECTOR_PUBLISHER_PUBLIC_KEY, public_key);
|
|
CHECK(p2p_ed25519_verify(public_key, message, message_size, answer + 39),
|
|
"the answer's signature is not the publisher's");
|
|
return 0;
|
|
}
|
|
/* a probe answered only as it is, from a master of masters.txt, with a
|
|
publisher key, a nonce once a second */
|
|
CASE("probe-refusals")
|
|
{
|
|
unsigned char probe[64], other[64], answer[128];
|
|
int size, index;
|
|
|
|
start_publisher();
|
|
now = 1760900000;
|
|
size = hex_decode(VECTOR_PROBE_PACKET, probe, sizeof(probe));
|
|
memcpy(other, probe, (size_t)size);
|
|
CHECK(!probe_answered(probe, size - 1, answer), "a probe of 34 bytes was answered");
|
|
CHECK(!probe_answered(probe, size + 1, answer), "a probe of 36 bytes was answered");
|
|
other[10] = 2;
|
|
CHECK(!probe_answered(other, size, answer), "a probe of version 2 was answered");
|
|
other[10] = 1;
|
|
other[0] = 'o';
|
|
CHECK(!probe_answered(other, size, answer), "a probe with another magic was answered");
|
|
other[0] = probe[0];
|
|
other[11] ^= 1;
|
|
CHECK(!probe_answered(other, size, answer), "a probe of a master not in masters.txt was answered");
|
|
other[11] = probe[11];
|
|
CHECK(probe_answered(probe, size, answer), "the vectors' probe was not answered");
|
|
CHECK(!probe_answered(probe, size, answer), "a nonce was answered twice in a second");
|
|
/* (a master's retry, two seconds later, with the same nonce) */
|
|
now += 2;
|
|
CHECK(probe_answered(probe, size, answer), "the master's retry was not answered");
|
|
(void)index;
|
|
/* (no publisher key: nothing answered) */
|
|
now += 1;
|
|
master.has_key = 0;
|
|
other[34] = 0xEF;
|
|
CHECK(!probe_answered(other, size, answer), "a probe was answered without a key");
|
|
return 0;
|
|
}
|
|
/* the vectors' signed list, taken while it is fresh */
|
|
CASE("list-verify")
|
|
{
|
|
unsigned long time_value = strtoul(VECTOR_LIST_TIME, NULL, 10);
|
|
unsigned long size = (unsigned long)strlen(VECTOR_LIST_BODY);
|
|
|
|
CHECK(verify_list(VECTOR_LIST_BODY, size, VECTOR_LIST_SIGNATURE_HEADER, VECTOR_LIST_KEY_HEADER,
|
|
VECTOR_MASTER_PUBLIC_KEY, time_value), "the vectors' list was refused");
|
|
CHECK(verify_list(VECTOR_LIST_BODY, size, VECTOR_LIST_SIGNATURE_HEADER, VECTOR_LIST_KEY_HEADER,
|
|
VECTOR_MASTER_PUBLIC_KEY, time_value + 300), "a list 300 seconds old was refused");
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, VECTOR_LIST_SIGNATURE_HEADER, VECTOR_LIST_KEY_HEADER,
|
|
VECTOR_MASTER_PUBLIC_KEY, time_value + 301), "a list 301 seconds old was taken");
|
|
CHECK(verify_list(VECTOR_LIST_BODY, size, VECTOR_LIST_SIGNATURE_HEADER, VECTOR_LIST_KEY_HEADER,
|
|
VECTOR_MASTER_PUBLIC_KEY, time_value - 60), "a list of a master's clock ahead was refused");
|
|
/* (dated ahead of this machine's clock: 300 seconds at most) */
|
|
CHECK(verify_list(VECTOR_LIST_BODY, size, VECTOR_LIST_SIGNATURE_HEADER, VECTOR_LIST_KEY_HEADER,
|
|
VECTOR_MASTER_PUBLIC_KEY, time_value - 300), "a list 300 seconds ahead was refused");
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, VECTOR_LIST_SIGNATURE_HEADER, VECTOR_LIST_KEY_HEADER,
|
|
VECTOR_MASTER_PUBLIC_KEY, time_value - 301), "a list 301 seconds ahead was taken");
|
|
return 0;
|
|
}
|
|
/* a list altered, signed by another key, or with a malformed header */
|
|
CASE("list-refusals")
|
|
{
|
|
unsigned long time_value = strtoul(VECTOR_LIST_TIME, NULL, 10);
|
|
unsigned long size = (unsigned long)strlen(VECTOR_LIST_BODY);
|
|
const char *signature = VECTOR_LIST_SIGNATURE_HEADER;
|
|
char body[512], header[256];
|
|
unsigned long offsets[3];
|
|
int index;
|
|
|
|
offsets[0] = 0;
|
|
offsets[1] = size / 2;
|
|
offsets[2] = size - 1;
|
|
for (index = 0; index < 3; index++)
|
|
{
|
|
snprintf(body, sizeof(body), "%s", VECTOR_LIST_BODY);
|
|
body[offsets[index]] ^= 1;
|
|
CHECK(!verify_list(body, size, signature, VECTOR_LIST_KEY_HEADER, VECTOR_MASTER_PUBLIC_KEY, time_value),
|
|
"a list with byte %lu changed was taken", offsets[index]);
|
|
}
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size - 1, signature, VECTOR_LIST_KEY_HEADER, VECTOR_MASTER_PUBLIC_KEY,
|
|
time_value), "a list without its last byte was taken");
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, signature, NULL, VECTOR_PUBLISHER_PUBLIC_KEY, time_value),
|
|
"a list was taken with another master's key");
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, signature, VECTOR_LIST_KEY_HEADER, VECTOR_PUBLISHER_PUBLIC_KEY,
|
|
time_value), "a list was taken with another master's key");
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, signature, VECTOR_PUBLISHER_KEY_ID, VECTOR_MASTER_PUBLIC_KEY,
|
|
time_value), "a list was taken whose Signature-Key is another key's");
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, signature, "39f713d0a644253", VECTOR_MASTER_PUBLIC_KEY,
|
|
time_value), "a list was taken whose Signature-Key is cut short");
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, signature, NULL, VECTOR_MASTER_PUBLIC_KEY, time_value) &&
|
|
!verify_list(VECTOR_LIST_BODY, size, signature, "", VECTOR_MASTER_PUBLIC_KEY, time_value),
|
|
"a list was taken without its Signature-Key");
|
|
/* (the Signature header malformed) */
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, "", VECTOR_LIST_KEY_HEADER, VECTOR_MASTER_PUBLIC_KEY, time_value),
|
|
"a list without a signature was taken");
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, "ed25519=", VECTOR_LIST_KEY_HEADER, VECTOR_MASTER_PUBLIC_KEY,
|
|
time_value), "an empty signature was taken");
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, signature + 8, VECTOR_LIST_KEY_HEADER, VECTOR_MASTER_PUBLIC_KEY,
|
|
time_value), "a signature without ed25519= was taken");
|
|
snprintf(header, sizeof(header), "ed25519:%s", signature + 8);
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, header, VECTOR_LIST_KEY_HEADER, VECTOR_MASTER_PUBLIC_KEY,
|
|
time_value), "ed25519: was taken for ed25519=");
|
|
snprintf(header, sizeof(header), "Ed25519=%s", signature + 8);
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, header, VECTOR_LIST_KEY_HEADER, VECTOR_MASTER_PUBLIC_KEY,
|
|
time_value), "Ed25519= was taken for ed25519=");
|
|
snprintf(header, sizeof(header), "%.*s", (int)strlen(signature) - 4, signature);
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, header, VECTOR_LIST_KEY_HEADER, VECTOR_MASTER_PUBLIC_KEY,
|
|
time_value), "a signature cut short was taken");
|
|
snprintf(header, sizeof(header), "%sAAAA", signature);
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, header, VECTOR_LIST_KEY_HEADER, VECTOR_MASTER_PUBLIC_KEY,
|
|
time_value), "a signature too long was taken");
|
|
snprintf(header, sizeof(header), "%s", signature);
|
|
header[10] = header[10] == 'A' ? 'B' : 'A';
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, header, VECTOR_LIST_KEY_HEADER, VECTOR_MASTER_PUBLIC_KEY,
|
|
time_value), "a signature changed was taken");
|
|
snprintf(header, sizeof(header), "ed25519=%s", "!!!!");
|
|
CHECK(!verify_list(VECTOR_LIST_BODY, size, header, VECTOR_LIST_KEY_HEADER, VECTOR_MASTER_PUBLIC_KEY,
|
|
time_value), "a signature that is not base64 was taken");
|
|
return 0;
|
|
}
|
|
/* a body the master signed, but not a list of protocol 1 by it */
|
|
CASE("list-body")
|
|
{
|
|
static const char *refused[] =
|
|
{
|
|
"{\"protocol\":2,\"master\":{\"name\":\"m\",\"key_id\":\"39f713d0a644253f\",\"time\":1760900000},\"servers\":[]}",
|
|
"{\"protocol\":\"1\",\"master\":{\"name\":\"m\",\"key_id\":\"39f713d0a644253f\",\"time\":1760900000},\"servers\":[]}",
|
|
"{\"master\":{\"name\":\"m\",\"key_id\":\"39f713d0a644253f\",\"time\":1760900000},\"servers\":[]}",
|
|
"{\"protocol\":1,\"master\":{\"name\":\"m\",\"key_id\":\"21fe31dfa154a261\",\"time\":1760900000},\"servers\":[]}",
|
|
"{\"protocol\":1,\"master\":{\"name\":\"m\",\"key_id\":\"39f713d0a644253f\"},\"servers\":[]}",
|
|
"{\"protocol\":1,\"master\":{\"name\":\"m\",\"key_id\":\"39f713d0a644253f\",\"time\":\"1760900000\"}}",
|
|
"{\"protocol\":1,\"servers\":[]}",
|
|
"[{\"protocol\":1,\"master\":{\"name\":\"m\",\"key_id\":\"39f713d0a644253f\",\"time\":1760900000}}]",
|
|
"{\"protocol\":1,\"master\":{\"name\":\"m\",\"key_id\":\"39f713d0a644253f\",\"time\":1760900000}",
|
|
"not json",
|
|
};
|
|
static const char *taken[] =
|
|
{
|
|
"{\"protocol\":1,\"master\":{\"name\":\"m\",\"key_id\":\"39f713d0a644253f\",\"time\":1760900000},\"servers\":[],"
|
|
"\"later\":{\"field\":[1,2,3]}}",
|
|
"{\"servers\":[{\"id\":\"4c5f15237c866f6f\"}],\"master\":{\"time\":1760900000,\"key_id\":\"39f713d0a644253f\"},"
|
|
"\"protocol\":1}",
|
|
};
|
|
int index;
|
|
|
|
for (index = 0; index < (int)(sizeof(refused) / sizeof(refused[0])); index++)
|
|
{
|
|
CHECK(!verify_list(refused[index], strlen(refused[index]), master_signs(refused[index]),
|
|
VECTOR_LIST_KEY_HEADER, VECTOR_MASTER_PUBLIC_KEY, 1760900000), "taken: %s", refused[index]);
|
|
}
|
|
for (index = 0; index < (int)(sizeof(taken) / sizeof(taken[0])); index++)
|
|
{
|
|
CHECK(verify_list(taken[index], strlen(taken[index]), master_signs(taken[index]), VECTOR_LIST_KEY_HEADER,
|
|
VECTOR_MASTER_PUBLIC_KEY, 1760900000), "refused: %s", taken[index]);
|
|
}
|
|
return 0;
|
|
}
|
|
/* masters.txt: a URL and its key a line, comments, https or this
|
|
machine's http, the first four */
|
|
CASE("masters-file")
|
|
{
|
|
char urls[MASTER_LIST_MAXIMUM_MASTERS + 2][MASTER_LIST_URL_SIZE];
|
|
unsigned char keys[MASTER_LIST_MAXIMUM_MASTERS + 2][MASTER_LIST_KEY_SIZE];
|
|
unsigned char key[32];
|
|
char text[4096];
|
|
int count;
|
|
|
|
key_of(VECTOR_MASTER_PUBLIC_KEY, key);
|
|
snprintf(text, sizeof(text),
|
|
"# OpenCE's masters\n"
|
|
"\n"
|
|
"https://master1.example.org/ %s # the first\r\n"
|
|
"http://master2.example.org %s\n"
|
|
"https://master3.example.org ed25519:%.62s\n"
|
|
"https://master4.example.org ed25519:%.62sxy\n"
|
|
"https://master5.example.org %s0\n"
|
|
"https://master6.example.org\n"
|
|
"#https://master7.example.org %s\n"
|
|
"http://127.0.0.1.example.net %s\n"
|
|
" http://127.0.0.1:8080 %s\n"
|
|
"https://master9.example.org/opence// %s\n"
|
|
"http://localhost:9000 %s\n"
|
|
"https://master11.example.org %s\n",
|
|
VECTOR_MASTER_PUBLIC_KEY_TEXT, VECTOR_MASTER_PUBLIC_KEY_TEXT, VECTOR_MASTER_PUBLIC_KEY,
|
|
VECTOR_MASTER_PUBLIC_KEY, VECTOR_MASTER_PUBLIC_KEY_TEXT, VECTOR_MASTER_PUBLIC_KEY_TEXT,
|
|
VECTOR_MASTER_PUBLIC_KEY_TEXT, VECTOR_MASTER_PUBLIC_KEY_TEXT, VECTOR_MASTER_PUBLIC_KEY_TEXT,
|
|
VECTOR_MASTER_PUBLIC_KEY_TEXT, VECTOR_MASTER_PUBLIC_KEY_TEXT);
|
|
file_masters = text;
|
|
memset(keys, 0, sizeof(keys));
|
|
count = master_list_read_file("masters.txt", urls, keys, MASTER_LIST_MAXIMUM_MASTERS);
|
|
CHECK(count == MASTER_LIST_MAXIMUM_MASTERS, "%d masters read", count);
|
|
CHECK(!strcmp(urls[0], "https://master1.example.org"), "the first is %s", urls[0]);
|
|
CHECK(!strcmp(urls[1], "http://127.0.0.1:8080"), "the second is %s", urls[1]);
|
|
CHECK(!strcmp(urls[2], "https://master9.example.org/opence"), "the third is %s", urls[2]);
|
|
CHECK(!strcmp(urls[3], "http://localhost:9000"), "the fourth is %s", urls[3]);
|
|
for (count = 0; count < MASTER_LIST_MAXIMUM_MASTERS; count++)
|
|
CHECK(!memcmp(keys[count], key, 32), "master %d's key is %s", count, hex_of(keys[count], 32));
|
|
CHECK(master_list_read_file("masters.txt", urls, keys, 1) == 1, "more than the most were read");
|
|
file_masters = NULL;
|
|
CHECK(master_list_read_file("masters.txt", urls, keys, MASTER_LIST_MAXIMUM_MASTERS) == 0,
|
|
"masters read from no file");
|
|
return 0;
|
|
}
|
|
/* the official master, built in: asked when masters.txt names none */
|
|
CASE("masters-default")
|
|
{
|
|
char urls[MASTER_LIST_MAXIMUM_MASTERS][MASTER_LIST_URL_SIZE];
|
|
unsigned char keys[MASTER_LIST_MAXIMUM_MASTERS][MASTER_LIST_KEY_SIZE];
|
|
unsigned char official[32];
|
|
static const char *const none[] = { NULL, "", "# comments alone\n\n#https://master1.example.org\n",
|
|
"https://master1.example.org ed25519:abc\n" };
|
|
static char named[256];
|
|
int index;
|
|
|
|
key_of("89f4eda71229f11f3538679f49ef7fb0c2b407b380093f7f4fe02105b92225aa", official);
|
|
for (index = 0; index < (int)(sizeof(none) / sizeof(none[0])); index++)
|
|
{
|
|
file_masters = none[index];
|
|
memset(urls, 0, sizeof(urls));
|
|
CHECK(master_list_read("masters.txt", urls, keys, MASTER_LIST_MAXIMUM_MASTERS) == 1 &&
|
|
!strcmp(urls[0], "https://master.opencommunityedition.com"),
|
|
"masters.txt %d naming none: %s", index, urls[0]);
|
|
CHECK(!memcmp(keys[0], official, 32), "the official master's key is %s", hex_of(keys[0], 32));
|
|
}
|
|
snprintf(named, sizeof(named), "https://master1.example.org %s\n", VECTOR_MASTER_PUBLIC_KEY_TEXT);
|
|
file_masters = named;
|
|
CHECK(master_list_read("masters.txt", urls, keys, MASTER_LIST_MAXIMUM_MASTERS) == 1 &&
|
|
!strcmp(urls[0], "https://master1.example.org"), "masters.txt's master was not alone: %s", urls[0]);
|
|
file_masters = NULL;
|
|
return 0;
|
|
}
|
|
/* the web site's masters.json */
|
|
CASE("masters-json")
|
|
{
|
|
/* (room past the most: a fault that reads more is seen, not a crash) */
|
|
char urls[MASTER_LIST_MAXIMUM_MASTERS + 4][MASTER_LIST_URL_SIZE];
|
|
unsigned char keys[MASTER_LIST_MAXIMUM_MASTERS + 4][MASTER_LIST_KEY_SIZE];
|
|
unsigned char key[32];
|
|
char text[2048];
|
|
int count, index, length;
|
|
|
|
key_of(VECTOR_MASTER_PUBLIC_KEY, key);
|
|
snprintf(text, sizeof(text),
|
|
"{\"masters\":[{\"url\":\"https://master1.example.org/\",\"key\":\"%s\"},"
|
|
"{\"url\":\"http://master2.example.org\",\"key\":\"%s\"},"
|
|
"{\"url\":\"https://master3.example.org\",\"key\":\"ed25519:%.62s\"},"
|
|
"{\"url\":\"https://master4.example.org\"},"
|
|
"{\"url\":1,\"key\":\"%s\"},"
|
|
"\"https://master6.example.org\","
|
|
"{\"key\":\"%s\",\"url\":\"https://master7.example.org\",\"name\":\"later\"}]}",
|
|
VECTOR_MASTER_PUBLIC_KEY_TEXT, VECTOR_MASTER_PUBLIC_KEY_TEXT, VECTOR_MASTER_PUBLIC_KEY,
|
|
VECTOR_MASTER_PUBLIC_KEY_TEXT, VECTOR_MASTER_PUBLIC_KEY_TEXT);
|
|
count = master_list_read_json(text, strlen(text), urls, keys, MASTER_LIST_MAXIMUM_MASTERS);
|
|
CHECK(count == 2, "%d masters read", count);
|
|
CHECK(!strcmp(urls[0], "https://master1.example.org") && !strcmp(urls[1], "https://master7.example.org"),
|
|
"read %s and %s", urls[0], urls[1]);
|
|
CHECK(!memcmp(keys[0], key, 32) && !memcmp(keys[1], key, 32), "another key was read");
|
|
CHECK(master_list_read_json("{}", 2, urls, keys, MASTER_LIST_MAXIMUM_MASTERS) == 0, "masters read from {}");
|
|
CHECK(master_list_read_json("not json", 8, urls, keys, MASTER_LIST_MAXIMUM_MASTERS) == 0,
|
|
"masters read from what is not JSON");
|
|
/* (six good ones: the first four) */
|
|
length = snprintf(text, sizeof(text), "{\"masters\":[");
|
|
for (index = 0; index < MASTER_LIST_MAXIMUM_MASTERS + 2; index++)
|
|
{
|
|
length += snprintf(text + length, sizeof(text) - (size_t)length, "%s{\"url\":\"https://m%d.example.org\","
|
|
"\"key\":\"%s\"}", index ? "," : "", index, VECTOR_MASTER_PUBLIC_KEY_TEXT);
|
|
}
|
|
length += snprintf(text + length, sizeof(text) - (size_t)length, "]}");
|
|
count = master_list_read_json(text, (unsigned long)length, urls, keys, MASTER_LIST_MAXIMUM_MASTERS);
|
|
CHECK(count == MASTER_LIST_MAXIMUM_MASTERS && !strcmp(urls[3], "https://m3.example.org"),
|
|
"%d masters read of %d", count, MASTER_LIST_MAXIMUM_MASTERS + 2);
|
|
return 0;
|
|
}
|
|
|
|
/* the vectors' listings as p2p_lobby.c reads them: each field, and the
|
|
signature over "hceu-lobby-1" and the signed bytes */
|
|
CASE("listing-read")
|
|
{
|
|
unsigned char bytes[400], host_key[32];
|
|
struct listing listing;
|
|
int index, size;
|
|
|
|
key_of(VECTOR_HOST_PUBLIC_KEY, host_key);
|
|
for (index = 0; index < (int)(sizeof(vector_listings) / sizeof(vector_listings[0])); index++)
|
|
{
|
|
struct vector_listing const *vector = &vector_listings[index];
|
|
|
|
size = hex_decode(vector->bytes, bytes, sizeof(bytes));
|
|
memset(&listing, 0, sizeof(listing));
|
|
CHECK(listing_read(bytes, size, &listing), "%s: not read", vector->description);
|
|
CHECK(listing.signed_size == vector->signed_size && !memcmp(listing.key, host_key, 32),
|
|
"%s: signed %d bytes, by %s", vector->description, listing.signed_size, hex_of(listing.key, 32));
|
|
CHECK(listing_signed(bytes, &listing) == vector->valid, "%s: its signature %s", vector->description,
|
|
vector->valid ? "fails" : "checks");
|
|
}
|
|
/* (the first: an open team game of a dedicated server) */
|
|
size = hex_decode(vector_listings[0].bytes, bytes, sizeof(bytes));
|
|
listing_read(bytes, size, &listing);
|
|
CHECK(listing.version == 38 && listing.flags == (_listing_open | _listing_has_teams | _listing_dedicated) &&
|
|
listing.sequence == 412 && listing.time == 1760900000UL, "version %d, flags %d, sequence %lu, time %lu",
|
|
listing.version, listing.flags, listing.sequence, listing.time);
|
|
CHECK(!strcmp(hex_of(listing.token, 16), "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"), "token %s", hex_of(listing.token, 16));
|
|
CHECK(listing.player_count == 5 && listing.maximum_player_count == 16 && listing.engine_type == 1 &&
|
|
listing.kind == 0, "%d of %d, engine %d, kind %d", listing.player_count, listing.maximum_player_count,
|
|
listing.engine_type, listing.kind);
|
|
CHECK(!strcmp(listing.name, "OpenCE Dedicated") && !strcmp(listing.map, "bloodgulch") &&
|
|
!strcmp(listing.gametype, "CTF"), "named %s, %s, %s", listing.name, listing.map, listing.gametype);
|
|
/* (the second: a password's game in progress, its token sealed, SWAT
|
|
with Hardcore) */
|
|
size = hex_decode(vector_listings[1].bytes, bytes, sizeof(bytes));
|
|
listing_read(bytes, size, &listing);
|
|
CHECK(listing.flags == (_listing_open | _listing_in_progress | _listing_password) && listing.kind == 0x81 &&
|
|
!memcmp(listing.sealed_token, bytes + 46, P2P_SEALED_TOKEN_SIZE) &&
|
|
listing.player_count == bytes[46 + P2P_SEALED_TOKEN_SIZE], "the password's game was read as flags %d, kind %d",
|
|
listing.flags, listing.kind);
|
|
/* (the third: a tombstone) */
|
|
size = hex_decode(vector_listings[2].bytes, bytes, sizeof(bytes));
|
|
listing_read(bytes, size, &listing);
|
|
CHECK((listing.flags & _listing_closed) && listing.sequence == 413, "the tombstone was read as flags %d, "
|
|
"sequence %lu", listing.flags, listing.sequence);
|
|
/* (a byte changed anywhere in the signed part, or the signature) */
|
|
size = hex_decode(vector_listings[0].bytes, bytes, sizeof(bytes));
|
|
for (index = 0; index < size; index += 7)
|
|
{
|
|
bytes[index] ^= 0x20;
|
|
CHECK(!listing_good(bytes, size), "the first listing with byte %d changed was taken", index);
|
|
bytes[index] ^= 0x20;
|
|
}
|
|
CHECK(listing_good(bytes, size), "the first listing was refused");
|
|
return 0;
|
|
}
|
|
/* a listing is 140 to 268 bytes, of format 1, its names within their
|
|
fields and its stamp and signature ending it exactly */
|
|
CASE("listing-bounds")
|
|
{
|
|
unsigned char bytes[600];
|
|
struct listing listing;
|
|
int size;
|
|
|
|
CHECK(MINIMUM_LISTING_SIZE == 140 && MAXIMUM_LISTING_SIZE == 268, "listings of %d to %d bytes",
|
|
MINIMUM_LISTING_SIZE, MAXIMUM_LISTING_SIZE);
|
|
size = make_listing(bytes, 1, _listing_open, 0, 0, 0);
|
|
CHECK(size == 140 && listing_good(bytes, size), "the shortest listing (%d bytes) was refused", size);
|
|
size = make_listing(bytes, 1, _listing_open | _listing_password, P2P_LISTING_NAME_SIZE, P2P_LISTING_MAP_SIZE,
|
|
P2P_LISTING_GAMETYPE_SIZE);
|
|
CHECK(size == 268 && listing_good(bytes, size), "the longest listing (%d bytes) was refused", size);
|
|
listing_read(bytes, size, &listing);
|
|
CHECK(strlen(listing.name) == 32 && strlen(listing.map) == 32 && strlen(listing.gametype) == 24 &&
|
|
listing.kind == 3 && listing.engine_type == 2, "the longest listing was read as %s / %s / %s",
|
|
listing.name, listing.map, listing.gametype);
|
|
CHECK(!listing_read(bytes, size - 1, &listing) && !listing_read(bytes, 139, &listing),
|
|
"a listing cut short was read");
|
|
size = make_listing(bytes, 1, _listing_open, 4, 4, 4);
|
|
bytes[size] = 0;
|
|
CHECK(!listing_read(bytes, size + 1, &listing), "a listing with a byte after its signature was read");
|
|
size = make_listing(bytes, 2, _listing_open, 4, 4, 4);
|
|
CHECK(!listing_read(bytes, size, &listing), "a listing of format 2 was read");
|
|
size = make_listing(bytes, 1, _listing_open, 4, 4, 4);
|
|
bytes[0] = 'h';
|
|
CHECK(!listing_read(bytes, size, &listing), "a listing without HL was read");
|
|
/* (a name past its field, which ends the listing exactly) */
|
|
size = make_listing(bytes, 1, _listing_open, P2P_LISTING_NAME_SIZE + 1, 0, 0);
|
|
CHECK(!listing_read(bytes, size, &listing), "a name of 33 was read");
|
|
size = make_listing(bytes, 1, _listing_open, 0, 0, P2P_LISTING_GAMETYPE_SIZE + 1);
|
|
CHECK(!listing_read(bytes, size, &listing), "a gametype of 25 was read");
|
|
/* (a length past the end) */
|
|
size = make_listing(bytes, 1, _listing_open, 0, 0, 0);
|
|
bytes[65] = 200;
|
|
CHECK(!listing_read(bytes, size, &listing), "a name's length past the end was read");
|
|
/* (a name's bytes kept to printable ASCII, a zero ending it) */
|
|
size = make_listing(bytes, 1, _listing_open, 6, 0, 0);
|
|
bytes[66] = 0x01;
|
|
bytes[67] = 0xC3;
|
|
bytes[69] = 0;
|
|
CHECK(listing_read(bytes, size, &listing) && !strcmp(listing.name, "??a"), "a name was read as %s", listing.name);
|
|
return 0;
|
|
}
|
|
/* a publish: its body (the listing, the tunnel's port, dedicated or not,
|
|
closed or not), its URL and the path signed under a master's base path;
|
|
a listing withdrawn once */
|
|
CASE("publish-body")
|
|
{
|
|
static const struct
|
|
{
|
|
unsigned short network_order;
|
|
const char *port;
|
|
} ports[] = { { 0xFE08, "2302" }, { 0x0100, "1" }, { 0xFFFF, "65535" }, { 0x0001, "256" } };
|
|
char base64[400];
|
|
int index;
|
|
|
|
start_publisher();
|
|
now = 1760900000;
|
|
host_vector_listing();
|
|
server_base64_encode(host_listing, host_listing_size, base64);
|
|
snprintf(master.masters[0].url, sizeof(master.masters[0].url), "https://m.example.org/opence");
|
|
tunnel_port = 0xFE08;
|
|
setting_region = "eu";
|
|
memset(&requested, 0, sizeof(requested));
|
|
publish_to(&master.masters[0], 0);
|
|
CHECK(requested.count == 1 && !strcmp(requested.method, "PUT") &&
|
|
!strcmp(requested.url, "https://m.example.org/opence/v1/servers/" VECTOR_SERVER_ID), "published %s %s",
|
|
requested.method, requested.url);
|
|
CHECK(strstr((const char *)signed_message, "\nPUT\n/opence/v1/servers/" VECTOR_SERVER_ID "\n") != NULL,
|
|
"signed %.*s", signed_size, (const char *)signed_message);
|
|
CHECK(!strncmp(requested.headers, "Authorization: OpenCE-Ed25519 keyid=" VECTOR_PUBLISHER_KEY_ID ", ", 52),
|
|
"the headers were %s", requested.headers);
|
|
CHECK(!strcmp(body_member(requested.body, "protocol"), "1") && !strcmp(body_member(requested.body, "instance"),
|
|
"main") && !strcmp(body_member(requested.body, "listing"), base64), "the body was %s", requested.body);
|
|
CHECK(!strcmp(body_member(requested.body, "tunnel_port"), "2302") &&
|
|
!strcmp(body_member(requested.body, "dedicated"), "true") &&
|
|
!strcmp(body_member(requested.body, "closed"), "false") &&
|
|
!strcmp(body_member(requested.body, "region"), "eu") &&
|
|
!strcmp(body_member(requested.body, "build"), "build-206") &&
|
|
!strcmp(body_member(requested.body, "address"), "(none)"), "the body was %s", requested.body);
|
|
CHECK(master.masters[0].published && master.masters[0].listed, "the master's listing was not kept");
|
|
/* (a master at its origin signs the path alone) */
|
|
snprintf(master.masters[0].url, sizeof(master.masters[0].url), "https://m.example.org");
|
|
publish_to(&master.masters[0], 0);
|
|
CHECK(!strcmp(requested.url, "https://m.example.org/v1/servers/" VECTOR_SERVER_ID) &&
|
|
strstr((const char *)signed_message, "\nPUT\n/v1/servers/" VECTOR_SERVER_ID "\n") != NULL,
|
|
"published %s, signed %.*s", requested.url, signed_size, (const char *)signed_message);
|
|
/* (the port as the socket has it, in network byte order) */
|
|
for (index = 0; index < (int)(sizeof(ports) / sizeof(ports[0])); index++)
|
|
{
|
|
tunnel_port = ports[index].network_order;
|
|
publish_to(&master.masters[0], 0);
|
|
CHECK(!strcmp(body_member(requested.body, "tunnel_port"), ports[index].port), "port %04x was sent as %s",
|
|
ports[index].network_order, body_member(requested.body, "tunnel_port"));
|
|
}
|
|
/* (a player's host's, and its public address when asked) */
|
|
server_mode = 0;
|
|
setting_publish_address = 1;
|
|
publish_to(&master.masters[0], 0);
|
|
CHECK(!strcmp(body_member(requested.body, "dedicated"), "false") &&
|
|
!strcmp(body_member(requested.body, "address"), "203.0.113.5:2302"), "the body was %s", requested.body);
|
|
server_mode = 1;
|
|
setting_publish_address = 0;
|
|
/* (a tombstone) */
|
|
publish_to(&master.masters[0], 1);
|
|
CHECK(!strcmp(body_member(requested.body, "closed"), "true") && !master.masters[0].published,
|
|
"a tombstone was %s", requested.body);
|
|
/* (no listing: nothing, unless one was published, which is withdrawn
|
|
once) */
|
|
host_listing_size = 0;
|
|
requested.count = 0;
|
|
publish_to(&master.masters[0], 0);
|
|
CHECK(!requested.count, "a publish was made with no listing to withdraw");
|
|
master.masters[0].published = 1;
|
|
publish_to(&master.masters[0], 0);
|
|
CHECK(requested.count == 1 && !strcmp(body_member(requested.body, "closed"), "true") &&
|
|
!strcmp(body_member(requested.body, "listing"), "(none)") && !master.masters[0].published,
|
|
"a listing was not withdrawn (%d: %s)", requested.count, requested.body);
|
|
publish_to(&master.masters[0], 0);
|
|
CHECK(requested.count == 1, "a listing was withdrawn twice");
|
|
return 0;
|
|
}
|
|
/* what a master's answers make the publisher do: wait as Retry-After
|
|
says (its name in any case), stop after a revocation, say how far off
|
|
the clock is */
|
|
CASE("publish-answers")
|
|
{
|
|
start_publisher();
|
|
now = 1760900000;
|
|
host_vector_listing();
|
|
tunnel_port = 0xFE08;
|
|
answer_status = 429;
|
|
answer_headers = "Content-Type: application/json\r\nretry-after: 120\r\n";
|
|
answer_body = "{\"ok\":false,\"error\":{\"code\":\"rate_limited\",\"message\":\"slow down\"}}";
|
|
memset(&requested, 0, sizeof(requested));
|
|
publish_to(&master.masters[0], 0);
|
|
CHECK(master.masters[0].retry_at == now + 120, "Retry-After: 120 waited until %lu", master.masters[0].retry_at);
|
|
now += 119;
|
|
publish_to(&master.masters[0], 0);
|
|
CHECK(requested.count == 1, "a publish was made before Retry-After");
|
|
now += 1;
|
|
answer_headers = "RETRY-AFTER: 999999999\r\n";
|
|
publish_to(&master.masters[0], 0);
|
|
CHECK(requested.count == 2 && master.masters[0].retry_at == now + 86400, "Retry-After past a day waited until %lu",
|
|
master.masters[0].retry_at);
|
|
now += 86400;
|
|
answer_headers = "";
|
|
publish_to(&master.masters[0], 0);
|
|
CHECK(master.masters[0].retry_at == now + 30, "no Retry-After waited until %lu", master.masters[0].retry_at);
|
|
now += 30;
|
|
/* (the clock: the difference, from the answer's master_time: this
|
|
machine's 450 seconds behind) */
|
|
answer_status = 401;
|
|
answer_body = "{\"ok\":false,\"error\":{\"code\":\"clock\",\"message\":\"ts\"},\"master_time\":1760987000}";
|
|
publish_to(&master.masters[0], 0);
|
|
CHECK(!strcmp(master.masters[0].code, "clock") && strstr(master.masters[0].message, "seconds behind") &&
|
|
strstr(master.masters[0].message, " 450 "), "the clock's answer was kept as %s: %s", master.masters[0].code,
|
|
master.masters[0].message);
|
|
/* (a revoked key: not published there again, nor withdrawn) */
|
|
answer_status = 403;
|
|
answer_body = "{\"ok\":false,\"error\":{\"code\":\"key_revoked\",\"message\":\"revoked\"}}";
|
|
master.masters[0].retry_at = 0;
|
|
publish_to(&master.masters[0], 0);
|
|
CHECK(master.masters[0].revoked && !master.masters[0].published, "a revoked key was not stopped");
|
|
requested.count = 0;
|
|
now += 3600;
|
|
publish_to(&master.masters[0], 0);
|
|
publish_to(&master.masters[0], 1);
|
|
CHECK(!requested.count, "a revoked key published again (%d)", requested.count);
|
|
answer_status = 200;
|
|
answer_headers = "";
|
|
answer_body = "{\"ok\":true,\"listed\":true,\"probe\":\"ok\"}";
|
|
return 0;
|
|
}
|
|
/* a list's entries: one that is not an entry is skipped, not the list;
|
|
the publisher's name and region kept to what the ticker shows; the
|
|
entry's word on a dedicated server passed on */
|
|
CASE("take-list")
|
|
{
|
|
char base64[400], body[8192];
|
|
unsigned char bytes[400], large[600];
|
|
char large_base64[900];
|
|
int size;
|
|
|
|
size = hex_decode(vector_listings[0].bytes, bytes, sizeof(bytes));
|
|
server_base64_encode(bytes, size, base64);
|
|
memset(large, 'x', sizeof(large));
|
|
server_base64_encode(large, (int)sizeof(large) - 150, large_base64);
|
|
snprintf(body, sizeof(body), "{\"protocol\":1,\"servers\":["
|
|
"{\"id\":\"a\",\"listing\":\"%s\",\"publisher\":{\"name\":\"Mil\\u0001enko's \\u00e9 servers, a name past "
|
|
"thirty-two\"},\"region\":\"eu west!\",\"network_version\":38,\"dedicated\":true},"
|
|
"{\"id\":\"b\",\"listing\":7},"
|
|
"{\"id\":\"c\"},"
|
|
"\"not an entry\","
|
|
"{\"id\":\"d\",\"listing\":\"not base64!\"},"
|
|
"{\"id\":\"e\",\"listing\":\"%s\"},"
|
|
"{\"id\":\"f\",\"listing\":\"%s\",\"region\":\"us-east-1\",\"dedicated\":\"yes\"}]}",
|
|
base64, large_base64, base64);
|
|
entries_count = 0;
|
|
CHECK(take_list(body, (unsigned long)strlen(body)) == 2 && entries_count == 2,
|
|
"%d entries taken of the 2 good ones", entries_count);
|
|
CHECK(entries_taken[0].size == size && entries_taken[0].dedicated == 1 && entries_taken[0].version == 38,
|
|
"the first entry was taken as %d bytes, dedicated %d, version %d", entries_taken[0].size,
|
|
entries_taken[0].dedicated, entries_taken[0].version);
|
|
CHECK(!strcmp(entries_taken[0].publisher, "Mil?enko's ?? servers, a name pa") &&
|
|
!strcmp(entries_taken[0].region, "eu?west?"), "the publisher %s, the region %s", entries_taken[0].publisher,
|
|
entries_taken[0].region);
|
|
CHECK(entries_taken[1].dedicated == 0 && !strcmp(entries_taken[1].region, "us-east-1") &&
|
|
!entries_taken[1].publisher[0], "the last entry was taken as dedicated %d, region %s",
|
|
entries_taken[1].dedicated, entries_taken[1].region);
|
|
entries_count = 0;
|
|
CHECK(!take_list("{\"servers\":7}", 13) && !take_list("not json", 8) && !entries_count,
|
|
"entries taken from what is no list");
|
|
return 0;
|
|
}
|
|
/* a list of 512 entries and more JSON tokens than a body of the API
|
|
may have: checked and taken, its first 512 entries */
|
|
CASE("list-large")
|
|
{
|
|
static char body[600 * 1024];
|
|
char base64[400];
|
|
unsigned char bytes[400];
|
|
int size, index, member, length;
|
|
|
|
size = hex_decode(vector_listings[0].bytes, bytes, sizeof(bytes));
|
|
server_base64_encode(bytes, size, base64);
|
|
length = snprintf(body, sizeof(body), "{\"protocol\":1,\"master\":{\"name\":\"m\",\"key_id\":\"%s\",\"time\":1760900000},"
|
|
"\"servers\":[", VECTOR_MASTER_KEY_ID);
|
|
for (index = 0; index < MAXIMUM_ENTRIES + 3; index++)
|
|
{
|
|
length += snprintf(body + length, sizeof(body) - (size_t)length, "%s{\"id\":\"s%d\",\"listing\":\"%s\"",
|
|
index ? "," : "", index, base64);
|
|
for (member = 0; member < 34; member++)
|
|
length += snprintf(body + length, sizeof(body) - (size_t)length, ",\"f%d\":%d", member, member);
|
|
length += snprintf(body + length, sizeof(body) - (size_t)length, "}");
|
|
}
|
|
length += snprintf(body + length, sizeof(body) - (size_t)length, "]}");
|
|
CHECK(length < (int)sizeof(body) - 1, "the test's list does not fit");
|
|
CHECK(verify_list(body, (unsigned long)length, master_signs(body), VECTOR_LIST_KEY_HEADER,
|
|
VECTOR_MASTER_PUBLIC_KEY, 1760900000), "a list of %d entries was refused", MAXIMUM_ENTRIES + 3);
|
|
entries_count = 0;
|
|
CHECK(take_list(body, (unsigned long)length) == MAXIMUM_ENTRIES && entries_count == MAXIMUM_ENTRIES,
|
|
"%d entries were taken of %d", entries_count, MAXIMUM_ENTRIES + 3);
|
|
return 0;
|
|
}
|
|
/* a nonce answered once a second, whatever came between: A, B, A in one
|
|
second answers A once; the remembered nonces are a ring */
|
|
CASE("probe-nonces")
|
|
{
|
|
unsigned char a[64], b[64], answer[128];
|
|
int size, index;
|
|
|
|
start_publisher();
|
|
now = 1760900100;
|
|
size = hex_decode(VECTOR_PROBE_PACKET, a, sizeof(a));
|
|
memcpy(b, a, (size_t)size);
|
|
b[34] ^= 0x55;
|
|
CHECK(probe_from(a, size, 0x01000001UL, answer), "nonce A was not answered");
|
|
CHECK(probe_from(b, size, 0x01000002UL, answer), "nonce B was not answered");
|
|
CHECK(!probe_from(a, size, 0x01000003UL, answer), "nonce A was answered again after B in one second");
|
|
now++;
|
|
CHECK(probe_from(a, size, 0x01000003UL, answer), "nonce A was not answered the next second");
|
|
/* (the ring: the oldest of REMEMBERED_NONCES + 1 is forgotten, the
|
|
newest are not) */
|
|
now++;
|
|
for (index = 0; index < REMEMBERED_NONCES; index++)
|
|
{
|
|
memcpy(b, a, (size_t)size);
|
|
b[19] = (unsigned char)index;
|
|
b[20] = 0xEE;
|
|
master.probe_answers = 0;
|
|
memset(master.source_seconds, 0, sizeof(master.source_seconds));
|
|
CHECK(probe_from(b, size, 0x02000000UL + (unsigned long)index, answer), "nonce %d was not answered", index);
|
|
}
|
|
b[19] = (unsigned char)(REMEMBERED_NONCES - 1);
|
|
master.probe_answers = 0;
|
|
CHECK(!probe_from(b, size, 0x03000000UL, answer), "the newest nonce was answered twice");
|
|
return 0;
|
|
}
|
|
/* two answers a second to an address, ten in all: probes forged from
|
|
one address do not use up the answers a master's probe needs; a nonce
|
|
resent counts against neither */
|
|
CASE("probe-sources")
|
|
{
|
|
unsigned char probe[64], answer[128];
|
|
int size, index, sources;
|
|
|
|
start_publisher();
|
|
now = 1760900200;
|
|
size = hex_decode(VECTOR_PROBE_PACKET, probe, sizeof(probe));
|
|
/* (one address flooding) */
|
|
for (index = 0; index < 20; index++)
|
|
{
|
|
probe[34] = (unsigned char)index;
|
|
CHECK(probe_from(probe, size, 0x0A0A0A0AUL, answer) == (index < PROBE_ANSWERS_PER_ADDRESS),
|
|
"probe %d from one address was %s", index + 1, index < PROBE_ANSWERS_PER_ADDRESS ? "refused" : "answered");
|
|
}
|
|
probe[34] = 0xF0;
|
|
CHECK(probe_from(probe, size, 0x0B0B0B0BUL, answer), "a master's probe was refused after another's flood");
|
|
CHECK(!probe_from(probe, size, 0x0C0C0C0CUL, answer), "a nonce resent was answered");
|
|
probe[34] = 0xF1;
|
|
CHECK(probe_from(probe, size, 0x0B0B0B0BUL, answer), "a nonce resent counted against its address");
|
|
/* (ten in all a second, whatever the addresses) */
|
|
now++;
|
|
for (index = 0, sources = 0; index < 20; index++)
|
|
{
|
|
probe[34] = (unsigned char)(0x40 + index);
|
|
sources += probe_from(probe, size, 0x0D000000UL + (unsigned long)index, answer);
|
|
}
|
|
CHECK(sources == PROBE_ANSWERS_PER_SECOND, "%d probes of 20 addresses were answered in a second", sources);
|
|
/* (the next second, an address's allowance again) */
|
|
now++;
|
|
probe[34] = 0x7F;
|
|
CHECK(probe_from(probe, size, 0x0A0A0A0AUL, answer), "a flooding address was not answered the next second");
|
|
return 0;
|
|
}
|
|
fprintf(stderr, "unknown case: %s\n", case_name);
|
|
return 2;
|
|
}
|