Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/sync-webkit-source.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { join, dirname } from "node:path";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";

const bunRepo = dirname(import.meta.dir);
const webkitRepo = join(bunRepo, "vendor/WebKit");
Expand Down
11 changes: 11 additions & 0 deletions src/bun.js/bindings/ErrorCode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2284,6 +2284,17 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_TLS_CERT_ALTNAME_FORMAT, "Invalid subject alternative name string"_s));
case ErrorCode::ERR_TLS_SNI_FROM_SERVER:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_TLS_SNI_FROM_SERVER, "Cannot issue SNI from a TLS server-side socket"_s));
case ErrorCode::ERR_SSL_NO_CIPHER_MATCH: {
auto err = createError(globalObject, ErrorCode::ERR_SSL_NO_CIPHER_MATCH, "No cipher match"_s);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in node the code is error:0A0000B9:SSL routines::no cipher match. how much do we care about retaining the boringssl message?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jarred-Sumner Any thoughts?


auto reason = JSC::jsString(vm, WTF::String("no cipher match"_s));
err->putDirect(vm, Identifier::fromString(vm, "reason"_s), reason);

auto library = JSC::jsString(vm, WTF::String("SSL routines"_s));
err->putDirect(vm, Identifier::fromString(vm, "library"_s), library);

return JSC::JSValue::encode(err);
}
case ErrorCode::ERR_INVALID_URI:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_INVALID_URI, "URI malformed"_s));
case ErrorCode::ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED:
Expand Down
1 change: 1 addition & 0 deletions src/bun.js/bindings/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ const errors: ErrorCodeMapping = [
["ERR_TLS_PSK_SET_IDENTITY_HINT_FAILED", Error],
["ERR_TLS_RENEGOTIATION_DISABLED", Error],
["ERR_TLS_SNI_FROM_SERVER", Error],
["ERR_SSL_NO_CIPHER_MATCH", Error],
["ERR_UNAVAILABLE_DURING_EXIT", Error],
["ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET", Error],
["ERR_UNESCAPED_CHARACTERS", TypeError],
Expand Down
1 change: 1 addition & 0 deletions src/js/builtins.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,7 @@ declare function $ERR_TLS_RENEGOTIATION_DISABLED(): Error;
declare function $ERR_UNAVAILABLE_DURING_EXIT(): Error;
declare function $ERR_TLS_CERT_ALTNAME_FORMAT(): SyntaxError;
declare function $ERR_TLS_SNI_FROM_SERVER(): Error;
declare function $ERR_SSL_NO_CIPHER_MATCH(): Error;
declare function $ERR_INVALID_URI(): URIError;
declare function $ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED(): TypeError;
declare function $ERR_HTTP2_INFO_STATUS_NOT_ALLOWED(): RangeError;
Expand Down
26 changes: 25 additions & 1 deletion src/js/internal/tls.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
const { isTypedArray, isArrayBuffer } = require("node:util/types");

const DEFAULT_CIPHERS =
"DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256";

const DEFAULT_CIPHERS_LIST = DEFAULT_CIPHERS.split(":");
const DEFAULT_CIPHERS_SET = new Set([...DEFAULT_CIPHERS_LIST.map(c => c.toLowerCase()), ...DEFAULT_CIPHERS_LIST]);

function isPemObject(obj: unknown): obj is { pem: unknown } {
return $isObject(obj) && "pem" in obj;
}
Expand Down Expand Up @@ -48,6 +54,24 @@ function isValidTLSArray(obj: unknown) {
return false;
}

function validateCiphers(ciphers: string) {
const requested = ciphers.split(":");
for (const r of requested) {
if (!DEFAULT_CIPHERS_SET.has(r)) {
throw $ERR_SSL_NO_CIPHER_MATCH();
}
}
}

const VALID_TLS_ERROR_MESSAGE_TYPES = "string or an instance of Buffer, TypedArray, DataView, or BunFile";

export { VALID_TLS_ERROR_MESSAGE_TYPES, isValidTLSArray, isValidTLSItem, throwOnInvalidTLSArray };
export {
DEFAULT_CIPHERS,
DEFAULT_CIPHERS_LIST,
DEFAULT_CIPHERS_SET,
VALID_TLS_ERROR_MESSAGE_TYPES,
isValidTLSArray,
isValidTLSItem,
throwOnInvalidTLSArray,
validateCiphers,
};
30 changes: 24 additions & 6 deletions src/js/node/tls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const net = require("node:net");
const { Duplex } = require("node:stream");
const [addServerName] = $zig("socket.zig", "createNodeTLSBinding");
const { throwNotImplemented } = require("internal/shared");
const { throwOnInvalidTLSArray } = require("internal/tls");
const { throwOnInvalidTLSArray, DEFAULT_CIPHERS, validateCiphers } = require("internal/tls");

const { Server: NetServer, Socket: NetSocket } = net;

Expand Down Expand Up @@ -264,8 +264,10 @@ var InternalSecureContext = class SecureContext {
}
};

function SecureContext(options) {
return new InternalSecureContext(options);
function SecureContext(options): void {
// TODO: The `never` exists because TypeScript only lets you construct functions that return void
// but in reality we should just be calling like InternalSecureContext.$call or similar
return new InternalSecureContext(options) as never;
}

function createSecureContext(options) {
Expand Down Expand Up @@ -311,6 +313,11 @@ function TLSSocket(socket?, options?) {

NetSocket.$call(this, options);

this.ciphers = options.ciphers;
if (this.ciphers) {
validateCiphers(options.ciphers);
}

if (typeof options === "object") {
const { ALPNProtocols } = options;
if (ALPNProtocols) {
Expand Down Expand Up @@ -481,6 +488,7 @@ TLSSocket.prototype[buntls] = function (port, host) {
session: this[ksession],
rejectUnauthorized: this._rejectUnauthorized,
requestCert: this._requestCert,
ciphers: this.ciphers,
...this[ksecureContext],
};
};
Expand Down Expand Up @@ -579,6 +587,16 @@ function Server(options, secureConnectionListener): void {
if (typeof rejectUnauthorized !== "undefined") {
this._rejectUnauthorized = rejectUnauthorized;
} else this._rejectUnauthorized = rejectUnauthorizedDefault;

if (typeof options.ciphers !== "undefined") {
if (typeof options.ciphers !== "string") {
throw $ERR_INVALID_ARG_TYPE("options.ciphers", "string", options.ciphers);
}

validateCiphers(options.ciphers);

// TODO: Pass the ciphers
}
}
};

Expand Down Expand Up @@ -619,8 +637,6 @@ function createServer(options, connectionListener) {
}
const DEFAULT_ECDH_CURVE = "auto",
// https://github.com/Jarred-Sumner/uSockets/blob/fafc241e8664243fc0c51d69684d5d02b9805134/src/crypto/openssl.c#L519-L523
DEFAULT_CIPHERS =
"DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
DEFAULT_MIN_VERSION = "TLSv1.2",
DEFAULT_MAX_VERSION = "TLSv1.3";

Expand Down Expand Up @@ -648,10 +664,12 @@ function normalizeConnectArgs(listArgs) {
function connect(...args) {
let normal = normalizeConnectArgs(args);
const options = normal[0];
const { ALPNProtocols } = options;
const { ALPNProtocols } = options as { ALPNProtocols?: unknown };

if (ALPNProtocols) {
convertALPNProtocols(ALPNProtocols, options);
}

return new TLSSocket(options).connect(normal);
}

Expand Down
4 changes: 4 additions & 0 deletions src/js/private.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,7 @@ declare function $newZigFunction<T = (...args: any) => any>(
*/
declare function $bindgenFn<T = (...args: any) => any>(filename: string, symbol: string): T;
// NOTE: $debug, $assert, and $isPromiseFulfilled omitted

declare module "node:net" {
export function _normalizeArgs(args: any[]): unknown[];
}
26 changes: 26 additions & 0 deletions test/js/node/test/parallel/test-tls-handshake-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use strict';

const common = require('../common');

if (!common.hasCrypto)
common.skip('missing crypto');

const assert = require('assert');
const tls = require('tls');

const fixtures = require('../common/fixtures');

const server = tls.createServer({
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem'),
rejectUnauthorized: true
}, common.mustNotCall()).listen(0, common.mustCall(function() {
assert.throws(() => {
tls.connect({
port: this.address().port,
ciphers: 'no-such-cipher'
}, common.mustNotCall());
}, /no cipher match/i);

server.close();
}));
25 changes: 25 additions & 0 deletions test/js/node/test/parallel/test-tls-set-ciphers-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict';
const common = require('../common');

if (!common.hasCrypto)
common.skip('missing crypto');

const assert = require('assert');
const tls = require('tls');
const fixtures = require('../common/fixtures');

{
const options = {
key: fixtures.readKey('agent2-key.pem'),
cert: fixtures.readKey('agent2-cert.pem'),
ciphers: 'aes256-sha'
};
assert.throws(() => tls.createServer(options, common.mustNotCall()),
/no[_ ]cipher[_ ]match/i);
options.ciphers = 'FOOBARBAZ';
assert.throws(() => tls.createServer(options, common.mustNotCall()),
/no[_ ]cipher[_ ]match/i);
options.ciphers = 'TLS_not_a_cipher';
assert.throws(() => tls.createServer(options, common.mustNotCall()),
/no[_ ]cipher[_ ]match/i);
}
61 changes: 61 additions & 0 deletions test/js/node/test/sequential/test-tls-connect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

'use strict';
const common = require('../common');

if (!common.hasCrypto)
common.skip('missing crypto');

const fixtures = require('../common/fixtures');

const assert = require('assert');
const tls = require('tls');

// https://github.com/joyent/node/issues/1218
// uncatchable exception on TLS connection error
{
const cert = fixtures.readKey('rsa_cert.crt');
const key = fixtures.readKey('rsa_private.pem');

const options = { cert: cert, key: key, port: common.PORT };
const conn = tls.connect(options, common.mustNotCall());

conn.on(
'error',
common.mustCall((e) => { assert.strictEqual(e.code, 'ECONNREFUSED'); })
);
}

// SSL_accept/SSL_connect error handling
{
const cert = fixtures.readKey('rsa_cert.crt');
const key = fixtures.readKey('rsa_private.pem');

assert.throws(() => {
tls.connect({
cert: cert,
key: key,
port: common.PORT,
ciphers: 'rick-128-roll'
}, common.mustNotCall());
}, /no cipher match/i);
}
49 changes: 49 additions & 0 deletions test/js/node/tls/node-tls-no-cipher-match-error.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, test } from "bun:test";
import * as tls from "node:tls";

const fixtures = require("../test/common/fixtures");

describe("TLS No Cipher Match Error code matches Node.js", () => {
test("The error should have all the same properties as Node.js", () => {
const options = {
key: fixtures.readKey("agent2-key.pem"),
cert: fixtures.readKey("agent2-cert.pem"),
ciphers: "aes256-sha",
};

expect(() =>
tls.createServer(options, () => {
throw new Error("should not be called");
}),
).toThrow({
code: "ERR_SSL_NO_CIPHER_MATCH",
message: "No cipher match",
library: "SSL routines",
reason: "no cipher match",
});

options.ciphers = "FOOBARBAZ";
expect(() =>
tls.createServer(options, () => {
throw new Error("should not be called");
}),
).toThrow({
code: "ERR_SSL_NO_CIPHER_MATCH",
message: "No cipher match",
library: "SSL routines",
reason: "no cipher match",
});

options.ciphers = "TLS_not_a_cipher";
expect(() =>
tls.createServer(options, () => {
throw new Error("should not be called");
}),
).toThrow({
code: "ERR_SSL_NO_CIPHER_MATCH",
message: "No cipher match",
library: "SSL routines",
reason: "no cipher match",
});
});
});
6 changes: 3 additions & 3 deletions test/js/web/encoding/text-decoder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,9 @@ describe("TextDecoder", () => {
});

it("should support undefined options", () => {
expect(() => {
const decoder = new TextDecoder("utf-8", undefined);
}).not.toThrow();
expect(() => {
const decoder = new TextDecoder("utf-8", undefined);
}).not.toThrow();
});
});

Expand Down