aboutsummaryrefslogtreecommitdiff
path: root/src/js/node/tls.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/node/tls.js')
-rw-r--r--src/js/node/tls.js351
1 files changed, 332 insertions, 19 deletions
diff --git a/src/js/node/tls.js b/src/js/node/tls.js
index 356c25cbd..310a36620 100644
--- a/src/js/node/tls.js
+++ b/src/js/node/tls.js
@@ -1,9 +1,30 @@
// Hardcoded module "node:tls"
-import { isTypedArray } from "util/types";
+import { isArrayBufferView, isTypedArray } from "util/types";
import net, { Server as NetServer } from "node:net";
const InternalTCPSocket = net[Symbol.for("::bunternal::")];
-
+const bunSocketInternal = Symbol.for("::bunnetsocketinternal::");
+
+const { RegExp, Array, String } = globalThis[Symbol.for("Bun.lazy")]("primordials");
+const SymbolReplace = Symbol.replace;
+const RegExpPrototypeSymbolReplace = RegExp.prototype[SymbolReplace];
+const RegExpPrototypeExec = RegExp.prototype.exec;
+
+const StringPrototypeStartsWith = String.prototype.startsWith;
+const StringPrototypeSlice = String.prototype.slice;
+const StringPrototypeIncludes = String.prototype.includes;
+const StringPrototypeSplit = String.prototype.split;
+const StringPrototypeIndexOf = String.prototype.indexOf;
+const StringPrototypeSubstring = String.prototype.substring;
+const StringPrototypeEndsWith = String.prototype.endsWith;
+
+const ArrayPrototypeIncludes = Array.prototype.includes;
+const ArrayPrototypeJoin = Array.prototype.join;
+const ArrayPrototypeForEach = Array.prototype.forEach;
+const ArrayPrototypePush = Array.prototype.push;
+const ArrayPrototypeSome = Array.prototype.some;
+const ArrayPrototypeReduce = Array.prototype.reduce;
function parseCertString() {
+ // Removed since JAN 2022 Node v18.0.0+ https://github.com/nodejs/node/pull/41479
throwNotImplemented("Not implemented");
}
@@ -18,6 +39,164 @@ function isValidTLSArray(obj) {
}
}
+function unfqdn(host) {
+ return RegExpPrototypeSymbolReplace(/[.]$/, host, "");
+}
+
+function splitHost(host) {
+ return StringPrototypeSplit.call(RegExpPrototypeSymbolReplace(/[A-Z]/g, unfqdn(host), toLowerCase), ".");
+}
+
+function check(hostParts, pattern, wildcards) {
+ // Empty strings, null, undefined, etc. never match.
+ if (!pattern) return false;
+
+ const patternParts = splitHost(pattern);
+
+ if (hostParts.length !== patternParts.length) return false;
+
+ // Pattern has empty components, e.g. "bad..example.com".
+ if (ArrayPrototypeIncludes.call(patternParts, "")) return false;
+
+ // RFC 6125 allows IDNA U-labels (Unicode) in names but we have no
+ // good way to detect their encoding or normalize them so we simply
+ // reject them. Control characters and blanks are rejected as well
+ // because nothing good can come from accepting them.
+ const isBad = s => RegExpPrototypeExec.call(/[^\u0021-\u007F]/u, s) !== null;
+ if (ArrayPrototypeSome.call(patternParts, isBad)) return false;
+
+ // Check host parts from right to left first.
+ for (let i = hostParts.length - 1; i > 0; i -= 1) {
+ if (hostParts[i] !== patternParts[i]) return false;
+ }
+
+ const hostSubdomain = hostParts[0];
+ const patternSubdomain = patternParts[0];
+ const patternSubdomainParts = StringPrototypeSplit.call(patternSubdomain, "*");
+
+ // Short-circuit when the subdomain does not contain a wildcard.
+ // RFC 6125 does not allow wildcard substitution for components
+ // containing IDNA A-labels (Punycode) so match those verbatim.
+ if (patternSubdomainParts.length === 1 || StringPrototypeIncludes.call(patternSubdomain, "xn--"))
+ return hostSubdomain === patternSubdomain;
+
+ if (!wildcards) return false;
+
+ // More than one wildcard is always wrong.
+ if (patternSubdomainParts.length > 2) return false;
+
+ // *.tld wildcards are not allowed.
+ if (patternParts.length <= 2) return false;
+
+ const { 0: prefix, 1: suffix } = patternSubdomainParts;
+
+ if (prefix.length + suffix.length > hostSubdomain.length) return false;
+
+ if (!StringPrototypeStartsWith.call(hostSubdomain, prefix)) return false;
+
+ if (!StringPrototypeEndsWith.call(hostSubdomain, suffix)) return false;
+
+ return true;
+}
+
+// This pattern is used to determine the length of escaped sequences within
+// the subject alt names string. It allows any valid JSON string literal.
+// This MUST match the JSON specification (ECMA-404 / RFC8259) exactly.
+const jsonStringPattern =
+ // eslint-disable-next-line no-control-regex
+ /^"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/;
+
+function splitEscapedAltNames(altNames) {
+ const result = [];
+ let currentToken = "";
+ let offset = 0;
+ while (offset !== altNames.length) {
+ const nextSep = StringPrototypeIndexOf.call(altNames, ", ", offset);
+ const nextQuote = StringPrototypeIndexOf.call(altNames, '"', offset);
+ if (nextQuote !== -1 && (nextSep === -1 || nextQuote < nextSep)) {
+ // There is a quote character and there is no separator before the quote.
+ currentToken += StringPrototypeSubstring.call(altNames, offset, nextQuote);
+ const match = RegExpPrototypeExec.call(jsonStringPattern, StringPrototypeSubstring.call(altNames, nextQuote));
+ if (!match) {
+ let error = new SyntaxError("ERR_TLS_CERT_ALTNAME_FORMAT: Invalid subject alternative name string");
+ error.name = ERR_TLS_CERT_ALTNAME_FORMAT;
+ throw error;
+ }
+ currentToken += JSON.parse(match[0]);
+ offset = nextQuote + match[0].length;
+ } else if (nextSep !== -1) {
+ // There is a separator and no quote before it.
+ currentToken += StringPrototypeSubstring.call(altNames, offset, nextSep);
+ ArrayPrototypePush.call(result, currentToken);
+ currentToken = "";
+ offset = nextSep + 2;
+ } else {
+ currentToken += StringPrototypeSubstring.call(altNames, offset);
+ offset = altNames.length;
+ }
+ }
+ ArrayPrototypePush.call(result, currentToken);
+ return result;
+}
+function checkServerIdentity(hostname, cert) {
+ const subject = cert.subject;
+ const altNames = cert.subjectaltname;
+ const dnsNames = [];
+ const ips = [];
+
+ hostname = "" + hostname;
+
+ if (altNames) {
+ const splitAltNames = StringPrototypeIncludes.call(altNames, '"')
+ ? splitEscapedAltNames(altNames)
+ : StringPrototypeSplit.call(altNames, ", ");
+ ArrayPrototypeForEach.call(splitAltNames, name => {
+ if (StringPrototypeStartsWith.call(name, "DNS:")) {
+ ArrayPrototypePush.call(dnsNames, StringPrototypeSlice.call(name, 4));
+ } else if (StringPrototypeStartsWith.call(name, "IP Address:")) {
+ ArrayPrototypePush.call(ips, canonicalizeIP(StringPrototypeSlice.call(name, 11)));
+ }
+ });
+ }
+
+ let valid = false;
+ let reason = "Unknown reason";
+
+ hostname = unfqdn(hostname); // Remove trailing dot for error messages.
+
+ if (net.isIP(hostname)) {
+ valid = ArrayPrototypeIncludes.call(ips, canonicalizeIP(hostname));
+ if (!valid) reason = `IP: ${hostname} is not in the cert's list: ` + ArrayPrototypeJoin.call(ips, ", ");
+ } else if (dnsNames.length > 0 || subject?.CN) {
+ const hostParts = splitHost(hostname);
+ const wildcard = pattern => check(hostParts, pattern, true);
+
+ if (dnsNames.length > 0) {
+ valid = ArrayPrototypeSome.call(dnsNames, wildcard);
+ if (!valid) reason = `Host: ${hostname}. is not in the cert's altnames: ${altNames}`;
+ } else {
+ // Match against Common Name only if no supported identifiers exist.
+ const cn = subject.CN;
+
+ if (ArrayIsArray(cn)) valid = ArrayPrototypeSome.call(cn, wildcard);
+ else if (cn) valid = wildcard(cn);
+
+ if (!valid) reason = `Host: ${hostname}. is not cert's CN: ${cn}`;
+ }
+ } else {
+ reason = "Cert does not contain a DNS name";
+ }
+
+ if (!valid) {
+ let error = new Error(`ERR_TLS_CERT_ALTNAME_INVALID: Hostname/IP does not match certificate's altnames: ${reason}`);
+ error.name = "ERR_TLS_CERT_ALTNAME_INVALID";
+ error.reason = reason;
+ error.host = host;
+ error.cert = cert;
+ return error;
+ }
+}
+
var InternalSecureContext = class SecureContext {
context;
@@ -83,6 +262,36 @@ function createSecureContext(options) {
return new SecureContext(options);
}
+// Translate some fields from the handle's C-friendly format into more idiomatic
+// javascript object representations before passing them back to the user. Can
+// be used on any cert object, but changing the name would be semver-major.
+function translatePeerCertificate(c) {
+ if (!c) return null;
+
+ if (c.issuerCertificate != null && c.issuerCertificate !== c) {
+ c.issuerCertificate = translatePeerCertificate(c.issuerCertificate);
+ }
+ if (c.infoAccess != null) {
+ const info = c.infoAccess;
+ c.infoAccess = { __proto__: null };
+
+ // XXX: More key validation?
+ RegExpPrototypeSymbolReplace(/([^\n:]*):([^\n]*)(?:\n|$)/g, info, (all, key, val) => {
+ if (val.charCodeAt(0) === 0x22) {
+ // The translatePeerCertificate function is only
+ // used on internally created legacy certificate
+ // objects, and any value that contains a quote
+ // will always be a valid JSON string literal,
+ // so this should never throw.
+ val = JSONParse(val);
+ }
+ if (key in c.infoAccess) ArrayPrototypePush.call(c.infoAccess[key], val);
+ else c.infoAccess[key] = [val];
+ });
+ }
+ return c;
+}
+
const buntls = Symbol.for("::buntls::");
var SocketClass;
@@ -107,8 +316,22 @@ const TLSSocket = (function (InternalTLSSocket) {
})(
class TLSSocket extends InternalTCPSocket {
#secureContext;
- constructor(options) {
- super(options);
+ ALPNProtocols;
+ #socket;
+
+ constructor(socket, options) {
+ super(socket instanceof InternalTCPSocket ? options : options || socket);
+ options = options || socket || {};
+ if (typeof options === "object") {
+ const { ALPNProtocols } = options;
+ if (ALPNProtocols) {
+ convertALPNProtocols(ALPNProtocols, this);
+ }
+ if (socket instanceof InternalTCPSocket) {
+ this.#socket = socket;
+ }
+ }
+
this.#secureContext = options.secureContext || createSecureContext(options);
this.authorized = false;
this.secureConnecting = true;
@@ -123,28 +346,52 @@ const TLSSocket = (function (InternalTLSSocket) {
secureConnecting = false;
_SNICallback;
servername;
- alpnProtocol;
authorized = false;
authorizationError;
encrypted = true;
- exportKeyingMaterial() {
- throw Error("Not implented in Bun yet");
+ _start() {
+ // some frameworks uses this _start internal implementation is suposed to start TLS handshake
+ // on Bun we auto start this after on_open callback and when wrapping we start it after the socket is attached to the net.Socket/tls.Socket
}
- setMaxSendFragment() {
+
+ exportKeyingMaterial(length, label, context) {
+ //SSL_export_keying_material
throw Error("Not implented in Bun yet");
}
- setServername() {
+ setMaxSendFragment(size) {
+ // SSL_set_max_send_fragment
throw Error("Not implented in Bun yet");
}
+ setServername(name) {
+ if (this.isServer) {
+ let error = new Error("ERR_TLS_SNI_FROM_SERVER: Cannot issue SNI from a TLS server-side socket");
+ error.name = "ERR_TLS_SNI_FROM_SERVER";
+ throw error;
+ }
+ // if the socket is detached we can't set the servername but we set this property so when open will auto set to it
+ this.servername = name;
+ this[bunSocketInternal]?.setServername(name);
+ }
setSession() {
throw Error("Not implented in Bun yet");
}
getPeerCertificate() {
+ // need to implement peerCertificate on socket.zig
+ // const cert = this[bunSocketInternal]?.peerCertificate;
+ // if(cert) {
+ // return translatePeerCertificate(cert);
+ // }
throw Error("Not implented in Bun yet");
}
getCertificate() {
+ // need to implement certificate on socket.zig
+ // const cert = this[bunSocketInternal]?.certificate;
+ // if(cert) {
+ // It's not a peer cert, but the formatting is identical.
+ // return translatePeerCertificate(cert);
+ // }
throw Error("Not implented in Bun yet");
}
getPeerX509Certificate() {
@@ -154,16 +401,17 @@ const TLSSocket = (function (InternalTLSSocket) {
throw Error("Not implented in Bun yet");
}
- [buntls](port, host) {
- var { servername } = this;
- if (servername) {
- return {
- serverName: typeof servername === "string" ? servername : host,
- ...this.#secureContext,
- };
- }
+ get alpnProtocol() {
+ return this[bunSocketInternal]?.alpnProtocol;
+ }
- return true;
+ [buntls](port, host) {
+ return {
+ socket: this.#socket,
+ ALPNProtocols: this.ALPNProtocols,
+ serverName: this.servername || host || "localhost",
+ ...this.#secureContext,
+ };
}
},
);
@@ -177,9 +425,12 @@ class Server extends NetServer {
_rejectUnauthorized;
_requestCert;
servername;
+ ALPNProtocols;
+ #checkServerIdentity;
constructor(options, secureConnectionListener) {
super(options, secureConnectionListener);
+ this.#checkServerIdentity = options?.checkServerIdentity || checkServerIdentity;
this.setSecureContext(options);
}
emit(event, args) {
@@ -197,6 +448,12 @@ class Server extends NetServer {
options = options.context;
}
if (options) {
+ const { ALPNProtocols } = options;
+
+ if (ALPNProtocols) {
+ convertALPNProtocols(ALPNProtocols, this);
+ }
+
let key = options.key;
if (key) {
if (!isValidTLSArray(key)) {
@@ -277,6 +534,8 @@ class Server extends NetServer {
// Client always is NONE on set_verify
rejectUnauthorized: isClient ? false : this._rejectUnauthorized,
requestCert: isClient ? false : this._requestCert,
+ ALPNProtocols: this.ALPNProtocols,
+ checkServerIdentity: this.#checkServerIdentity,
},
SocketClass,
];
@@ -296,6 +555,11 @@ const CLIENT_RENEG_LIMIT = 3,
DEFAULT_MAX_VERSION = "TLSv1.3",
createConnection = (port, host, connectListener) => {
if (typeof port === "object") {
+ port.checkServerIdentity || checkServerIdentity;
+ const { ALPNProtocols } = port;
+ if (ALPNProtocols) {
+ convertALPNProtocols(ALPNProtocols, port);
+ }
// port is option pass Socket options and let connect handle connection options
return new TLSSocket(port).connect(port, host, connectListener);
}
@@ -312,7 +576,55 @@ function getCurves() {
return;
}
-function convertALPNProtocols(protocols, out) {}
+// Convert protocols array into valid OpenSSL protocols list
+// ("\x06spdy/2\x08http/1.1\x08http/1.0")
+function convertProtocols(protocols) {
+ const lens = new Array(protocols.length);
+ const buff = Buffer.allocUnsafe(
+ ArrayPrototypeReduce.call(
+ protocols,
+ (p, c, i) => {
+ const len = Buffer.byteLength(c);
+ if (len > 255) {
+ throw new RangeError(
+ "The byte length of the protocol at index " + `${i} exceeds the maximum length.`,
+ "<= 255",
+ len,
+ true,
+ );
+ }
+ lens[i] = len;
+ return p + 1 + len;
+ },
+ 0,
+ ),
+ );
+
+ let offset = 0;
+ for (let i = 0, c = protocols.length; i < c; i++) {
+ buff[offset++] = lens[i];
+ buff.write(protocols[i], offset);
+ offset += lens[i];
+ }
+
+ return buff;
+}
+
+function convertALPNProtocols(protocols, out) {
+ // If protocols is Array - translate it into buffer
+ if (Array.isArray(protocols)) {
+ out.ALPNProtocols = convertProtocols(protocols);
+ } else if (isTypedArray(protocols)) {
+ // Copy new buffer not to be modified by user.
+ out.ALPNProtocols = Buffer.from(protocols);
+ } else if (isArrayBufferView(protocols)) {
+ out.ALPNProtocols = Buffer.from(
+ protocols.buffer.slice(protocols.byteOffset, protocols.byteOffset + protocols.byteLength),
+ );
+ } else if (Buffer.isBuffer(protocols)) {
+ out.ALPNProtocols = protocols;
+ }
+}
var exports = {
[Symbol.for("CommonJS")]: 0,
@@ -351,6 +663,7 @@ export {
getCurves,
parseCertString,
SecureContext,
+ checkServerIdentity,
Server,
TLSSocket,
exports as default,