Trust no certificate for an empty ca in TLS clients

tls.connect, https.request and http2.connect verified the server against
the default CAs when ca was an empty Buffer, because the runtime read a
zero-length ca as none. The compiled calls now pass a null ca for none
(an omitted ca, or an empty string, which Node reads as no ca) and a
non-null one for any Buffer, and runtime option records read ca the same
way, so an empty Buffer or array trusts no certificate, as in Node. The
connection pool keys tell an empty ca from none. An empty array, which
threw, is an empty trust store too. A runtime-valued empty string ca in
http2.connect is refused instead of being read as an empty store.
This commit is contained in:
Chris Tate
2026-10-24 10:06:35 -05:00
parent 76611d1151
commit e35621323d
15 changed files with 300 additions and 40 deletions
@@ -2,6 +2,9 @@
On a TLS socket held in an `unknown` or `any` value, or in an untyped JavaScript parameter, they throw an error that names the call as not supported.
Client certificates follow Node: `tls.createServer`, `https.createServer`, and `new tls.TLSSocket` take `requestCert`, `rejectUnauthorized`, and `ca`, and `tls.connect` and `https.request` take `cert` and `key`.
A server's `authorized` and `authorizationError` report the client certificate's verification as Node does, and a server, client, or `tls.createSecureContext` given a `ca` also sends the chain it completes, as OpenSSL's automatic chain does.
A server verifies client certificates against the `ca` of the context it serves, including contexts from `addContext()` and `setSecureContext()`: an empty `ca` (an empty Buffer or array) trusts no certificate, and an omitted `ca` trusts the default CAs.
The same holds for a client's `ca` in `tls.connect`, `https.request`, and `http2.connect`, where an empty string is no `ca`, as in Node.
In `http2.connect`, a `ca` held in an `unknown` or `any` value that is an empty string is refused.
`requestCert` beside a TLSSocket's `secureContext` or an `SNICallback`, client certificates on http2 secure servers, and a `cert` without a `key` (or the reverse) are refused.
`server.addContext()` and `server.setSecureContext()` follow Node on tls and https servers, including wildcard names, which match one label without regard to case.
A name with regular-expression groups or alternation, or one outside ASCII, is refused, as are both methods on a server that is not a tls or https server.
@@ -73,6 +73,26 @@ export function rawBytes(host: LlvmEmitterContext, value: LlValue): AbiArgument[
return [ptr(data), { type: host.sizeType, name: length }];
}
/** A TLS client's `ca` as rawBytes, with a null pointer for none: an empty
* string, which Node reads as no ca (and which an omitted ca lowers to).
* A Buffer's pointer is never null, so an empty one is an empty trust
* store. */
export function caBytes(host: LlvmEmitterContext, value: LlValue): AbiArgument[] {
const B = host.B;
const [data, length] = rawBytes(host, value);
const test = B.tmp(),
pointer = B.tmp();
if (value.type.kind === "string") {
B.line(`${test} = icmp eq ${host.sizeType} ${length!.name}, 0`);
B.line(`${pointer} = select i1 ${test}, ptr null, ptr ${data!.name}`);
} else {
// An empty Buffer's data pointer may be null; its object stands in.
B.line(`${test} = icmp eq ptr ${data!.name}, null`);
B.line(`${pointer} = select i1 ${test}, ptr ${value.name}, ptr ${data!.name}`);
}
return [ptr(pointer), length!];
}
export function callbackType(value: LlValue): IrType & { kind: "func" } {
if (value.type.kind !== "func")
throw new InternalCompilerError("runtime callback must be a function");
@@ -22,7 +22,7 @@ import {
} from "./lib-shared.js";
import { isDynUndefined } from "../../ir/runtime-features.js";
import { DYN_KIND } from "./dyn.js";
import { rawBytes } from "./lib-abi.js";
import { caBytes } from "./lib-abi.js";
import { emitInlineAtomicsOp } from "./expr-bytes.js";
import { regexLiteralCaptures } from "./regex-matchers.js";
@@ -595,7 +595,8 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
// The https URL row is the http one with the TLS entry point — same
// three arguments, same response-callback adapter. The https options
// row is wider: rejectUnauthorized stays an i1, while its ScrStr or
// ScrBytes CA value expands to the runtime's raw pointer + length.
// ScrBytes CA value expands to the runtime's raw pointer + length
// (caBytes: a null pointer for none).
const isTls = e.fn.startsWith("https.");
const isUrl = e.fn.includes("requestUrl");
const isTlsOptions = isTls && !isUrl;
@@ -629,7 +630,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
if (isTlsOptions) {
callArgs = [
...callArgs.slice(0, 8),
...rawBytes(host, args[8]!).map((arg) => `${arg.type} ${arg.name}`),
...caBytes(host, args[8]!).map((arg) => `${arg.type} ${arg.name}`),
];
host.declare(
`declare ptr @scr_https_request(ptr, double, ptr, ptr, double, ptr, i1 zeroext, i1 zeroext, ptr, ${host.sizeType}, ptr, ptr)`,
@@ -9,6 +9,7 @@ import {
callbackType,
finishRuntimeCall,
ptr,
caBytes,
rawBytes,
requestHandler,
} from "./lib-abi.js";
@@ -204,7 +205,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa
);
return finishRuntimeCall(host, e, "scr_http2_connect", [
...a.slice(0, 2),
...rawBytes(host, args[2]!),
...caBytes(host, args[2]!),
...handler,
]);
}
@@ -8,6 +8,7 @@ import {
finishRuntimeCall,
nullableReference,
ptr,
caBytes,
rawBytes,
requestHandler,
} from "./lib-abi.js";
@@ -184,7 +185,7 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
);
return finishRuntimeCall(host, e, "scr_https_request_agent", [
...a.slice(0, 8),
...rawBytes(host, args[8]!),
...caBytes(host, args[8]!),
a[9]!,
...handler,
]);
@@ -204,7 +205,7 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
["ptr", "ptr"],
);
// Evaluate arguments once before selecting which transport consumes the callback.
const pem = rawBytes(host, args[9]!);
const pem = caBytes(host, args[9]!);
const B = host.B,
tls = B.newLabel("request.tls"),
plain = B.newLabel("request.plain"),
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8176-tls-empty-context-ca.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8177-tls-empty-client-ca.ts"
],
"diags": []
}
+2 -2
View File
@@ -2617,11 +2617,11 @@ static void scr_h2_on_established(void *ctx) {
* TLS client transport with ALPN ["h2"] (scr_tls.c — linked whenever
* this unit is): the same preface/SETTINGS buffer through the handshake
* and everything after establishment is protocol-identical. `ca`
* replaces the client trust anchors (len 0 = none) and
* replaces the client trust anchors (NULL = none) and
* reject_unauthorized false disables the verify gate — the local-CA
* self-connect shapes. Throws catchably on unparsable input. */
ScrH2Session *scr_http2_connect(ScrStr *url /*borrowed*/, bool reject_unauthorized,
const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
const char *ca /*borrowed, NULL = none*/, size_t ca_len,
ScrClosure *cb /*moves, nullable*/, ScrH2ConnectFn fn) {
const char *p = url->data;
const char *sep = strstr(p, "://");
+3 -3
View File
@@ -69,7 +69,7 @@ void scr_http2_server_on_session(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrH
* reject_unauthorized false disables the verify gate — the local-CA
* self-connect shapes); throws catchably on bad input */
ScrH2Session *scr_http2_connect(ScrStr *url /*borrowed*/, bool reject_unauthorized,
const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
const char *ca /*borrowed, NULL = none*/, size_t ca_len,
ScrClosure *cb /*moves, nullable*/,
ScrH2ConnectFn fn); /* +1 */
/* end_stream: -1 = the method's payload-meaningless default, 0/1 explicit */
@@ -173,7 +173,7 @@ ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, bool reject_unauthorized,
const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
const char *ca /*borrowed, NULL = none*/, size_t ca_len,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */
/* The URL-string form (https.get('https://host/path')): scr_http.c's
* shared parse, then the options form's dial with Node's no-options
@@ -187,7 +187,7 @@ ScrHttpClientReq *scr_https_request_agent(ScrStr *host /*borrowed*/, double port
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, bool reject_unauthorized,
const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
const char *ca /*borrowed, NULL = none*/, size_t ca_len,
const struct ScrDyn *agent /*borrowed*/,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */
/* The fetch unit's https leg (defined in scr_tls.c): a client transport
+71 -28
View File
@@ -253,7 +253,10 @@ struct ScrSecureCtx {
mbedtls_x509_crt cert;
mbedtls_pk_context pk;
/* the ca option: a socket created from this context (a TLSSocket's
* secureContext) sends the chain it completes */
* secureContext) sends the chain it completes, and a server that serves
* the context verifies client certificates against it. has_ca tells an
* empty ca (an empty trust store) from none (the default store). */
bool has_ca;
mbedtls_x509_crt ca;
};
@@ -573,6 +576,9 @@ static void scr_tls_srv_client_auth(ScrTlsSrv *s, const ScrTlsClientAuth *auth)
typedef struct ScrTlsCli {
mbedtls_ssl_config conf;
/* the ca option, which replaces the default store; has_ca tells an
* empty ca (an empty trust store) from none */
bool has_ca;
mbedtls_x509_crt ca;
char *hostname; /* the SNI name; NULL sends none (an IP literal, servername: "") */
char *verify_name; /* the name the leaf is checked against; NULL fails the check */
@@ -696,6 +702,7 @@ static ScrTlsCli *scr_tls_cli_new2(ScrStr *host, bool reject_unauthorized, const
mbedtls_ssl_conf_rng(&c->conf, mbedtls_ctr_drbg_random, &scr_tls_drbg);
c->verify_recorded = reject_unauthorized || verify_optional;
c->reject = reject_unauthorized;
c->has_ca = ca != NULL;
if (ca_len > 0) {
int r = scr_tls_parse_crt(&c->ca, ca, ca_len);
if (r != 0) scr_tls_bad_material("ca certificate", r);
@@ -705,7 +712,7 @@ static ScrTlsCli *scr_tls_cli_new2(ScrStr *host, bool reject_unauthorized, const
} else {
mbedtls_ssl_conf_authmode(&c->conf, reject_unauthorized ? MBEDTLS_SSL_VERIFY_REQUIRED
: MBEDTLS_SSL_VERIFY_OPTIONAL);
mbedtls_ssl_conf_ca_chain(&c->conf, ca_len > 0 ? &c->ca : scr_tls_system_ca(), NULL);
mbedtls_ssl_conf_ca_chain(&c->conf, c->has_ca ? &c->ca : scr_tls_system_ca(), NULL);
}
/* The host is the verified name; it is also the SNI name unless it is
* an IP literal. */
@@ -799,6 +806,9 @@ 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) */
/* scr_tls_f_cert verified client certificates against sni_ctx's ca
* (or the default store without one), not the server's */
bool ctx_verifies;
/* 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) */
@@ -1889,8 +1899,8 @@ static ScrStr *scr_tls_pool_key(ScrStr *host, bool reject_unauthorized, const ch
uint64_t h = 1469598103934665603ull;
for (size_t i = 0; i < ca_len; i++) h = (h ^ (unsigned char)ca[i]) * 1099511628211ull;
char buf[64];
int n = snprintf(buf, sizeof buf, "%d:%zu:%016llx:", reject_unauthorized ? 1 : 0, ca_len,
(unsigned long long)h);
int n = snprintf(buf, sizeof buf, "%d:%s%zu:%016llx:", reject_unauthorized ? 1 : 0,
ca != NULL ? "" : "-", ca_len, (unsigned long long)h);
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_write(&b, buf, (size_t)n);
@@ -2148,7 +2158,7 @@ ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, bool reject_unauthorized,
const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
const char *ca /*borrowed, NULL = none*/, size_t ca_len,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) {
ScrStr *servername = scr_https_take_servername();
ScrTlsCli *cli = scr_tls_cli_for(host, reject_unauthorized, ca, ca_len, servername);
@@ -2189,7 +2199,7 @@ ScrHttpClientReq *scr_https_request_agent(ScrStr *host /*borrowed*/, double port
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, bool reject_unauthorized,
const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
const char *ca /*borrowed, NULL = none*/, size_t ca_len,
const ScrDyn *agent /*borrowed*/,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) {
ScrStr *servername = scr_https_take_servername();
@@ -2242,6 +2252,14 @@ static bool scr_tls_dyn_truthy(const ScrDyn *v) {
* the exception pending. `what` names the fence ("a tls.createServer
* 'cert' option"). */
static ScrBytes *scr_tls_pem_dyn(const ScrDyn *v, const char *what, bool concat) {
if (v->kind == SCR_DYN_TYPED_REF) {
/* a typed array (an array literal in a typed record): its entries */
ScrDyn *plain = scr_dyn_typed_ref_materialize(v);
if (plain == NULL) return NULL;
ScrBytes *b = scr_tls_pem_dyn(plain, what, concat);
scr_dyn_release(plain);
return b;
}
if (v->kind == SCR_DYN_STR) {
ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)v->v.str->len);
memcpy(b->data, v->v.str->data, v->v.str->len);
@@ -2292,6 +2310,33 @@ static ScrBytes *scr_tls_pem_dyn(const ScrDyn *v, const char *what, bool concat)
return NULL;
}
/* A `ca` option from a runtime value: NULL for none (an empty string,
* which Node reads as no ca), empty bytes for an empty Buffer or array
* (an empty trust store), else the PEM bundle (+1). False with the
* exception pending. */
static bool scr_tls_ca_dyn(const ScrDyn *v, const char *what, ScrBytes **out) {
*out = NULL;
ScrDyn *plain = v->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(v) : NULL;
if (v->kind == SCR_DYN_TYPED_REF && plain == NULL) return false;
const ScrDyn *ca = plain != NULL ? plain : v;
bool ok = true;
if (ca->kind == SCR_DYN_ARR && ca->v.arr.len == 0) {
*out = scr_bytes_new(SCR_BYTES_U8, 0);
} else if (ca->kind != SCR_DYN_STR || ca->v.str->len > 0) {
*out = scr_tls_pem_dyn(ca, what, true);
ok = *out != NULL;
}
scr_dyn_release(plain);
return ok;
}
/* A client's ca for scr_tls_cli_new2: NULL for none, and never NULL for a
* given ca, even an empty one. */
static const char *scr_tls_ca_data(const ScrBytes *ca) {
if (ca == NULL) return NULL;
return ca->data != NULL ? (const char *)ca->data : "";
}
/* https.request/get with a runtime options record: the http walk, then
* the TLS options the literal https path lowers (rejectUnauthorized, ca,
* servername) and a client certificate (cert and key). Other tls.connect
@@ -2330,8 +2375,7 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au
if (r != NULL && r->kind == SCR_DYN_BOOL && !r->v.b) reject = false;
const ScrDyn *c = ok ? scr_dyn_obj_get(rec, "ca", 2) : NULL;
if (c != NULL && c->kind != SCR_DYN_UNDEF && c->kind != SCR_DYN_NULL) {
ca = scr_tls_pem_dyn(c, "an https.request 'ca' option", true);
if (ca == NULL) ok = false;
ok = scr_tls_ca_dyn(c, "an https.request 'ca' option", &ca);
}
const ScrDyn *oc = ok ? scr_dyn_obj_get(rec, "cert", 4) : NULL;
if (oc != NULL && oc->kind != SCR_DYN_UNDEF && oc->kind != SCR_DYN_NULL) {
@@ -2366,7 +2410,7 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au
if (cb) scr_closure_release(cb);
return NULL;
}
const char *ca_data = ca ? (const char *)ca->data : NULL;
const char *ca_data = scr_tls_ca_data(ca);
size_t ca_len = ca ? ca->len : 0;
ScrTlsCli *cli = scr_tls_cli_for(o.host, reject, ca_data, ca_len, servername);
ScrStr *key = scr_tls_pool_key(o.host, reject, ca_data, ca_len, servername);
@@ -2397,13 +2441,17 @@ ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool au
/* The literal path's runtime-valued cert/key extraction (the compiler's
* tls.pemDyn libCall): `what` arrives precomposed. undefined throws — the
* literal walk required the member's presence. */
* literal walk required the member's presence — and so does an empty
* string, which Node reads as no material while empty bytes here are an
* empty ca. */
ScrBytes *scr_tls_pem_from_dyn(const ScrDyn *v, const char *what) {
if (v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL) {
bool empty = v->kind == SCR_DYN_STR && v->v.str->len == 0;
if (v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL || empty) {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, what);
scr_jb_puts(&b, " holding undefined has no static lowering — pass PEM material (a string or a Buffer)");
scr_jb_puts(&b, empty ? " holding an empty string" : " holding undefined");
scr_jb_puts(&b, " has no static lowering — pass PEM material (a string or a Buffer)");
scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b));
return NULL;
}
@@ -2565,14 +2613,7 @@ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrByt
char what[96];
snprintf(what, sizeof what, "a %s 'ca' option", api);
scr_bytes_release(auth->ca);
/* An empty string is no ca, as Node reads it; an empty Buffer is
* an empty trust store. */
if (v->kind == SCR_DYN_STR && v->v.str->len == 0) {
auth->ca = NULL;
} else {
auth->ca = scr_tls_pem_dyn(v, what, true);
if (auth->ca == NULL) ok = false;
}
if (!scr_tls_ca_dyn(v, what, &auth->ca)) ok = false;
}
continue;
}
@@ -2658,6 +2699,7 @@ ScrSecureCtx *scr_tls_create_secure_context_dyn(const ScrDyn *opts /*borrowed*/)
int r = scr_tls_parse_crt(&c->ca, (const char *)auth.ca->data, auth.ca->len);
if (r != 0) scr_tls_bad_material("ca certificate", r);
}
c->has_ca = true;
scr_bytes_release(auth.ca);
}
scr_bytes_release(cert);
@@ -2810,8 +2852,10 @@ static int scr_tls_f_cert(mbedtls_ssl_context *ssl) {
scr_tls_chained_copy(&t->served_chain, &pick->cert, store);
own = &t->served_chain;
}
if (s->request_cert)
mbedtls_ssl_set_hs_ca_chain(ssl, pick->ca.raw.len > 0 ? &pick->ca : scr_tls_system_ca(), NULL);
if (s->request_cert) {
t->ctx_verifies = true;
mbedtls_ssl_set_hs_ca_chain(ssl, pick->has_ca ? &pick->ca : scr_tls_system_ca(), NULL);
}
return mbedtls_ssl_set_hs_own_cert(ssl, own, &pick->pk);
}
@@ -3261,8 +3305,7 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed,
}
if (strcmp(e->key, "ca") == 0) {
scr_bytes_release(ca);
ca = scr_tls_pem_dyn(v, "a tls.connect 'ca' option", true);
if (ca == NULL) ok = false;
if (!scr_tls_ca_dyn(v, "a tls.connect 'ca' option", &ca)) ok = false;
continue;
}
if (strcmp(e->key, "cert") == 0 || strcmp(e->key, "key") == 0) {
@@ -3386,8 +3429,7 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed,
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);
scr_tls_ca_data(ca), ca != NULL ? ca->len : 0, !reject);
if (servername != NULL) scr_tls_cli_set_servername(cli, servername);
if (own_cert != NULL) scr_tls_cli_set_own(cli, own_cert, own_key);
scr_bytes_release(own_cert);
@@ -3441,7 +3483,7 @@ void scr_tls_server_wrap_h2(ScrNetServer *s, const char *cert, size_t cert_len,
/* The h2-over-TLS CLIENT wrap (http2.connect with an https authority):
* a client transport offering ALPN ["h2"] — SNI/verification against
* the authority host; `ca` replaces the trust anchors ("" = none) and
* the authority host; `ca` replaces the trust anchors (NULL = none) and
* reject_unauthorized false records like tls.connect's. The session's
* preface/SETTINGS ride the socket's write buffer until establishment. */
void scr_tls_h2_client_wrap(ScrNetSocket *sock, ScrStr *host /*borrowed*/,
@@ -3595,7 +3637,8 @@ void scr_tls_sock_each_trusted(ScrNetSocket *s, bool (*visit)(void *ctx, const u
void *ctx) {
ScrTlsSock *t = scr_tls_sock_of(s);
if (t == NULL || (t->cli == NULL && t->srv == NULL)) return;
const mbedtls_x509_crt *c = t->cli != NULL ? (t->cli->ca.raw.len > 0 ? &t->cli->ca : scr_tls_system_ca())
const mbedtls_x509_crt *c = t->cli != NULL ? (t->cli->has_ca ? &t->cli->ca : scr_tls_system_ca())
: t->ctx_verifies ? (t->sni_ctx->has_ca ? &t->sni_ctx->ca : scr_tls_system_ca())
: t->srv->has_ca ? &t->srv->ca
: scr_tls_system_ca();
for (; c != NULL && c->raw.len > 0; c = c->next)
+1 -1
View File
@@ -85,7 +85,7 @@ void scr_tls_sock_on_secure(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once
* every http2-using binary links the TLS unit (the moduleUsesTls
* switch), so no registration indirection is needed. */
void scr_tls_server_wrap_h2(ScrNetServer *s, const char *cert, size_t cert_len, const char *key, size_t key_len, ScrNetNativeConnFn h2_conn, void *h2_ctx /*moves*/, void (*h2_ctx_free)(void *), ScrClosure *sni_cb /*moves, nullable*/, void *sni_answer_fn);
void scr_tls_h2_client_wrap(ScrNetSocket *sock, ScrStr *host /*borrowed*/, bool reject_unauthorized, const char *ca /*borrowed, len 0 = none*/, size_t ca_len);
void scr_tls_h2_client_wrap(ScrNetSocket *sock, ScrStr *host /*borrowed*/, bool reject_unauthorized, const char *ca /*borrowed, NULL = none*/, size_t ca_len);
/* ── tls.SecureContext + the SNI callback (scr_tls.c) ─────────────────
* A SecureContext is an opaque refcounted parsed cert/key pair
+73
View File
@@ -0,0 +1,73 @@
// An explicitly empty ca is an empty trust store for client certificates,
// in the server's options, in setSecureContext and in addContext, even when
// the client certificate's issuer is a default CA
// (tls.setDefaultCACertificates); an omitted ca trusts the default CAs.
// Each server has requestCert, with rejectUnauthorized false (the server's
// verdict) and true (the client is dropped).
import { readFileSync } from "node:fs";
import type { AddressInfo } from "node:net";
import * as tls from "node:tls";
const D = "tests/fixtures/server/certs";
const r = (f: string): Buffer => readFileSync(`${D}/${f}`);
const ca = r("ca.pem"), cert = r("localhost.pem"), key = r("localhost-key.pem");
const alt = r("alt.pem"), altKey = r("alt-key.pem");
const client = r("client.pem"), clientKey = r("client-key.pem");
tls.setDefaultCACertificates([ca.toString()]);
function listen(server: tls.Server): Promise<number> {
return new Promise((res) => server.listen(0, () => res((server.address() as AddressInfo).port)));
}
type Handler = (s: tls.TLSSocket) => void;
async function run(label: string, make: (h: Handler) => tls.Server, servername: string): Promise<void> {
const events: string[] = [];
const server = make((s) => {
events.push(`server authorized=${s.authorized} error=${s.authorizationError}`);
s.end("ok");
});
const port = await listen(server);
await new Promise<void>((done) => {
const c = tls.connect({ port, servername, ca, cert: client, key: clientKey });
c.on("secureConnect", () => events.push("client secureConnect"));
c.on("data", (d: Buffer) => events.push(`client data ${d}`));
c.on("error", (e: NodeJS.ErrnoException) => events.push(`client error ${e.code}`));
c.on("close", () => setTimeout(done, 50));
});
console.log(label, "|", events.join(" ; "));
await new Promise<void>((res) => server.close(() => res()));
}
for (const reject of [false, true]) {
const opts = { cert, key, requestCert: true, rejectUnauthorized: reject };
const tag = reject ? "reject" : "request";
await run(`${tag} omitted`, (h) => tls.createServer(opts, h), "localhost");
await run(`${tag} empty array`, (h) => tls.createServer({ ...opts, ca: [] }, h), "localhost");
await run(`${tag} setSecureContext empty`, (h) => {
const server = tls.createServer(opts, h);
server.setSecureContext({ cert, key, ca: [] });
return server;
}, "localhost");
await run(`${tag} setSecureContext omitted`, (h) => {
const server = tls.createServer({ ...opts, ca: [] }, h);
server.setSecureContext({ cert, key });
return server;
}, "localhost");
await run(`${tag} addContext empty array`, (h) => {
const server = tls.createServer(opts, h);
server.addContext("alt.localhost", { cert: alt, key: altKey, ca: [] });
return server;
}, "alt.localhost");
await run(`${tag} addContext empty Buffer`, (h) => {
const server = tls.createServer(opts, h);
server.addContext("alt.localhost", tls.createSecureContext({ cert: alt, key: altKey, ca: Buffer.alloc(0) }));
return server;
}, "alt.localhost");
await run(`${tag} addContext omitted`, (h) => {
const server = tls.createServer({ ...opts, ca: [] }, h);
server.addContext("alt.localhost", { cert: alt, key: altKey });
return server;
}, "alt.localhost");
}
+104
View File
@@ -0,0 +1,104 @@
// An explicitly empty ca is an empty trust store for a client's view of
// the server certificate, in tls.connect, https.request and http2.connect,
// even when the server certificate's issuer is a default CA
// (tls.setDefaultCACertificates); an omitted ca, or an empty string, trusts
// the default CAs.
import { readFileSync } from "node:fs";
import * as http2 from "node:http2";
import * as https from "node:https";
import type { AddressInfo } from "node:net";
import * as tls from "node:tls";
const D = "tests/fixtures/server/certs";
const r = (f: string): Buffer => readFileSync(`${D}/${f}`);
const ca = r("ca.pem"), key = r("localhost-key.pem");
// The servers send their issuer too, so each client sees the same chain.
const cert = Buffer.concat([r("localhost.pem"), ca]);
tls.setDefaultCACertificates([ca.toString()]);
function listen(server: tls.Server | https.Server | http2.Http2SecureServer): Promise<number> {
return new Promise((res) => server.listen(0, () => res((server.address() as AddressInfo).port)));
}
async function watch(label: string, c: tls.TLSSocket): Promise<void> {
const events: string[] = [];
await new Promise<void>((done) => {
c.on("secureConnect", () => {
events.push(`secureConnect authorized=${c.authorized} error=${c.authorizationError}`);
c.end();
});
c.on("error", (e: NodeJS.ErrnoException) => events.push(`error ${e.code}`));
c.on("close", () => done());
});
console.log("tls.connect", label, "|", events.join(" ; "));
}
{
const server = tls.createServer({ cert, key }, (s) => s.end());
const port = await listen(server);
const host = "localhost";
await watch("omitted", tls.connect({ port, host }));
await watch("empty string", tls.connect({ port, host, ca: "" }));
await watch("empty Buffer", tls.connect({ port, host, ca: Buffer.alloc(0) }));
await watch("empty array", tls.connect({ port, host, ca: [] }));
await watch(
"empty Buffer, rejectUnauthorized false",
tls.connect({ port, host, ca: Buffer.alloc(0), rejectUnauthorized: false }),
);
await watch("empty array, rejectUnauthorized false", tls.connect({ port, host, ca: [], rejectUnauthorized: false }));
server.close();
}
function report(label: string, req: ReturnType<typeof https.request>, done: () => void): void {
req.on("response", (res) => {
console.log("https.request", label, "| status", res.statusCode);
res.resume();
res.on("end", done);
});
req.on("error", (e: NodeJS.ErrnoException) => {
console.log("https.request", label, "| error", e.code);
done();
});
req.end();
}
{
const server = https.createServer({ cert, key }, (_req, res) => res.end("ok"));
const port = await listen(server);
await new Promise<void>((done) =>
report("omitted", https.request({ host: "localhost", port, path: "/", agent: false }), done),
);
await new Promise<void>((done) =>
report("empty string", https.request({ host: "localhost", port, path: "/", agent: false, ca: "" }), done),
);
await new Promise<void>((done) =>
report(
"empty Buffer",
https.request({ host: "localhost", port, path: "/", agent: false, ca: Buffer.alloc(0) }),
done,
),
);
const options: unknown = { host: "localhost", port, path: "/", agent: false, ca: [] };
await new Promise<void>((done) =>
report("empty array in a runtime record", https.request(options as https.RequestOptions), done),
);
server.close();
}
{
const server = http2.createSecureServer({ cert, key }, (_req, res) => res.end("ok"));
const port = await listen(server);
for (const [label, ca] of [["omitted", undefined], ["empty Buffer", Buffer.alloc(0)]] as const) {
await new Promise<void>((done) => {
const session = ca === undefined ? http2.connect(`https://localhost:${port}`) : http2.connect(`https://localhost:${port}`, { ca });
session.on("connect", () => {
console.log("http2.connect", label, "| connect");
session.close();
});
session.on("error", (e: NodeJS.ErrnoException) => console.log("http2.connect", label, "| error", e.code));
session.on("close", () => done());
});
}
server.close();
}
@@ -0,0 +1 @@
{ "plain": 7322, "san": 94064 }
@@ -0,0 +1 @@
{ "plain": 4335, "san": 61880 }