Files
scriptc/packages/runtime/test/test_cipher.c
T
Chris Tate 10539c9c42 Fail closed on cipher backend errors and on missing chacha20-poly1305 tags
- Every mbedTLS call in the cipher runtime is checked. A failed update(),
  final() or setAAD() returns no output and retires the context, so later
  calls throw and no tag is returned or accepted. An update past the
  AES-GCM limit of 2^36 - 32 bytes per message throws Node's "Trying to
  add data in unsupported state" error; it used to return zeroed output
  and leave the tag covering only the earlier data.
- chacha20-poly1305 decryption without setAuthTag() fails in final()
  again. Node 24.22 skips the tag check when authTagLength is not passed
  either and returns unverified plaintext; limitations.mdx records the
  difference.
- A runtime unit test moves contexts next to the GCM message and AAD
  limits and checks that the failure is permanent; errors.test.ts covers
  decryption without a tag.
2026-10-15 00:06:26 -05:00

217 lines
8.1 KiB
C

/* Unit tests for the createCipheriv/createDecipheriv runtime
* (scr_cipher.c), built with ASan + -DSCR_RC_AUDIT by cipher.test.ts. The
* unit is included directly so a test can move a context next to a
* backend limit (AES-GCM's 2^36 - 32 byte message limit, the 2^61 byte AAD
* limit) without processing that much data, then check that the failure
* returns no output and retires the context: every later update(),
* final(), setAAD(), setAuthTag() and getAuthTag() throws. */
#include "../src/scr_cipher.c"
#include <stdio.h>
#include <string.h>
static long total = 0, failed = 0;
static void check(bool ok, const char *what) {
total++;
if (!ok) {
failed++;
fprintf(stderr, "FAIL: %s\n", what);
}
}
/* GCM's limit on encrypted bytes per message (NIST SP 800-38D). */
static const uint64_t GCM_MAX = 0xFFFFFFFE0ull;
static ScrDyn *str(const char *text) {
ScrStr *s = scr_str_new(text, strlen(text));
ScrDyn *d = scr_dyn_new_str(s);
scr_str_release(s);
return d;
}
static ScrDyn *buffer(size_t len, unsigned char fill) {
ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)len);
b->is_buffer = true;
memset(b->data, fill, len);
ScrDyn *d = scr_dyn_new_buffer(b);
scr_bytes_release(b);
return d;
}
/* The pending exception's "Name: message" (and code), cleared. */
static bool take_error(const char *want, const char *want_code) {
if (!scr_exc_pending()) return false;
ScrCaught *caught = scr_exc_take();
ScrError *error = caught->payload;
ScrStr *name = scr_error_default_name(error);
char text[256];
snprintf(text, sizeof text, "%.*s: %.*s", (int)name->len, name->data, (int)error->message->len,
error->message->data);
scr_str_release(name);
bool ok = strcmp(text, want) == 0;
if (!ok) fprintf(stderr, " got \"%s\" want \"%s\"\n", text, want);
char code[64] = "(none)";
if (error->code) snprintf(code, sizeof code, "%.*s", (int)error->code->len, error->code->data);
if (strcmp(code, want_code ? want_code : "(none)") != 0) {
fprintf(stderr, " got code %s want %s\n", code, want_code ? want_code : "(none)");
ok = false;
}
scr_caught_release(caught);
return ok;
}
static ScrDyn *call(ScrDyn *self, const char *method, ScrDyn *a, ScrDyn *b) {
ScrDyn *args[2] = {a ? a : scr_dyn_undefined(), b ? b : scr_dyn_undefined()};
return sc_invoke(self->v.handle.ptr, self, method, args, 2, "test");
}
static ScrDyn *create(bool decrypt, const char *name, size_t key_len, size_t iv_len) {
ScrDyn *n = str(name), *key = buffer(key_len, 1), *iv = buffer(iv_len, 2);
ScrDyn *h = scr_crypto_cipher_new(decrypt, n, key, iv, scr_dyn_undefined());
scr_dyn_release(n);
scr_dyn_release(key);
scr_dyn_release(iv);
return h;
}
static ScCipher *ctx(ScrDyn *h) { return h->v.handle.ptr; }
static const char *const UPDATE_STATE = "Error: Trying to add data in unsupported state";
/* After a failure: every method throws, and no data or tag comes back. */
static void check_retired(ScrDyn *h, bool decrypt, const char *label) {
char what[128];
ScrDyn *data = buffer(16, 7), *empty = buffer(0, 0), *tag = buffer(16, 0);
ScrDyn *r = call(h, "update", data, NULL);
snprintf(what, sizeof what, "%s: update() throws", label);
check(!r && take_error(UPDATE_STATE, NULL), what);
r = call(h, "update", empty, NULL);
snprintf(what, sizeof what, "%s: empty update() throws", label);
check(!r && take_error(UPDATE_STATE, NULL), what);
r = call(h, "setAAD", data, NULL);
snprintf(what, sizeof what, "%s: setAAD() throws", label);
check(!r && take_error("Error: Invalid state for operation setAAD", "ERR_CRYPTO_INVALID_STATE"), what);
if (decrypt) {
r = call(h, "setAuthTag", tag, NULL);
snprintf(what, sizeof what, "%s: setAuthTag() throws", label);
check(!r && take_error("Error: Invalid state for operation setAuthTag", "ERR_CRYPTO_INVALID_STATE"), what);
}
r = call(h, "final", NULL, NULL);
snprintf(what, sizeof what, "%s: final() throws", label);
check(!r && take_error("Error: Invalid state", "ERR_CRYPTO_INVALID_STATE"), what);
if (!decrypt) {
r = call(h, "getAuthTag", NULL, NULL);
snprintf(what, sizeof what, "%s: getAuthTag() throws", label);
check(!r && take_error("Error: Invalid state for operation getAuthTag", "ERR_CRYPTO_INVALID_STATE"), what);
}
scr_dyn_release(data);
scr_dyn_release(empty);
scr_dyn_release(tag);
}
/* An update that crosses the GCM message limit throws Node's update error
* (as Node 24.22 does for the same input) and returns no output. */
static void test_gcm_message_limit(bool decrypt) {
const char *label = decrypt ? "gcm decipher limit" : "gcm cipher limit";
char what[128];
ScrDyn *h = create(decrypt, "aes-256-gcm", 32, 12);
check(h != NULL, "create aes-256-gcm");
if (!h) return;
if (decrypt) {
ScrDyn *tag = buffer(16, 0);
scr_dyn_release(call(h, "setAuthTag", tag, NULL));
scr_dyn_release(tag);
}
/* As if GCM_MAX - 32 bytes had been processed. */
ctx(h)->gcm.MBEDTLS_PRIVATE(len) = GCM_MAX - 32;
ctx(h)->data_started = true;
ScrDyn *block = buffer(16, 9);
ScrDyn *r = call(h, "update", block, NULL);
snprintf(what, sizeof what, "%s: update up to the limit", label);
ScrBytes *out = r ? scr_bytes_buffer_source(r) : NULL;
check(out && out->len == 16, what);
scr_bytes_release(out);
scr_dyn_release(r);
r = call(h, "update", block, NULL);
snprintf(what, sizeof what, "%s: update at the limit", label);
check(r != NULL, what);
scr_dyn_release(r);
ScrDyn *one = buffer(1, 9);
r = call(h, "update", one, NULL);
snprintf(what, sizeof what, "%s: update past the limit throws", label);
check(!r && take_error(UPDATE_STATE, NULL), what);
check_retired(h, decrypt, label);
scr_dyn_release(one);
scr_dyn_release(block);
scr_dyn_release(h);
}
/* A setAAD() the backend refuses retires the context as well. */
static void test_gcm_aad_limit(void) {
ScrDyn *h = create(false, "aes-128-gcm", 16, 12);
check(h != NULL, "create aes-128-gcm");
if (!h) return;
ctx(h)->gcm.MBEDTLS_PRIVATE(add_len) = (1ull << 61) - 8;
ScrDyn *aad = buffer(16, 3);
ScrDyn *r = call(h, "setAAD", aad, NULL);
check(!r && take_error("Error: Invalid state for operation setAAD", "ERR_CRYPTO_INVALID_STATE"),
"gcm aad limit: setAAD() throws");
check_retired(h, false, "gcm aad limit");
scr_dyn_release(aad);
scr_dyn_release(h);
}
/* Without setAuthTag(), chacha20-poly1305 decryption fails in final(),
* with or without authTagLength. */
static void test_chacha_without_tag(void) {
ScrDyn *h = create(true, "chacha20-poly1305", 32, 12);
check(h != NULL, "create chacha20-poly1305");
if (!h) return;
ScrDyn *data = buffer(40, 5);
scr_dyn_release(call(h, "update", data, NULL));
ScrDyn *r = call(h, "final", NULL, NULL);
check(!r && take_error("Error: Unsupported state or unable to authenticate data", NULL),
"chacha20-poly1305 without a tag: final() throws");
scr_dyn_release(data);
scr_dyn_release(h);
}
/* The same contexts still work when they stay inside the limits. */
static void test_gcm_round_trip(void) {
ScrDyn *e = create(false, "aes-128-gcm", 16, 12);
ScrDyn *data = buffer(100, 4);
ScrDyn *ct = call(e, "update", data, NULL);
scr_dyn_release(call(e, "final", NULL, NULL));
ScrDyn *tag = call(e, "getAuthTag", NULL, NULL);
check(ct && tag, "gcm round trip: encrypt");
ScrDyn *d = create(true, "aes-128-gcm", 16, 12);
scr_dyn_release(call(d, "setAuthTag", tag, NULL));
ScrDyn *pt = call(d, "update", ct, NULL);
ScrDyn *fin = call(d, "final", NULL, NULL);
check(pt && fin && !scr_exc_pending(), "gcm round trip: decrypt");
if (pt) {
ScrBytes *a = scr_bytes_buffer_source(pt), *b = scr_bytes_buffer_source(data);
check(a->len == b->len && memcmp(a->data, b->data, a->len) == 0, "gcm round trip: plaintext");
scr_bytes_release(a);
scr_bytes_release(b);
}
scr_dyn_release(fin);
scr_dyn_release(pt);
scr_dyn_release(d);
scr_dyn_release(tag);
scr_dyn_release(ct);
scr_dyn_release(data);
scr_dyn_release(e);
}
int main(void) {
test_gcm_message_limit(false);
test_gcm_message_limit(true);
test_gcm_aad_limit();
test_chacha_without_tag();
test_gcm_round_trip();
fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total);
return failed == 0 ? 0 : 1;
}