Merge main

This commit is contained in:
Chris Tate
2026-10-22 06:59:47 -05:00
274 changed files with 19956 additions and 1001 deletions
+91 -39
View File
@@ -623,6 +623,10 @@ struct ScrFiber {
* awaiter destroys the record when it takes the result. */
bool direct;
struct ScrFiber *direct_awaiter;
/* While an async body runs its synchronous prefix (until its first await
* or its end), the AsyncLocalStorage slot of the execution that spawned
* it; NULL afterwards (scr_als_prefix_end). */
ScrAlsCtx **prefix_slot;
#ifdef SCR_ASAN_FIBERS
void *fake_stack;
/* Live stackless async fibers (scr_async_spawn_stackless) form a list
@@ -670,6 +674,61 @@ void scr_als_ctx_release(ScrAlsCtx *c) {
free(c);
}
/* enterWith on the main stack lasts until that code returns to the loop,
* so the store does not reach callbacks scheduled without it: the loop
* clears the main slot when the main module's body returns, and an
* enterWith inside a callback the loop runs puts a tick that clears it at
* the front of the tick queue, so it runs as soon as that callback
* returns. A store that an async callee's prefix or a generator step
* enters becomes the caller's at scr_als_prefix_end and is cleared the
* same way. */
static SCR_TL bool scr_loop_started = false;
static void scr_als_main_clear(void) {
scr_als_ctx_release(scr_als_main_slot);
scr_als_main_slot = NULL;
}
static void scr_next_tick_raw_first(void (*fn)(void));
/* A new store on the main slot: one a loop callback enters ends with it. */
static void scr_als_main_entered(void) {
if (scr_loop_started) scr_next_tick_raw_first(scr_als_main_clear);
}
void scr_als_entered_with(void) {
if (SCR_ALS_SLOT() == &scr_als_main_slot) scr_als_main_entered();
}
/* The end of an async body's synchronous prefix: its first await, or its
* end. Node runs that prefix inside the caller's execution, so a store it
* entered (enterWith) is still the caller's store after the call; the body
* keeps its own copy of the context for what runs after its await. */
static void scr_als_prefix_end(ScrFiber *f) {
ScrAlsCtx **slot = f->prefix_slot;
if (slot == NULL) return;
f->prefix_slot = NULL;
if (*slot != f->als) {
scr_als_ctx_release(*slot);
*slot = scr_als_ctx_retain(f->als);
if (f->als != NULL && slot == &scr_als_main_slot) scr_als_main_entered();
}
}
/* A synchronous generator has no context of its own: each step runs in its
* resumer's execution, as Node runs it. A fiber-backed one switches to its
* own slot, so its step starts from the resumer's store and a store it
* enters is the resumer's after the step (scr_als_prefix_end on the way
* back); a stackless one runs in the resumer's slot (scr_gen_coro_hop). */
static void scr_als_step_begin(ScrFiber *f) {
ScrAlsCtx **slot = SCR_ALS_SLOT();
if (f->als != *slot) {
scr_als_ctx_release(f->als);
f->als = scr_als_ctx_retain(*slot);
}
f->prefix_slot = slot;
}
/* The lifetime region a new async fiber carries (ScrFiber.region). */
static inline void *scr_fiber_region_capture(void) {
const ScrRgnFiberHooks *regions = atomic_load_explicit(&scr_region_fiber_hooks, memory_order_relaxed);
@@ -726,26 +785,6 @@ ScrDyn *scr_als_peek(double id) {
return NULL;
}
/* enterWith on the main stack lasts until that code returns to the loop,
* so the store does not reach callbacks scheduled without it: the loop
* clears the main slot when the main module's body returns, and an
* enterWith inside a callback the loop runs puts a tick that clears it at
* the front of the tick queue, so it runs as soon as that callback
* returns. */
static SCR_TL bool scr_loop_started = false;
static void scr_als_main_clear(void) {
scr_als_ctx_release(scr_als_main_slot);
scr_als_main_slot = NULL;
}
static void scr_next_tick_raw_first(void (*fn)(void));
void scr_als_entered_with(void) {
if (scr_loop_started && SCR_ALS_SLOT() == &scr_als_main_slot)
scr_next_tick_raw_first(scr_als_main_clear);
}
static SCR_TL ScrCtx scr_loop_ctx; /* the main stack (scheduler home) */
static SCR_TL ScrFiber *scr_current = NULL;
/* A finished fiber whose last switch went to the loop (scr_resume_fiber). */
@@ -1889,6 +1928,7 @@ static void scr_trampoline(void) {
ScrFiber *self = scr_current;
self->entry(self, self->argpack);
scr_fiber_finish(self);
scr_als_prefix_end(self);
/* The loop reaps the fiber that comes back to it finished, which need
* not be the one it resumed: a fiber that continued in place for the
* awaiters it parked (scr_await_yield) returns to the loop too. */
@@ -2010,6 +2050,7 @@ static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void
/* AsyncLocalStorage: the child runs in the SPAWNER's context (Node's
* init-time capture); snapshots are immutable, so a retain suffices. */
f->als = scr_als_ctx_retain(*SCR_ALS_SLOT());
f->prefix_slot = SCR_ALS_SLOT();
f->region = scr_fiber_region_capture();
scr_fibers_live++;
@@ -2023,13 +2064,15 @@ static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void
#elif defined(__wasi__)
(void)awaiter;
ScrFiber *spawner = scr_current;
ScrAlsCtx **spawner_slot = scr_als_active;
scr_current = f;
scr_exc_swap_cell(&f->exc);
scr_als_active = &f->als;
entry(f, argpack); /* eager prefix; returns at suspend/final suspend */
scr_als_prefix_end(f);
scr_current = spawner;
scr_exc_swap_cell(spawner ? &spawner->exc : NULL);
scr_als_active = spawner ? &spawner->als : &scr_als_main_slot;
scr_als_active = spawner_slot;
ScrPromise *result = scr_promise_retain(f->promise);
if (f->done) {
scr_promise_release(f->promise);
@@ -2057,6 +2100,7 @@ static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void
#endif
f->return_to = home;
ScrFiber *spawner = scr_current;
ScrAlsCtx **spawner_slot = scr_als_active;
scr_switch(home, &f->ctx, f);
/* back: the fiber suspended or finished. The switch back targeted NULL
* (the child doesn't know who spawned it), which pointed both the current
@@ -2066,7 +2110,7 @@ static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void
* `undefined` rejection while the real payload leaks). */
scr_current = spawner;
scr_exc_swap_cell(spawner ? &spawner->exc : NULL);
scr_als_active = spawner ? &spawner->als : &scr_als_main_slot;
scr_als_active = spawner_slot;
if (awaited != NULL) {
/* Parked by the child: the loop owns (and may have reaped) it. */
if (awaiter->await_parked) return awaited;
@@ -2172,6 +2216,7 @@ static ScrFiber *scr_spawn_stackless_fiber(void (*entry)(ScrFiber *, void *), vo
scr_fiber_track(f);
f->stackless = true;
f->als = scr_als_ctx_retain(*SCR_ALS_SLOT());
f->prefix_slot = SCR_ALS_SLOT();
/* The lifetime region the eager prefix runs in, which every later step
* resumes in (scr_resume_fiber), as a fiber's body does. */
f->region = scr_fiber_region_capture();
@@ -2183,6 +2228,7 @@ static ScrFiber *scr_spawn_stackless_fiber(void (*entry)(ScrFiber *, void *), vo
scr_stackless_fibers = f;
#endif
ScrFiber *spawner = scr_current;
ScrAlsCtx **spawner_slot = scr_als_active;
#ifndef _WIN32
uintptr_t guard = scr_stack_guard;
if (guard != 0 && (uintptr_t)__builtin_frame_address(0) < guard + SCR_STACKLESS_PREFIX_ROOM) {
@@ -2195,9 +2241,10 @@ static ScrFiber *scr_spawn_stackless_fiber(void (*entry)(ScrFiber *, void *), vo
scr_als_active = &f->als;
entry(f, argpack); /* the eager prefix: returns at a suspension or the end */
}
scr_als_prefix_end(f);
scr_current = spawner;
scr_exc_swap_cell(spawner ? &spawner->exc : NULL);
scr_als_active = spawner ? &spawner->als : &scr_als_main_slot;
scr_als_active = spawner_slot;
return f;
}
@@ -2303,8 +2350,10 @@ void *scr_async_spawn_await_ref(void (*entry)(ScrFiber *, void *), void *argpack
* settled promise is created and settled in the same order, rejections
* enter the unhandled ledger through the same settle path, and the frame
* isolates what a fresh fiber would — the Error.stack frame chain
* (cell->stack) and the AsyncLocalStorage context (enterWith inside the
* body stays inside it). The caller's exception cell is clean at any call
* (cell->stack). The AsyncLocalStorage context is the caller's throughout:
* the whole body is the synchronous prefix Node runs in the caller's
* execution, so a store it enters stays entered after the call
* (scr_als_prefix_end). The caller's exception cell is clean at any call
* (try/finally stashes in-flight exceptions), so a pending exception
* after the body is the body's own throw. */
static void scr_inline_park_trap(void) {
@@ -2321,21 +2370,12 @@ void scr_async_inline_enter(ScrAsyncInline *frame) {
ScrExcCell *cell = scr_exc_current_cell();
frame->stack = cell->stack;
cell->stack = NULL;
frame->als = scr_als_ctx_retain(*SCR_ALS_SLOT());
if (scr_current != NULL) scr_current->inline_depth++;
}
void scr_async_inline_exit(ScrAsyncInline *frame) {
if (scr_current != NULL) scr_current->inline_depth--;
scr_exc_current_cell()->stack = (ScrStackFrame *)frame->stack;
ScrAlsCtx *saved = (ScrAlsCtx *)frame->als;
ScrAlsCtx **slot = SCR_ALS_SLOT();
if (*slot != saved) {
scr_als_ctx_release(*slot);
*slot = saved; /* the frame's reference moves back in */
} else {
scr_als_ctx_release(saved);
}
}
ScrPromise *scr_async_inline_leave(ScrAsyncInline *frame) {
@@ -2379,11 +2419,14 @@ ScrPromise *scr_async_spawn_after(ScrPromise *dependency,
* switch to its own resumer: park it here, for each such awaiter up the
* chain, and switch there directly. Nothing runs in between either way. */
static ScrCtx *scr_fiber_park_awaiters(ScrFiber *self) {
scr_als_prefix_end(self);
ScrCtx *to = self->return_to;
#ifdef SCR_SPAWN_AWAIT
for (ScrFiber *cur = self; cur->awaiter != NULL;) {
ScrFiber *aw = cur->awaiter;
cur->awaiter = NULL;
/* The awaiter's await is its own suspension: its prefix ends here. */
scr_als_prefix_end(aw);
scr_promise_add_waiter(cur->promise, aw);
aw->await_parked = true;
to = aw->return_to;
@@ -5675,9 +5718,13 @@ __attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bo
if (retain) scr_gen_retain(g);
g->state = SCR_GEN_RUNNING;
f->resumer = scr_current;
ScrAlsCtx **resumer_slot = scr_als_active;
scr_current = f;
scr_exc_swap_cell(&f->exc);
scr_als_active = &f->als;
/* A synchronous generator has no AsyncLocalStorage context of its own:
* its step runs in the resumer's slot, as Node runs it in the execution
* that called next(). An async generator's step runs in its own. */
if (g->is_async) scr_als_active = &f->als;
if (f->coro == NULL) f->entry(f, f->argpack);
else scr_coro_resume(f->coro);
/* A running generator cannot be resumed again, so neither its fiber nor
@@ -5686,7 +5733,7 @@ __attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bo
ScrFiber *me = f->resumer;
scr_current = me;
scr_exc_swap_cell(me != NULL ? &me->exc : NULL);
scr_als_active = me != NULL ? &me->als : &scr_als_main_slot;
scr_als_active = resumer_slot;
scr_gen_switched_back(g, f, retain, true);
}
@@ -5708,6 +5755,7 @@ static void scr_gen_switch_in(ScrGen *g) {
#endif
scr_gen_retain(g);
g->state = SCR_GEN_RUNNING;
if (!g->is_async) scr_als_step_begin(f);
#ifdef _WIN32
ScrCtx here = scr_win_self();
#elif defined(__wasi__)
@@ -5717,6 +5765,7 @@ static void scr_gen_switch_in(ScrGen *g) {
#endif
f->return_to = &here;
ScrFiber *me = scr_current;
ScrAlsCtx **resumer_slot = scr_als_active;
#ifdef __wasi__
scr_current = f;
scr_exc_swap_cell(&f->exc);
@@ -5725,14 +5774,17 @@ static void scr_gen_switch_in(ScrGen *g) {
else scr_wasi_coro_resume(f->coro);
scr_current = me;
scr_exc_swap_cell(me != NULL ? &me->exc : NULL);
scr_als_active = me != NULL ? &me->als : &scr_als_main_slot;
scr_als_active = resumer_slot;
#else
scr_switch(&here, &f->ctx, f);
/* Back on the consumer: restore identity + the consumer's cell (the
* yield/finish switch targeted NULL — main's cell). */
/* Back on the consumer: restore identity, the consumer's cell and its
* AsyncLocalStorage slot (the yield/finish switch targeted NULL — main's
* cell and slot). */
scr_current = me;
scr_exc_swap_cell(me != NULL ? &me->exc : NULL);
scr_als_active = resumer_slot;
#endif
scr_als_prefix_end(f);
scr_gen_switched_back(g, f, true, false);
}
+6 -4
View File
@@ -62,12 +62,14 @@ ScrPromise *scr_async_spawn_after(ScrPromise *dependency,
/* Fiberless async call (native targets): the compiler proves an async body
* can never park its execution context and runs it directly on the
* caller's stack between enter and leave. The frame saves what a fresh
* fiber would isolate — the Error.stack frame chain and the
* AsyncLocalStorage context — and leave restores both, returning a fresh
* pending promise (+1) the caller settles from the body's outcome. */
* fiber would isolate, the Error.stack frame chain, and leave restores it,
* returning a fresh pending promise (+1) the caller settles from the body's
* outcome. The body runs in the caller's AsyncLocalStorage context, as
* Node runs an async body's synchronous prefix. The second word is unused;
* emitted frames keep its size. */
typedef struct ScrAsyncInline {
void *stack;
void *als;
void *reserved;
} ScrAsyncInline;
void scr_async_inline_enter(ScrAsyncInline *frame);
ScrPromise *scr_async_inline_leave(ScrAsyncInline *frame);
+1 -1
View File
@@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) {
}
#endif
void scr_runtime_abi_v25(void) {}
void scr_runtime_abi_v26(void) {}
void scr_init(void) {
#ifdef SCR_WORKERS
+1 -1
View File
@@ -81,7 +81,7 @@ void scr_init(void);
/* Program objects emitted by the bundled LLVM helper reference this symbol.
* Its versioned spelling makes a mismatched manual runtime link fail before
* the program can start. */
void scr_runtime_abi_v25(void);
void scr_runtime_abi_v26(void);
void scr_runtime_workers_v8(void);
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
+16
View File
@@ -85,6 +85,10 @@ ScrDyn *scr_crypto_create_secret_key(ScrDyn *key, ScrDyn *encoding);
ScrDyn *scr_crypto_generate_key_sync(ScrDyn *type, ScrDyn *options);
ScrBytes *scr_crypto_key_object_input(ScrDyn *key);
bool scr_crypto_is_key_object(const ScrDyn *value);
/* Throws Node's ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE for a private or public
* KeyObject used as a secret key. scr_pki.c, which makes those keys,
* installs it with them, so its messages stay out of other programs. */
extern void (*scr_crypto_reject_asymmetric_secret)(const ScrDyn *key);
const unsigned char *scr_key_object_bytes(const ScrDyn *value, size_t *len);
/* crypto.createCipheriv (decrypt false) and crypto.createDecipheriv
* (decrypt true) over checked-dynamic arguments (scr_cipher.c, linked
@@ -268,5 +272,17 @@ ScrStr *scr_crypto_x509_valid_from(ScrBytes *data);
ScrStr *scr_crypto_x509_valid_from_str(ScrStr *pem);
ScrStr *scr_crypto_x509_valid_to(ScrBytes *data);
ScrStr *scr_crypto_x509_valid_to_str(ScrStr *pem);
/* Asymmetric keys and signatures (scr_pki.c, the pki unit). Arguments
* borrowed; results +1, or NULL with the error pending. */
ScrDyn *scr_crypto_create_private_key(ScrDyn *key);
ScrDyn *scr_crypto_create_public_key(ScrDyn *key);
ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options);
void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *callback);
ScrDyn *scr_crypto_sign_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key);
ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, ScrDyn *signature);
ScrDyn *scr_crypto_create_sign(ScrDyn *algorithm);
ScrDyn *scr_crypto_create_verify(ScrDyn *algorithm);
bool scr_crypto_is_asymmetric_key(const ScrDyn *value);
const char *scr_crypto_asymmetric_key_type(const ScrDyn *value);
#endif /* SCR_CRYPTO_H */
+8
View File
@@ -158,6 +158,11 @@ typedef enum {
X(DIR, 44) /* fs.Dir (scr_lib.c) */ \
X(DISPOSABLE_STACK, 49) /* DisposableStack (scr_symbol.c) */ \
X(ECDH, 64) /* node:crypto ECDH (scr_ecdh.c) */ \
X(PRIVATE_KEY, 65) /* node:crypto PrivateKeyObject (scr_pki.c) */ \
X(PUBLIC_KEY, 66) /* node:crypto PublicKeyObject (scr_pki.c) */ \
X(SIGN, 67) /* node:crypto Sign (scr_pki.c) */ \
X(VERIFY, 68) /* node:crypto Verify (scr_pki.c) */ \
X(ASYMMETRIC_CRYPTO_KEY, 69) /* WebCrypto ECDSA and RSA CryptoKey (scr_webcrypto_pki.c) */ \
X(EVENT, 14) /* ScrEvent — new Event() and AbortSignal's abort events */ \
X(EVENT_TARGET, 39) /* ScrEventTarget — new EventTarget() */ \
X(FETCH_HEADERS, 13) /* native static-fetch response Headers */ \
@@ -759,6 +764,9 @@ ScrDyn *scr_dyn_array_constructor(void);
/* The interned builtin constructor `name` (Object, Date, RegExp, Map, Set,
* Promise, WeakMap, WeakSet) as a dynamic function value: +1. */
ScrDyn *scr_dyn_global_constructor(const ScrStr *name);
/* The interned function value of an error constructor ("TypeError"), or
* NULL for other names. */
ScrDyn *scr_dyn_error_constructor(const char *name);
/* A builtin global function (setTimeout, setImmediate, process.nextTick, ...)
* as an interned dynamic function value: +1. */
ScrDyn *scr_dyn_builtin_function(const ScrStr *name);
+34 -24
View File
@@ -34,8 +34,7 @@
* receiver without a reference; the hook closures outlive the record.
*
* Not supported here (each throws the "not supported yet" Error where it
* runs): objectMode and the other options scr_stream.c's dyn option walk
* refuses, `_writev` and `_construct`, symbol event names, listeners() /
* runs): the options scr_stream.c's dyn option walk refuses, `_writev` and `_construct`, symbol event names, listeners() /
* rawListeners(), `wrap()`, and pipes into values that are neither runtime
* streams nor constructor-function streams. */
#include "scr_runtime.h"
@@ -275,11 +274,10 @@ static ScrDyn *bcs_done_call(ScrClosure *closure, ScrDyn *const *args, size_t ar
break;
}
default: {
/* the data is pushed as push(data) pushes it */
ScrDyn *data = bcs_arg(args, argc, 1);
ScrBytes *bytes = data->kind == SCR_DYN_BYTES ? scr_bytes_raw_view(data->v.bytes) : NULL;
ScrStr *text = data->kind == SCR_DYN_STR ? scr_str_retain(data->v.str) : NULL;
if (which == BCS_DONE_TRANSFORM) scr_stream_transform_done(s, err, bytes, text);
else scr_stream_flush_done(s, err, bytes, text);
if (which == BCS_DONE_TRANSFORM) scr_stream_transform_done_value(s, err, data);
else scr_stream_flush_done_value(s, err, data);
break;
}
}
@@ -362,13 +360,14 @@ static void bcs_read_inv(ScrClosure *cb, ScrStream *s, double size) {
scr_dyn_release(object);
}
/* _write(chunk, 'buffer', cb) and _transform(chunk, 'buffer', cb): a
* throw propagates to the write() caller, as in Node. */
/* _write(chunk, 'buffer', cb) and _transform(chunk, 'buffer', cb), or the
* value and the write's encoding argument on an object-mode writable
* side: a throw propagates to the write() caller, as in Node. */
static void bcs_chunk_call(ScrClosure *cb, ScrStream *s, ScrBytes *chunk, const char *method, int done) {
ScrDyn *object = scr_box_get_ref(cb->caps[0]);
ScrDyn *args[3];
args[0] = scr_dyn_new_buffer(chunk);
args[1] = bcs_str("buffer");
args[0] = scr_stream_chunk_value(s, chunk);
args[1] = scr_stream_chunk_encoding_value(s);
args[2] = bcs_done(s, done);
ScrDyn *result = bcs_invoke(object, method, args, 3);
for (int i = 0; i < 3; i++) scr_dyn_release(args[i]);
@@ -488,8 +487,8 @@ static void bcs_listener0(ScrClosure *adapter) {
bcs_listener_call(adapter, NULL, 0);
}
static void bcs_listener_data(ScrClosure *adapter, ScrBytes *bytes, ScrStr *text) {
ScrDyn *arg = text ? scr_dyn_new_str(text) : scr_dyn_new_buffer(bytes);
static void bcs_listener_data(ScrClosure *adapter, ScrBytes *bytes, ScrStr *text, ScrDyn *value) {
ScrDyn *arg = value ? scr_dyn_retain(value) : text ? scr_dyn_new_str(text) : scr_dyn_new_buffer(bytes);
bcs_listener_call(adapter, &arg, 1);
scr_dyn_release(arg);
}
@@ -583,7 +582,7 @@ static bool bcs_add_listener(ScrStream *s, ScrDyn *self, ScrDyn *type, ScrDyn *l
break;
case 1:
chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_data),
scr_ee_inv_fixed2, once, prepend);
scr_ee_inv_fixed3, once, prepend);
break;
case 2:
chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_error),
@@ -625,14 +624,15 @@ static ScrDyn *bcs_emit(ScrStream *s, ScrDyn *const *args, size_t argc) {
had = scr_emitter_emit(em, name);
break;
case 1:
/* 'data' carries the bytes, string and value slots (scr_stream_emit_data) */
if (first->kind == SCR_DYN_BYTES) {
ScrBytes *bytes = scr_bytes_raw_view(first->v.bytes);
had = scr_emitter_emit(em, name, bytes, (ScrStr *)NULL);
had = scr_emitter_emit(em, name, bytes, (ScrStr *)NULL, first);
scr_bytes_release(bytes);
} else if (first->kind == SCR_DYN_STR) {
had = scr_emitter_emit(em, name, (ScrBytes *)NULL, first->v.str);
had = scr_emitter_emit(em, name, (ScrBytes *)NULL, first->v.str, first);
} else {
bcs_unsupported("emit('data') with a chunk other than a Buffer or string on a constructor-function stream is not supported yet");
had = scr_emitter_emit(em, name, (ScrBytes *)NULL, (ScrStr *)NULL, first);
}
break;
case 2:
@@ -1037,10 +1037,16 @@ static ScrDyn *bcs_write(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t
ScrDyn *cb = bcs_arg(args, argc, 2);
if (scr_dyn_is_callable(encoding)) {
cb = encoding;
encoding = scr_dyn_undefined();
encoding = scr_stream_writes_values(s) ? scr_dyn_new_null() : scr_dyn_undefined();
}
bool below;
if (scr_stream_writes_values(s)) {
/* an object-mode side hands the encoding argument to _write */
below = scr_stream_write_enc(s, chunk, encoding, bcs_callback(cb, self, s));
} else {
if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL;
below = scr_stream_write_dyn(s, chunk, bcs_callback(cb, self, s));
}
if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL;
bool below = scr_stream_write_dyn(s, chunk, bcs_callback(cb, self, s));
if (scr_exc_pending()) return NULL;
return scr_dyn_new_bool(below);
}
@@ -1055,11 +1061,17 @@ static ScrDyn *bcs_end(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t a
encoding = scr_dyn_undefined();
} else if (scr_dyn_is_callable(encoding)) {
cb = encoding;
encoding = scr_dyn_undefined();
encoding = scr_dyn_new_null();
}
if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL;
ScrStream *chained = NULL;
if (bcs_nullish(chunk)) {
if (!bcs_nullish(chunk) && scr_stream_writes_values(s)) {
/* end(chunk) writes the value with the encoding argument */
scr_stream_write_enc(s, chunk, encoding, NULL);
if (scr_exc_pending()) return NULL;
chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s));
} else if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) {
return NULL;
} else if (bcs_nullish(chunk)) {
chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s));
} else if (chunk->kind == SCR_DYN_STR) {
chained = scr_stream_end(s, NULL, chunk->v.str, bcs_callback(cb, self, s));
@@ -1376,8 +1388,6 @@ static ScrDyn *bcs_getter_call(ScrClosure *closure, ScrDyn *const *args, size_t
return bcs_flowing(s);
case BCS_P_ERRORED:
return bcs_errored(s);
case BCS_P_WRITABLE_OBJECT_MODE:
return scr_dyn_new_bool(false);
case BCS_P_READABLE_LENGTH:
case BCS_P_READABLE_HWM:
case BCS_P_WRITABLE_LENGTH:
+2
View File
@@ -1923,6 +1923,8 @@ static ScrDyn *dyn_function_constructor(void) {
ScrDyn *scr_dyn_global_constructor(const ScrStr *name) {
if (strcmp(name->data, dyn_s20) == 0) return dyn_function_constructor();
ScrDyn *error = scr_dyn_error_constructor(name->data);
if (error) return error;
ScrDyn *ctor = dyn_global_ctor(name->data);
if (!ctor) scr_trap(dyn_s3);
return ctor;
+76
View File
@@ -0,0 +1,76 @@
/* Ed25519 for scr_pki.c: libsodium's ref10 implementation (vendored, see
* ../vendor/README.md), textually included so it builds as this one unit.
* It links only into programs that name Ed25519 (the ed25519 runtime
* feature) and installs itself into scr_pki.c at startup; without it,
* Ed25519 signing, verification and public-key derivation throw a coded
* SC2020 error.
*
* Signing runs in constant time. Verification rejects a non-canonical S
* and the small-order R and public-key encodings, then checks the
* cofactorless equation, which is what Node 24 answers. */
#include "scr_runtime.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include "scr_pki.h"
#define CONFIGURED 1
#include "../vendor/libsodium/ed25519_ref10.c"
#include "../vendor/libsodium/hash_sha512_cp.c"
#include "../vendor/libsodium/verify.c"
#include "../vendor/libsodium/sign.c"
#include "../vendor/libsodium/open.c"
#include "../vendor/libsodium/keypair.c"
#undef CONFIGURED
/* The libsodium utilities those units call. */
void sodium_memzero(void *const pnt, const size_t len) {
volatile unsigned char *p = (volatile unsigned char *)pnt;
for (size_t i = 0; i < len; i++) p[i] = 0U;
}
int sodium_memcmp(const void *const b1_, const void *const b2_, size_t len) {
const volatile unsigned char *b1 = (const volatile unsigned char *)b1_;
const volatile unsigned char *b2 = (const volatile unsigned char *)b2_;
volatile unsigned char d = 0U;
for (size_t i = 0; i < len; i++) d |= b1[i] ^ b2[i];
return (1 & ((d - 1) >> 8)) - 1;
}
int sodium_is_zero(const unsigned char *n, const size_t nlen) {
volatile unsigned char d = 0U;
for (size_t i = 0; i < nlen; i++) d |= n[i];
return 1 & ((d - 1) >> 8);
}
void randombytes_buf(void *const buf, const size_t size) { arc4random_buf(buf, size); }
static void se_public_key(uint8_t pub[32], const uint8_t seed[32]) {
uint8_t sk[64];
crypto_sign_ed25519_seed_keypair(pub, sk, seed);
sodium_memzero(sk, sizeof sk);
}
static void se_sign(uint8_t sig[64], const uint8_t seed[32], const uint8_t pub[32], const uint8_t *m,
size_t n) {
uint8_t sk[64];
memcpy(sk, seed, 32);
memcpy(sk + 32, pub, 32);
crypto_sign_ed25519_detached(sig, NULL, m, n, sk);
sodium_memzero(sk, sizeof sk);
}
static bool se_verify(const uint8_t pub[32], const uint8_t *m, size_t n, const uint8_t sig[64]) {
return crypto_sign_ed25519_verify_detached(sig, m, n, pub) == 0;
}
static const ScrPkiEd25519 se_table = {
.public_key = se_public_key,
.sign = se_sign,
.verify = se_verify,
};
__attribute__((constructor)) static void se_install(void) { scr_pki_ed25519 = &se_table; }
+22 -17
View File
@@ -2918,22 +2918,6 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *o
conn->trailer_bytes = 0;
conn->trailer_fields = 0;
if (!http10 && conn->srv->require_host_header) {
bool has_host = false;
for (size_t i = 0; i < req->nheaders; i++) {
const ScrStr *name = req->hnames[i];
if (name->len == 4 && memcmp(name->data, "host", 4) == 0) {
has_host = true;
break;
}
}
if (!has_host) {
scr_http_req_release(req);
scr_http_conn_missing_host(conn);
return true;
}
}
/* framing: chunked wins over Content-Length (RFC 9112) */
bool chunked = false;
long long content_length = 0;
@@ -2958,12 +2942,33 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *o
}
}
/* HTTP/1.1 requires Host (requireHostHeader). CONNECT requests, and
* upgrade requests an 'upgrade' listener takes, leave the parser before
* Node's check. */
bool is_connect = req->method->len == 7 && memcmp(req->method->data, "CONNECT", 7) == 0;
bool takes_upgrade = has_upgrade_hdr && conn_upgrade_token && conn->srv->upgrade_ls.n > 0;
if (!http10 && conn->srv->require_host_header && !is_connect && !takes_upgrade) {
bool has_host = false;
for (size_t i = 0; i < req->nheaders; i++) {
const ScrStr *name = req->hnames[i];
if (name->len == 4 && memcmp(name->data, "host", 4) == 0) {
has_host = true;
break;
}
}
if (!has_host) {
scr_http_req_release(req);
scr_http_conn_missing_host(conn);
return true;
}
}
/* A CONNECT request: 'connect' fires INSTEAD of 'request' — the
* upgrade machinery's exact shape ((req, socket, head); the parser
* steps aside; no listener destroys the socket, Node's default). The
* h2 compat server's HTTP/1.1 arm lands here too; its HTTP/2 arm is
* dispatched separately with the compatibility response handle. */
if (req->method->len == 7 && memcmp(req->method->data, "CONNECT", 7) == 0) {
if (is_connect) {
memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
conn->len -= head_len;
if (!scr_net_ls_has_live(&conn->srv->connect_ls)) {
+68 -11
View File
@@ -1814,6 +1814,55 @@ static void scr_builtin_object_prototype_cleanup(void) {
scr_dyn_release(prototype);
}
/* The error constructors as values (an error's `constructor`, and the
* globals in JavaScript): one interned function per constructor and thread,
* indexed like scr_error_vts, with the constructor's name and length.
* Calling one through the value is not supported yet, as for the other
* interned constructors (scr_dyn_global_constructor). */
static const char *const scr_error_ctor_names[] = {
"Error", "TypeError", "RangeError", "SyntaxError", NULL,
"ReferenceError", "EvalError", "URIError", "AggregateError",
};
#define SCR_ERROR_CTORS (sizeof scr_error_ctor_names / sizeof scr_error_ctor_names[0])
static SCR_TL ScrDyn *scr_error_ctor_values[SCR_ERROR_CTORS];
static SCR_TL bool scr_error_ctors_registered;
static void scr_error_ctors_cleanup(void) {
for (size_t i = 0; i < SCR_ERROR_CTORS; i++) {
scr_dyn_release(scr_error_ctor_values[i]);
scr_error_ctor_values[i] = NULL;
}
}
static ScrDyn *scr_error_ctor_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)args;
(void)argc;
char message[128];
int length = snprintf(message, sizeof message,
"calling the %s constructor through a stored value is not supported yet",
(const char *)closure->fn);
scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, "SC2020");
return NULL;
}
ScrDyn *scr_dyn_error_constructor(const char *name) {
for (size_t i = 0; i < SCR_ERROR_CTORS; i++) {
if (!scr_error_ctor_names[i] || strcmp(scr_error_ctor_names[i], name) != 0) continue;
if (!scr_error_ctors_registered) {
scr_error_ctors_registered = true;
scr_atexit(scr_error_ctors_cleanup);
}
if (!scr_error_ctor_values[i]) {
uint32_t length = strcmp(name, "AggregateError") == 0 ? 2 : 1;
scr_error_ctor_values[i] =
scr_dyn_new_func(scr_closure_new((void *)scr_error_ctor_names[i], 0), scr_error_ctor_call, length,
"native:constructor", scr_error_ctor_names[i]);
}
return scr_dyn_retain(scr_error_ctor_values[i]);
}
return NULL;
}
ScrDyn *scr_dyn_object_prototype(void) {
if (!scr_builtin_object_prototype) {
scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ);
@@ -7951,6 +8000,17 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
return out;
}
case SCR_DYN_FUNC: {
/* The interned builtin constructors print with their names, as in Node
* (function Map() { [native code] }); other functions without a source
* text print the anonymous native form. */
if (d->v.fn.sig && strcmp(d->v.fn.sig, "native:constructor") == 0 && d->v.fn.name) {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "function ");
scr_jb_puts(&b, d->v.fn.name);
scr_jb_puts(&b, "() { [native code] }");
return scr_jb_finish(&b);
}
static const char f[] = "function () { [native code] }";
return scr_str_new(f, sizeof f - 1);
}
@@ -11943,21 +12003,18 @@ static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_
ScrDyn *own = scr_dyn_obj_get(d, key, key_len);
if (own) return scr_dyn_retain(own);
if (key_len == 11 && memcmp(key, "constructor", 11) == 0) {
static const char *const tokens[] = {
"[builtin Error]", "[builtin TypeError]",
"[builtin RangeError]", "[builtin SyntaxError]",
NULL, "[builtin ReferenceError]", "[builtin EvalError]", "[builtin URIError]",
"[builtin AggregateError]",
/* An error's constructor: the interned function value its global
* reads as (scr_dyn_error_constructor). */
static const char *const names[] = {
"Error", "TypeError", "RangeError", "SyntaxError", NULL,
"ReferenceError", "EvalError", "URIError", "AggregateError",
};
size_t hit = scr_errdyn_find(d, false);
const ScrVt *vt = hit == SIZE_MAX ? NULL : scr_errdyn_cache[hit].err->vt;
for (size_t kind = 0; vt && kind < sizeof tokens / sizeof tokens[0]; kind++) {
if (!tokens[kind]) continue;
for (size_t kind = 0; vt && kind < sizeof names / sizeof names[0]; kind++) {
if (!names[kind]) continue;
if (vt != &scr_error_vts[kind]) continue;
ScrStr *token = scr_str_new(tokens[kind], strlen(tokens[kind]));
ScrDyn *result = scr_dyn_new_str(token);
scr_str_release(token);
return result;
return scr_dyn_error_constructor(names[kind]);
}
}
return scr_dyn_retain(scr_dyn_undefined());
+11 -1
View File
@@ -9225,6 +9225,10 @@ ScrCryptoHash *scr_crypto_hmac_new_dyn(ScrStr *alg, ScrDyn *key, ScrDyn *options
if (key_object) {
bytes = scr_bytes_new(SCR_BYTES_U8, (double)key_object_len);
if (key_object_len) memcpy(bytes->data, key_object, key_object_len);
} else if (scr_crypto_is_key_object(key)) {
/* A private or public KeyObject: scr_pki.c reports Node's error. */
scr_crypto_reject_asymmetric_secret(key);
return NULL;
} else if (key->kind == SCR_DYN_STR) {
ScrStr *utf8 = encoding ? NULL : scr_str_new("utf8", 4);
bytes = scr_bytes_from_str_checked(key->v.str, encoding ? encoding : utf8);
@@ -9856,9 +9860,15 @@ const unsigned char *scr_key_object_bytes(const ScrDyn *value, size_t *len) {
return key->data;
}
/* `value instanceof KeyObject`. */
void (*scr_crypto_reject_asymmetric_secret)(const ScrDyn *key) = NULL;
/* `value instanceof KeyObject`: a secret key, or a private or public key
* (scr_pki.c, matched by its handle tags so this unit does not link it). */
bool scr_crypto_is_key_object(const ScrDyn *value) {
size_t len;
if (value->kind == SCR_DYN_HANDLE &&
(value->v.handle.tag == SCR_DYNH_PRIVATE_KEY || value->v.handle.tag == SCR_DYNH_PUBLIC_KEY))
return true;
return scr_key_object_bytes(value, &len) != NULL;
}
+256 -58
View File
@@ -517,6 +517,9 @@ struct ScrNetSocket {
int remote_port_cache;
bool remote_v6_cache;
ScrStr *pending_err; /* deferred connect failure */
/* pending_err's code when its message does not name one (static
* storage; NULL = the code the message maps to) */
const char *pending_code;
/* the caller-lookup dial (net.connect with a lookup option): true while
* the lookup decides; dial_ips holds its answered addresses (+1 each),
* dialed in order — a connect failure closes the fd and tries
@@ -531,6 +534,9 @@ struct ScrNetSocket {
size_t dial_n, dial_i;
/* a host name the dial resolves first (+1; NULL for IP literals) */
ScrStr *dial_host;
/* the host a client dial was given, Node's socket._host (+1; NULL on
* server sockets): tls.connect({ socket }) verifies against it */
ScrStr *host_name;
/* the in-flight dial chain's LAST failure — surfaced (moved into
* pending_err) only when the list exhausts; a later success drops it */
ScrStr *dial_err;
@@ -607,6 +613,12 @@ struct ScrNetSocket {
* until the proxy answers 200: writes buffer and the transport
* handshake waits until the tunnel is up */
struct ScrNetTunnel *tunnel;
/* new tls.TLSSocket(socket) and tls.connect({ socket }): on the wrapped
* socket, the socket that took over its connection (+1; end() and
* destroy() act on it); on that socket, the one it took over (+1; its
* 'close' fires just before this socket's, as in Node) */
struct ScrNetSocket *adopted_by;
struct ScrNetSocket *adopted_from;
};
/* The CONNECT head still being sent from out_off, and the proxy's
@@ -616,6 +628,9 @@ typedef struct ScrNetTunnel {
size_t out_len, out_off;
char *in;
size_t in_len;
/* bytes a wrapped socket left unsent, which go out before the
* transport's own; no response head is read */
bool send_only;
} ScrNetTunnel;
/* The 'lookup' event's half of the runtime, installed by the first
@@ -712,6 +727,7 @@ void scr_net_sock_release(ScrNetSocket *s) {
free(s->dial_ips);
scr_str_release(s->dial_err);
scr_str_release(s->dial_host);
scr_str_release(s->host_name);
scr_str_release(s->unix_path);
scr_net_ls_drop(&s->lookup_ls);
if (s->lookup_evs != NULL) scr_net_lookup_ops->free(s);
@@ -721,6 +737,8 @@ void scr_net_sock_release(ScrNetSocket *s) {
if (s->pipe_dst) scr_net_sock_release(s->pipe_dst);
if (s->conn_pending) scr_net_server_release(s->conn_pending);
if (s->server) scr_net_server_release(s->server);
if (s->adopted_by) scr_net_sock_release(s->adopted_by);
if (s->adopted_from) scr_net_sock_release(s->adopted_from);
if (s->fd >= 0) scr_net_close_fd_raw(s->fd);
#ifdef SCR_RC_AUDIT
scr_net_live--;
@@ -962,7 +980,7 @@ static ScrNetSocket *scr_net_sock_new(void) {
static void scr_net_sock_update_read(ScrNetSocket *s) {
bool want = s->fd >= 0 && !s->connecting && !s->rd_eof && !s->user_paused &&
(s->data_ls.n > 0 || s->pipe_dst != NULL || s->native_data != NULL ||
s->tunnel != NULL /* the proxy's CONNECT answer */ ||
(s->tunnel != NULL && !s->tunnel->send_only) /* the proxy's CONNECT answer */ ||
s->flowing /* resume(): flow (and discard sans listeners) */ ||
s->readable_ls.n > 0 /* paused-mode consumer: bytes buffer */ ||
s->tops != NULL /* a transport is ALWAYS a consumer: the TLS
@@ -977,7 +995,8 @@ static void scr_net_sock_update_read(ScrNetSocket *s) {
* finished socket the same way */ ||
(!s->eof_pending && s->rlen - s->rhead < SCR_NET_READ_AHEAD)
/* no consumer yet: read ahead into the buffer, as Node's
* stream does up to its high-water mark */);
* stream does up to its high-water mark */) &&
!(s->tunnel != NULL && s->tunnel->send_only);
if (want && !s->read_armed) {
scr_net_watch_read(s->fd, s, true);
s->read_armed = true;
@@ -1056,6 +1075,23 @@ static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len) {
* dead or ended write half (Node fires 'error' there; this slice drops —
* SEMANTICS.md documents the bound). */
static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len) {
if (s->adopted_by != NULL) {
/* Node writes these bytes to the wire as they are, between the TLS
* records of the socket that took over (the peer then fails the
* session). Bytes the kernel cannot take now end that socket. */
ScrNetSocket *t = s->adopted_by;
while (len > 0 && t->fd >= 0) {
ssize_t sent = scr_net_send(t->fd, data, len);
if (sent < 0 && errno == EINTR) continue;
if (sent <= 0) {
scr_net_sock_destroy(t);
break;
}
data += sent;
len -= (size_t)sent;
}
return;
}
/* A socket whose dial hasn't STARTED yet (the agent's deferred dial,
* the caller-lookup wait) has no fd but is logically connecting —
* bytes buffer and flush at establishment like any mid-connect write. */
@@ -1490,6 +1526,12 @@ static void scr_net_sock_tunnel_pump(ScrNetSocket *s) {
}
scr_net_sock_update_write(s);
if (s->fd < 0 || t->out_off < t->out_len) return;
if (t->send_only) {
scr_net_tunnel_free(t);
s->tunnel = NULL;
scr_net_sock_connected(s);
return;
}
for (;;) {
char buf[4096];
ssize_t n = read(s->fd, buf, sizeof buf);
@@ -1592,6 +1634,10 @@ static void scr_net_sock_transport_pump(ScrNetSocket *s) {
if (scr_exc_pending()) return;
scr_net_fire0_this(&s->ready_ls, s, SCR_DYNH_NET_SOCKET);
if (scr_exc_pending()) return;
if (s->tops->after_established) {
s->tops->after_established(s->tctx);
if (scr_exc_pending()) return;
}
scr_net_sock_flush(s);
scr_net_sock_update_read(s);
scr_net_sock_update_write(s);
@@ -2250,6 +2296,8 @@ ScrStr *scr_net_blocking_lookup(ScrStr *host /*borrowed*/) {
* autoselect), and any other name is resolved when the dial starts. */
static void scr_net_sock_set_peer(ScrNetSocket *s, ScrStr *host /*borrowed, nullable*/) {
const char *h = host && host->len > 0 ? host->data : "localhost";
scr_str_release(s->host_name);
s->host_name = scr_str_new(h, strlen(h));
if (strcmp(h, "localhost") == 0) {
h = "127.0.0.1";
if (scr_net_lookup_ops != NULL) scr_net_lookup_ops->record(s, "localhost", 9, h, 4, NULL);
@@ -3024,6 +3072,10 @@ void scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) {
}
void scr_net_sock_end(ScrNetSocket *s) {
if (s->adopted_by != NULL) {
scr_net_sock_end(s->adopted_by);
return;
}
if (s->fd < 0 || s->wr_ending) return;
s->wr_ending = true;
scr_net_sock_maybe_finish_write(s);
@@ -3136,7 +3188,10 @@ void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/) {
double scr_net_sock_bytes_written(ScrNetSocket *s) { return (double)s->bytes_written; }
/* socket.readable: true until the read half is done (peer FIN / destroy). */
bool scr_net_sock_readable(ScrNetSocket *s) { return s->fd >= 0 && !s->rd_eof; }
bool scr_net_sock_readable(ScrNetSocket *s) {
if (s->adopted_by != NULL) return scr_net_sock_readable(s->adopted_by);
return s->fd >= 0 && !s->rd_eof;
}
/* socket.setTimeout(ms): ms > 0 arms the idle timer NOW (Node starts the
* clock at the call — connecting time counts), ms <= 0 disables. */
@@ -3206,6 +3261,7 @@ ScrStr *scr_net_sock_remote_family(ScrNetSocket *s) {
/* socket.localAddress / localPort / localFamily: read from the open
* socket (undefined while connecting and after close, as in Node). */
static bool scr_net_sock_local(ScrNetSocket *s, char *addr, size_t cap, int *port, bool *v6) {
if (s->adopted_by != NULL) return scr_net_sock_local(s->adopted_by, addr, cap, port, v6);
if (s->fd < 0 || s->connecting || s->dial_deferred || s->lookup_wait) return false;
struct sockaddr_storage ss;
socklen_t len = sizeof ss;
@@ -4652,6 +4708,10 @@ bool scr_net_is_ipv4(const ScrDyn *input) { return scr_net_is_ip(input) == 4; }
bool scr_net_is_ipv6(const ScrDyn *input) { return scr_net_is_ip(input) == 6; }
void scr_net_sock_destroy(ScrNetSocket *s) {
if (s->adopted_by != NULL) {
scr_net_sock_destroy(s->adopted_by);
return;
}
if (s->fd < 0 && !s->in_registry) return;
s->rd_eof = true;
s->wr_done = true;
@@ -4833,7 +4893,8 @@ void scr_net_sock_on_error(ScrNetSocket *s, ScrClosure *cb /*moves*/, ScrChildEr
}
void scr_net_sock_on_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) {
if (s->close_emitted || (!s->connecting && s->fd < 0)) {
/* a socket that took over another's connection never emits 'connect' */
if (s->close_emitted || (!s->connecting && s->fd < 0) || s->adopted_from != NULL) {
scr_closure_release(cb);
return;
}
@@ -4939,12 +5000,15 @@ void scr_net_sock_clear_native_reader(ScrNetSocket *s) {
/* socket.destroyed — true once the fd is gone (destroy() or full close),
* Node's flag for "this stream is done". */
bool scr_net_sock_destroyed(ScrNetSocket *s) { return s->fd < 0; }
bool scr_net_sock_destroyed(ScrNetSocket *s) {
return s->adopted_by != NULL ? scr_net_sock_destroyed(s->adopted_by) : s->fd < 0;
}
/* socket.writable — Node's stream flag: the write half is open (no end()
* yet, no FIN sent, fd alive). Connecting sockets answer true — writes
* queue, exactly Node. */
bool scr_net_sock_writable(ScrNetSocket *s) {
if (s->adopted_by != NULL) return scr_net_sock_writable(s->adopted_by);
return s->fd >= 0 && !s->wr_ending && !s->wr_done;
}
@@ -4996,8 +5060,78 @@ void scr_net_sock_set_transport(ScrNetSocket *s, const ScrNetTransportOps *ops,
scr_net_sock_update_write(s);
}
/* A fresh socket takes over raw's open connection: the fd, the bytes it
* buffered either way and the peer identity. raw keeps its listeners and
* its server's accounting but does no I/O of its own; end() and destroy()
* on it act on the new socket, and its 'close' fires just before the new
* socket's. Bytes raw had not sent yet go out first. +1; the caller sets
* the transport, then starts it with scr_net_sock_adopt_start. NULL when
* raw has no open, idle connection to give (connecting, closed, already
* wrapped, or carrying a transport). */
ScrNetSocket *scr_net_sock_adopt(ScrNetSocket *raw) {
if (raw->fd < 0 || scr_net_sock_connecting(raw) || raw->adopted_by != NULL || raw->tops != NULL)
return NULL;
ScrNetSocket *t = scr_net_sock_new();
scr_str_release(scr_net_sock_remote_address(raw)); /* cache the peer while the fd is raw's */
if (raw->remote_cache) {
t->remote_cache = scr_str_retain(raw->remote_cache);
t->remote_port_cache = raw->remote_port_cache;
t->remote_v6_cache = raw->remote_v6_cache;
}
memcpy(t->peer_ip, raw->peer_ip, sizeof t->peer_ip);
t->peer_port = raw->peer_port;
t->unrefd = raw->unrefd;
t->rd_eof = raw->rd_eof;
scr_net_sock_timer_cancel(raw);
if (raw->read_armed) scr_net_watch_read(raw->fd, raw, false);
if (raw->write_armed) scr_net_watch_write(raw->fd, raw, false);
raw->read_armed = raw->write_armed = false;
t->fd = raw->fd;
raw->fd = -1;
if (raw->rlen > raw->rhead) scr_net_sock_append_rbuf(t, raw->rbuf + raw->rhead, raw->rlen - raw->rhead);
raw->rhead = raw->rlen = 0;
while (raw->wlen > raw->whead) {
ssize_t sent = scr_net_send(t->fd, raw->wbuf + raw->whead, raw->wlen - raw->whead);
if (sent < 0 && errno == EINTR) continue;
if (sent <= 0) break; /* EAGAIN, or an error the transport will meet */
raw->whead += (size_t)sent;
}
if (raw->wlen > raw->whead) {
ScrNetTunnel *tun = calloc(1, sizeof *tun);
if (!tun) scr_net_oom();
tun->out_len = raw->wlen - raw->whead;
tun->out = malloc(tun->out_len);
if (!tun->out) scr_net_oom();
memcpy(tun->out, raw->wbuf + raw->whead, tun->out_len);
tun->send_only = true;
t->tunnel = tun;
}
raw->whead = raw->wlen = 0;
raw->adopted_by = scr_net_sock_retain(t);
t->adopted_from = scr_net_sock_retain(raw);
scr_net_sock_register(t);
return t;
}
/* Starts an adopted socket once its transport is set: the unsent bytes
* drain first, then the handshake runs (over any bytes raw had already
* read). */
void scr_net_sock_adopt_start(ScrNetSocket *t) {
if (t->tunnel != NULL) {
scr_net_sock_update_write(t);
return;
}
/* the handshake starts from the poller (the socket is writable), never
* the constructing stack — Node's start is asynchronous too */
scr_net_sock_transport_want_write(t);
}
int scr_net_sock_fd(ScrNetSocket *s) { return s->fd; }
/* The host a client socket was told to dial (Node's socket._host). +1;
* NULL on server sockets. */
ScrStr *scr_net_sock_dial_name(ScrNetSocket *s) { return scr_str_retain(s->host_name); }
/* Drain up to n bytes of the receive buffer (peeked/unshifted bytes) —
* consumers call this BEFORE the fd so re-queued bytes keep their
* position in the stream. */
@@ -5050,6 +5184,32 @@ void scr_net_sock_replay(ScrNetSocket *s, const char *data, size_t len) {
s->rcap = total;
}
/* The peer closed before the handshake finished, on a socket whose
* stream the program already holds (new tls.TLSSocket): 'end', then
* 'close' without an error, as Node reports it. */
void scr_net_sock_transport_eof(ScrNetSocket *s) {
scr_net_sock_eof(s);
if (s->fd >= 0) scr_net_sock_close_fd(s);
}
/* A listener for the handshake's completion once TCP is up (a socket
* tls.connect({ socket }) took over is up from the start): it registers
* until the transport establishes. */
void scr_net_sock_on_established(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) {
if (s->close_emitted || s->fd < 0 || s->tops == NULL || s->t_est) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&s->conn_ls, cb, NULL, once);
}
/* scr_net_sock_transport_error with the error's code given (static
* storage), for messages whose text does not name it. */
void scr_net_sock_transport_error_code(ScrNetSocket *s, const char *msg, const char *code) {
if (!s->pending_err) s->pending_code = code;
scr_net_sock_transport_error(s, msg);
}
void scr_net_sock_transport_error(ScrNetSocket *s, const char *msg) {
if (msg != NULL) {
if (!s->pending_err) s->pending_err = scr_str_new(msg, strlen(msg));
@@ -5141,6 +5301,72 @@ static void scr_net_server_settle(ScrNetServer *srv) {
scr_net_server_unregister(srv);
}
/* Fires a closed socket's 'close' and settles it: its server's count
* drains, the protocol layer hears it, listeners drop and it leaves the
* registry. A socket that took over another's connection settles that one
* first, so the wrapped socket's 'close' comes just before its own. False
* with an exception pending. */
static bool scr_net_sock_settle(ScrNetSocket *sock) {
ScrNetSocket *raw = sock->adopted_from;
if (raw != NULL) {
sock->adopted_from = NULL;
if (raw->adopted_by == sock) {
raw->adopted_by = NULL;
scr_net_sock_release(sock);
}
bool ok = raw->close_emitted || scr_net_sock_settle(raw);
scr_net_sock_release(raw);
if (!ok) return false;
}
sock->close_emitted = true;
sock->emit_close = false;
/* The owning server's count drains FIRST, and a now-drained closing
* server settles before this socket's own 'close' fires — Node's
* observed order (differential: the net-echo fixture). */
if (sock->server) {
ScrNetServer *srv = sock->server;
sock->server = NULL;
srv->nconns--;
if (srv->closing && srv->nconns == 0 && !srv->close_emitted) {
scr_net_server_settle(srv);
}
scr_net_server_release(srv);
if (scr_exc_pending()) return false;
}
if (sock->native_closed) {
sock->native_closed(sock->native_ctx);
if (scr_exc_pending()) return false;
}
{
ScrDyn *had_error = scr_dyn_new_bool(sock->had_error);
scr_net_fire_arg_this(&sock->close_ls, had_error, sock, SCR_DYNH_NET_SOCKET);
scr_dyn_release(had_error);
}
/* settle: listeners drop (cycle story) */
scr_net_ls_drop(&sock->data_ls);
scr_net_ls_drop(&sock->end_ls);
scr_net_ls_drop(&sock->close_ls);
scr_net_ls_drop(&sock->err_ls);
scr_net_ls_drop(&sock->conn_ls);
scr_net_ls_drop(&sock->timeout_ls);
scr_net_ls_drop(&sock->readable_ls);
scr_net_ls_drop(&sock->finish_ls);
scr_net_ls_drop(&sock->wcb_ls);
scr_net_ls_drop(&sock->ready_ls);
scr_net_ls_drop(&sock->lookup_ls);
if (sock->tops && sock->tops->settle) sock->tops->settle(sock->tctx);
if (sock->pipe_dst) {
scr_net_sock_release(sock->pipe_dst);
sock->pipe_dst = NULL;
}
if (sock->conn_pending) {
scr_net_server_release(sock->conn_pending);
sock->conn_pending = NULL;
}
scr_net_sock_unregister(sock);
return !scr_exc_pending();
}
/* Sockets first (a dying connection may drain its server), servers after.
* Every fire checks for a pending exception and bails — the loop surfaces
* it as an uncaught throw. */
@@ -5200,7 +5426,19 @@ static void scr_net_sweep(void) {
* listeners — and the unhandled-'error' exit */
bool consumed = false;
if (sock->native_err) consumed = sock->native_err(sock->native_ctx, msg);
if (!consumed && !scr_exc_pending()) scr_net_fire_err_this(&sock->err_ls, msg, sock, SCR_DYNH_NET_SOCKET);
if (!consumed && !scr_exc_pending()) {
ScrError *coded = NULL;
ScrError *saved = scr_event_error_payload;
if (sock->pending_code != NULL) {
coded = scr_error_new(SCR_ERR_ERROR, msg);
coded->code = scr_str_new(sock->pending_code, strlen(sock->pending_code));
scr_event_error_payload = coded;
}
scr_net_fire_err_this(&sock->err_ls, msg, sock, SCR_DYNH_NET_SOCKET);
scr_event_error_payload = saved;
if (coded != NULL) scr_error_release(coded);
}
sock->pending_code = NULL;
scr_str_release(msg);
if (scr_exc_pending()) {
scr_net_sock_release(sock);
@@ -5208,58 +5446,7 @@ static void scr_net_sweep(void) {
}
}
if (sock->emit_close && !sock->close_emitted && sock->fd < 0) {
sock->close_emitted = true;
sock->emit_close = false;
/* The owning server's count drains FIRST, and a now-drained closing
* server settles before this socket's own 'close' fires — Node's
* observed order (differential: the net-echo fixture). */
if (sock->server) {
ScrNetServer *srv = sock->server;
sock->server = NULL;
srv->nconns--;
if (srv->closing && srv->nconns == 0 && !srv->close_emitted) {
scr_net_server_settle(srv);
}
scr_net_server_release(srv);
if (scr_exc_pending()) {
scr_net_sock_release(sock);
return;
}
}
if (sock->native_closed) {
sock->native_closed(sock->native_ctx);
if (scr_exc_pending()) {
scr_net_sock_release(sock);
return;
}
}
{
ScrDyn *had_error = scr_dyn_new_bool(sock->had_error);
scr_net_fire_arg_this(&sock->close_ls, had_error, sock, SCR_DYNH_NET_SOCKET);
scr_dyn_release(had_error);
}
/* settle: listeners drop (cycle story) */
scr_net_ls_drop(&sock->data_ls);
scr_net_ls_drop(&sock->end_ls);
scr_net_ls_drop(&sock->close_ls);
scr_net_ls_drop(&sock->err_ls);
scr_net_ls_drop(&sock->conn_ls);
scr_net_ls_drop(&sock->timeout_ls);
scr_net_ls_drop(&sock->readable_ls);
scr_net_ls_drop(&sock->finish_ls);
scr_net_ls_drop(&sock->wcb_ls);
scr_net_ls_drop(&sock->ready_ls);
scr_net_ls_drop(&sock->lookup_ls);
if (sock->pipe_dst) {
scr_net_sock_release(sock->pipe_dst);
sock->pipe_dst = NULL;
}
if (sock->conn_pending) {
scr_net_server_release(sock->conn_pending);
sock->conn_pending = NULL;
}
scr_net_sock_unregister(sock);
if (scr_exc_pending()) {
if (!scr_net_sock_settle(sock)) {
scr_net_sock_release(sock);
return;
}
@@ -5504,6 +5691,17 @@ static void scr_net_cleanup_atexit(void) {
scr_net_server_release(s->server);
s->server = NULL;
}
/* a wrapped socket and the socket that took it over hold each other */
if (s->adopted_by) {
ScrNetSocket *t = s->adopted_by;
s->adopted_by = NULL;
scr_net_sock_release(t);
}
if (s->adopted_from) {
ScrNetSocket *raw = s->adopted_from;
s->adopted_from = NULL;
scr_net_sock_release(raw);
}
scr_net_sock_unregister(s);
}
while (scr_net_servers) {
+27
View File
@@ -351,8 +351,22 @@ typedef struct ScrNetTransportOps {
bool (*pending)(void *tctx);
void (*shutdown_write)(void *tctx);
void (*free)(void *tctx);
/* nullable: runs after the established socket's 'connect' listeners
* (the TLS 'secure' event, which Node emits after secureConnect) */
void (*after_established)(void *tctx);
/* nullable: runs when the closed socket settles, after its own
* listeners drop; the transport drops the listeners it holds */
void (*settle)(void *tctx);
} ScrNetTransportOps;
void scr_net_sock_set_transport(ScrNetSocket *s, const ScrNetTransportOps *ops, void *tctx);
/* new tls.TLSSocket(socket) and tls.connect({ socket }): a fresh socket
* takes over an open connection (fd, buffered bytes, peer identity); the
* old socket stays open without I/O, forwards end() and destroy(), and
* fires 'close' just before the new one. +1, or NULL when the socket has
* no open, idle connection to give. Set the transport on the result, then
* call scr_net_sock_adopt_start. */
ScrNetSocket *scr_net_sock_adopt(ScrNetSocket *raw);
void scr_net_sock_adopt_start(ScrNetSocket *t);
/* Routes a client socket through an HTTP CONNECT proxy. Call it right
* after the dial (scr_net_connect to the proxy) and before any transport
* is set: once TCP is up, `request` (the CONNECT head, copied) is sent
@@ -366,6 +380,9 @@ void scr_net_sock_set_tunnel(ScrNetSocket *s, const char *request, size_t len);
int scr_net_tunnel_status(const char *head, size_t len);
void scr_net_tunnel_failure(int status, char *msg, size_t cap);
int scr_net_sock_fd(ScrNetSocket *s);
/* The host a client socket was told to dial (Node's socket._host). +1;
* NULL on server sockets. */
ScrStr *scr_net_sock_dial_name(ScrNetSocket *s);
/* Drain up to n bytes of the socket's receive buffer (peeked/unshifted
* bytes — the demux path); consumers call it before the fd. */
size_t scr_net_sock_take_buffered(ScrNetSocket *s, char *buf, size_t n);
@@ -385,6 +402,16 @@ void scr_net_sock_replay(ScrNetSocket *s, const char *data, size_t len);
* 'error' + teardown to the sweep; NULL tears down without an 'error'
* event, Node's default for an unhandled server-side handshake failure. */
void scr_net_sock_transport_error(ScrNetSocket *s, const char *msg);
/* The same with the error's code given (static storage), for messages
* whose text does not name it. */
void scr_net_sock_transport_error_code(ScrNetSocket *s, const char *msg, const char *code);
/* The peer closed before the handshake finished on a wrapped socket:
* 'end', then 'close' without an error. */
void scr_net_sock_transport_eof(ScrNetSocket *s);
/* Registers a listener that fires when the transport establishes, once
* TCP is up (a socket that took over an open connection is up from the
* start). */
void scr_net_sock_on_established(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once);
/* Defer the server's 'connection' listeners to handshake completion —
* tls.createServer's handler is the 'secureConnection' event, fired with
* the socket only once the TLS layer stands (Node's timing). */
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
/* The key and signature core of scr_pki.c, for the WebCrypto layer
* (scr_webcrypto_pki.c) and the Ed25519 unit (scr_ed25519.c). Keys are
* reference-counted RSA keys, EC keys on P-256, P-384, P-521 or secp256k1,
* or Ed25519 keys. Functions answering NULL or false set no error unless
* they say so; byte results are +1. */
#ifndef SCR_PKI_H
#define SCR_PKI_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
typedef struct SpKey ScrPkiKey;
/* Throws an error of `kind` (with `code`, or none for NULL) whose message
* is the printf format's whole output, at any length. */
void scr_pki_throw_fmt(int kind, const char *code, const char *fmt, ...) __attribute__((format(printf, 3, 4)));
/* The Ed25519 arithmetic (scr_ed25519.c), installed at startup in
* programs that name Ed25519; NULL otherwise. Keys are the 32-byte seed
* and the 32-byte encoded public point. */
typedef struct {
void (*public_key)(uint8_t pub[32], const uint8_t seed[32]);
void (*sign)(uint8_t sig[64], const uint8_t seed[32], const uint8_t pub[32], const uint8_t *m, size_t n);
bool (*verify)(const uint8_t pub[32], const uint8_t *m, size_t n, const uint8_t sig[64]);
} ScrPkiEd25519;
extern const ScrPkiEd25519 *scr_pki_ed25519;
/* Whether the Ed25519 arithmetic is linked; false with a coded SC2020
* error pending otherwise. */
bool scr_pki_ed25519_ready(void);
ScrPkiKey *scr_pki_key_retain(ScrPkiKey *key);
void scr_pki_key_release(ScrPkiKey *key);
bool scr_pki_key_is_private(const ScrPkiKey *key);
bool scr_pki_key_is_rsa(const ScrPkiKey *key);
bool scr_pki_key_is_ed25519(const ScrPkiKey *key);
/* The EC key's JWK curve name ("P-256", ...), or NULL for RSA keys. */
const char *scr_pki_key_curve(const ScrPkiKey *key);
/* The RSA modulus length in bits. */
size_t scr_pki_key_bits(const ScrPkiKey *key);
/* The RSA public exponent's big-endian bytes, without leading zeros. */
ScrBytes *scr_pki_key_exponent(const ScrPkiKey *key);
/* A key from SubjectPublicKeyInfo (public) or PKCS#8 (private) DER. */
ScrPkiKey *scr_pki_key_parse_der(const uint8_t *der, size_t len, bool private_key);
/* A key from a JWK object (borrowed; a plain object). */
ScrPkiKey *scr_pki_key_from_jwk(const ScrDyn *jwk, bool private_key);
/* A public EC key from an uncompressed or compressed point. */
ScrPkiKey *scr_pki_key_from_point(const char *curve, const uint8_t *point, size_t len);
ScrPkiKey *scr_pki_key_public_of(ScrPkiKey *key);
ScrPkiKey *scr_pki_generate_rsa(unsigned bits, uint64_t exponent);
ScrPkiKey *scr_pki_generate_ec(const char *curve);
/* NULL with a coded SC2020 error pending when the program does not link
* the Ed25519 unit. */
ScrPkiKey *scr_pki_generate_ed25519(void);
/* A public Ed25519 key from its 32-byte encoding. */
ScrPkiKey *scr_pki_key_from_ed25519_raw(const uint8_t *raw, size_t len);
/* The Ed25519 public key's 32-byte encoding. */
ScrBytes *scr_pki_export_ed25519_raw(ScrPkiKey *key);
/* SubjectPublicKeyInfo (public keys) or PKCS#8 (private keys) DER. */
ScrBytes *scr_pki_export_der(ScrPkiKey *key);
/* The EC public key's uncompressed point. */
ScrBytes *scr_pki_export_point(ScrPkiKey *key);
/* The JWK members in Node's order (kty, n, e, ... or kty, x, y, crv, d),
* +1. */
ScrDyn *scr_pki_export_jwk(ScrPkiKey *key);
/* Signs `data` with the digest named by its WebCrypto name ("SHA-256"):
* RSA PKCS#1 v1.5 (pss false), RSA-PSS with `salt` bytes, or ECDSA as
* IEEE P1363 r || s. Ed25519 keys sign the message itself (hash
* ignored). */
ScrBytes *scr_pki_sign(ScrPkiKey *key, const char *hash, bool pss, int salt, const uint8_t *data,
size_t len);
bool scr_pki_verify(ScrPkiKey *key, const char *hash, bool pss, int salt, const uint8_t *data,
size_t len, const uint8_t *signature, size_t signature_len);
#endif
File diff suppressed because it is too large Load Diff
+25
View File
@@ -272,6 +272,7 @@ ScrStream *scr_stream_set_push_encoding(ScrStream *s, ScrStr *enc);
bool scr_stream_push_null(ScrStream *s);
void scr_stream_unshift(ScrStream *s, ScrBytes *chunk);
void scr_stream_unshift_str(ScrStream *s, ScrStr *str);
void scr_stream_unshift_dyn(ScrStream *s, const ScrDyn *d);
ScrBytes *scr_stream_read(ScrStream *s, double size);
ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once, bool prepend);
ScrDyn *scr_stream_read_dyn(ScrStream *s, ScrDyn *size); /* +1 or NULL; size < 0 = all/first; throws once encoded (the static type is Buffer|null) */
@@ -291,6 +292,8 @@ typedef struct ScrPromise ScrPromise; /* full declaration below (C11 repeat) */
* each chunk before asking again). */
bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d); /* borrows d; tag dispatch */
bool scr_stream_write_dyn(ScrStream *s, const ScrDyn *d, ScrClosure *cb); /* cb moves */
/* write(chunk, encoding, cb): enc borrowed, NULL or undefined when absent */
bool scr_stream_write_enc(ScrStream *s, const ScrDyn *chunk, ScrDyn *enc, ScrClosure *cb);
ScrPromise *scr_stream_next_chunk(ScrStream *s);
ScrPromise *scr_stream_next_chunk_dyn(ScrStream *s);
/* node:stream/promises — the promise forms over the finished/pipeline
@@ -475,6 +478,28 @@ void scr_stream_final_done(ScrStream *s, ScrError *err);
void scr_stream_destroy_done(ScrStream *s, ScrError *err);
void scr_stream_transform_done(ScrStream *s, ScrError *err, ScrBytes *data, ScrStr *data_str);
void scr_stream_flush_done(ScrStream *s, ScrError *err, ScrBytes *data, ScrStr *data_str);
/* The same completions with a checked-dynamic data value (borrowed; NULL,
* null and undefined push nothing): an object-mode Transform's data, or an
* untyped callback's. err moves. */
/* objectMode: scr_stream_set_object_mode applies the option's sides right
* after construction; scr_stream_writes_values answers whether the write
* and transform steps receive dyn values instead of Buffers. */
void scr_stream_set_object_mode(ScrStream *s, bool readable, bool writable);
ScrStream *scr_stream_object_mode(ScrStream *s, bool readable, bool writable);
bool scr_stream_writes_values(ScrStream *s);
/* A compiled write or transform callback's chunk by its declared type: a
* Buffer (+1, or NULL with a TypeError for another object-mode value), a
* dyn value (+1), and the encoding argument (+1). */
ScrBytes *scr_stream_chunk_bytes(ScrStream *s, ScrBytes *chunk);
struct ScrDyn *scr_stream_chunk_value(ScrStream *s, ScrBytes *chunk);
ScrStr *scr_stream_chunk_encoding(ScrStream *s);
ScrDyn *scr_stream_chunk_encoding_value(ScrStream *s);
/* emit('data', value) with a checked-dynamic chunk (borrowed). */
bool scr_stream_emit_value(ScrStream *s, ScrStr *name, struct ScrDyn *v);
/* Whether a dyn for-await step (readable.nextChunkDyn) answered the end. */
bool scr_stream_chunk_done(const struct ScrDyn *v);
void scr_stream_transform_done_value(ScrStream *s, ScrError *err, struct ScrDyn *v);
void scr_stream_flush_done_value(ScrStream *s, ScrError *err, struct ScrDyn *v);
/* Runtime-backed streams (the fs streams in scr_fs_stream.c).
* - construct: Node's _construct. The hook runs on a tick after
+1 -11
View File
@@ -305,7 +305,7 @@ static ScrPromise *src_next(OpSrc *src) {
}
static bool src_done(const OpSrc *src, const ScrDyn *v) {
return src->s ? v->kind == SCR_DYN_UNDEF : v == op_k_done;
return src->s ? scr_stream_chunk_done(v) : v == op_k_done;
}
/* The Readable iterator's return(): the iterator generator resumes at its
@@ -1492,16 +1492,6 @@ static void op_from_step_k(void *self, ScrDyn *v) {
}
int pushed = scr_stream_push_object(out, v);
scr_dyn_release(v);
if (pushed == -2) {
op_from_destroy_with(out, op_unsupported(
"a stream operator output that mixes strings and Buffers"));
return;
}
if (pushed < 0) {
op_from_destroy_with(out, op_unsupported(
"a stream operator value that is not a string or Buffer (object-mode streams of other values)"));
return;
}
if (pushed) {
from_next(f, out);
return;
+315 -12
View File
@@ -368,6 +368,10 @@ typedef struct ScrTlsSrv {
/* maxVersion TLSv1.2: the ClientHello is pre-parsed so a hello that
* offers only later versions gets OpenSSL's protocol_version alert. */
bool max_tls12;
/* new tls.TLSSocket(socket, { secureContext }): the context whose
* certificate and key this config serves (+1; NULL when cert/key are the
* config's own) */
ScrSecureCtx *ctx;
} ScrTlsSrv;
/* The ALPN answers (static storage — mbedTLS keeps the pointers). Plain
@@ -405,6 +409,28 @@ static ScrTlsSrv *scr_tls_srv_new(const char *cert, size_t cert_len, const char
return s;
}
/* A server config that serves a SecureContext's certificate and key. */
static ScrTlsSrv *scr_tls_srv_from_ctx(ScrSecureCtx *ctx /*borrowed*/) {
scr_tls_rng_init();
ScrTlsSrv *s = calloc(1, sizeof *s);
if (!s) scr_tls_oom();
s->rc = 1;
mbedtls_x509_crt_init(&s->cert);
mbedtls_pk_init(&s->pk);
mbedtls_ssl_config_init(&s->conf);
if (mbedtls_ssl_config_defaults(&s->conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM,
MBEDTLS_SSL_PRESET_DEFAULT) != 0) {
scr_tls_oom();
}
mbedtls_ssl_conf_rng(&s->conf, mbedtls_ctr_drbg_random, &scr_tls_drbg);
mbedtls_ssl_conf_authmode(&s->conf, MBEDTLS_SSL_VERIFY_NONE);
s->ctx = scr_secure_ctx_retain(ctx);
if (mbedtls_ssl_conf_own_cert(&s->conf, &ctx->cert, &ctx->pk) != 0) {
scr_tls_bad_material("certificate/key pair", 0);
}
return s;
}
static ScrTlsSrv *scr_tls_srv_retain(ScrTlsSrv *s) {
s->rc++;
return s;
@@ -419,6 +445,7 @@ static void scr_tls_srv_release(ScrTlsSrv *s) {
mbedtls_x509_crt_free(&s->cert);
mbedtls_pk_free(&s->pk);
scr_tls_alpn_free(s->alpn);
scr_secure_ctx_release(s->ctx);
free(s);
}
@@ -629,6 +656,15 @@ typedef struct ScrTlsSock {
unsigned char *hello; /* the raw ClientHello flight, replayed at DONE */
size_t hello_len, hello_cap;
ScrSecureCtx *sni_ctx; /* owned; the answer (NULL = default cert) */
/* new tls.TLSSocket(socket, { isServer: true }): the program holds this
* socket before the handshake, so a failed handshake is its 'error'
* (tls.createServer's sockets fail without one) */
bool wrapped;
unsigned char head[5]; /* the first bytes the peer sent (the error's wording) */
size_t head_len;
/* the 'secure' event's listeners */
ScrNetLs secure_ls;
bool settled; /* the socket closed: listeners added later never fire */
} ScrTlsSock;
/* checkServerIdentity for an IP literal: the address must be one of the
@@ -703,6 +739,16 @@ static int scr_tls_bio_send(void *ctx, const unsigned char *buf, size_t len) {
return MBEDTLS_ERR_NET_SEND_FAILED;
}
/* Keeps the first bytes a wrapped socket's peer sends: a handshake that
* fails on them reports what they looked like, as OpenSSL does. */
static void scr_tls_note_head(ScrTlsSock *t, const unsigned char *buf, size_t n) {
if (!t->wrapped || t->head_len == sizeof t->head) return;
size_t take = sizeof t->head - t->head_len;
if (take > n) take = n;
memcpy(t->head + t->head_len, buf, take);
t->head_len += take;
}
static int scr_tls_bio_recv(void *ctx, unsigned char *buf, size_t len) {
ScrTlsSock *t = (ScrTlsSock *)ctx;
int fd = scr_net_sock_fd(t->sock);
@@ -710,8 +756,12 @@ static int scr_tls_bio_recv(void *ctx, unsigned char *buf, size_t len) {
/* bytes a consumer peeked and unshifted (the demux path) re-enter the
* stream here, ahead of the kernel's */
size_t took = scr_net_sock_take_buffered(t->sock, (char *)buf, len);
if (took > 0) return (int)took;
if (took > 0) {
scr_tls_note_head(t, buf, took);
return (int)took;
}
ssize_t n = read(fd, buf, len);
if (n > 0) scr_tls_note_head(t, buf, (size_t)n);
if (n >= 0) return (int)n; /* 0 = EOF; mbedTLS maps it to CONN_EOF */
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
return MBEDTLS_ERR_SSL_WANT_READ;
@@ -1026,6 +1076,43 @@ void scr_tls_sni_answer(ScrClosure *self, bool has_err, ScrSecureCtx *ctx /*move
scr_net_sock_release(sock);
}
/* A wrapped server socket's failed handshake, reported as Node reports it
* on the TLSSocket: a peer that closed first ends the stream, a reset is
* ECONNRESET, and bytes that are not a TLS record name what they were
* (OpenSSL's reasons without the per-process prefix and source location,
* SEMANTICS.md divergence 55). */
static void scr_tls_wrapped_failure(ScrTlsSock *t, int r) {
if (r == MBEDTLS_ERR_SSL_CONN_EOF) {
scr_net_sock_transport_eof(t->sock);
return;
}
if (r == MBEDTLS_ERR_NET_CONN_RESET) {
scr_net_sock_transport_error(t->sock, "read ECONNRESET");
return;
}
const unsigned char *h = t->head;
bool record = t->head_len >= 3 && h[0] >= 20 && h[0] <= 23 && h[1] == 3;
if (t->head_len >= 5 && !record) {
static const char *const http[] = { "GET ", "POST ", "HEAD ", "PATCH", "OPTIO",
"DELET", "TRACE", "PUT ", NULL };
const char *reason = "error:0A00010B:SSL routines:tls_validate_record_header:wrong version number";
const char *code = "ERR_SSL_WRONG_VERSION_NUMBER";
for (size_t i = 0; http[i] != NULL; i++) {
if (memcmp(h, http[i], strlen(http[i])) == 0) {
reason = "error:0A00009C:SSL routines:tls_validate_record_header:http request";
code = "ERR_SSL_HTTP_REQUEST";
}
}
if (memcmp(h, "CONNE", 5) == 0) {
reason = "error:0A00009B:SSL routines:tls_validate_record_header:https proxy request";
code = "ERR_SSL_HTTPS_PROXY_REQUEST";
}
scr_net_sock_transport_error_code(t->sock, reason, code);
return;
}
scr_net_sock_transport_error(t->sock, "server TLS handshake failed");
}
static int scr_tls_handshake(void *tctx) {
ScrTlsSock *t = (ScrTlsSock *)tctx;
if (t->cli != NULL && t->cli->no_protocols) {
@@ -1046,6 +1133,10 @@ static int scr_tls_handshake(void *tctx) {
scr_tls_current = NULL;
if (r == 0) return 1;
if (r == MBEDTLS_ERR_SSL_WANT_READ || r == MBEDTLS_ERR_SSL_WANT_WRITE) return 0;
if (t->srv != NULL && t->wrapped) {
scr_tls_wrapped_failure(t, r);
return -1;
}
if (t->srv != NULL) {
/* Node's server default: 'tlsClientError' with no listener destroys
* the socket without an error event — exactly what portless (no
@@ -1133,11 +1224,31 @@ static void scr_tls_free(void *tctx) {
if (t->srv) scr_tls_srv_release(t->srv);
if (t->cli) scr_tls_cli_free(t->cli);
scr_net_ls_drop(&t->session_ls);
scr_net_ls_drop(&t->secure_ls);
free(t->hello);
scr_secure_ctx_release(t->sni_ctx);
free(t);
}
/* The 'secure' event: after a client's secureConnect listeners, as Node
* emits it. */
static void scr_tls_after_established(void *tctx) {
ScrTlsSock *t = (ScrTlsSock *)tctx;
scr_net_fire0_this(&t->secure_ls, t->sock, SCR_DYNH_NET_SOCKET);
scr_net_ls_drop(&t->secure_ls);
}
/* The closed socket settles: its 'secure' and 'session' listeners can no
* longer fire, and a listener that captures the socket would otherwise
* keep it, and this transport, alive (a handshake that fails never fires
* 'secure'). */
static void scr_tls_settle(void *tctx) {
ScrTlsSock *t = (ScrTlsSock *)tctx;
t->settled = true;
scr_net_ls_drop(&t->secure_ls);
scr_net_ls_drop(&t->session_ls);
}
static const ScrNetTransportOps SCR_TLS_OPS = {
.xread = scr_tls_xread,
.xwrite = scr_tls_xwrite,
@@ -1146,6 +1257,8 @@ static const ScrNetTransportOps SCR_TLS_OPS = {
.pending = scr_tls_pending,
.shutdown_write = scr_tls_shutdown_write,
.free = scr_tls_free,
.settle = scr_tls_settle,
.after_established = scr_tls_after_established,
};
/* ── the server surface ──────────────────────────────────────────────── */
@@ -2088,6 +2201,129 @@ ScrNetServer *scr_https_create_server_dyn(const ScrDyn *opts /*borrowed*/,
return s;
}
/* ── new tls.TLSSocket(socket, options) ───────────────────────────────── */
/* The server-mode wrap: a fresh TLS socket takes over raw's connection
* (scr_net_sock_adopt) and runs the server handshake over it. It serves
* `ctx` (options.secureContext, which the lowering passes apart from the
* record; borrowed, nullable) or the record's cert and key. The record
* reads at runtime like tls.createServer's: ALPNProtocols, minVersion and
* maxVersion apply, and options without a lowering throw the fence. Node
* never starts the handshake of a client-mode TLSSocket (no isServer), so
* that form throws the fence too. +1, or NULL with the exception pending. */
static const char *const SCR_TLS_WRAP_FENCED_OPTIONS[] = {
"server", "session", "requestOCSP", "pskCallback", "enableTrace", NULL,
};
/* The socket-level options of a wrap that serves a secureContext, which
* the server-options walk would otherwise check: requestCert (client
* certificates are not modeled), SNICallback and ALPNCallback fence.
* False with the exception pending. */
static bool scr_tls_wrap_socket_opts_check(const ScrDyn *record, const char *api) {
for (size_t i = 0; record != NULL && i < record->v.obj.len; i++) {
const ScrDynEntry *e = &record->v.obj.entries[i];
if (e->value->kind == SCR_DYN_UNDEF) continue;
if (strcmp(e->key, "requestCert") == 0) {
if (!scr_tls_dyn_truthy(e->value)) continue;
scr_tls_opt_fence(api, "requestCert",
"client-certificate handshakes are not modeled (SEMANTICS.md divergence 55) — requestCert stays at Node's false default");
return false;
}
if (strcmp(e->key, "SNICallback") == 0 || strcmp(e->key, "ALPNCallback") == 0) {
scr_tls_opt_fence(api, e->key,
"isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members");
return false;
}
}
return true;
}
ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrowed*/,
ScrSecureCtx *ctx /*borrowed, nullable*/) {
static const char api[] = "new tls.TLSSocket";
bool is_server = false;
const ScrDyn *record = opts != NULL && opts->kind == SCR_DYN_OBJ ? opts : NULL;
for (size_t i = 0; record != NULL && i < record->v.obj.len; i++) {
const ScrDynEntry *e = &record->v.obj.entries[i];
if (e->value->kind == SCR_DYN_UNDEF) continue;
if (strcmp(e->key, "isServer") == 0) {
is_server = scr_tls_dyn_truthy(e->value);
continue;
}
if (strcmp(e->key, "secureContext") == 0) {
scr_tls_opt_fence(api, "secureContext",
"secureContext lowers as a member of an options object literal");
return NULL;
}
for (size_t k = 0; SCR_TLS_WRAP_FENCED_OPTIONS[k] != NULL; k++) {
if (strcmp(e->key, SCR_TLS_WRAP_FENCED_OPTIONS[k]) == 0) {
scr_tls_opt_fence(api, e->key,
"isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members");
return NULL;
}
}
}
if (!is_server) {
scr_tls_opt_fence(api, "isServer",
"Node starts no handshake for a client-mode TLSSocket; tls.connect({ socket }) runs the client handshake over an open socket");
return NULL;
}
/* Node serves a secureContext as it is: the record's cert, key and
* other context options are not read then. The socket's own options
* still apply, so the ones without a lowering fence either way:
* requestCert asks for a client certificate this runtime cannot
* verify. */
int min_version = 12, max_version = 13;
ScrBytes *cert = NULL, *key = NULL;
if (ctx == NULL) {
if (!scr_tls_versions_dyn(record, &min_version, &max_version)) return NULL;
if (!scr_tls_srv_opts_walk_ex(record, api, &cert, &key, true)) return NULL;
} else if (!scr_tls_wrap_socket_opts_check(record, api)) {
return NULL;
}
char **alpn = NULL;
const ScrDyn *protocols = record != NULL ? scr_dyn_obj_get(record, "ALPNProtocols", 13) : NULL;
if (protocols != NULL && protocols->kind != SCR_DYN_UNDEF && !scr_tls_alpn_dyn(protocols, &alpn)) {
scr_bytes_release(cert);
scr_bytes_release(key);
return NULL;
}
ScrTlsSrv *srv = ctx != NULL ? scr_tls_srv_from_ctx(ctx)
: scr_tls_srv_new((const char *)cert->data, cert->len,
(const char *)key->data, key->len, false);
scr_bytes_release(cert);
scr_bytes_release(key);
srv->alpn = alpn; /* moves */
if (alpn != NULL) mbedtls_ssl_conf_alpn_protocols(&srv->conf, (const char **)srv->alpn);
if (!scr_tls_conf_versions(&srv->conf, min_version, max_version)) {
scr_tls_srv_release(srv);
scr_tls_opt_fence(api, "maxVersion",
"a version range with neither TLSv1.2 nor TLSv1.3 has no lowering");
return NULL;
}
srv->max_tls12 = max_version == 12;
ScrNetSocket *t = scr_net_sock_adopt(raw);
if (t == NULL) {
scr_tls_srv_release(srv);
static const char msg[] =
"new tls.TLSSocket over a socket that is not connected, or is already wrapped, is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
return NULL;
}
ScrTlsSock *ts = calloc(1, sizeof *ts);
if (!ts) scr_tls_oom();
mbedtls_ssl_init(&ts->ssl);
if (mbedtls_ssl_setup(&ts->ssl, &srv->conf) != 0) scr_tls_oom();
ts->sock = t;
ts->srv = srv; /* moves */
ts->wrapped = true;
if (srv->max_tls12) ts->sni_state = SCR_SNI_COLLECT; /* pre-parse the hello */
mbedtls_ssl_set_bio(&ts->ssl, ts, scr_tls_bio_send, scr_tls_bio_recv, NULL);
scr_net_sock_set_transport(t, &SCR_TLS_OPS, ts);
scr_net_sock_adopt_start(t);
return t;
}
/* ── tls.connect (the TLS client socket) ──────────────────────────────── */
/* Node's inspect of a Buffer inside ERR_INVALID_ARG_VALUE's message:
@@ -2239,7 +2475,7 @@ static const char *const SCR_TLS_CONNECT_FENCED_OPTIONS[] = {
"localAddress", "localPort", "lookup", "minDHSize", "noDelay", "onread", "passphrase", "path", "pfx",
"privateKeyEngine", "privateKeyIdentifier", "pskCallback", "readable",
"secureContext", "secureOptions", "secureProtocol", "session",
"sessionIdContext", "sigalgs", "signal", "socket", "ticketKeys",
"sessionIdContext", "sigalgs", "signal", "ticketKeys",
"writable", NULL,
};
@@ -2255,6 +2491,7 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
const ScrDyn *alpn_value = NULL;
const ScrDyn *timeout_value = NULL;
int min_version = 12, max_version = 13;
ScrNetSocket *wrap = NULL; /* options.socket: the open socket to take over */
bool ok = true;
if (opts != NULL && opts->kind != SCR_DYN_UNDEF && opts->kind != SCR_DYN_NULL) {
if (opts->kind != SCR_DYN_OBJ) {
@@ -2322,6 +2559,15 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
timeout_value = scr_tls_dyn_truthy(v) ? v : NULL;
continue;
}
if (strcmp(e->key, "socket") == 0) {
if (v->kind == SCR_DYN_HANDLE && v->v.handle.tag == SCR_DYNH_NET_SOCKET) {
wrap = (ScrNetSocket *)v->v.handle.ptr;
} else {
scr_tls_opt_fence("tls.connect", "socket", "a net.Socket is the implemented socket value");
ok = false;
}
continue;
}
if (strcmp(e->key, "servername") == 0) {
if (v->kind == SCR_DYN_STR) servername = v->v.str;
else {
@@ -2333,7 +2579,7 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
for (size_t k = 0; SCR_TLS_CONNECT_FENCED_OPTIONS[k] != NULL; k++) {
if (strcmp(e->key, SCR_TLS_CONNECT_FENCED_OPTIONS[k]) == 0) {
scr_tls_opt_fence("tls.connect", e->key,
"port/host/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout are the implemented members of a runtime options record");
"port/host/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout are the implemented members of a runtime options record");
ok = false;
break;
}
@@ -2361,7 +2607,7 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
timeout_ms = timeout_value->v.num;
}
}
if (ok && port < 0) {
if (ok && port < 0 && wrap == NULL) {
scr_dyn_arg_type_fail("options.port", "of type number", scr_dyn_undefined());
ok = false;
}
@@ -2377,6 +2623,9 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
* name otherwise (Node's servername default); "" sends no SNI. */
ScrStr *verify_host = dial_host;
ScrStr *localhost_fallback = NULL;
/* over options.socket, the host that socket dialed (Node's socket._host) */
ScrStr *socket_host = verify_host == NULL && wrap != NULL ? scr_net_sock_dial_name(wrap) : NULL;
if (socket_host != NULL) verify_host = socket_host;
if (verify_host == NULL) {
localhost_fallback = scr_str_new("localhost", 9);
verify_host = localhost_fallback;
@@ -2384,9 +2633,25 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
/* Resolve for the dial only (the http client's rule): verify_host above
* already holds the NAME, which is what SNI and certificate verification
* must see. A null dial_host stays null — scr_net_connect's own arm. */
ScrStr *dial_addr = dial_host != NULL ? scr_net_blocking_lookup(dial_host) : NULL;
ScrNetSocket *s = scr_net_connect(port, dial_addr, cb); /* cb fires post-handshake — secureConnect */
if (dial_addr) scr_str_release(dial_addr);
ScrNetSocket *s;
if (wrap != NULL) {
s = scr_net_sock_adopt(wrap);
if (s == NULL) {
scr_tls_alpn_free(alpn);
scr_bytes_release(ca);
scr_closure_release(cb);
scr_str_release(localhost_fallback);
scr_str_release(socket_host);
static const char msg[] =
"tls.connect over a socket that is not connected, or is already wrapped, is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
return NULL;
}
} else {
ScrStr *dial_addr = dial_host != NULL ? scr_net_blocking_lookup(dial_host) : NULL;
s = scr_net_connect(port, dial_addr, cb); /* cb fires post-handshake — secureConnect */
if (dial_addr) scr_str_release(dial_addr);
}
ScrTlsCli *cli = scr_tls_cli_new2(verify_host, reject,
ca != NULL ? (const char *)ca->data : NULL,
ca != NULL ? ca->len : 0, !reject);
@@ -2397,8 +2662,13 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
}
cli->no_protocols = !scr_tls_conf_versions(&cli->conf, min_version, max_version);
scr_tls_cli_wrap(s, cli);
if (wrap != NULL) {
if (cb != NULL) scr_net_sock_on_established(s, cb, true); /* secureConnect */
scr_net_sock_adopt_start(s);
}
if (timeout_ms > 0) scr_net_sock_set_timeout(s, timeout_ms);
scr_str_release(localhost_fallback);
scr_str_release(socket_host);
scr_bytes_release(ca);
return s;
}
@@ -2610,12 +2880,39 @@ ScrDyn *scr_tls_sock_alpn_protocol(ScrNetSocket *s) {
return d;
}
/* secureConnect: a client socket's 'connect' list fires at establishment
* (scr_net.c's transport pump), Node's secureConnect timing. Once TCP is up
* (a socket tls.connect({ socket }) took over is up from the start) the
* listener waits for the handshake instead. Server-side sockets never emit
* it. */
static void scr_tls_add_secure_connect(ScrNetSocket *s, ScrTlsSock *t, ScrClosure *cb, bool once) {
if (t->srv != NULL) {
scr_closure_release(cb);
return;
}
if (scr_net_sock_connecting(s)) scr_net_sock_on_connect(s, cb, once);
else scr_net_sock_on_established(s, cb, once);
}
void scr_tls_sock_on_secure_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) {
if (scr_tls_sock_of(s) == NULL) {
ScrTlsSock *t = scr_tls_sock_of(s);
if (t == NULL) {
scr_closure_release(cb); /* a plain socket never fires it, like Node */
return;
}
scr_net_sock_on_connect(s, cb, once); /* fires at establishment — secureConnect timing */
scr_tls_add_secure_connect(s, t, cb, once);
}
/* The 'secure' event: the handshake completed (after secureConnect on a
* client). A plain socket never emits it, and a registration after the
* handshake comes too late, as in Node. */
void scr_tls_sock_on_secure(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) {
ScrTlsSock *t = scr_tls_sock_of(s);
if (t == NULL || t->established || t->settled) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&t->secure_ls, cb, NULL, once);
}
void scr_tls_sock_on_session(ScrNetSocket *s, ScrClosure *cb /*moves*/, void *fn, bool once) {
@@ -2624,6 +2921,10 @@ void scr_tls_sock_on_session(ScrNetSocket *s, ScrClosure *cb /*moves*/, void *fn
scr_closure_release(cb); /* a plain socket never fires it, like Node */
return;
}
if (t->settled) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&t->session_ls, cb, fn, once);
if (t->established) scr_tls_fire_session(t); /* the ticket already arrived */
}
@@ -2660,9 +2961,11 @@ static bool scr_tls_dynh_on(ScrNetSocket *s, const char *event, const ScrDyn *cb
ScrTlsSock *t = scr_tls_sock_of(s);
if (t == NULL) return false;
if (strcmp(event, "secureConnect") == 0) {
/* a TLS socket's 'connect' list fires at establishment — exactly
* Node's secureConnect timing (scr_net.c's transport pump) */
scr_net_sock_on_connect(s, scr_dyn_listener_closure0(cb), once);
scr_tls_add_secure_connect(s, t, scr_dyn_listener_closure0(cb), once);
return true;
}
if (strcmp(event, "secure") == 0) {
scr_tls_sock_on_secure(s, scr_dyn_listener_closure0(cb), once);
return true;
}
if (strcmp(event, "session") == 0) {
+9
View File
@@ -62,6 +62,10 @@ ScrDyn *scr_tls_sock_get_cipher(ScrNetSocket *s); /* +1: object, undefined or
ScrDyn *scr_tls_sock_alpn_protocol(ScrNetSocket *s); /* +1: string, false or null */
void scr_tls_sock_on_secure_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once);
void scr_tls_sock_on_session(ScrNetSocket *s, ScrClosure *cb /*moves*/, void *fn, bool once);
/* The 'secure' event: fires when the handshake completes (after the
* secureConnect listeners on a client); released unread on a plain socket
* or after the handshake. */
void scr_tls_sock_on_secure(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once);
/* ── h2 over TLS (scr_tls.c ↔ scr_http2.c): the ALPN=h2 server wrap and
* the https-authority client wrap. scr_http2.c calls both directly —
@@ -93,6 +97,11 @@ ScrSecureCtx *scr_tls_create_secure_context(const char *cert, size_t cert_len, c
* validations first, then the pem walk (+1, or NULL with the exception
* pending). Borrowed. */
ScrSecureCtx *scr_tls_create_secure_context_dyn(const ScrDyn *opts);
/* new tls.TLSSocket(socket, options) in server mode: a fresh TLS socket
* takes over socket's connection and runs the server handshake, serving
* ctx (options.secureContext; borrowed, nullable) or the options record's
* cert and key. +1, or NULL with the exception pending. */
ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrowed*/, ScrSecureCtx *ctx /*borrowed, nullable*/);
/* tls.getCACertificates(type): validateString + the documented name set,
* then the compiler-rendered fence — always leaves an exception pending. */
void scr_tls_ca_certs_chk(const ScrDyn *type, const ScrStr *fence);
+152 -62
View File
@@ -23,6 +23,7 @@
* compiler links the unit only into programs that call these methods
* (the webcrypto feature). */
#include "scr_runtime.h"
#include "scr_webcrypto.h"
#include <math.h>
#include <stdarg.h>
@@ -58,13 +59,12 @@ static const char *const scr_wc_key_names[] = {
"MLKEM768-X25519", "MLKEM1024-P384", "RSA-OAEP", "RSA-PSS", "RSASSA-PKCS1-v1_5",
"X25519", "X448",
};
static const char *const scr_wc_digest_names[] = {
const char *const scr_wc_digest_names[] = {
"SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384",
"SHA3-512", "cSHAKE128", "cSHAKE256", "KT128", "KT256", "TurboSHAKE128",
"TurboSHAKE256",
};
#define SCR_WC_COUNT(names) (sizeof(names) / sizeof((names)[0]))
/* The implemented hashes: WebCrypto name and node:crypto digest name. */
static const struct {
@@ -90,7 +90,7 @@ static bool scr_wc_ascii_ieq(const ScrStr *s, const char *name) {
}
/* The canonical registered spelling of `name`, or NULL. */
static const char *scr_wc_lookup(const char *const *names, size_t count, const ScrStr *name) {
const char *scr_wc_lookup(const char *const *names, size_t count, const ScrStr *name) {
for (size_t i = 0; i < count; i++)
if (scr_wc_ascii_ieq(name, names[i])) return names[i];
return NULL;
@@ -104,11 +104,17 @@ static const char *scr_wc_native_hash(const char *name) {
/* ── errors ────────────────────────────────────────────────────────── */
static const char *const scr_wc_ordinals[] = {"1st", "2nd", "3rd", "4th", "5th", "6th", "7th"};
const char *const scr_wc_ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"};
static void scr_wc_type_error(const char *code, const char *fmt, ...)
__attribute__((format(printf, 2, 3)));
static void scr_wc_type_error(const char *code, const char *fmt, ...) {
/* An argument's ordinal: unwrapKey's seven arguments run past the shared
* table. */
static const char *scr_wc_ordinal(size_t index) {
static const char *const later[] = {"6th", "7th"};
size_t shared = SCR_WC_COUNT(scr_wc_ordinals);
return index < shared ? scr_wc_ordinals[index] : later[index - shared];
}
void scr_wc_type_error(const char *code, const char *fmt, ...) {
char message[512];
va_list ap;
va_start(ap, fmt);
@@ -120,7 +126,7 @@ static void scr_wc_type_error(const char *code, const char *fmt, ...) {
}
/* An operation or algorithm Node supports that this slice does not. */
static void scr_wc_unsupported(const char *method, const char *what) {
void scr_wc_unsupported(const char *method, const char *what) {
char message[256];
int n = snprintf(message, sizeof message, "crypto.subtle.%s with %s is not supported yet", method,
what);
@@ -128,7 +134,7 @@ static void scr_wc_unsupported(const char *method, const char *what) {
scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020");
}
static void scr_wc_unsupported_algorithm(const char *method, const char *name) {
void scr_wc_unsupported_algorithm(const char *method, const char *name) {
char what[96];
snprintf(what, sizeof what, "the %s algorithm", name);
scr_wc_unsupported(method, what);
@@ -138,7 +144,7 @@ static void scr_wc_unsupported_algorithm(const char *method, const char *name) {
* Each answers NULL/false with the TypeError pending. `prefix` is
* "Failed to execute '<method>' on 'SubtleCrypto'". */
static bool scr_wc_is_object(const ScrDyn *v) {
bool scr_wc_is_object(const ScrDyn *v) {
switch (v->kind) {
case SCR_DYN_UNDEF:
case SCR_DYN_NULL:
@@ -154,14 +160,14 @@ static bool scr_wc_is_object(const ScrDyn *v) {
}
/* A typed reference capsule as its plain view; other values retained. */
static ScrDyn *scr_wc_plain(ScrDyn *v) {
ScrDyn *scr_wc_plain(ScrDyn *v) {
if (v->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(v);
return scr_dyn_retain(v);
}
/* BufferSource: the bytes of a TypedArray, DataView, Buffer or
* ArrayBuffer as a u8 view (+1). */
static ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value) {
ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value) {
ScrDyn *plain = scr_wc_plain(value);
if (!plain) return NULL;
ScrBytes *view = NULL;
@@ -171,21 +177,21 @@ static ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *
scr_wc_type_error("ERR_INVALID_ARG_TYPE",
"%s: %s argument is not instance of ArrayBuffer, Buffer, TypedArray, or "
"DataView.",
prefix, scr_wc_ordinals[index]);
prefix, scr_wc_ordinal(index));
}
scr_dyn_release(plain);
return view;
}
/* DOMString conversion (+1). */
static ScrStr *scr_wc_dom_string(const ScrDyn *value) { return scr_dyn_string_coerce_js(value); }
ScrStr *scr_wc_dom_string(const ScrDyn *value) { return scr_dyn_string_coerce_js(value); }
static const char *const scr_wc_key_formats[] = {
"raw", "raw-public", "raw-seed", "raw-secret", "raw-private", "pkcs8", "spki", "jwk",
};
/* KeyFormat: the canonical enum spelling, or NULL. */
static const char *scr_wc_key_format(const char *prefix, ScrDyn *value) {
const char *scr_wc_key_format(const char *prefix, ScrDyn *value) {
ScrStr *s = scr_wc_dom_string(value);
if (!s) return NULL;
for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_key_formats); i++) {
@@ -203,22 +209,11 @@ static const char *scr_wc_key_format(const char *prefix, ScrDyn *value) {
}
/* Node's canonical usage order; the bit is the index. */
static const char *const scr_wc_usages[] = {
const char *const scr_wc_usages[] = {
"encrypt", "decrypt", "sign", "verify",
"deriveKey", "deriveBits", "wrapKey", "unwrapKey",
"encapsulateKey", "encapsulateBits", "decapsulateKey", "decapsulateBits",
};
enum {
SCR_WC_USAGE_ENCRYPT = 1u << 0,
SCR_WC_USAGE_DECRYPT = 1u << 1,
SCR_WC_USAGE_SIGN = 1u << 2,
SCR_WC_USAGE_VERIFY = 1u << 3,
SCR_WC_USAGE_DERIVE_KEY = 1u << 4,
SCR_WC_USAGE_DERIVE_BITS = 1u << 5,
SCR_WC_USAGE_WRAP_KEY = 1u << 6,
SCR_WC_USAGE_UNWRAP_KEY = 1u << 7,
};
/* The usage index of a KeyUsage string, or -1. */
static int scr_wc_usage_index(const ScrStr *s) {
for (size_t k = 0; k < SCR_WC_COUNT(scr_wc_usages); k++)
@@ -228,12 +223,12 @@ static int scr_wc_usage_index(const ScrStr *s) {
}
/* sequence<KeyUsage> over an array: the usage mask (-1 on failure). */
static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) {
long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) {
ScrDyn *plain = scr_wc_plain(value);
if (!plain) return -1;
if (plain->kind != SCR_DYN_ARR) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s argument cannot be converted to sequence.",
prefix, scr_wc_ordinals[index]);
prefix, scr_wc_ordinal(index));
scr_dyn_release(plain);
return -1;
}
@@ -251,7 +246,7 @@ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) {
if (k < 0) {
scr_wc_type_error("ERR_INVALID_ARG_VALUE",
"%s: %s argument[%.0f] '%.*s' is not a valid enum value of type KeyUsage.",
prefix, scr_wc_ordinals[index], i, (int)(s->len > 200 ? 200 : s->len),
prefix, scr_wc_ordinal(index), i, (int)(s->len > 200 ? 200 : s->len),
s->data);
scr_str_release(s);
scr_dyn_release(plain);
@@ -274,9 +269,9 @@ typedef struct {
double length;
} ScrWcAlgorithm;
static const char scr_wc_normalize_prefix[] = "Failed to normalize algorithm";
const char scr_wc_normalize_prefix[] = "Failed to normalize algorithm";
static void scr_wc_missing_member(const char *dict, const char *member) {
void scr_wc_missing_member(const char *dict, const char *member) {
scr_wc_type_error("ERR_MISSING_OPTION",
"%s: passed algorithm cannot be converted to '%s' because '%s' is required in "
"'%s'.",
@@ -284,7 +279,7 @@ static void scr_wc_missing_member(const char *dict, const char *member) {
}
/* Reads `key` off an algorithm dictionary (+1; undefined when absent). */
static ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) {
ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) {
if (dict->kind != SCR_DYN_OBJ) return scr_dyn_retain(scr_dyn_undefined());
ScrDyn *v = scr_dyn_obj_read(dict, key, strlen(key));
return v ? v : scr_dyn_retain(scr_dyn_undefined());
@@ -315,7 +310,7 @@ static bool scr_wc_enforce_range(const char *prefix, const char *context, const
/* The name an AlgorithmIdentifier carries (+1), with the Algorithm
* dictionary's missing-name TypeError. `dict` names the dictionary the
* value converts to. */
static ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict) {
ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict) {
if (!scr_wc_is_object(identifier)) return scr_wc_dom_string(identifier);
ScrDyn *plain = scr_wc_plain(identifier);
if (!plain) return NULL;
@@ -330,7 +325,7 @@ static ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict) {
/* The digest a HashAlgorithmIdentifier names: canonical name, or NULL
* with NotSupportedError / the SC2020 refusal pending. */
static const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *method) {
const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *method) {
ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm");
if (!name) return NULL;
const char *canonical = scr_wc_lookup(scr_wc_digest_names, SCR_WC_COUNT(scr_wc_digest_names), name);
@@ -528,10 +523,10 @@ static ScrCryptoKey *scr_crypto_key_of(const ScrDyn *value) {
bool scr_webcrypto_is_crypto_key(const ScrDyn *value) {
if (value->kind == SCR_DYN_TYPED_REF) return false;
return scr_crypto_key_of(value) != NULL;
return scr_crypto_key_of(value) != NULL || (scr_wc_asymmetric && scr_wc_asymmetric->is_key(value));
}
static ScrDyn *scr_wc_cstr(const char *s) {
ScrDyn *scr_wc_cstr(const char *s) {
ScrStr *str = scr_str_new(s, strlen(s));
ScrDyn *out = scr_dyn_new_str(str);
scr_str_release(str);
@@ -871,7 +866,55 @@ static ScrBytes *scr_wc_jwk_secret(const ScrWcJwk *jwk, bool extractable, long u
* Each takes its argument vector as one dyn array (borrowed) and answers
* a +1 promise. */
static ScrPromise *scr_wc_rejected(void) {
const ScrWcAsymmetric *scr_wc_asymmetric = NULL;
static const char *const scr_wc_asymmetric_names[] = {"ECDSA", "Ed25519", "RSA-PSS", "RSASSA-PKCS1-v1_5"};
/* Whether the AlgorithmIdentifier at args[index] names ECDSA,
* RSASSA-PKCS1-v1_5, RSA-PSS or Ed25519, read without side effects or errors: a
* string, or the name member of an object (a statically typed one through
* its plain view). A value that does not read cleanly takes the HMAC path,
* which reports its errors. */
static const char *scr_wc_asymmetric_name(const ScrDyn *args, size_t index) {
if ((size_t)scr_dyn_arr_len(args) <= index) return NULL;
ScrDyn *identifier = scr_dyn_arr_at(args, (double)index);
ScrDyn *plain = scr_wc_plain(identifier);
if (!plain) scr_exc_clear();
const char *found = NULL;
if (plain) {
const ScrDyn *name = plain;
if (plain->kind == SCR_DYN_OBJ) name = scr_dyn_obj_get(plain, "name", 4);
if (name && name->kind == SCR_DYN_STR)
found = scr_wc_lookup(scr_wc_asymmetric_names, SCR_WC_COUNT(scr_wc_asymmetric_names),
name->v.str);
scr_dyn_release(plain);
}
scr_dyn_release(identifier);
return found;
}
/* Whether `value` is an ECDSA, Ed25519 or RSA CryptoKey, which
* scr_webcrypto_pki.c holds; wrapKey and unwrapKey wrap secret keys only. */
static bool scr_wc_is_asymmetric_key(const ScrDyn *value) {
return scr_wc_asymmetric && scr_wc_asymmetric->is_key(value);
}
static bool scr_wc_is_asymmetric_algorithm(const char *name) {
for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_asymmetric_names); i++)
if (strcmp(name, scr_wc_asymmetric_names[i]) == 0) return true;
return false;
}
/* The asymmetric algorithms run in scr_webcrypto_pki.c, which the compiler
* links when a string literal in the program names one of them. */
static ScrPromise *scr_wc_asymmetric_missing(const char *method, const char *name) {
char what[160];
snprintf(what, sizeof what, "the %s algorithm named only by a computed string", name);
scr_wc_unsupported(method, what);
return scr_wc_rejected();
}
ScrPromise *scr_wc_rejected(void) {
ScrPromise *p = scr_promise_new();
scr_promise_reject_pending(p);
return p;
@@ -892,7 +935,7 @@ static ScrPromise *scr_wc_deferred(ScrDyn *value) {
/* The argument count check every method starts with (after the receiver
* check the compiler's call shape already guarantees). */
static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) {
bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) {
size_t argc = (size_t)scr_dyn_arr_len(args);
if (argc >= required) return true;
scr_wc_type_error("ERR_MISSING_ARGS", "%s: %zu arguments required, but only %zu present.", prefix,
@@ -900,15 +943,12 @@ static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t requi
return false;
}
typedef struct {
ScrDyn *v[5];
} ScrWcArgs;
static void scr_wc_args_take(ScrWcArgs *a, const ScrDyn *args, size_t n) {
void scr_wc_args_take(ScrWcArgs *a, const ScrDyn *args, size_t n) {
for (size_t i = 0; i < n; i++) a->v[i] = scr_dyn_arr_at(args, (double)i);
}
static void scr_wc_args_drop(ScrWcArgs *a, size_t n) {
void scr_wc_args_drop(ScrWcArgs *a, size_t n) {
for (size_t i = 0; i < n; i++) scr_dyn_release(a->v[i]);
}
@@ -1098,6 +1138,11 @@ done:
}
ScrPromise *scr_webcrypto_import_key(ScrDyn *args) {
const char *asymmetric = scr_wc_asymmetric_name(args, 2);
if (asymmetric) {
return scr_wc_asymmetric ? scr_wc_asymmetric->import_key(args)
: scr_wc_asymmetric_missing("importKey", asymmetric);
}
ScrDyn *key = scr_wc_import(args);
if (!key) return scr_wc_rejected();
return scr_wc_resolved(key);
@@ -1106,8 +1151,14 @@ ScrPromise *scr_webcrypto_import_key(ScrDyn *args) {
/* generateKey(algorithm, extractable, keyUsages) for HMAC and AES keys:
* random bytes of the requested length (an HMAC key defaults to its
* hash's block size; a length that is not a whole number of bytes clears
* the final byte's unused low bits). */
* the final byte's unused low bits). ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and
* RSA-PSS key pairs generate in scr_webcrypto_pki.c. */
ScrPromise *scr_webcrypto_generate_key(ScrDyn *args) {
const char *asymmetric = scr_wc_asymmetric_name(args, 0);
if (asymmetric) {
return scr_wc_asymmetric ? scr_wc_asymmetric->generate_key(args)
: scr_wc_asymmetric_missing("generateKey", asymmetric);
}
static const char prefix[] = "Failed to execute 'generateKey' on 'SubtleCrypto'";
if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected();
ScrWcArgs a;
@@ -1193,8 +1244,10 @@ static ScrStr *scr_wc_jwk_k(const ScrCryptoKey *key) {
}
/* exportKeySync for secret keys: raw and raw-secret answer an ArrayBuffer
* of the key bytes, jwk an object { key_ops, ext, alg, kty, k }. */
static ScrDyn *scr_wc_export(const ScrDyn *args) {
* of the key bytes, jwk an object { key_ops, ext, alg, kty, k }. An ECDSA,
* Ed25519 or RSA key sets *asymmetric after the format check instead
* (scr_webcrypto_pki.c exports it). */
static ScrDyn *scr_wc_export(const ScrDyn *args, bool *asymmetric) {
static const char prefix[] = "Failed to execute 'exportKey' on 'SubtleCrypto'";
if (!scr_wc_required(prefix, args, 2)) return NULL;
ScrWcArgs a;
@@ -1203,6 +1256,10 @@ static ScrDyn *scr_wc_export(const ScrDyn *args) {
const char *format = scr_wc_key_format(prefix, a.v[0]);
if (!format) goto done;
ScrCryptoKey *key = scr_crypto_key_of(a.v[1]);
if (!key && scr_wc_asymmetric && scr_wc_asymmetric->is_key(a.v[1])) {
*asymmetric = true;
goto done;
}
if (!key) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix);
goto done;
@@ -1230,7 +1287,9 @@ done:
}
ScrPromise *scr_webcrypto_export_key(ScrDyn *args) {
ScrDyn *result = scr_wc_export(args);
bool asymmetric = false;
ScrDyn *result = scr_wc_export(args, &asymmetric);
if (asymmetric) return scr_wc_asymmetric->export_key(args);
if (!result) return scr_wc_rejected();
return scr_wc_resolved(result);
}
@@ -1255,7 +1314,8 @@ static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char ma
scr_str_release(s);
}
ScrCryptoKey *key = scr_crypto_key_of(a.v[1]);
if (!key) {
bool asymmetric_key = !key && scr_wc_asymmetric && scr_wc_asymmetric->is_key(a.v[1]);
if (!key && !asymmetric_key) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix);
goto done;
}
@@ -1273,7 +1333,7 @@ static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char ma
scr_wc_unsupported_algorithm(method, algorithm.name);
goto done;
}
if (strcmp(key->name, algorithm.name) != 0) {
if (asymmetric_key || strcmp(key->name, algorithm.name) != 0) {
scr_throw_domex("InvalidAccessError", "Key algorithm mismatch");
goto done;
}
@@ -1297,6 +1357,11 @@ done:
}
ScrPromise *scr_webcrypto_sign(ScrDyn *args) {
const char *asymmetric = scr_wc_asymmetric_name(args, 0);
if (asymmetric) {
return scr_wc_asymmetric ? scr_wc_asymmetric->sign(args)
: scr_wc_asymmetric_missing("sign", asymmetric);
}
unsigned char mac[64];
size_t mac_len = 0;
if (!scr_wc_sign_verify(args, false, mac, &mac_len, NULL)) return scr_wc_rejected();
@@ -1319,6 +1384,11 @@ static void scr_wc_settle_verified(ScrClosure *cb, ScrError *error, ScrBytes *va
}
ScrPromise *scr_webcrypto_verify(ScrDyn *args) {
const char *asymmetric = scr_wc_asymmetric_name(args, 0);
if (asymmetric) {
return scr_wc_asymmetric ? scr_wc_asymmetric->verify(args)
: scr_wc_asymmetric_missing("verify", asymmetric);
}
unsigned char mac[64];
size_t mac_len = 0;
ScrBytes *signature = NULL;
@@ -1619,7 +1689,9 @@ ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn a
ScrPromise *result = NULL;
if (!scr_wc_algorithm_identifier(a.v[0])) goto done;
ScrCryptoKey *key = scr_crypto_key_of(a.v[1]);
if (!key) {
/* an ECDSA, Ed25519 or RSA key (scr_webcrypto_pki.c) is no cipher key */
bool asymmetric_key = !key && scr_wc_asymmetric && scr_wc_asymmetric->is_key(a.v[1]);
if (!key && !asymmetric_key) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix);
goto done;
}
@@ -1627,7 +1699,7 @@ ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn a
if (!data) goto done;
const char *name = scr_wc_normalize_cipher(a.v[0], method, &params);
if (!name) goto done;
if (strcmp(key->name, name) != 0) {
if (asymmetric_key || strcmp(key->name, name) != 0) {
scr_throw_domex("InvalidAccessError", "Key algorithm mismatch");
goto done;
}
@@ -1717,19 +1789,20 @@ ScrPromise *scr_webcrypto_wrap(ScrDyn *args, ScrWebCryptoAesFn aes) {
const char *format = scr_wc_key_format(prefix, a.v[0]);
if (!format) goto done;
ScrCryptoKey *key = scr_crypto_key_of(a.v[1]);
if (!key) {
if (!key && !scr_wc_is_asymmetric_key(a.v[1])) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix);
goto done;
}
ScrCryptoKey *wrapping = scr_crypto_key_of(a.v[2]);
if (!wrapping) {
if (!wrapping && !scr_wc_is_asymmetric_key(a.v[2])) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 3rd argument is not of type CryptoKey.", prefix);
goto done;
}
if (!scr_wc_algorithm_identifier(a.v[3])) goto done;
const char *name = scr_wc_normalize_wrap(a.v[3], "wrapKey", &params);
if (!name) goto done;
if (strcmp(wrapping->name, name) != 0) {
/* An ECDSA, Ed25519 or RSA key never has an AES algorithm. */
if (!wrapping || strcmp(wrapping->name, name) != 0) {
scr_throw_domex("InvalidAccessError", "Key algorithm mismatch");
goto done;
}
@@ -1737,6 +1810,10 @@ ScrPromise *scr_webcrypto_wrap(ScrDyn *args, ScrWebCryptoAesFn aes) {
scr_throw_domex("InvalidAccessError", "Unable to use this key to wrapKey");
goto done;
}
if (!key) {
scr_wc_unsupported("wrapKey", "an ECDSA, Ed25519 or RSA key");
goto done;
}
if (!scr_wc_exportable(format, key)) goto done;
payload = scr_wc_wrap_payload(format, key, strcmp(name, "AES-KW") == 0);
result = scr_wc_run_cipher(aes, name, false, wrapping, &params, payload);
@@ -1782,7 +1859,7 @@ ScrPromise *scr_webcrypto_unwrap(ScrDyn *args, ScrWebCryptoAesFn aes) {
wrapped = scr_wc_buffer_source(prefix, 1, v[1]);
if (!wrapped) goto done;
ScrCryptoKey *unwrapping = scr_crypto_key_of(v[2]);
if (!unwrapping) {
if (!unwrapping && !scr_wc_is_asymmetric_key(v[2])) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 3rd argument is not of type CryptoKey.", prefix);
goto done;
}
@@ -1796,7 +1873,7 @@ ScrPromise *scr_webcrypto_unwrap(ScrDyn *args, ScrWebCryptoAesFn aes) {
if (!scr_wc_normalize(v[4], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names),
SCR_WC_OP_IMPORT, &imported))
goto done;
if (strcmp(unwrapping->name, name) != 0) {
if (!unwrapping || strcmp(unwrapping->name, name) != 0) {
scr_throw_domex("InvalidAccessError", "Key algorithm mismatch");
goto done;
}
@@ -1804,6 +1881,10 @@ ScrPromise *scr_webcrypto_unwrap(ScrDyn *args, ScrWebCryptoAesFn aes) {
scr_throw_domex("InvalidAccessError", "Unable to use this key to unwrapKey");
goto done;
}
if (scr_wc_is_asymmetric_algorithm(imported.name)) {
scr_wc_unsupported_algorithm("unwrapKey", imported.name);
goto done;
}
if (!scr_wc_max_length(wrapped, "data")) goto done;
ScrBytes *bytes = scr_wc_cipher_bytes(aes, name, true, unwrapping, &params, wrapped);
if (!bytes) {
@@ -2002,7 +2083,8 @@ static ScrBytes *scr_wc_kdf_derive(const ScrWcKdfParams *params, const ScrCrypto
/* The base key checks deriveBits and deriveKey share. */
static bool scr_wc_check_base_key(const ScrCryptoKey *key, const char *algorithm, unsigned usage,
const char *usage_name) {
if (!(key->usages & usage)) {
/* NULL: an asymmetric key, which never has a derive usage */
if (key == NULL || !(key->usages & usage)) {
char message[64];
snprintf(message, sizeof message, "baseKey does not have %s usage", usage_name);
scr_throw_domex("InvalidAccessError", message);
@@ -2028,7 +2110,9 @@ ScrPromise *scr_webcrypto_derive_bits(ScrDyn *args) {
ScrPromise *result = NULL;
if (!scr_wc_algorithm_identifier(a.v[0])) goto done;
ScrCryptoKey *key = scr_crypto_key_of(a.v[1]);
if (!key) {
/* an ECDSA, Ed25519 or RSA key (scr_webcrypto_pki.c) has no derive usage */
bool asymmetric_key = !key && scr_wc_asymmetric && scr_wc_asymmetric->is_key(a.v[1]);
if (!key && !asymmetric_key) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix);
goto done;
}
@@ -2038,7 +2122,9 @@ ScrPromise *scr_webcrypto_derive_bits(ScrDyn *args) {
if (!null_length && !scr_wc_enforce_range(prefix, "3rd argument", a.v[2], 4294967295.0, &length))
goto done;
if (!scr_wc_normalize_kdf(a.v[0], "deriveBits", &params)) goto done;
if (!scr_wc_check_base_key(key, params.name, SCR_WC_USAGE_DERIVE_BITS, "deriveBits")) goto done;
if (!scr_wc_check_base_key(asymmetric_key ? NULL : key, params.name, SCR_WC_USAGE_DERIVE_BITS,
"deriveBits"))
goto done;
if (!scr_wc_kdf_length(&params, null_length, length)) goto done;
if (length == 0) {
ScrBytes *empty = scr_bytes_new(SCR_BYTES_U8, 0);
@@ -2099,7 +2185,9 @@ ScrPromise *scr_webcrypto_derive_key(ScrDyn *args) {
ScrPromise *result = NULL;
if (!scr_wc_algorithm_identifier(a.v[0])) goto done;
ScrCryptoKey *key = scr_crypto_key_of(a.v[1]);
if (!key) {
/* an ECDSA, Ed25519 or RSA key (scr_webcrypto_pki.c) has no derive usage */
bool asymmetric_key = !key && scr_wc_asymmetric && scr_wc_asymmetric->is_key(a.v[1]);
if (!key && !asymmetric_key) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix);
goto done;
}
@@ -2120,7 +2208,9 @@ ScrPromise *scr_webcrypto_derive_key(ScrDyn *args) {
scr_wc_unsupported_algorithm("deriveKey", imported.name);
goto done;
}
if (!scr_wc_check_base_key(key, params.name, SCR_WC_USAGE_DERIVE_KEY, "deriveKey")) goto done;
if (!scr_wc_check_base_key(asymmetric_key ? NULL : key, params.name, SCR_WC_USAGE_DERIVE_KEY,
"deriveKey"))
goto done;
if (!scr_wc_kdf_length(&params, null_length, length)) goto done;
ScrBytes *bits = scr_wc_kdf_derive(&params, key, length);
if (!bits) goto done;
+67
View File
@@ -0,0 +1,67 @@
/* The Web IDL and error helpers of scr_webcrypto.c that the asymmetric
* algorithms (scr_webcrypto_pki.c) share, and the table through which
* scr_webcrypto.c reaches them when that unit is linked. */
#ifndef SCR_WEBCRYPTO_H
#define SCR_WEBCRYPTO_H
#include <stdbool.h>
#include <stddef.h>
#define SCR_WC_COUNT(names) (sizeof(names) / sizeof((names)[0]))
enum {
SCR_WC_USAGE_ENCRYPT = 1u << 0,
SCR_WC_USAGE_DECRYPT = 1u << 1,
SCR_WC_USAGE_SIGN = 1u << 2,
SCR_WC_USAGE_VERIFY = 1u << 3,
SCR_WC_USAGE_DERIVE_KEY = 1u << 4,
SCR_WC_USAGE_DERIVE_BITS = 1u << 5,
SCR_WC_USAGE_WRAP_KEY = 1u << 6,
SCR_WC_USAGE_UNWRAP_KEY = 1u << 7,
};
/* Node's canonical usage order; the bit is the index. */
extern const char *const scr_wc_usages[12];
extern const char *const scr_wc_ordinals[5];
extern const char scr_wc_normalize_prefix[];
extern const char *const scr_wc_digest_names[13];
typedef struct {
ScrDyn *v[5];
} ScrWcArgs;
void scr_wc_type_error(const char *code, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
void scr_wc_unsupported(const char *method, const char *what);
void scr_wc_unsupported_algorithm(const char *method, const char *name);
bool scr_wc_is_object(const ScrDyn *v);
ScrDyn *scr_wc_plain(ScrDyn *v);
ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value);
ScrStr *scr_wc_dom_string(const ScrDyn *value);
const char *scr_wc_key_format(const char *prefix, ScrDyn *value);
long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value);
void scr_wc_missing_member(const char *dict, const char *member);
ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key);
ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict);
const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *method);
const char *scr_wc_lookup(const char *const *names, size_t count, const ScrStr *name);
ScrDyn *scr_wc_cstr(const char *s);
ScrPromise *scr_wc_rejected(void);
bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required);
void scr_wc_args_take(ScrWcArgs *a, const ScrDyn *args, size_t n);
void scr_wc_args_drop(ScrWcArgs *a, size_t n);
/* ECDSA, RSASSA-PKCS1-v1_5 and RSA-PSS (scr_webcrypto_pki.c, linked with
* the pki unit): each takes the method's argument vector (borrowed) and
* answers a +1 promise. NULL until that unit installs it. */
typedef struct {
ScrPromise *(*generate_key)(const ScrDyn *args);
ScrPromise *(*import_key)(const ScrDyn *args);
ScrPromise *(*export_key)(const ScrDyn *args);
ScrPromise *(*sign)(const ScrDyn *args);
ScrPromise *(*verify)(const ScrDyn *args);
bool (*is_key)(const ScrDyn *value);
} ScrWcAsymmetric;
extern const ScrWcAsymmetric *scr_wc_asymmetric;
#endif
+841
View File
@@ -0,0 +1,841 @@
/* WebCrypto's ECDSA, RSASSA-PKCS1-v1_5, RSA-PSS and Ed25519 algorithms:
* crypto.subtle.generateKey, importKey (spki, pkcs8, jwk, and raw for ECDSA
* and Ed25519 public keys), exportKey, sign and verify, and the CryptoKey
* values of these keys. ECDSA keys are on P-256, P-384 and P-521; ECDSA
* signatures are IEEE P1363 r || s.
*
* Argument conversion, algorithm normalization and the DOMException
* shapes follow Node 24's lib/internal/crypto/webcrypto.js, ec.js and
* rsa.js, through the Web IDL helpers of scr_webcrypto.c. The keys and
* signatures come from the pki unit (scr_pki.c). This unit installs
* itself into scr_webcrypto.c's dispatch table at startup; the compiler
* links it when a program uses WebCrypto and names one of these
* algorithms. All arguments are borrowed. */
#include "scr_runtime.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "scr_pki.h"
#include "scr_webcrypto.h"
typedef enum { SW_ECDSA, SW_RSASSA, SW_PSS, SW_ED25519 } SwKind;
static const char *const sw_names[] = {"ECDSA", "RSASSA-PKCS1-v1_5", "RSA-PSS", "Ed25519"};
static const char *const sw_curves[] = {"P-256", "P-384", "P-521"};
/* ── CryptoKey ────────────────────────────────────────────────────────── */
typedef struct SwKey {
size_t rc;
ScrPkiKey *key;
SwKind kind;
const char *hash; /* RSA keys: the WebCrypto hash name */
unsigned usages;
bool extractable;
ScrDyn *algorithm; /* cached getter results, or NULL */
ScrDyn *usage_list;
} SwKey;
static void *sw_key_retain(void *p) {
((SwKey *)p)->rc++;
return p;
}
static void sw_key_release(void *p) {
SwKey *k = p;
if (!k || --k->rc != 0) return;
scr_pki_key_release(k->key);
if (k->algorithm) scr_dyn_release(k->algorithm);
if (k->usage_list) scr_dyn_release(k->usage_list);
free(k);
}
static SwKey *sw_key_of(const ScrDyn *value) {
if (value->kind != SCR_DYN_HANDLE || value->v.handle.tag != SCR_DYNH_ASYMMETRIC_CRYPTO_KEY) return NULL;
return value->v.handle.ptr;
}
static bool sw_is_key(const ScrDyn *value) { return sw_key_of(value) != NULL; }
static ScrDyn *sw_key_algorithm(SwKey *k) {
if (!k->algorithm) {
ScrDyn *algorithm = scr_dyn_new_obj();
scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr(sw_names[k->kind]));
if (k->kind == SW_ED25519) {
/* { name } only */
} else if (k->kind == SW_ECDSA) {
scr_dyn_obj_set(algorithm, "namedCurve", 10, scr_wc_cstr(scr_pki_key_curve(k->key)));
} else {
scr_dyn_obj_set(algorithm, "modulusLength", 13, scr_dyn_new_num((double)scr_pki_key_bits(k->key)));
ScrBytes *exponent = scr_pki_key_exponent(k->key);
scr_dyn_obj_set(algorithm, "publicExponent", 14, scr_dyn_new_bytes(exponent));
scr_bytes_release(exponent);
ScrDyn *hash = scr_dyn_new_obj();
scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(k->hash));
scr_dyn_obj_set(algorithm, "hash", 4, hash);
}
k->algorithm = algorithm;
}
return scr_dyn_retain(k->algorithm);
}
static ScrDyn *sw_usage_array(unsigned usages) {
ScrDyn *list = scr_dyn_new_arr();
for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++)
if (usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i]));
return list;
}
static ScrDyn *sw_key_usages(SwKey *k) {
if (!k->usage_list) k->usage_list = sw_usage_array(k->usages);
return scr_dyn_retain(k->usage_list);
}
static bool sw_name_is(const char *name, size_t len, const char *literal) {
return len == strlen(literal) && memcmp(name, literal, len) == 0;
}
static ScrDyn *sw_key_get(void *h, const char *name, size_t len) {
SwKey *k = h;
if (sw_name_is(name, len, "type"))
return scr_wc_cstr(scr_pki_key_is_private(k->key) ? "private" : "public");
if (sw_name_is(name, len, "extractable")) return scr_dyn_new_bool(k->extractable);
if (sw_name_is(name, len, "algorithm")) return sw_key_algorithm(k);
if (sw_name_is(name, len, "usages")) return sw_key_usages(k);
return NULL;
}
static bool sw_key_set(void *h, const char *name, size_t len, const ScrDyn *value) {
(void)h;
(void)value;
if (!sw_name_is(name, len, "type") && !sw_name_is(name, len, "extractable") &&
!sw_name_is(name, len, "algorithm") && !sw_name_is(name, len, "usages"))
return false;
scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "Cannot set property %.*s of #<CryptoKey> which has only a getter", (int)len,
name);
return true;
}
static ScrDyn *sw_key_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc,
const char *what) {
(void)h;
(void)self;
(void)args;
(void)argc;
if (strcmp(method, "toString") == 0) return scr_wc_cstr("[object CryptoKey]");
scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "%s is not a function", what);
return NULL;
}
static ScrDyn *sw_key_symbol_get(void *h, const ScrDyn *key) {
(void)h;
if (key->kind != SCR_DYN_SYMBOL || !key->v.symbol.description) return NULL;
ScrStr *description = key->v.symbol.description(key->v.symbol.value);
bool tag = description && description->len == 18 && memcmp(description->data, "Symbol.toStringTag", 18) == 0;
if (description) scr_str_release(description);
return tag ? scr_wc_cstr("CryptoKey") : NULL;
}
/* As for HMAC CryptoKeys, util.inspect refuses these keys rather than
* printing a different shape than Node's custom inspector. */
static const ScrDynHandleOps sw_key_ops = {
.cls = "CryptoKey",
.retain = sw_key_retain,
.release = sw_key_release,
.invoke = sw_key_invoke,
.get = sw_key_get,
.set = sw_key_set,
.json_empty_object = true,
.to_string_tag = "CryptoKey",
.symbol_get = sw_key_symbol_get,
};
/* Boxes a CryptoKey over `key` (consumed). */
static ScrDyn *sw_key_new(ScrPkiKey *key, SwKind kind, const char *hash, unsigned usages, bool extractable) {
static SCR_TL bool installed;
if (!installed) {
installed = true;
scr_dyn_handle_install(SCR_DYNH_ASYMMETRIC_CRYPTO_KEY, &sw_key_ops);
}
SwKey *k = calloc(1, sizeof *k);
if (!k) scr_trap("scriptc: out of memory\n");
k->rc = 1;
k->key = key;
k->kind = kind;
k->hash = hash;
k->usages = usages;
k->extractable = extractable;
ScrDyn *box = scr_dyn_new_handle(k, SCR_DYNH_ASYMMETRIC_CRYPTO_KEY);
sw_key_release(k);
return box;
}
/* ── algorithm parameters ─────────────────────────────────────────────── */
static SwKind sw_kind_of(const char *name) {
if (strcmp(name, "ECDSA") == 0) return SW_ECDSA;
if (strcmp(name, "Ed25519") == 0) return SW_ED25519;
return strcmp(name, "RSA-PSS") == 0 ? SW_PSS : SW_RSASSA;
}
static bool sw_is_rsa_kind(SwKind kind) { return kind == SW_RSASSA || kind == SW_PSS; }
/* The canonical algorithm name of an AlgorithmIdentifier the dispatcher
* already recognized as one of this unit's algorithms, or NULL with the
* error pending. */
static const char *sw_algorithm_name(ScrDyn *identifier) {
ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm");
if (!name) return NULL;
const char *found = scr_wc_lookup(sw_names, SCR_WC_COUNT(sw_names), name);
scr_str_release(name);
if (!found) scr_throw_domex("NotSupportedError", "Unrecognized algorithm name");
return found;
}
/* A required dictionary member (+1), or NULL with ERR_MISSING_OPTION. */
static ScrDyn *sw_required(ScrDyn *plain, const char *dict, const char *member) {
ScrDyn *v = scr_wc_member(plain, member);
if (v->kind == SCR_DYN_UNDEF) {
scr_dyn_release(v);
scr_wc_missing_member(dict, member);
return NULL;
}
return v;
}
/* namedCurve (DOMString, required): one of the implemented curves, or
* NULL with the error pending. */
static const char *sw_named_curve(ScrDyn *plain, const char *dict) {
ScrDyn *value = sw_required(plain, dict, "namedCurve");
if (!value) return NULL;
ScrStr *s = scr_wc_dom_string(value);
scr_dyn_release(value);
if (!s) return NULL;
const char *curve = NULL;
for (size_t i = 0; i < SCR_WC_COUNT(sw_curves); i++)
if (s->len == strlen(sw_curves[i]) && memcmp(s->data, sw_curves[i], s->len) == 0) curve = sw_curves[i];
scr_str_release(s);
if (!curve) scr_throw_domex("NotSupportedError", "Unrecognized namedCurve");
return curve;
}
/* hash (HashAlgorithmIdentifier, required): with `sha_only`, Node's
* ensureSHA check first ("Only SHA hashes are supported in <dict>"). */
static const char *sw_hash(ScrDyn *plain, const char *dict, bool sha_only, const char *method) {
ScrDyn *hash = sw_required(plain, dict, "hash");
if (!hash) return NULL;
if (sha_only) {
ScrStr *name = NULL;
if (scr_wc_is_object(hash)) {
ScrDyn *hp = scr_wc_plain(hash);
ScrDyn *member = hp ? scr_wc_member(hp, "name") : NULL;
if (hp) scr_dyn_release(hp);
if (member && member->kind == SCR_DYN_STR) name = scr_str_retain(member->v.str);
if (member) scr_dyn_release(member);
} else {
name = scr_wc_dom_string(hash);
if (!name) {
scr_dyn_release(hash);
return NULL;
}
}
bool sha = name && name->len >= 3 && (name->data[0] | 32) == 's' && (name->data[1] | 32) == 'h' &&
(name->data[2] | 32) == 'a';
if (name) scr_str_release(name);
if (!sha) {
char message[96];
snprintf(message, sizeof message, "Only SHA hashes are supported in %s", dict);
scr_throw_domex("NotSupportedError", message);
scr_dyn_release(hash);
return NULL;
}
}
const char *canonical = scr_wc_digest_algorithm(hash, method);
scr_dyn_release(hash);
return canonical;
}
/* An [EnforceRange] unsigned long member; false with the TypeError
* pending. */
static bool sw_unsigned_long(ScrDyn *value, const char *member, double *out) {
double x;
if (!scr_dyn_number_coerce_js(value, &x)) return false;
if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s in passed algorithm is not a finite number.",
scr_wc_normalize_prefix, member);
return false;
}
x = trunc(x);
if (x < 0 || x > 4294967295.0) {
scr_wc_type_error("ERR_OUT_OF_RANGE",
"%s: %s in passed algorithm is outside the expected range of 0 to 4294967295.",
scr_wc_normalize_prefix, member);
return false;
}
*out = x == 0 ? 0 : x;
return true;
}
static void sw_unsupported_usage(SwKind kind) {
char message[96];
snprintf(message, sizeof message, "Unsupported key usage for %s key", sw_names[kind]);
scr_throw_domex("SyntaxError", message);
}
/* ── generateKey ──────────────────────────────────────────────────────── */
static ScrPromise *sw_generate_key(const ScrDyn *args) {
static const char prefix[] = "Failed to execute 'generateKey' on 'SubtleCrypto'";
if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected();
ScrWcArgs a;
scr_wc_args_take(&a, args, 3);
ScrDyn *pair = NULL;
ScrDyn *plain = NULL;
if (!scr_wc_is_object(a.v[0])) {
ScrStr *s = scr_wc_dom_string(a.v[0]);
if (!s) goto done;
scr_str_release(s);
}
bool extractable = scr_dyn_truthy(a.v[1]);
long usages = scr_wc_key_usages(prefix, 2, a.v[2]);
if (usages < 0) goto done;
const char *name = sw_algorithm_name(a.v[0]);
if (!name) goto done;
SwKind kind = sw_kind_of(name);
plain = scr_wc_plain(a.v[0]);
if (!plain) goto done;
const char *curve = NULL;
const char *hash = NULL;
double bits = 0;
ScrBytes *exponent = NULL;
if (kind == SW_ED25519) {
/* no parameters */
} else if (kind == SW_ECDSA) {
if (!scr_wc_is_object(plain)) {
scr_wc_missing_member("EcKeyGenParams", "namedCurve");
goto done;
}
curve = sw_named_curve(plain, "EcKeyGenParams");
if (!curve) goto done;
} else {
static const char dict[] = "RsaHashedKeyGenParams";
if (!scr_wc_is_object(plain)) {
scr_wc_missing_member(dict, "hash");
goto done;
}
/* Dictionary members convert in name order: hash, modulusLength,
* publicExponent. */
hash = sw_hash(plain, dict, true, "generateKey");
if (!hash) goto done;
ScrDyn *length = sw_required(plain, dict, "modulusLength");
if (!length) goto done;
bool ok = sw_unsigned_long(length, "modulusLength", &bits);
scr_dyn_release(length);
if (!ok) goto done;
ScrDyn *e = sw_required(plain, dict, "publicExponent");
if (!e) goto done;
if (e->kind != SCR_DYN_BYTES || e->v.bytes->elem != SCR_BYTES_U8) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: publicExponent in passed algorithm is not instance of Uint8Array.",
scr_wc_normalize_prefix);
scr_dyn_release(e);
goto done;
}
exponent = scr_bytes_buffer_source(e);
scr_dyn_release(e);
}
if (usages & ~(long)(SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY)) {
sw_unsupported_usage(kind);
if (exponent) scr_bytes_release(exponent);
goto done;
}
ScrPkiKey *key;
if (kind == SW_ED25519) {
key = scr_pki_generate_ed25519();
if (!key) goto done;
} else if (kind == SW_ECDSA) {
key = scr_pki_generate_ec(curve);
} else {
/* publicExponent: big-endian, at most a 32-bit value. */
size_t n = (size_t)scr_bytes_byte_len(exponent);
uint64_t value = 0;
bool fits = true;
for (size_t i = 0; i < n; i++) {
if (value > 0xffffffu && fits) fits = false;
value = (value << 8) | exponent->data[i];
}
scr_bytes_release(exponent);
if (!fits || value > 0xffffffffu) {
scr_throw_domex("OperationError", "The publicExponent must be equivalent to an unsigned 32-bit value");
goto done;
}
if (bits < 512) {
scr_throw_domex("OperationError", "algorithm.modulusLength must be at least 512");
goto done;
}
if ((value & 1) == 0) {
scr_throw_domex("OperationError", "algorithm.publicExponent must be odd");
goto done;
}
if (bits > 16384 || value < 3) {
scr_wc_unsupported("generateKey", "this RSA modulus length or public exponent");
goto done;
}
key = scr_pki_generate_rsa((unsigned)bits, value);
}
if (!key) {
scr_throw_domex("OperationError", "Key generation failed");
goto done;
}
unsigned private_usages = (unsigned)usages & SCR_WC_USAGE_SIGN;
unsigned public_usages = (unsigned)usages & SCR_WC_USAGE_VERIFY;
if (private_usages == 0) {
scr_pki_key_release(key);
scr_throw_domex("SyntaxError", "Usages cannot be empty when creating a key.");
goto done;
}
ScrPkiKey *public_key = scr_pki_key_public_of(key);
if (!public_key) {
scr_pki_key_release(key);
scr_throw_domex("OperationError", "Key generation failed");
goto done;
}
pair = scr_dyn_new_obj();
scr_dyn_obj_set(pair, "publicKey", 9, sw_key_new(public_key, kind, hash, public_usages, true));
scr_dyn_obj_set(pair, "privateKey", 10, sw_key_new(key, kind, hash, private_usages, extractable));
done:
if (plain) scr_dyn_release(plain);
scr_wc_args_drop(&a, 3);
if (!pair) return scr_wc_rejected();
ScrPromise *p = scr_promise_new();
scr_crypto_defer_dyn(pair, NULL, scr_crypto_promise_callback(p), scr_crypto_promise_settle_dyn);
return p;
}
/* ── importKey ────────────────────────────────────────────────────────── */
/* A JWK string member, or NULL. */
static const ScrDyn *sw_jwk_member(const ScrDyn *jwk, const char *key) {
const ScrDyn *v = scr_dyn_obj_get(jwk, key, strlen(key));
return v && v->kind != SCR_DYN_UNDEF ? v : NULL;
}
static bool sw_jwk_is(const ScrDyn *v, const char *text) {
return v && v->kind == SCR_DYN_STR && v->v.str->len == strlen(text) &&
memcmp(v->v.str->data, text, v->v.str->len) == 0;
}
/* The JWK checks Node runs before reading the key: use, key_ops and ext. */
static bool sw_jwk_checks(const ScrDyn *jwk, unsigned usages, bool extractable) {
const ScrDyn *use = sw_jwk_member(jwk, "use");
if (use && !sw_jwk_is(use, "sig")) {
scr_throw_domex("DataError", "Invalid JWK \"use\" Parameter");
return false;
}
const ScrDyn *ops = sw_jwk_member(jwk, "key_ops");
if (ops) {
if (ops->kind != SCR_DYN_ARR) {
scr_throw_domex("DataError", "Key operations and usage mismatch");
return false;
}
for (size_t u = 0; u < SCR_WC_COUNT(scr_wc_usages); u++) {
if (!(usages & (1u << u))) continue;
bool listed = false;
double n = scr_dyn_arr_len(ops);
for (double i = 0; i < n && !listed; i++) {
ScrDyn *item = scr_dyn_arr_at(ops, i);
listed = sw_jwk_is(item, scr_wc_usages[u]);
scr_dyn_release(item);
}
if (!listed) {
scr_throw_domex("DataError", "Key operations and usage mismatch");
return false;
}
}
}
const ScrDyn *ext = sw_jwk_member(jwk, "ext");
if (ext && ext->kind == SCR_DYN_BOOL && !ext->v.b && extractable) {
scr_throw_domex("DataError", "JWK \"ext\" Parameter and extractable mismatch");
return false;
}
return true;
}
/* The JWK "alg" an RSA key with this hash carries ("RS256", "PS384"). */
static void sw_rsa_jwk_alg(SwKind kind, const char *hash, char out[8]) {
const char *bits = strcmp(hash, "SHA-1") == 0 ? "1" : hash + 4;
snprintf(out, 8, "%s%s", kind == SW_PSS ? "PS" : "RS", bits);
}
static ScrDyn *sw_import(const ScrDyn *args) {
static const char prefix[] = "Failed to execute 'importKey' on 'SubtleCrypto'";
if (!scr_wc_required(prefix, args, 5)) return NULL;
ScrWcArgs a;
scr_wc_args_take(&a, args, 5);
ScrDyn *result = NULL;
ScrBytes *data = NULL;
ScrDyn *jwk = NULL;
ScrDyn *plain = NULL;
const char *format = scr_wc_key_format(prefix, a.v[0]);
if (!format) goto done;
bool is_jwk = strcmp(format, "jwk") == 0;
if (is_jwk) {
if (!scr_wc_is_object(a.v[1])) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type JsonWebKey.", prefix);
goto done;
}
jwk = scr_wc_plain(a.v[1]);
if (!jwk) goto done;
} else {
data = scr_wc_buffer_source(prefix, 1, a.v[1]);
if (!data) goto done;
}
if (!scr_wc_is_object(a.v[2])) {
ScrStr *s = scr_wc_dom_string(a.v[2]);
if (!s) goto done;
scr_str_release(s);
}
bool extractable = scr_dyn_truthy(a.v[3]);
long usages = scr_wc_key_usages(prefix, 4, a.v[4]);
if (usages < 0) goto done;
const char *name = sw_algorithm_name(a.v[2]);
if (!name) goto done;
SwKind kind = sw_kind_of(name);
plain = scr_wc_plain(a.v[2]);
if (!plain) goto done;
const char *curve = NULL;
const char *hash = NULL;
if (kind == SW_ED25519) {
/* no parameters */
} else if (kind == SW_ECDSA) {
if (!scr_wc_is_object(plain)) {
scr_wc_missing_member("EcKeyImportParams", "namedCurve");
goto done;
}
curve = sw_named_curve(plain, "EcKeyImportParams");
if (!curve) goto done;
} else {
if (!scr_wc_is_object(plain)) {
scr_wc_missing_member("RsaHashedImportParams", "hash");
goto done;
}
hash = sw_hash(plain, "RsaHashedImportParams", true, "importKey");
if (!hash) goto done;
}
bool private_key;
if (is_jwk) {
private_key = sw_jwk_member(jwk, "d") != NULL;
} else if (strcmp(format, "pkcs8") == 0) {
private_key = true;
} else if (strcmp(format, "spki") == 0 ||
((kind == SW_ECDSA || kind == SW_ED25519) && strcmp(format, "raw") == 0)) {
private_key = false;
} else {
char message[96];
snprintf(message, sizeof message, "Unable to import %s using %s format", sw_names[kind], format);
scr_throw_domex("NotSupportedError", message);
goto done;
}
unsigned allowed = private_key ? SCR_WC_USAGE_SIGN : SCR_WC_USAGE_VERIFY;
ScrPkiKey *key = NULL;
/* An Ed25519 private key derives its public key in the Ed25519 unit. */
if (kind == SW_ED25519 && private_key && !scr_pki_ed25519_ready()) goto done;
if (is_jwk) {
const ScrDyn *kty = sw_jwk_member(jwk, "kty");
if (!kty) {
scr_throw_domex("DataError", "Invalid keyData");
goto done;
}
if (!sw_jwk_is(kty, kind == SW_ECDSA ? "EC" : kind == SW_ED25519 ? "OKP" : "RSA")) {
scr_throw_domex("DataError", "Invalid JWK \"kty\" Parameter");
goto done;
}
if (kind == SW_ECDSA && !sw_jwk_is(sw_jwk_member(jwk, "crv"), curve)) {
scr_throw_domex("DataError", "JWK \"crv\" does not match the requested algorithm");
goto done;
}
if (kind == SW_ED25519 && !sw_jwk_is(sw_jwk_member(jwk, "crv"), "Ed25519")) {
scr_throw_domex("DataError", "JWK \"crv\" Parameter and algorithm name mismatch");
goto done;
}
if ((unsigned)usages & ~allowed) {
sw_unsupported_usage(kind);
goto done;
}
if (!sw_jwk_checks(jwk, (unsigned)usages, extractable)) goto done;
const ScrDyn *alg = sw_jwk_member(jwk, "alg");
if (kind == SW_ED25519 && alg && !sw_jwk_is(alg, "Ed25519") && !sw_jwk_is(alg, "EdDSA")) {
scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm");
goto done;
}
if (sw_is_rsa_kind(kind) && alg) {
char expected[8];
sw_rsa_jwk_alg(kind, hash, expected);
if (!sw_jwk_is(alg, expected)) {
scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm");
goto done;
}
}
key = scr_pki_key_from_jwk(jwk, private_key);
} else {
if ((unsigned)usages & ~allowed) {
sw_unsupported_usage(kind);
goto done;
}
ScrBytes *view = data;
SCR_BYTES_SNAPSHOT(view);
size_t len = (size_t)scr_bytes_byte_len(view);
if (strcmp(format, "raw") == 0)
key = kind == SW_ED25519 ? scr_pki_key_from_ed25519_raw(view->data, len)
: scr_pki_key_from_point(curve, view->data, len);
else key = scr_pki_key_parse_der(view->data, len, private_key);
}
if (!key) {
scr_throw_domex("DataError", "Invalid keyData");
goto done;
}
bool key_matches = kind == SW_ED25519 ? scr_pki_key_is_ed25519(key)
: kind == SW_ECDSA ? !scr_pki_key_is_rsa(key) && !scr_pki_key_is_ed25519(key)
: scr_pki_key_is_rsa(key);
if (!key_matches) {
scr_pki_key_release(key);
scr_throw_domex("DataError", "Invalid key type");
goto done;
}
if (kind == SW_ECDSA && strcmp(scr_pki_key_curve(key), curve) != 0) {
scr_pki_key_release(key);
scr_throw_domex("DataError", "Named curve mismatch");
goto done;
}
if (private_key && usages == 0) {
scr_pki_key_release(key);
scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a private key.");
goto done;
}
result = sw_key_new(key, kind, hash, (unsigned)usages, extractable);
done:
if (data) scr_bytes_release(data);
if (jwk) scr_dyn_release(jwk);
if (plain) scr_dyn_release(plain);
scr_wc_args_drop(&a, 5);
return result;
}
static ScrPromise *sw_import_key(const ScrDyn *args) {
ScrDyn *key = sw_import(args);
if (!key) return scr_wc_rejected();
return scr_promise_settled_ref(key, &scr_dyn_retain_v, &scr_dyn_release_v, NULL);
}
/* ── exportKey ────────────────────────────────────────────────────────── */
static ScrDyn *sw_array_buffer(ScrBytes *bytes) {
ScrDyn *out = scr_array_buffer_from_bytes(bytes);
scr_bytes_release(bytes);
return out;
}
static ScrDyn *sw_export(const ScrDyn *args) {
ScrWcArgs a;
scr_wc_args_take(&a, args, 2);
ScrDyn *result = NULL;
const char *format = scr_wc_key_format("Failed to execute 'exportKey' on 'SubtleCrypto'", a.v[0]);
SwKey *k = sw_key_of(a.v[1]);
bool private_key = scr_pki_key_is_private(k->key);
if (!format) goto done;
if (!k->extractable) {
scr_throw_domex("InvalidAccessError", "key is not extractable");
goto done;
}
if (strcmp(format, "spki") == 0 || strcmp(format, "pkcs8") == 0) {
bool want_private = strcmp(format, "pkcs8") == 0;
if (want_private != private_key) {
scr_throw_domex("InvalidAccessError", want_private ? "Key must be a private key" : "Key must be a public key");
goto done;
}
ScrBytes *der = scr_pki_export_der(k->key);
if (!der) {
scr_throw_domex("OperationError", "Failed to export key");
goto done;
}
result = sw_array_buffer(der);
} else if (strcmp(format, "raw") == 0 && (k->kind == SW_ECDSA || k->kind == SW_ED25519) &&
!private_key) {
ScrBytes *point = k->kind == SW_ED25519 ? scr_pki_export_ed25519_raw(k->key) : scr_pki_export_point(k->key);
if (!point) {
scr_throw_domex("OperationError", "Failed to export key");
goto done;
}
result = sw_array_buffer(point);
} else if (strcmp(format, "jwk") == 0) {
ScrDyn *members = scr_pki_export_jwk(k->key);
ScrDyn *jwk = scr_dyn_new_obj();
scr_dyn_obj_set(jwk, "key_ops", 7, sw_usage_array(k->usages));
scr_dyn_obj_set(jwk, "ext", 3, scr_dyn_new_bool(k->extractable));
if (sw_is_rsa_kind(k->kind)) {
char alg[8];
sw_rsa_jwk_alg(k->kind, k->hash, alg);
scr_dyn_obj_set(jwk, "alg", 3, scr_wc_cstr(alg));
} else if (k->kind == SW_ED25519) {
scr_dyn_obj_set(jwk, "alg", 3, scr_wc_cstr("Ed25519"));
}
/* The key members in Node's order for each key type. */
static const char *const rsa_names[] = {"kty", "n", "e", "d", "p", "q", "dp", "dq", "qi"};
static const char *const ec_names[] = {"kty", "x", "y", "crv", "d"};
static const char *const okp_names[] = {"kty", "crv", "x", "d"};
const char *const *names = sw_is_rsa_kind(k->kind) ? rsa_names : k->kind == SW_ED25519 ? okp_names : ec_names;
size_t count = sw_is_rsa_kind(k->kind) ? SCR_WC_COUNT(rsa_names)
: k->kind == SW_ED25519 ? SCR_WC_COUNT(okp_names)
: SCR_WC_COUNT(ec_names);
for (size_t i = 0; i < count; i++) {
const ScrDyn *v = scr_dyn_obj_get(members, names[i], strlen(names[i]));
if (v) scr_dyn_obj_set(jwk, names[i], strlen(names[i]), scr_dyn_retain((ScrDyn *)v));
}
scr_dyn_release(members);
result = jwk;
} else {
char message[128];
snprintf(message, sizeof message, "Unable to export %s %s key using %s format", sw_names[k->kind],
private_key ? "private" : "public", format);
scr_throw_domex("NotSupportedError", message);
}
done:
scr_wc_args_drop(&a, 2);
return result;
}
static ScrPromise *sw_export_key(const ScrDyn *args) {
ScrDyn *value = sw_export(args);
if (!value) return scr_wc_rejected();
return scr_promise_settled_ref(value, &scr_dyn_retain_v, &scr_dyn_release_v, NULL);
}
/* ── sign and verify ──────────────────────────────────────────────────── */
/* The deferred verify result: a dyn boolean. */
static void sw_settle_verified(ScrClosure *cb, ScrError *error, ScrBytes *value) {
(void)error;
ScrPromise *p;
memcpy(&p, &cb->caps[0]->slot, sizeof p);
ScrDyn *verified = (ScrDyn *)value;
scr_promise_fulfill_bool(p, scr_dyn_truthy(verified));
scr_dyn_release(verified);
}
static ScrPromise *sw_sign_verify(const ScrDyn *args, bool verify) {
const char *prefix = verify ? "Failed to execute 'verify' on 'SubtleCrypto'"
: "Failed to execute 'sign' on 'SubtleCrypto'";
const char *method = verify ? "verify" : "sign";
size_t n = verify ? 4 : 3;
if (!scr_wc_required(prefix, args, n)) return scr_wc_rejected();
ScrWcArgs a;
scr_wc_args_take(&a, args, n);
ScrPromise *result = NULL;
ScrBytes *data = NULL;
ScrBytes *signature = NULL;
ScrDyn *plain = NULL;
if (!scr_wc_is_object(a.v[0])) {
ScrStr *s = scr_wc_dom_string(a.v[0]);
if (!s) goto done;
scr_str_release(s);
}
SwKey *k = sw_key_of(a.v[1]);
if (!k && !scr_webcrypto_is_crypto_key(a.v[1])) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix);
goto done;
}
if (verify) {
signature = scr_wc_buffer_source(prefix, 2, a.v[2]);
if (!signature) goto done;
}
data = scr_wc_buffer_source(prefix, verify ? 3 : 2, a.v[verify ? 3 : 2]);
if (!data) goto done;
const char *name = sw_algorithm_name(a.v[0]);
if (!name) goto done;
SwKind kind = sw_kind_of(name);
plain = scr_wc_plain(a.v[0]);
if (!plain) goto done;
const char *hash = NULL;
int salt = 0;
if (kind == SW_ED25519) {
/* no parameters */
} else if (kind == SW_ECDSA) {
if (!scr_wc_is_object(plain)) {
scr_wc_missing_member("EcdsaParams", "hash");
goto done;
}
hash = sw_hash(plain, "EcdsaParams", false, method);
if (!hash) goto done;
} else if (kind == SW_PSS) {
if (!scr_wc_is_object(plain)) {
scr_wc_missing_member("RsaPssParams", "saltLength");
goto done;
}
ScrDyn *value = sw_required(plain, "RsaPssParams", "saltLength");
if (!value) goto done;
double x;
bool ok = sw_unsigned_long(value, "saltLength", &x);
scr_dyn_release(value);
if (!ok) goto done;
salt = x > 65536 ? 65536 : (int)x;
}
if (!k || k->kind != kind) {
scr_throw_domex("InvalidAccessError", "Key algorithm mismatch");
goto done;
}
if (!(k->usages & (verify ? SCR_WC_USAGE_VERIFY : SCR_WC_USAGE_SIGN))) {
char message[64];
snprintf(message, sizeof message, "Unable to use this key to %s", method);
scr_throw_domex("InvalidAccessError", message);
goto done;
}
if (sw_is_rsa_kind(kind)) hash = k->hash;
if (kind == SW_ED25519 && !scr_pki_ed25519_ready()) goto done;
{
ScrBytes *view = data;
SCR_BYTES_SNAPSHOT(view);
size_t len = (size_t)scr_bytes_byte_len(view);
if (verify) {
ScrBytes *sig = signature;
SCR_BYTES_SNAPSHOT(sig);
bool ok = scr_pki_verify(k->key, hash, kind == SW_PSS, salt, view->data, len, sig->data,
(size_t)scr_bytes_byte_len(sig));
result = scr_promise_new();
scr_crypto_defer_dyn(scr_dyn_new_bool(ok), NULL, scr_crypto_promise_callback(result), sw_settle_verified);
} else {
ScrBytes *out = scr_pki_sign(k->key, hash, kind == SW_PSS, salt, view->data, len);
if (!out) {
scr_throw_domex("OperationError", "Failed to sign");
goto done;
}
result = scr_promise_new();
scr_crypto_defer_bytes(out, NULL, scr_crypto_promise_callback(result), scr_crypto_promise_settle_array_buffer);
}
}
done:
if (data) scr_bytes_release(data);
if (signature) scr_bytes_release(signature);
if (plain) scr_dyn_release(plain);
scr_wc_args_drop(&a, n);
return result ? result : scr_wc_rejected();
}
static ScrPromise *sw_sign(const ScrDyn *args) { return sw_sign_verify(args, false); }
static ScrPromise *sw_verify(const ScrDyn *args) { return sw_sign_verify(args, true); }
static const ScrWcAsymmetric sw_table = {
.generate_key = sw_generate_key,
.import_key = sw_import_key,
.export_key = sw_export_key,
.sign = sw_sign,
.verify = sw_verify,
.is_key = sw_is_key,
};
/* Installed before main: scr_webcrypto.c dispatches these algorithms
* here. */
__attribute__((constructor)) static void sw_install(void) { scr_wc_asymmetric = &sw_table; }