From 7242c1b67074bdc76d260d39e6a1b0e44fdfc4a8 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 17 Jan 2025 05:24:45 -0800 Subject: [PATCH] Implement X509Certificate in node:crypto (#16173) Co-authored-by: Jarred-Sumner Co-authored-by: Dylan Conway Co-authored-by: dylan-conway <35280289+dylan-conway@users.noreply.github.com> --- .clangd | 3 + packages/bun-types/bun.d.ts | 3 + src/bun.js/api/BunObject.zig | 58 +- src/bun.js/api/bun/socket.zig | 33 + src/bun.js/api/bun/x509.zig | 517 +-- src/bun.js/api/sockets.classes.ts | 22 +- src/bun.js/bindings/AsymmetricKeyValue.h | 23 + src/bun.js/bindings/BunProcess.cpp | 11 + src/bun.js/bindings/BunString.h | 50 + src/bun.js/bindings/ErrorCode.cpp | 39 +- src/bun.js/bindings/ErrorCode.h | 5 + src/bun.js/bindings/ErrorCode.ts | 89 +- src/bun.js/bindings/JSBuffer.cpp | 8 + src/bun.js/bindings/JSBuffer.h | 2 + src/bun.js/bindings/JSBufferEncodingType.cpp | 5 +- .../bindings/JSDOMExceptionHandling.cpp | 14 +- src/bun.js/bindings/JSDOMExceptionHandling.h | 3 + src/bun.js/bindings/JSX509Certificate.cpp | 1217 +++++++ src/bun.js/bindings/JSX509Certificate.h | 156 + .../bindings/JSX509CertificateConstructor.cpp | 11 + .../bindings/JSX509CertificateConstructor.h | 15 + .../bindings/JSX509CertificatePrototype.cpp | 752 ++++ .../bindings/JSX509CertificatePrototype.h | 49 + src/bun.js/bindings/KeyObject.cpp | 122 +- src/bun.js/bindings/KeyObject.h | 4 +- src/bun.js/bindings/NodeCrypto.cpp | 441 +++ src/bun.js/bindings/NodeCrypto.h | 11 + src/bun.js/bindings/ZigGlobalObject.cpp | 8 +- src/bun.js/bindings/ZigGlobalObject.h | 1 + src/bun.js/bindings/bindings.zig | 21 +- src/bun.js/bindings/dh-primes.h | 76 + src/bun.js/bindings/ncrpyto_engine.cpp | 106 + src/bun.js/bindings/ncrypto.cpp | 3196 +++++++++++++++++ src/bun.js/bindings/ncrypto.h | 1119 ++++++ .../bindings/webcore/DOMClientIsoSubspaces.h | 1 + src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 2 +- .../webcore/SerializedScriptValue.cpp | 79 +- .../bindings/webcrypto/CryptoAlgorithm.cpp | 24 +- .../bindings/webcrypto/CryptoAlgorithm.h | 2 +- .../webcrypto/CryptoAlgorithmAES_CBC.cpp | 18 +- .../webcrypto/CryptoAlgorithmAES_CFB.cpp | 18 +- .../webcrypto/CryptoAlgorithmAES_CTR.cpp | 18 +- .../webcrypto/CryptoAlgorithmAES_GCM.cpp | 30 +- .../webcrypto/CryptoAlgorithmAES_KW.cpp | 20 +- .../webcrypto/CryptoAlgorithmECDH.cpp | 38 +- .../webcrypto/CryptoAlgorithmECDSA.cpp | 32 +- .../webcrypto/CryptoAlgorithmEd25519.cpp | 30 +- .../webcrypto/CryptoAlgorithmHKDF.cpp | 8 +- .../webcrypto/CryptoAlgorithmHMAC.cpp | 16 +- .../webcrypto/CryptoAlgorithmPBKDF2.cpp | 8 +- .../CryptoAlgorithmRSAES_PKCS1_v1_5.cpp | 30 +- .../CryptoAlgorithmRSASSA_PKCS1_v1_5.cpp | 30 +- .../webcrypto/CryptoAlgorithmRSA_OAEP.cpp | 30 +- .../webcrypto/CryptoAlgorithmRSA_PSS.cpp | 30 +- .../webcrypto/CryptoAlgorithmSHA1.cpp | 2 +- .../webcrypto/CryptoAlgorithmSHA224.cpp | 2 +- .../webcrypto/CryptoAlgorithmSHA256.cpp | 2 +- .../webcrypto/CryptoAlgorithmSHA384.cpp | 2 +- .../webcrypto/CryptoAlgorithmSHA512.cpp | 2 +- src/bun.js/bindings/webcrypto/CryptoKey.cpp | 5 +- src/bun.js/bindings/webcrypto/CryptoKey.h | 4 - src/bun.js/bindings/webcrypto/JSCryptoKey.cpp | 22 + src/bun.js/bindings/webcrypto/JSCryptoKey.h | 2 + .../bindings/webcrypto/JSSubtleCrypto.cpp | 6 +- .../bindings/webcrypto/SubtleCrypto.cpp | 78 +- src/bun.js/node/node_crypto_binding.zig | 2 +- src/bun.js/node/types.zig | 4 +- src/bun.js/node/util/validators.zig | 8 +- src/bun.js/webcore.zig | 2 +- src/bun.js/webcore/response.zig | 2 +- src/deps/boringssl.translated.zig | 15 +- src/fmt.zig | 19 +- src/js/internal/crypto/x509.ts | 3 + src/js/node/crypto.ts | 179 +- src/js/node/net.ts | 544 +-- src/js/node/tls.ts | 105 +- test/js/bun/http/bun-connect-x509.test.ts | 81 + test/js/bun/http/proxy.test.js | 10 +- test/js/node/crypto/pbkdf2.test.ts | 15 +- test/js/node/dns/node-dns.test.js | 2 +- test/js/node/test/common/index.js | 4 + .../test/parallel/test-crypto-aes-wrap.js | 68 + .../test-crypto-authenticated-stream.js | 147 + .../parallel/test-crypto-authenticated.js | 709 ++++ .../test/parallel/test-crypto-certificate.js | 127 + .../test/parallel/test-crypto-des3-wrap.js | 31 + .../test/parallel/test-crypto-dh-curves.js | 191 + .../parallel/test-crypto-dh-group-setters.js | 19 + .../parallel/test-crypto-dh-modp2-views.js | 30 + .../test/parallel/test-crypto-dh-modp2.js | 49 + test/js/node/test/parallel/test-crypto-ecb.js | 60 + .../parallel/test-crypto-ecdh-convert-key.js | 125 + .../js/node/test/parallel/test-crypto-fips.js | 285 ++ .../parallel/test-crypto-getcipherinfo.js | 74 + .../test/parallel/test-crypto-key-objects.js | 894 +++++ .../test-crypto-keygen-deprecation.js | 55 + .../node/test/parallel/test-crypto-keygen.js | 826 +++++ .../node/test/parallel/test-crypto-pbkdf2.js | 241 ++ .../test-crypto-psychic-signatures.js | 100 + .../node/test/parallel/test-crypto-rsa-dsa.js | 549 +++ .../test/parallel/test-crypto-secure-heap.js | 82 + .../parallel/test-crypto-verify-failure.js | 67 + ...pto-webcrypto-aes-decrypt-tag-too-small.js | 29 + .../js/node/test/parallel/test-crypto-x509.js | 446 +++ test/js/node/test/parallel/test-fs-fchmod.js | 4 +- test/js/node/test/parallel/test-fs-fchown.js | 4 +- test/js/node/test/parallel/test-fs-lchmod.js | 2 +- .../test-webcrypto-derivebits-cfrg.js | 230 ++ .../parallel/test-webcrypto-derivekey-cfrg.js | 193 + .../test/parallel/test-webcrypto-derivekey.js | 211 ++ .../test/parallel/test-webcrypto-digest.js | 174 + .../test-webcrypto-encrypt-decrypt-aes.js | 241 ++ .../parallel/test-webcrypto-sign-verify.js | 146 + .../parallel/test-webcrypto-wrap-unwrap.js | 310 ++ test/js/node/tls/node-tls-connect.test.ts | 4 +- test/js/node/tls/node-tls-server.test.ts | 4 +- 116 files changed, 15235 insertions(+), 1268 deletions(-) create mode 100644 src/bun.js/bindings/AsymmetricKeyValue.h create mode 100644 src/bun.js/bindings/BunString.h create mode 100644 src/bun.js/bindings/JSX509Certificate.cpp create mode 100644 src/bun.js/bindings/JSX509Certificate.h create mode 100644 src/bun.js/bindings/JSX509CertificateConstructor.cpp create mode 100644 src/bun.js/bindings/JSX509CertificateConstructor.h create mode 100644 src/bun.js/bindings/JSX509CertificatePrototype.cpp create mode 100644 src/bun.js/bindings/JSX509CertificatePrototype.h create mode 100644 src/bun.js/bindings/NodeCrypto.cpp create mode 100644 src/bun.js/bindings/NodeCrypto.h create mode 100644 src/bun.js/bindings/dh-primes.h create mode 100644 src/bun.js/bindings/ncrpyto_engine.cpp create mode 100644 src/bun.js/bindings/ncrypto.cpp create mode 100644 src/bun.js/bindings/ncrypto.h create mode 100644 src/js/internal/crypto/x509.ts create mode 100644 test/js/bun/http/bun-connect-x509.test.ts create mode 100644 test/js/node/test/parallel/test-crypto-aes-wrap.js create mode 100644 test/js/node/test/parallel/test-crypto-authenticated-stream.js create mode 100644 test/js/node/test/parallel/test-crypto-authenticated.js create mode 100644 test/js/node/test/parallel/test-crypto-certificate.js create mode 100644 test/js/node/test/parallel/test-crypto-des3-wrap.js create mode 100644 test/js/node/test/parallel/test-crypto-dh-curves.js create mode 100644 test/js/node/test/parallel/test-crypto-dh-group-setters.js create mode 100644 test/js/node/test/parallel/test-crypto-dh-modp2-views.js create mode 100644 test/js/node/test/parallel/test-crypto-dh-modp2.js create mode 100644 test/js/node/test/parallel/test-crypto-ecb.js create mode 100644 test/js/node/test/parallel/test-crypto-ecdh-convert-key.js create mode 100644 test/js/node/test/parallel/test-crypto-fips.js create mode 100644 test/js/node/test/parallel/test-crypto-getcipherinfo.js create mode 100644 test/js/node/test/parallel/test-crypto-key-objects.js create mode 100644 test/js/node/test/parallel/test-crypto-keygen-deprecation.js create mode 100644 test/js/node/test/parallel/test-crypto-keygen.js create mode 100644 test/js/node/test/parallel/test-crypto-pbkdf2.js create mode 100644 test/js/node/test/parallel/test-crypto-psychic-signatures.js create mode 100644 test/js/node/test/parallel/test-crypto-rsa-dsa.js create mode 100644 test/js/node/test/parallel/test-crypto-secure-heap.js create mode 100644 test/js/node/test/parallel/test-crypto-verify-failure.js create mode 100644 test/js/node/test/parallel/test-crypto-webcrypto-aes-decrypt-tag-too-small.js create mode 100644 test/js/node/test/parallel/test-crypto-x509.js create mode 100644 test/js/node/test/parallel/test-webcrypto-derivebits-cfrg.js create mode 100644 test/js/node/test/parallel/test-webcrypto-derivekey-cfrg.js create mode 100644 test/js/node/test/parallel/test-webcrypto-derivekey.js create mode 100644 test/js/node/test/parallel/test-webcrypto-digest.js create mode 100644 test/js/node/test/parallel/test-webcrypto-encrypt-decrypt-aes.js create mode 100644 test/js/node/test/parallel/test-webcrypto-sign-verify.js create mode 100644 test/js/node/test/parallel/test-webcrypto-wrap-unwrap.js diff --git a/.clangd b/.clangd index b0ceeaa684..2d2d52668c 100644 --- a/.clangd +++ b/.clangd @@ -6,3 +6,6 @@ CompileFlags: Diagnostics: UnusedIncludes: None + +HeaderInsertion: + IncludeBlocks: Preserve # Do not auto-include headers. diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 2fb43f806a..06df76e2ef 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -18,6 +18,7 @@ declare module "bun" { import type { Encoding as CryptoEncoding } from "crypto"; import type { CipherNameAndProtocol, EphemeralKeyInfo, PeerCertificate } from "tls"; import type { Stats } from "node:fs"; + import type { X509Certificate } from "node:crypto"; interface Env { NODE_ENV?: string; /** @@ -5372,6 +5373,7 @@ declare module "bun" { * socket has been destroyed, `null` will be returned. */ getCertificate(): PeerCertificate | object | null; + getX509Certificate(): X509Certificate | undefined; /** * Returns an object containing information on the negotiated cipher suite. @@ -5410,6 +5412,7 @@ declare module "bun" { * @return A certificate object. */ getPeerCertificate(): PeerCertificate; + getPeerX509Certificate(): X509Certificate; /** * See [SSL\_get\_shared\_sigalgs](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_shared_sigalgs.html) for more information. diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index f498dc2ac9..687e98cd4e 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -1126,6 +1126,7 @@ pub const Crypto = struct { sha512, @"sha512-224", @"sha512-256", + @"sha3-224", @"sha3-256", @"sha3-384", @@ -1139,6 +1140,7 @@ pub const Crypto = struct { .blake2b512 => BoringSSL.EVP_blake2b512(), .md4 => BoringSSL.EVP_md4(), .md5 => BoringSSL.EVP_md5(), + .ripemd160 => BoringSSL.EVP_ripemd160(), .sha1 => BoringSSL.EVP_sha1(), .sha224 => BoringSSL.EVP_sha224(), .sha256 => BoringSSL.EVP_sha256(), @@ -1359,28 +1361,37 @@ pub const Crypto = struct { return globalThis.throwNotEnoughArguments("pbkdf2", 5, arguments.len); } - if (!arguments[3].isAnyInt()) { - return globalThis.throwInvalidArgumentTypeValue("keylen", "integer", arguments[3]); + if (!arguments[3].isNumber()) { + return globalThis.throwInvalidArgumentTypeValue("keylen", "number", arguments[3]); } - const length = arguments[3].coerce(i64, globalThis); + const keylen_num = arguments[3].asNumber(); - if (!globalThis.hasException() and (length < 0 or length > std.math.maxInt(i32))) { - return globalThis.throwInvalidArguments("keylen must be > 0 and < {d}", .{std.math.maxInt(i32)}); + if (std.math.isInf(keylen_num) or std.math.isNan(keylen_num)) { + return globalThis.throwRangeError(keylen_num, .{ + .field_name = "keylen", + .msg = "an integer", + }); } + if (keylen_num < 0 or keylen_num > std.math.maxInt(i32)) { + return globalThis.throwRangeError(keylen_num, .{ .field_name = "keylen", .min = 0, .max = std.math.maxInt(i32) }); + } + + const keylen: i32 = @intFromFloat(keylen_num); + if (globalThis.hasException()) { return error.JSError; } if (!arguments[2].isAnyInt()) { - return globalThis.throwInvalidArgumentTypeValue("iteration count", "integer", arguments[2]); + return globalThis.throwInvalidArgumentTypeValue("iterations", "number", arguments[2]); } const iteration_count = arguments[2].coerce(i64, globalThis); - if (!globalThis.hasException() and (iteration_count < 1 or iteration_count > std.math.maxInt(u32))) { - return globalThis.throwInvalidArguments("iteration count must be >= 1 and <= maxInt", .{}); + if (!globalThis.hasException() and (iteration_count < 1 or iteration_count > std.math.maxInt(i32))) { + return globalThis.throwRangeError(iteration_count, .{ .field_name = "iterations", .min = 1, .max = std.math.maxInt(i32) + 1 }); } if (globalThis.hasException()) { @@ -1389,23 +1400,28 @@ pub const Crypto = struct { const algorithm = brk: { if (!arguments[4].isString()) { - return globalThis.throwInvalidArgumentTypeValue("algorithm", "string", arguments[4]); + return globalThis.throwInvalidArgumentTypeValue("digest", "string", arguments[4]); } - break :brk EVP.Algorithm.map.fromJSCaseInsensitive(globalThis, arguments[4]) orelse { - if (!globalThis.hasException()) { - const slice = arguments[4].toSlice(globalThis, bun.default_allocator); - defer slice.deinit(); - const name = slice.slice(); - return globalThis.ERR_CRYPTO_INVALID_DIGEST("Unsupported algorithm \"{s}\"", .{name}).throw(); + invalid: { + switch (EVP.Algorithm.map.fromJSCaseInsensitive(globalThis, arguments[4]) orelse break :invalid) { + .shake128, .shake256, .@"sha3-224", .@"sha3-256", .@"sha3-384", .@"sha3-512" => break :invalid, + else => |alg| break :brk alg, } - return error.JSError; - }; + } + + if (!globalThis.hasException()) { + const slice = arguments[4].toSlice(globalThis, bun.default_allocator); + defer slice.deinit(); + const name = slice.slice(); + return globalThis.ERR_CRYPTO_INVALID_DIGEST("Invalid digest: {s}", .{name}).throw(); + } + return error.JSError; }; var out = PBKDF2{ .iteration_count = @intCast(iteration_count), - .length = @truncate(length), + .length = keylen, .algorithm = algorithm, }; defer { @@ -1436,6 +1452,12 @@ pub const Crypto = struct { return globalThis.throwInvalidArguments("password is too long", .{}); } + if (is_async) { + if (!arguments[5].isFunction()) { + return globalThis.throwInvalidArgumentTypeValue("callback", "function", arguments[5]); + } + } + return out; } }; diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 7a2fef50eb..fd45fcfed5 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -3218,6 +3218,39 @@ fn NewSocket(comptime ssl: bool) type { return JSValue.jsUndefined(); } + pub fn getPeerX509Certificate( + this: *This, + globalObject: *JSC.JSGlobalObject, + _: *JSC.CallFrame, + ) bun.JSError!JSValue { + if (comptime ssl == false) { + return JSValue.jsUndefined(); + } + const ssl_ptr = this.socket.ssl() orelse return JSValue.jsUndefined(); + const cert = BoringSSL.SSL_get_peer_certificate(ssl_ptr); + if (cert) |x509| { + return X509.toJSObject(x509, globalObject); + } + return JSValue.jsUndefined(); + } + + pub fn getX509Certificate( + this: *This, + globalObject: *JSC.JSGlobalObject, + _: *JSC.CallFrame, + ) bun.JSError!JSValue { + if (comptime ssl == false) { + return JSValue.jsUndefined(); + } + + const ssl_ptr = this.socket.ssl() orelse return JSValue.jsUndefined(); + const cert = BoringSSL.SSL_get_certificate(ssl_ptr); + if (cert) |x509| { + return X509.toJSObject(x509.ref(), globalObject); + } + return JSValue.jsUndefined(); + } + pub fn getServername( this: *This, globalObject: *JSC.JSGlobalObject, diff --git a/src/bun.js/api/bun/x509.zig b/src/bun.js/api/bun/x509.zig index c3efe96669..fc2f09aac4 100644 --- a/src/bun.js/api/bun/x509.zig +++ b/src/bun.js/api/bun/x509.zig @@ -6,72 +6,6 @@ const JSC = bun.JSC; const JSValue = JSC.JSValue; const JSGlobalObject = JSC.JSGlobalObject; -fn x509GetNameObject(globalObject: *JSGlobalObject, name: ?*BoringSSL.X509_NAME) bun.JSError!JSValue { - const cnt = BoringSSL.X509_NAME_entry_count(name); - if (cnt <= 0) { - return JSValue.jsUndefined(); - } - var result = JSValue.createEmptyObject(globalObject, 1); - - for (0..@as(usize, @intCast(cnt))) |i| { - const entry = BoringSSL.X509_NAME_get_entry(name, @as(c_int, @intCast(i))) orelse continue; - // We intentionally ignore the value of X509_NAME_ENTRY_set because the - // representation as an object does not allow grouping entries into sets - // anyway, and multi-value RDNs are rare, i.e., the vast majority of - // Relative Distinguished Names contains a single type-value pair only. - const type_ = BoringSSL.X509_NAME_ENTRY_get_object(entry); - - // If BoringSSL knows the type, use the short name of the type as the key, and - // the numeric representation of the type's OID otherwise. - const type_nid = BoringSSL.OBJ_obj2nid(type_); - var type_buf: [80]u8 = undefined; - var name_slice: []const u8 = undefined; - if (type_nid != BoringSSL.NID_undef) { - const type_str = BoringSSL.OBJ_nid2sn(type_nid); - if (type_str == null) { - continue; - } - name_slice = type_str[0..bun.len(type_str)]; - } else { - const length = BoringSSL.OBJ_obj2txt(&type_buf, @sizeOf(@TypeOf(type_buf)), type_, 1); - if (length <= 0) { - continue; - } - name_slice = type_buf[0..@as(usize, @intCast(length))]; - } - - const value_data = BoringSSL.X509_NAME_ENTRY_get_data(entry); - - var value_str: [*c]u8 = undefined; - const value_str_len = BoringSSL.ASN1_STRING_to_UTF8(&value_str, value_data); - if (value_str_len < 0) { - continue; - } - const value_slice = value_str[0..@as(usize, @intCast(value_str_len))]; - defer BoringSSL.OPENSSL_free(value_str); - // For backward compatibility, we only create arrays if multiple values - // exist for the same key. That is not great but there is not much we can - // change here without breaking things. Note that this creates nested data - // structures, yet still does not allow representing Distinguished Names - // accurately. - if (try result.getTruthy(globalObject, name_slice)) |value| { - if (value.jsType().isArray()) { - value.push(globalObject, JSC.ZigString.fromUTF8(value_slice).toJS(globalObject)); - } else { - const prop_name = JSC.ZigString.fromUTF8(name_slice); - const array = JSValue.createEmptyArray(globalObject, 2); - array.putIndex(globalObject, 0, value); - array.putIndex(globalObject, 1, JSC.ZigString.fromUTF8(value_slice).toJS(globalObject)); - result.put(globalObject, &prop_name, array); - } - } else { - const prop_name = JSC.ZigString.fromUTF8(name_slice); - result.put(globalObject, &prop_name, JSC.ZigString.fromUTF8(value_slice).toJS(globalObject)); - } - } - return result; -} - pub inline fn isSafeAltName(name: []const u8, utf8: bool) bool { for (name) |c| { switch (c) { @@ -112,448 +46,13 @@ pub inline fn isSafeAltName(name: []const u8, utf8: bool) bool { return true; } -inline fn printAltName(out: *BoringSSL.BIO, name: []const u8, utf8: bool, safe_prefix: ?[*]const u8) void { - if (isSafeAltName(name, utf8)) { - // For backward-compatibility, append "safe" names without any - // modifications. - if (safe_prefix) |prefix| { - _ = BoringSSL.BIO_printf(out, "%s:", prefix); - } - _ = BoringSSL.BIO_write(out, @as([*]const u8, @ptrCast(name.ptr)), @as(c_int, @intCast(name.len))); - } else { - // If a name is not "safe", we cannot embed it without special - // encoding. This does not usually happen, but we don't want to hide - // it from the user either. We use JSON compatible escaping here. - _ = BoringSSL.BIO_write(out, "\"", 1); - if (safe_prefix) |prefix| { - _ = BoringSSL.BIO_printf(out, "%s:", prefix); - } - for (name) |c| { - if (c == '\\') { - _ = BoringSSL.BIO_write(out, "\\\\", 2); - } else if (c == '"') { - _ = BoringSSL.BIO_write(out, "\\\"", 2); - } else if ((c >= ' ' and c != ',' and c <= '~') or (utf8 and (c & 0x80) != 0)) { - // Note that the above condition explicitly excludes commas, which means - // that those are encoded as Unicode escape sequences in the "else" - // block. That is not strictly necessary, and Node.js itself would parse - // it correctly either way. We only do this to account for third-party - // code that might be splitting the string at commas (as Node.js itself - // used to do). - _ = BoringSSL.BIO_write(out, bun.cast([*]const u8, &c), 1); - } else { - // Control character or non-ASCII character. We treat everything as - // Latin-1, which corresponds to the first 255 Unicode code points. - const hex = "0123456789abcdef"; - const u = [_]u8{ '\\', 'u', '0', '0', hex[(c & 0xf0) >> 4], hex[c & 0x0f] }; - _ = BoringSSL.BIO_write(out, &u, @sizeOf(@TypeOf(u))); - } - } - _ = BoringSSL.BIO_write(out, "\"", 1); - } -} - -inline fn printLatin1AltName(out: *BoringSSL.BIO, name: *BoringSSL.ASN1_IA5STRING, safe_prefix: ?[*]const u8) void { - printAltName(out, name.data[0..@as(usize, @intCast(name.length))], false, safe_prefix); -} - -inline fn printUTF8AltName(out: *BoringSSL.BIO, name: *BoringSSL.ASN1_UTF8STRING, safe_prefix: ?[*]const u8) void { - printAltName(out, name.data[0..@as(usize, @intCast(name.length))], true, safe_prefix); -} - -pub const kX509NameFlagsRFC2253WithinUtf8JSON = BoringSSL.XN_FLAG_RFC2253 & ~BoringSSL.ASN1_STRFLGS_ESC_MSB & ~BoringSSL.ASN1_STRFLGS_ESC_CTRL; - -// This function emulates the behavior of i2v_GENERAL_NAME in a safer and less -// ambiguous way. "othername:" entries use the GENERAL_NAME_print format. -fn x509PrintGeneralName(out: *BoringSSL.BIO, name: *BoringSSL.GENERAL_NAME) bool { - if (name.name_type == .GEN_DNS) { - _ = BoringSSL.BIO_write(out, "DNS:", 4); - // Note that the preferred name syntax (see RFCs 5280 and 1034) with - // wildcards is a subset of what we consider "safe", so spec-compliant DNS - // names will never need to be escaped. - printLatin1AltName(out, name.d.dNSName, null); - } else if (name.name_type == .GEN_EMAIL) { - _ = BoringSSL.BIO_write(out, "email:", 6); - printLatin1AltName(out, name.d.rfc822Name, null); - } else if (name.name_type == .GEN_URI) { - _ = BoringSSL.BIO_write(out, "URI:", 4); - // The set of "safe" names was designed to include just about any URI, - // with a few exceptions, most notably URIs that contains commas (see - // RFC 2396). In other words, most legitimate URIs will not require - // escaping. - printLatin1AltName(out, name.d.uniformResourceIdentifier, null); - } else if (name.name_type == .GEN_DIRNAME) { - // Earlier versions of Node.js used X509_NAME_oneline to print the X509_NAME - // object. The format was non standard and should be avoided. The use of - // X509_NAME_oneline is discouraged by OpenSSL but was required for backward - // compatibility. Conveniently, X509_NAME_oneline produced ASCII and the - // output was unlikely to contains commas or other characters that would - // require escaping. However, it SHOULD NOT produce ASCII output since an - // RFC5280 AttributeValue may be a UTF8String. - // Newer versions of Node.js have since switched to X509_NAME_print_ex to - // produce a better format at the cost of backward compatibility. The new - // format may contain Unicode characters and it is likely to contain commas, - // which require escaping. Fortunately, the recently safeguarded function - // printAltName handles all of that safely. - _ = BoringSSL.BIO_printf(out, "DirName:"); - - const tmp = BoringSSL.BIO_new(BoringSSL.BIO_s_mem()) orelse return false; - - if (BoringSSL.X509_NAME_print_ex(tmp, name.d.dirn, 0, kX509NameFlagsRFC2253WithinUtf8JSON) < 0) { - return false; - } - var oline: [*]const u8 = undefined; - const n_bytes = BoringSSL.BIO_get_mem_data(tmp, @as([*c][*c]u8, @ptrCast(&oline))); - if (n_bytes <= 0) return false; - printAltName(out, oline[0..@as(usize, @intCast(n_bytes))], true, null); - } else if (name.name_type == .GEN_OTHERNAME) { - // The format that is used here is based on OpenSSL's implementation of - // GENERAL_NAME_print (as of OpenSSL 3.0.1). Earlier versions of Node.js - // instead produced the same format as i2v_GENERAL_NAME, which was somewhat - // awkward, especially when passed to translatePeerCertificate. - var unicode: bool = true; - var prefix: ?[*]const u8 = null; - - const nid = BoringSSL.OBJ_obj2nid(name.d.otherName.type_id); - switch (nid) { - BoringSSL.NID_id_on_SmtpUTF8Mailbox => { - prefix = "SmtpUTF8Mailbox"; - }, - BoringSSL.NID_XmppAddr => { - prefix = "XmppAddr"; - }, - BoringSSL.NID_SRVName => { - prefix = "SRVName"; - unicode = false; - }, - BoringSSL.NID_ms_upn => { - prefix = "UPN"; - }, - BoringSSL.NID_NAIRealm => { - prefix = "NAIRealm"; - }, - else => { - prefix = null; - }, - } - if (name.d.otherName.value) |v| { - const val_type = v.type; - if (prefix == null or - (unicode and val_type != BoringSSL.V_ASN1_UTF8STRING) or - (!unicode and val_type != BoringSSL.V_ASN1_IA5STRING)) - { - _ = BoringSSL.BIO_printf(out, "othername:"); - } else { - _ = BoringSSL.BIO_printf(out, "othername:"); - if (unicode) { - printUTF8AltName(out, v.value.utf8string, prefix); - } else { - printLatin1AltName(out, v.value.ia5string, prefix); - } - } - } else { - _ = BoringSSL.BIO_printf(out, "othername:"); - } - } else if (name.name_type == .GEN_IPADD) { - _ = BoringSSL.BIO_printf(out, "IP Address:"); - const ip = name.d.ip; - const b = ip.data; - if (ip.length == 4) { - _ = BoringSSL.BIO_printf(out, "%d.%d.%d.%d", b[0], b[1], b[2], b[3]); - } else if (ip.length == 16) { - for (0..8) |j| { - const pair: u16 = (@as(u16, @intCast(b[2 * j])) << 8) | @as(u16, @intCast(b[2 * j + 1])); - _ = BoringSSL.BIO_printf(out, if (j == 0) "%X" else ":%X", pair); - } - } else { - _ = BoringSSL.BIO_printf(out, "", ip.length); - } - } else if (name.name_type == .GEN_RID) { - // Unlike OpenSSL's default implementation, never print the OID as text and - // instead always print its numeric representation. - var oline: [256]u8 = undefined; - _ = BoringSSL.OBJ_obj2txt(&oline, @sizeOf(@TypeOf(oline)), name.d.rid, 1); - // Workaround for https://github.com/ziglang/zig/issues/16197 - _ = BoringSSL.BIO_printf(out, "Registered ID:%s", @as([*]const u8, &oline)); - } else if (name.name_type == .GEN_X400) { - _ = BoringSSL.BIO_printf(out, "X400Name:"); - } else if (name.name_type == .GEN_EDIPARTY) { - _ = BoringSSL.BIO_printf(out, "EdiPartyName:"); - } else { - return false; - } - return true; -} - -fn x509InfoAccessPrint(out: *BoringSSL.BIO, ext: *BoringSSL.X509_EXTENSION) bool { - const method = BoringSSL.X509V3_EXT_get(ext); - if (method != BoringSSL.X509V3_EXT_get_nid(BoringSSL.NID_info_access)) { - return false; - } - - if (BoringSSL.X509V3_EXT_d2i(ext)) |descs_| { - const descs: *BoringSSL.AUTHORITY_INFO_ACCESS = bun.cast(*BoringSSL.AUTHORITY_INFO_ACCESS, descs_); - defer BoringSSL.sk_ACCESS_DESCRIPTION_pop_free(descs, BoringSSL.sk_ACCESS_DESCRIPTION_free); - for (0..BoringSSL.sk_ACCESS_DESCRIPTION_num(descs)) |i| { - const gen = BoringSSL.sk_ACCESS_DESCRIPTION_value(descs, i); - if (gen) |desc| { - if (i != 0) { - _ = BoringSSL.BIO_write(out, "\n", 1); - } - var tmp: [80]u8 = undefined; - _ = BoringSSL.i2t_ASN1_OBJECT(&tmp, @sizeOf(@TypeOf(tmp)), desc.method); - // Workaround for https://github.com/ziglang/zig/issues/16197 - _ = BoringSSL.BIO_printf(out, "%s - ", @as([*]const u8, &tmp)); - - if (!x509PrintGeneralName(out, desc.location)) { - return false; - } - } - } - return true; - } - return false; -} -fn x509SubjectAltNamePrint(out: *BoringSSL.BIO, ext: *BoringSSL.X509_EXTENSION) bool { - const method = BoringSSL.X509V3_EXT_get(ext); - if (method != BoringSSL.X509V3_EXT_get_nid(BoringSSL.NID_subject_alt_name)) { - return false; - } - - if (BoringSSL.X509V3_EXT_d2i(ext)) |names_| { - const names: *BoringSSL.struct_stack_st_GENERAL_NAME = bun.cast(*BoringSSL.struct_stack_st_GENERAL_NAME, names_); - defer BoringSSL.sk_GENERAL_NAME_pop_free(names, BoringSSL.sk_GENERAL_NAME_free); - for (0..BoringSSL.sk_GENERAL_NAME_num(names)) |i| { - const gen = BoringSSL.sk_GENERAL_NAME_value(names, i); - if (gen) |gen_name| { - if (i != 0) { - _ = BoringSSL.BIO_write(out, ", ", 2); - } - - if (!x509PrintGeneralName(out, gen_name)) { - return false; - } - } - } - - return true; - } - return false; -} - -fn x509GetSubjectAltNameString(globalObject: *JSGlobalObject, bio: *BoringSSL.BIO, cert: *BoringSSL.X509) JSValue { - const index = BoringSSL.X509_get_ext_by_NID(cert, BoringSSL.NID_subject_alt_name, -1); - if (index < 0) - return JSValue.jsUndefined(); - - defer _ = BoringSSL.BIO_reset(bio); - - const ext = BoringSSL.X509_get_ext(cert, index) orelse return JSValue.jsUndefined(); - - if (!x509SubjectAltNamePrint(bio, ext)) { - return JSValue.jsNull(); - } - - return JSC.ZigString.fromUTF8(bio.slice()).toJS(globalObject); -} - -fn x509GetInfoAccessString(globalObject: *JSGlobalObject, bio: *BoringSSL.BIO, cert: *BoringSSL.X509) JSValue { - const index = BoringSSL.X509_get_ext_by_NID(cert, BoringSSL.NID_info_access, -1); - if (index < 0) - return JSValue.jsUndefined(); - defer _ = BoringSSL.BIO_reset(bio); - const ext = BoringSSL.X509_get_ext(cert, index) orelse return JSValue.jsUndefined(); - - if (!x509InfoAccessPrint(bio, ext)) { - return JSValue.jsNull(); - } - - return JSC.ZigString.fromUTF8(bio.slice()).toJS(globalObject); -} - -fn addFingerprintDigest(md: []const u8, mdSize: c_uint, fingerprint: []u8) usize { - const hex: []const u8 = "0123456789ABCDEF"; - var idx: usize = 0; - - const slice = md[0..@as(usize, @intCast(mdSize))]; - for (slice) |byte| { - fingerprint[idx] = hex[(byte & 0xF0) >> 4]; - fingerprint[idx + 1] = hex[byte & 0x0F]; - fingerprint[idx + 2] = ':'; - idx += 3; - } - const length = if (idx > 0) (idx - 1) else 0; - fingerprint[length] = 0; - return length; -} - -fn getFingerprintDigest(cert: *BoringSSL.X509, method: *const BoringSSL.EVP_MD, globalObject: *JSGlobalObject) JSValue { - var md: [BoringSSL.EVP_MAX_MD_SIZE]u8 = undefined; - var md_size: c_uint = 0; - var fingerprint: [BoringSSL.EVP_MAX_MD_SIZE * 3]u8 = undefined; - - if (BoringSSL.X509_digest(cert, method, @as([*c]u8, @ptrCast(&md)), &md_size) != 0) { - const length = addFingerprintDigest(&md, md_size, &fingerprint); - return JSC.ZigString.fromUTF8(fingerprint[0..length]).toJS(globalObject); - } - return JSValue.jsUndefined(); -} - -fn getSerialNumber(cert: *BoringSSL.X509, globalObject: *JSGlobalObject) JSValue { - const serial_number = BoringSSL.X509_get_serialNumber(cert); - if (serial_number != null) { - const bignum = BoringSSL.ASN1_INTEGER_to_BN(serial_number, null); - if (bignum != null) { - const data = BoringSSL.BN_bn2hex(bignum); - if (data != null) { - const slice = data[0..bun.len(data)]; - // BoringSSL prints the hex value of the serialNumber in lower case, but we need upper case - toUpper(slice); - return JSC.ZigString.fromUTF8(slice).toJS(globalObject); - } - } - } - return JSValue.jsUndefined(); -} - -fn getRawDERCertificate(cert: *BoringSSL.X509, globalObject: *JSGlobalObject) JSValue { - const size = BoringSSL.i2d_X509(cert, null); - var buffer = JSValue.createBufferFromLength(globalObject, @as(usize, @intCast(size))); - var buffer_ptr = buffer.asArrayBuffer(globalObject).?.ptr; - const result_size = BoringSSL.i2d_X509(cert, &buffer_ptr); - bun.assert(result_size == size); - return buffer; -} - -fn toUpper(slice: []u8) void { - for (0..slice.len) |i| { - const c = slice[i]; - if (c >= 'a' and c <= 'z') { - slice[i] &= 223; - } - } -} - pub fn toJS(cert: *BoringSSL.X509, globalObject: *JSGlobalObject) bun.JSError!JSValue { - const bio = BoringSSL.BIO_new(BoringSSL.BIO_s_mem()) orelse { - return globalObject.throw("Failed to create BIO", .{}); - }; - defer _ = BoringSSL.BIO_free(bio); - var result = JSValue.createEmptyObject(globalObject, 8); - // X509_check_ca() returns a range of values. Only 1 means "is a CA" - const is_ca = BoringSSL.X509_check_ca(cert) == 1; - const subject = BoringSSL.X509_get_subject_name(cert); - result.put(globalObject, ZigString.static("subject"), try x509GetNameObject(globalObject, subject)); - const issuer = BoringSSL.X509_get_issuer_name(cert); - result.put(globalObject, ZigString.static("issuer"), try x509GetNameObject(globalObject, issuer)); - result.put(globalObject, ZigString.static("subjectaltname"), x509GetSubjectAltNameString(globalObject, bio, cert)); - result.put(globalObject, ZigString.static("infoAccess"), x509GetInfoAccessString(globalObject, bio, cert)); - result.put(globalObject, ZigString.static("ca"), JSValue.jsBoolean(is_ca)); - - const pkey = BoringSSL.X509_get_pubkey(cert); - - switch (BoringSSL.EVP_PKEY_id(pkey)) { - BoringSSL.EVP_PKEY_RSA => { - const rsa_key = BoringSSL.EVP_PKEY_get1_RSA(pkey); - if (rsa_key) |rsa| { - var n: [*c]const BoringSSL.BIGNUM = undefined; - var e: [*c]const BoringSSL.BIGNUM = undefined; - BoringSSL.RSA_get0_key(rsa, @as([*c][*c]const BoringSSL.BIGNUM, @ptrCast(&n)), @as([*c][*c]const BoringSSL.BIGNUM, @ptrCast(&e)), null); - _ = BoringSSL.BN_print(bio, n); - - var bits = JSValue.jsUndefined(); - - const bits_value = BoringSSL.BN_num_bits(n); - if (bits_value > 0) { - bits = JSValue.jsNumber(bits_value); - } - - result.put(globalObject, ZigString.static("bits"), bits); - const slice = bio.slice(); - // BoringSSL prints the hex value of the modulus in lower case, but we need upper case - toUpper(slice); - const modulus = JSC.ZigString.fromUTF8(slice).toJS(globalObject); - _ = BoringSSL.BIO_reset(bio); - result.put(globalObject, ZigString.static("modulus"), modulus); - - const exponent_word = BoringSSL.BN_get_word(e); - _ = BoringSSL.BIO_printf(bio, "0x" ++ BoringSSL.BN_HEX_FMT1, exponent_word); - const exponent = JSC.ZigString.fromUTF8(bio.slice()).toJS(globalObject); - _ = BoringSSL.BIO_reset(bio); - result.put(globalObject, ZigString.static("exponent"), exponent); - - const size = BoringSSL.i2d_RSA_PUBKEY(rsa, null); - if (size <= 0) { - return globalObject.throw("Failed to get public key length", .{}); - } - - var buffer = JSValue.createBufferFromLength(globalObject, @as(usize, @intCast(size))); - var buffer_ptr = @as([*c]u8, @ptrCast(buffer.asArrayBuffer(globalObject).?.ptr)); - - _ = BoringSSL.i2d_RSA_PUBKEY(rsa, &buffer_ptr); - - result.put(globalObject, ZigString.static("pubkey"), buffer); - } - }, - BoringSSL.EVP_PKEY_EC => { - const ec_key = BoringSSL.EVP_PKEY_get1_EC_KEY(pkey); - if (ec_key) |ec| { - const group = BoringSSL.EC_KEY_get0_group(ec); - var bits = JSValue.jsUndefined(); - if (group) |g| { - const bits_value = BoringSSL.EC_GROUP_order_bits(g); - if (bits_value > 0) { - bits = JSValue.jsNumber(bits_value); - } - } - result.put(globalObject, ZigString.static("bits"), bits); - - const ec_pubkey = BoringSSL.EC_KEY_get0_public_key(ec); - if (ec_pubkey) |point| { - const form = BoringSSL.EC_KEY_get_conv_form(ec); - const size = BoringSSL.EC_POINT_point2oct(group, point, form, null, 0, null); - if (size <= 0) { - return globalObject.throw("Failed to get public key length", .{}); - } - - var buffer = JSValue.createBufferFromLength(globalObject, @as(usize, @intCast(size))); - const buffer_ptr = @as([*c]u8, @ptrCast(buffer.asArrayBuffer(globalObject).?.ptr)); - - const result_size = BoringSSL.EC_POINT_point2oct(group, point, form, buffer_ptr, size, null); - bun.assert(result_size == size); - result.put(globalObject, ZigString.static("pubkey"), buffer); - } else { - result.put(globalObject, ZigString.static("pubkey"), JSValue.jsUndefined()); - } - const nid = BoringSSL.EC_GROUP_get_curve_name(group); - - if (nid != 0) { - // Curve is well-known, get its OID and NIST nick-name (if it has one). - const asn1Curve_str = BoringSSL.OBJ_nid2sn(nid); - if (asn1Curve_str != null) { - result.put(globalObject, ZigString.static("asn1Curve"), JSC.ZigString.fromUTF8(asn1Curve_str[0..bun.len(asn1Curve_str)]).toJS(globalObject)); - } - const nistCurve_str = BoringSSL.EC_curve_nid2nist(nid); - if (nistCurve_str != null) { - result.put(globalObject, ZigString.static("nistCurve"), JSC.ZigString.fromUTF8(nistCurve_str[0..bun.len(nistCurve_str)]).toJS(globalObject)); - } - } - } - }, - else => {}, - } - _ = BoringSSL.ASN1_TIME_print(bio, BoringSSL.X509_get0_notBefore(cert)); - result.put(globalObject, ZigString.static("valid_from"), JSC.ZigString.fromUTF8(bio.slice()).toJS(globalObject)); - _ = BoringSSL.BIO_reset(bio); - - _ = BoringSSL.ASN1_TIME_print(bio, BoringSSL.X509_get0_notAfter(cert)); - result.put(globalObject, ZigString.static("valid_to"), JSC.ZigString.fromUTF8(bio.slice()).toJS(globalObject)); - _ = BoringSSL.BIO_reset(bio); - - result.put(globalObject, ZigString.static("fingerprint"), getFingerprintDigest(cert, BoringSSL.EVP_sha1(), globalObject)); - result.put(globalObject, ZigString.static("fingerprint256"), getFingerprintDigest(cert, BoringSSL.EVP_sha256(), globalObject)); - result.put(globalObject, ZigString.static("fingerprint512"), getFingerprintDigest(cert, BoringSSL.EVP_sha512(), globalObject)); - result.put(globalObject, ZigString.static("serialNumber"), getSerialNumber(cert, globalObject)); - result.put(globalObject, ZigString.static("raw"), getRawDERCertificate(cert, globalObject)); - return result; + return Bun__X509__toJSLegacyEncoding(cert, globalObject); } + +pub fn toJSObject(cert: *BoringSSL.X509, globalObject: *JSGlobalObject) bun.JSError!JSValue { + return Bun__X509__toJS(cert, globalObject); +} + +extern fn Bun__X509__toJSLegacyEncoding(cert: *BoringSSL.X509, globalObject: *JSGlobalObject) JSValue; +extern fn Bun__X509__toJS(cert: *BoringSSL.X509, globalObject: *JSGlobalObject) JSValue; diff --git a/src/bun.js/api/sockets.classes.ts b/src/bun.js/api/sockets.classes.ts index 9b03d24a9d..e982a9e88a 100644 --- a/src/bun.js/api/sockets.classes.ts +++ b/src/bun.js/api/sockets.classes.ts @@ -2,7 +2,7 @@ import { define } from "../../codegen/class-definitions"; function generate(ssl) { return define({ - name: ssl ? "TCPSocket" : "TLSSocket", + name: !ssl ? "TCPSocket" : "TLSSocket", JSType: "0b11101110", hasPendingActivity: true, noConstructor: true, @@ -81,10 +81,7 @@ function generate(ssl) { fn: "getPeerCertificate", length: 1, }, - getCertificate: { - fn: "getCertificate", - length: 0, - }, + authorized: { getter: "getAuthorized", }, @@ -200,12 +197,27 @@ function generate(ssl) { length: 2, privateSymbol: "end", }, + getCertificate: { + fn: "getCertificate", + length: 0, + }, + ...(ssl ? sslOnly : {}), }, finalize: true, construct: true, klass: {}, }); } +const sslOnly = { + getPeerX509Certificate: { + fn: "getPeerX509Certificate", + length: 0, + }, + getX509Certificate: { + fn: "getX509Certificate", + length: 0, + }, +} as const; export default [ generate(true), generate(false), diff --git a/src/bun.js/bindings/AsymmetricKeyValue.h b/src/bun.js/bindings/AsymmetricKeyValue.h new file mode 100644 index 0000000000..a79ed8ab62 --- /dev/null +++ b/src/bun.js/bindings/AsymmetricKeyValue.h @@ -0,0 +1,23 @@ +#include "root.h" + +#include "openssl/evp.h" + +namespace WebCore { + +class CryptoKey; + +class AsymmetricKeyValue { +public: + EVP_PKEY* key = nullptr; + bool owned = false; + + operator EVP_PKEY*() const { return key; } + EVP_PKEY* operator*() const { return key; } + bool operator!() const { return !key; } + + ~AsymmetricKeyValue(); + AsymmetricKeyValue(EVP_PKEY* key, bool owned); + AsymmetricKeyValue(CryptoKey&); +}; + +}; diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index d00195efab..57812b1af3 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -44,6 +44,7 @@ #include "wtf/text/OrdinalNumber.h" #include "NodeValidator.h" #include "NodeModuleModule.h" +#include "JSX509Certificate.h" #include "AsyncContextFrame.h" #include "ErrorCode.h" @@ -2513,6 +2514,15 @@ inline JSValue processBindingConfig(Zig::GlobalObject* globalObject, JSC::VM& vm return config; } +JSValue createCryptoX509Object(JSGlobalObject* globalObject) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto cryptoX509 = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 1); + cryptoX509->putDirect(vm, JSC::Identifier::fromString(vm, "isX509Certificate"_s), JSC::JSFunction::create(vm, globalObject, 1, String("isX509Certificate"_s), jsIsX509Certificate, ImplementationVisibility::Public), 0); + return cryptoX509; +} + JSC_DEFINE_HOST_FUNCTION(Process_functionBinding, (JSGlobalObject * jsGlobalObject, CallFrame* callFrame)) { auto& vm = jsGlobalObject->vm(); @@ -2529,6 +2539,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionBinding, (JSGlobalObject * jsGlobalObje if (moduleName == "constants"_s) return JSValue::encode(globalObject->processBindingConstants()); if (moduleName == "contextify"_s) PROCESS_BINDING_NOT_IMPLEMENTED("contextify"); if (moduleName == "crypto"_s) PROCESS_BINDING_NOT_IMPLEMENTED("crypto"); + if (moduleName == "crypto/x509"_s) return JSValue::encode(createCryptoX509Object(globalObject)); if (moduleName == "fs"_s) PROCESS_BINDING_NOT_IMPLEMENTED_ISSUE("fs", "3546"); if (moduleName == "fs_event_wrap"_s) PROCESS_BINDING_NOT_IMPLEMENTED("fs_event_wrap"); if (moduleName == "http_parser"_s) PROCESS_BINDING_NOT_IMPLEMENTED("http_parser"); diff --git a/src/bun.js/bindings/BunString.h b/src/bun.js/bindings/BunString.h new file mode 100644 index 0000000000..ea5908eae4 --- /dev/null +++ b/src/bun.js/bindings/BunString.h @@ -0,0 +1,50 @@ +#pragma once + +#include "root.h" + +#include +#include + +namespace Bun { +class UTF8View { +public: + UTF8View() + { + m_isCString = false; + m_underlying = {}; + m_view = {}; + } + + UTF8View(WTF::StringView view) + { + if (view.is8Bit() && view.containsOnlyASCII()) { + m_view = view; + } else { + m_underlying = view.utf8(); + m_isCString = true; + } + } + UTF8View(const WTF::String& str) + { + if (str.is8Bit() && str.containsOnlyASCII()) { + m_view = str; + } else { + m_underlying = str.utf8(); + m_isCString = true; + } + } + + WTF::CString m_underlying {}; + WTF::StringView m_view {}; + bool m_isCString { false }; + + std::span span() const + { + if (m_isCString) { + return std::span(reinterpret_cast(m_underlying.data()), m_underlying.length()); + } + + return std::span(reinterpret_cast(m_view.span8().data()), m_view.length()); + } +}; +} diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index ad17a1ae31..b2d19dc3c6 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -24,7 +24,7 @@ #include "JavaScriptCore/ErrorInstanceInlines.h" #include "JavaScriptCore/JSInternalFieldObjectImplInlines.h" #include "JSDOMException.h" - +#include #include "ErrorCode.h" JSC_DEFINE_HOST_FUNCTION(NodeError_proto_toString, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) @@ -341,6 +341,13 @@ WTF::String determineSpecificType(JSC::JSGlobalObject* globalObject, JSValue val return str; } +extern "C" BunString Bun__ErrorCode__determineSpecificType(JSC::JSGlobalObject* globalObject, EncodedJSValue value) +{ + JSValue jsValue = JSValue::decode(value); + WTF::String typeString = determineSpecificType(globalObject, jsValue); + return Bun::toStringRef(typeString); +} + namespace Message { WTF::String ERR_INVALID_ARG_TYPE(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, const StringView& arg_name, const StringView& expected_type, JSValue actual_value) @@ -634,6 +641,20 @@ JSC::EncodedJSValue ASSERTION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* return {}; } +JSC::EncodedJSValue CRYPTO_INVALID_CURVE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject) +{ + auto message = "Invalid EC curve name"_s; + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_INVALID_CURVE, message)); + return {}; +} + +JSC::EncodedJSValue CRYPTO_JWK_UNSUPPORTED_CURVE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& curve) +{ + auto message = makeString("Unsupported JWK EC curve: "_s, curve); + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_JWK_UNSUPPORTED_CURVE, message)); + return {}; +} + } static JSC::JSValue ERR_INVALID_ARG_TYPE(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue arg0, JSValue arg1, JSValue arg2) @@ -680,6 +701,19 @@ extern "C" JSC::EncodedJSValue Bun__createErrorWithCode(JSC::JSGlobalObject* glo return JSValue::encode(createError(globalObject, code, message->toWTFString(BunString::ZeroCopy))); } +void throwBoringSSLError(JSC::VM& vm, JSC::ThrowScope& scope, JSGlobalObject* globalObject, int errorCode) +{ + char buf[256] = { 0 }; + ERR_error_string_n(static_cast(errorCode), buf, sizeof(buf)); + auto message = String::fromUTF8(buf); + scope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_INVALID_STATE, message)); +} + +void throwCryptoOperationFailed(JSGlobalObject* globalObject, JSC::ThrowScope& scope) +{ + scope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Crypto operation failed"_s)); +} + } // namespace Bun JSC_DEFINE_HOST_FUNCTION(jsFunctionMakeAbortError, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) @@ -740,7 +774,8 @@ JSC::JSObject* Bun::createInvalidThisError(JSC::JSGlobalObject* globalObject, JS JSC::EncodedJSValue Bun::throwError(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, Bun::ErrorCode code, const WTF::String& message) { - return JSC::JSValue::encode(scope.throwException(globalObject, createError(globalObject, code, message))); + scope.throwException(globalObject, createError(globalObject, code, message)); + return {}; } JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) diff --git a/src/bun.js/bindings/ErrorCode.h b/src/bun.js/bindings/ErrorCode.h index 45ae3c3798..932c6c3203 100644 --- a/src/bun.js/bindings/ErrorCode.h +++ b/src/bun.js/bindings/ErrorCode.h @@ -83,7 +83,12 @@ JSC::EncodedJSValue SOCKET_BAD_PORT(JSC::ThrowScope& throwScope, JSC::JSGlobalOb JSC::EncodedJSValue UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); JSC::EncodedJSValue ASSERTION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue msg); JSC::EncodedJSValue ASSERTION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, ASCIILiteral msg); +JSC::EncodedJSValue CRYPTO_INVALID_CURVE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); +JSC::EncodedJSValue CRYPTO_JWK_UNSUPPORTED_CURVE(JSC::ThrowScope&, JSC::JSGlobalObject*, const WTF::String&); } +void throwBoringSSLError(JSC::VM& vm, JSC::ThrowScope& scope, JSGlobalObject* globalObject, int errorCode); +void throwCryptoOperationFailed(JSGlobalObject* globalObject, JSC::ThrowScope& scope); + } diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index 8a6b4eab76..530eac2665 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -14,67 +14,75 @@ type ErrorCodeMapping = Array< const errors: ErrorCodeMapping = [ ["ABORT_ERR", Error, "AbortError"], + ["ERR_AMBIGUOUS_ARGUMENT", TypeError], + ["ERR_ASSERTION", Error], + ["ERR_BROTLI_INVALID_PARAM", RangeError], + ["ERR_BUFFER_OUT_OF_BOUNDS", RangeError], + ["ERR_BUFFER_TOO_LARGE", RangeError], + ["ERR_CRYPTO_ECDH_INVALID_FORMAT", TypeError], + ["ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY", Error], + ["ERR_CRYPTO_INCOMPATIBLE_KEY", Error], + ["ERR_CRYPTO_INVALID_CURVE", TypeError], ["ERR_CRYPTO_INVALID_DIGEST", TypeError], + ["ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE", TypeError], + ["ERR_CRYPTO_INVALID_SCRYPT_PARAMS", RangeError], + ["ERR_CRYPTO_INVALID_STATE", Error], + ["ERR_CRYPTO_JWK_UNSUPPORTED_CURVE", Error], + ["ERR_CRYPTO_OPERATION_FAILED", Error], + ["ERR_CRYPTO_SCRYPT_INVALID_PARAMETER", Error], + ["ERR_DLOPEN_FAILED", Error], ["ERR_ENCODING_INVALID_ENCODED_DATA", TypeError], + ["ERR_ILLEGAL_CONSTRUCTOR", TypeError], + ["ERR_INCOMPATIBLE_OPTION_PAIR", TypeError], ["ERR_INVALID_ARG_TYPE", TypeError], ["ERR_INVALID_ARG_VALUE", TypeError], + ["ERR_INVALID_CURSOR_POS", TypeError], + ["ERR_INVALID_IP_ADDRESS", TypeError], ["ERR_INVALID_PROTOCOL", TypeError], - ["ERR_INVALID_THIS", TypeError], ["ERR_INVALID_RETURN_VALUE", TypeError], + ["ERR_INVALID_STATE", Error, undefined, TypeError, RangeError], + ["ERR_INVALID_THIS", TypeError], + ["ERR_INVALID_URI", URIError], + ["ERR_INVALID_URL", TypeError], ["ERR_IPC_CHANNEL_CLOSED", Error], ["ERR_IPC_DISCONNECTED", Error], + ["ERR_IPC_ONE_PIPE", Error], + ["ERR_METHOD_NOT_IMPLEMENTED", Error], ["ERR_MISSING_ARGS", TypeError], - ["ERR_AMBIGUOUS_ARGUMENT", TypeError], + ["ERR_MULTIPLE_CALLBACK", Error], ["ERR_OUT_OF_RANGE", RangeError], ["ERR_PARSE_ARGS_INVALID_OPTION_VALUE", TypeError], ["ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL", TypeError], ["ERR_PARSE_ARGS_UNKNOWN_OPTION", TypeError], + ["ERR_SCRIPT_EXECUTION_INTERRUPTED", Error], + ["ERR_SCRIPT_EXECUTION_TIMEOUT", Error], ["ERR_SERVER_NOT_RUNNING", Error], + ["ERR_SOCKET_ALREADY_BOUND", Error], + ["ERR_SOCKET_BAD_BUFFER_SIZE", TypeError], + ["ERR_SOCKET_BAD_PORT", RangeError], ["ERR_SOCKET_BAD_TYPE", TypeError], + ["ERR_SOCKET_DGRAM_IS_CONNECTED", Error], + ["ERR_SOCKET_DGRAM_NOT_CONNECTED", Error], + ["ERR_SOCKET_DGRAM_NOT_RUNNING", Error], ["ERR_STREAM_ALREADY_FINISHED", Error], ["ERR_STREAM_CANNOT_PIPE", Error], ["ERR_STREAM_DESTROYED", Error], ["ERR_STREAM_NULL_VALUES", TypeError], - ["ERR_STREAM_WRITE_AFTER_END", Error], - ["ERR_ZLIB_INITIALIZATION_FAILED", Error], - ["ERR_STRING_TOO_LONG", Error], - ["ERR_CRYPTO_SCRYPT_INVALID_PARAMETER", Error], - ["ERR_CRYPTO_INVALID_SCRYPT_PARAMS", RangeError], - ["MODULE_NOT_FOUND", Error], - ["ERR_ILLEGAL_CONSTRUCTOR", TypeError], - ["ERR_INVALID_URL", TypeError], - ["ERR_BUFFER_TOO_LARGE", RangeError], - ["ERR_BROTLI_INVALID_PARAM", RangeError], - ["ERR_UNKNOWN_ENCODING", TypeError], - ["ERR_INVALID_STATE", Error, undefined, TypeError, RangeError], - ["ERR_BUFFER_OUT_OF_BOUNDS", RangeError], - ["ERR_UNKNOWN_SIGNAL", TypeError], - ["ERR_SOCKET_BAD_PORT", RangeError], + ["ERR_STREAM_PREMATURE_CLOSE", Error], + ["ERR_STREAM_PUSH_AFTER_EOF", Error], ["ERR_STREAM_RELEASE_LOCK", Error, "AbortError"], - ["ERR_INCOMPATIBLE_OPTION_PAIR", TypeError], - ["ERR_INVALID_IP_ADDRESS", TypeError], + ["ERR_STREAM_UNABLE_TO_PIPE", Error], + ["ERR_STREAM_UNSHIFT_AFTER_END_EVENT", Error], + ["ERR_STREAM_WRITE_AFTER_END", Error], + ["ERR_STRING_TOO_LONG", Error], ["ERR_UNAVAILABLE_DURING_EXIT", Error], - ["ERR_INVALID_URI", URIError], - ["ERR_SCRIPT_EXECUTION_TIMEOUT", Error], - ["ERR_SCRIPT_EXECUTION_INTERRUPTED", Error], + ["ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET", Error], ["ERR_UNHANDLED_ERROR", Error], ["ERR_UNKNOWN_CREDENTIAL", Error], - ["ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET", Error], - ["ERR_DLOPEN_FAILED", Error], - ["ERR_ASSERTION", Error], - ["ERR_IPC_ONE_PIPE", Error], - ["ERR_SOCKET_ALREADY_BOUND", Error], - ["ERR_SOCKET_BAD_BUFFER_SIZE", TypeError], - ["ERR_SOCKET_DGRAM_IS_CONNECTED", Error], - ["ERR_SOCKET_DGRAM_NOT_CONNECTED", Error], - ["ERR_SOCKET_DGRAM_NOT_RUNNING", Error], - ["ERR_INVALID_CURSOR_POS", TypeError], - ["ERR_MULTIPLE_CALLBACK", Error], - ["ERR_STREAM_PREMATURE_CLOSE", Error], - ["ERR_METHOD_NOT_IMPLEMENTED", Error], - ["ERR_STREAM_UNSHIFT_AFTER_END_EVENT", Error], - ["ERR_STREAM_PUSH_AFTER_EOF", Error], - ["ERR_STREAM_UNABLE_TO_PIPE", Error], + ["ERR_UNKNOWN_ENCODING", TypeError], + ["ERR_UNKNOWN_SIGNAL", TypeError], + ["ERR_ZLIB_INITIALIZATION_FAILED", Error], + ["MODULE_NOT_FOUND", Error], // Bun-specific ["ERR_FORMDATA_PARSE_ERROR", TypeError], @@ -88,6 +96,11 @@ const errors: ErrorCodeMapping = [ // DNS ["ERR_DNS_SET_SERVERS_FAILED", Error], + // TLS + ["ERR_TLS_CERT_ALTNAME_FORMAT", SyntaxError], + ["ERR_TLS_CERT_ALTNAME_INVALID", Error], + ["ERR_TLS_SNI_FROM_SERVER", Error], + // NET ["ERR_SOCKET_CLOSED_BEFORE_CONNECTION", Error], ["ERR_SOCKET_CLOSED", Error], diff --git a/src/bun.js/bindings/JSBuffer.cpp b/src/bun.js/bindings/JSBuffer.cpp index d40f59d2f6..927ee60966 100644 --- a/src/bun.js/bindings/JSBuffer.cpp +++ b/src/bun.js/bindings/JSBuffer.cpp @@ -1,6 +1,8 @@ + #include "root.h" +#include "ZigGlobalObject.h" #include "JavaScriptCore/ExceptionHelpers.h" #include "JavaScriptCore/JSString.h" #include "JavaScriptCore/Error.h" @@ -374,6 +376,12 @@ static inline JSC::EncodedJSValue writeToBuffer(JSC::JSGlobalObject* lexicalGlob return JSC::JSValue::encode(JSC::jsNumber(written)); } +JSC::JSUint8Array* createBuffer(JSC::JSGlobalObject* lexicalGlobalObject, Ref&& backingStore) +{ + size_t length = backingStore->byteLength(); + return JSC::JSUint8Array::create(lexicalGlobalObject, defaultGlobalObject(lexicalGlobalObject)->JSBufferSubclassStructure(), WTFMove(backingStore), 0, length); +} + JSC::JSUint8Array* createBuffer(JSC::JSGlobalObject* lexicalGlobalObject, const uint8_t* ptr, size_t length) { auto* buffer = createUninitializedBuffer(lexicalGlobalObject, length); diff --git a/src/bun.js/bindings/JSBuffer.h b/src/bun.js/bindings/JSBuffer.h index 04795e6143..7e6138fe6b 100644 --- a/src/bun.js/bindings/JSBuffer.h +++ b/src/bun.js/bindings/JSBuffer.h @@ -20,6 +20,7 @@ #pragma once +#include "JavaScriptCore/ArrayBuffer.h" #include "root.h" #include @@ -57,6 +58,7 @@ JSC::JSUint8Array* createBuffer(JSC::JSGlobalObject* lexicalGlobalObject, const JSC::JSUint8Array* createBuffer(JSC::JSGlobalObject* lexicalGlobalObject, const Vector& data); JSC::JSUint8Array* createBuffer(JSC::JSGlobalObject* lexicalGlobalObject, const std::span data); JSC::JSUint8Array* createBuffer(JSC::JSGlobalObject* lexicalGlobalObject, const char* ptr, size_t length); +JSC::JSUint8Array* createBuffer(JSC::JSGlobalObject* lexicalGlobalObject, Ref&& backingStore); JSC::JSUint8Array* createEmptyBuffer(JSC::JSGlobalObject* lexicalGlobalObject); JSC_DECLARE_HOST_FUNCTION(constructSlowBuffer); diff --git a/src/bun.js/bindings/JSBufferEncodingType.cpp b/src/bun.js/bindings/JSBufferEncodingType.cpp index cd4efd318d..88ba596fb2 100644 --- a/src/bun.js/bindings/JSBufferEncodingType.cpp +++ b/src/bun.js/bindings/JSBufferEncodingType.cpp @@ -127,11 +127,10 @@ std::optional parseEnumeration2(JSGlobalObject& lexicalGloba case 'h': case 'H': // hex - if (encoding[1] == 'e') - if (encoding[2] == 'x' && encoding[3] == '\0') - return BufferEncodingType::hex; if (WTF::equalIgnoringASCIICase(encoding, "hex"_s)) return BufferEncodingType::hex; + if (WTF::equalIgnoringASCIICase(encoding, "hex\0"_s)) + return BufferEncodingType::hex; break; } diff --git a/src/bun.js/bindings/JSDOMExceptionHandling.cpp b/src/bun.js/bindings/JSDOMExceptionHandling.cpp index ff9552fe7b..075026ab6d 100644 --- a/src/bun.js/bindings/JSDOMExceptionHandling.cpp +++ b/src/bun.js/bindings/JSDOMExceptionHandling.cpp @@ -296,6 +296,11 @@ String makeThisTypeErrorMessage(const char* interfaceName, const char* functionN return makeString("Can only call "_s, interfaceNameSpan, '.', span(functionName), " on instances of "_s, interfaceNameSpan); } +String makeThisTypeErrorMessage(ASCIILiteral interfaceName, ASCIILiteral functionName) +{ + return makeString("Can only call "_s, interfaceName, '.', functionName, " on instances of "_s, interfaceName); +} + String makeUnsupportedIndexedSetterErrorMessage(ASCIILiteral interfaceName) { return makeString("Failed to set an indexed property on "_s, interfaceName, ": Indexed property setter is not supported."_s); @@ -303,7 +308,14 @@ String makeUnsupportedIndexedSetterErrorMessage(ASCIILiteral interfaceName) EncodedJSValue throwThisTypeError(JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope, const char* interfaceName, const char* functionName) { - return JSValue::encode(scope.throwException(&lexicalGlobalObject, Bun::createInvalidThisError(&lexicalGlobalObject, makeThisTypeErrorMessage(interfaceName, functionName)))); + scope.throwException(&lexicalGlobalObject, Bun::createInvalidThisError(&lexicalGlobalObject, makeThisTypeErrorMessage(interfaceName, functionName))); + return {}; +} + +EncodedJSValue throwThisTypeError(JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope, ASCIILiteral interfaceName, ASCIILiteral attributeName) +{ + scope.throwException(&lexicalGlobalObject, Bun::createInvalidThisError(&lexicalGlobalObject, makeThisTypeErrorMessage(interfaceName, attributeName))); + return {}; } JSC::EncodedJSValue rejectPromiseWithThisTypeError(DeferredPromise& promise, const char* interfaceName, const char* methodName) diff --git a/src/bun.js/bindings/JSDOMExceptionHandling.h b/src/bun.js/bindings/JSDOMExceptionHandling.h index d4a3c7cd2c..3b0fad9f3b 100644 --- a/src/bun.js/bindings/JSDOMExceptionHandling.h +++ b/src/bun.js/bindings/JSDOMExceptionHandling.h @@ -25,6 +25,7 @@ #include "ExceptionDetails.h" #include "ExceptionOr.h" +#include "wtf/text/ASCIILiteral.h" #include namespace JSC { @@ -54,9 +55,11 @@ WEBCORE_EXPORT JSC::EncodedJSValue throwRequiredMemberTypeError(JSC::JSGlobalObj JSC::EncodedJSValue throwConstructorScriptExecutionContextUnavailableError(JSC::JSGlobalObject&, JSC::ThrowScope&, ASCIILiteral interfaceName); String makeThisTypeErrorMessage(const char* interfaceName, const char* attributeName); +String makeThisTypeErrorMessage(ASCIILiteral interfaceName, ASCIILiteral functionName); String makeUnsupportedIndexedSetterErrorMessage(ASCIILiteral interfaceName); WEBCORE_EXPORT JSC::EncodedJSValue throwThisTypeError(JSC::JSGlobalObject&, JSC::ThrowScope&, const char* interfaceName, const char* functionName); +WEBCORE_EXPORT JSC::EncodedJSValue throwThisTypeError(JSC::JSGlobalObject&, JSC::ThrowScope&, ASCIILiteral interfaceName, ASCIILiteral attributeName); WEBCORE_EXPORT JSC::EncodedJSValue rejectPromiseWithGetterTypeError(JSC::JSGlobalObject&, const JSC::ClassInfo*, JSC::PropertyName attributeName); WEBCORE_EXPORT JSC::EncodedJSValue rejectPromiseWithThisTypeError(DeferredPromise&, const char* interfaceName, const char* operationName); diff --git a/src/bun.js/bindings/JSX509Certificate.cpp b/src/bun.js/bindings/JSX509Certificate.cpp new file mode 100644 index 0000000000..9b84b3dc4a --- /dev/null +++ b/src/bun.js/bindings/JSX509Certificate.cpp @@ -0,0 +1,1217 @@ +#include "root.h" + +#include "JavaScriptCore/ArrayAllocationProfile.h" +#include "JavaScriptCore/JSArray.h" + +#include "ncrypto.h" +#include "openssl/x509.h" +#include "JavaScriptCore/InternalFunction.h" +#include "ErrorCode.h" +#include "JSX509Certificate.h" +#include "JSX509CertificatePrototype.h" +#include "ZigGlobalObject.h" +#include "wtf/Assertions.h" +#include "wtf/SharedTask.h" +#include "wtf/text/ASCIILiteral.h" + +#include +#include +#include +#include "JSCryptoKey.h" + +#include +#include "CryptoKeyEC.h" +#include "CryptoKeyRSA.h" +#include "openssl/evp.h" +#include "JavaScriptCore/ObjectPrototype.h" +#include "BunString.h" +#include +#include "JSBuffer.h" +#include "wtf/text/ExternalStringImpl.h" +#include +#include + +RefPtr toCryptoKey(EVP_PKEY* pkey) +{ + auto type = EVP_PKEY_base_id(pkey); + unsigned usages = 0; + usages |= WebCore::CryptoKeyUsageSign; + usages |= WebCore::CryptoKeyUsageVerify; + usages |= WebCore::CryptoKeyUsageDeriveKey; + usages |= WebCore::CryptoKeyUsageDeriveBits; + usages |= WebCore::CryptoKeyUsageWrapKey; + usages |= WebCore::CryptoKeyUsageUnwrapKey; + usages |= WebCore::CryptoKeyUsageEncrypt; + usages |= WebCore::CryptoKeyUsageDecrypt; + + if (type == EVP_PKEY_EC) { + return WebCore::CryptoKeyEC::create(WebCore::CryptoAlgorithmIdentifier::ECDH, WebCore::CryptoKeyEC::NamedCurve::P256, WebCore::CryptoKeyType::Public, WebCore::EvpPKeyPtr(pkey), true, usages); + } else if (type == EVP_PKEY_RSA) { + return WebCore::CryptoKeyRSA::create(WebCore::CryptoAlgorithmIdentifier::RSA_OAEP, WebCore::CryptoAlgorithmIdentifier::SHA_1, true, WebCore::CryptoKeyType::Public, WebCore::EvpPKeyPtr(pkey), true, usages); + } else if (type == EVP_PKEY_ED25519) { + return WebCore::CryptoKeyEC::create(WebCore::CryptoAlgorithmIdentifier::ECDH, WebCore::CryptoKeyEC::NamedCurve::P256, WebCore::CryptoKeyType::Public, WebCore::EvpPKeyPtr(pkey), true, usages); + } + + EVP_PKEY_free(pkey); + return nullptr; +} + +namespace Bun { + +using namespace JSC; + +Ref toExternalStringImpl(ncrypto::BIOPointer& bio, std::span span) +{ + return WTF::ExternalStringImpl::create({ reinterpret_cast(span.data()), span.size() }, bio.release(), [](void* context, void* ptr, unsigned len) { + ncrypto::BIOPointer deleter = ncrypto::BIOPointer(static_cast(context)); + }); +} + +WTF::String toWTFString(ncrypto::BIOPointer& bio) +{ + BUF_MEM* bptr; + BIO_get_mem_ptr(bio.get(), &bptr); + std::span span(bptr->data, bptr->length); + if (simdutf::validate_ascii(span.data(), span.size())) { + return toExternalStringImpl(bio, span); + } + return WTF::String::fromUTF8({ reinterpret_cast(bptr->data), bptr->length }); +} + +static JSC_DECLARE_HOST_FUNCTION(x509CertificateConstructorCall); +static JSC_DECLARE_HOST_FUNCTION(x509CertificateConstructorConstruct); + +class JSX509CertificateConstructor final : public JSC::InternalFunction { +public: + using Base = JSC::InternalFunction; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSX509CertificateConstructor* create(JSC::VM&, JSC::JSGlobalObject*, JSC::Structure*, JSC::JSObject* prototype); + + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return &vm.internalFunctionSpace(); + } + + DECLARE_INFO; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info()); + } + +private: + JSX509CertificateConstructor(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure, x509CertificateConstructorCall, x509CertificateConstructorConstruct) + { + } + + void finishCreation(JSC::VM&, JSC::JSGlobalObject*, JSC::JSObject* prototype); +}; + +const ClassInfo JSX509CertificateConstructor::s_info = { "X509Certificate"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSX509CertificateConstructor) }; + +JSX509CertificateConstructor* JSX509CertificateConstructor::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, JSObject* prototype) +{ + JSX509CertificateConstructor* constructor = new (NotNull, allocateCell(vm)) JSX509CertificateConstructor(vm, structure); + constructor->finishCreation(vm, globalObject, prototype); + return constructor; +} + +void JSX509CertificateConstructor::finishCreation(VM& vm, JSGlobalObject* globalObject, JSObject* prototype) +{ + Base::finishCreation(vm, 1, "X509Certificate"_s, PropertyAdditionMode::WithStructureTransition); +} +static JSValue createX509Certificate(JSC::VM& vm, JSGlobalObject* globalObject, Structure* structure, JSValue arg) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + Bun::UTF8View view; + std::span data; + + if (arg.isString()) { + view = arg.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + data = std::span(reinterpret_cast(view.span().data()), view.span().size()); + } else if (auto* typedArray = jsDynamicCast(arg)) { + if (UNLIKELY(typedArray->isDetached())) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "TypedArray is detached"_s); + return {}; + } + data = typedArray->span(); + } else if (auto* buffer = jsDynamicCast(arg)) { + auto* impl = buffer->impl(); + if (UNLIKELY(!impl)) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "Buffer is detached"_s); + return {}; + } + data = impl->span(); + } else { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "X509Certificate constructor argument must be a Buffer, TypedArray, or string"_s); + return {}; + } + + JSX509Certificate* certificate = JSX509Certificate::create(vm, structure, globalObject, data); + RETURN_IF_EXCEPTION(scope, {}); + return certificate; +} + +JSC_DEFINE_HOST_FUNCTION(x509CertificateConstructorCall, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + Bun::throwError(globalObject, scope, ErrorCode::ERR_ILLEGAL_CONSTRUCTOR, "X509Certificate constructor cannot be invoked without 'new'"_s); + return {}; +} + +JSC_DEFINE_HOST_FUNCTION(x509CertificateConstructorConstruct, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (!callFrame->argumentCount()) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_MISSING_ARGS, "X509Certificate constructor requires at least one argument"_s); + return {}; + } + + JSValue arg = callFrame->uncheckedArgument(0); + if (!arg.isCell()) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "X509Certificate constructor argument must be a Buffer, TypedArray, or string"_s); + return {}; + } + + auto* zigGlobalObject = defaultGlobalObject(globalObject); + Structure* structure = zigGlobalObject->m_JSX509CertificateClassStructure.get(zigGlobalObject); + JSValue newTarget = callFrame->newTarget(); + if (UNLIKELY(zigGlobalObject->m_JSX509CertificateClassStructure.constructor(zigGlobalObject) != newTarget)) { + auto scope = DECLARE_THROW_SCOPE(vm); + if (!newTarget) { + throwTypeError(globalObject, scope, "Class constructor Script cannot be invoked without 'new'"_s); + return {}; + } + + auto* functionGlobalObject = defaultGlobalObject(getFunctionRealm(globalObject, newTarget.getObject())); + RETURN_IF_EXCEPTION(scope, {}); + structure = InternalFunction::createSubclassStructure( + globalObject, newTarget.getObject(), functionGlobalObject->NodeVMScriptStructure()); + scope.release(); + } + + return JSValue::encode(createX509Certificate(vm, globalObject, structure, arg)); +} + +const ClassInfo JSX509Certificate::s_info = { "X509Certificate"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSX509Certificate) }; + +void JSX509Certificate::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + + m_fingerprint.initLater([](const JSC::LazyProperty::Initializer& init) { + init.set(init.owner->computeFingerprint(init.owner->view(), init.owner->globalObject())); + }); + m_subject.initLater([](const JSC::LazyProperty::Initializer& init) { + auto value = init.owner->computeSubject(init.owner->view(), init.owner->globalObject(), false); + if (!value.isString()) { + init.set(jsEmptyString(init.owner->vm())); + return; + } + + init.set(value.toString(init.owner->globalObject())); + }); + m_issuer.initLater([](const JSC::LazyProperty::Initializer& init) { + JSValue value = init.owner->computeIssuer(init.owner->view(), init.owner->globalObject(), false); + if (value.isString()) { + init.set(value.toString(init.owner->globalObject())); + } else { + init.property.setMayBeNull(init.owner->vm(), init.owner, nullptr); + } + }); + m_validFrom.initLater([](const JSC::LazyProperty::Initializer& init) { + init.set(init.owner->computeValidFrom(init.owner->view(), init.owner->globalObject())); + }); + m_validTo.initLater([](const JSC::LazyProperty::Initializer& init) { + init.set(init.owner->computeValidTo(init.owner->view(), init.owner->globalObject())); + }); + m_serialNumber.initLater([](const JSC::LazyProperty::Initializer& init) { + init.set(init.owner->computeSerialNumber(init.owner->view(), init.owner->globalObject())); + }); + m_fingerprint256.initLater([](const JSC::LazyProperty::Initializer& init) { + init.set(init.owner->computeFingerprint256(init.owner->view(), init.owner->globalObject())); + }); + m_fingerprint512.initLater([](const JSC::LazyProperty::Initializer& init) { + init.set(init.owner->computeFingerprint512(init.owner->view(), init.owner->globalObject())); + }); + m_raw.initLater([](const JSC::LazyProperty::Initializer& init) { + init.property.setMayBeNull(init.owner->vm(), init.owner, init.owner->computeRaw(init.owner->view(), init.owner->globalObject())); + }); + + m_infoAccess.initLater([](const JSC::LazyProperty::Initializer& init) { + JSValue value = init.owner->computeInfoAccess(init.owner->view(), init.owner->globalObject(), false); + if (value.isString()) { + init.set(value.toString(init.owner->globalObject())); + } else { + init.property.setMayBeNull(init.owner->vm(), init.owner, nullptr); + } + }); + m_subjectAltName.initLater([](const JSC::LazyProperty::Initializer& init) { + init.property.setMayBeNull(init.owner->vm(), init.owner, init.owner->computeSubjectAltName(init.owner->view(), init.owner->globalObject())); + }); + + m_publicKey.initLater([](const JSC::LazyProperty::Initializer& init) { + JSValue value = init.owner->computePublicKey(init.owner->view(), init.owner->globalObject()); + init.property.setMayBeNull(init.owner->vm(), init.owner, !value.isEmpty() && value.isCell() ? value.asCell() : nullptr); + }); +} + +JSX509Certificate* JSX509Certificate::create(VM& vm, Structure* structure) +{ + JSX509Certificate* ptr = new (NotNull, allocateCell(vm)) JSX509Certificate(vm, structure); + ptr->finishCreation(vm); + return ptr; +} + +JSX509Certificate* JSX509Certificate::create(JSC::VM& vm, JSC::Structure* structure, JSC::JSGlobalObject* globalObject, std::span der) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + + // Initialize the X509 certificate from the provided data + auto result = ncrypto::X509Pointer::Parse(ncrypto::Buffer { reinterpret_cast(der.data()), der.size() }); + if (!result) { + Bun::throwBoringSSLError(vm, scope, globalObject, result.error.value_or(0)); + return nullptr; + } + + return create(vm, structure, globalObject, std::move(result.value)); +} + +JSX509Certificate* JSX509Certificate::create(JSC::VM& vm, JSC::Structure* structure, JSC::JSGlobalObject* globalObject, ncrypto::X509Pointer&& cert) +{ + auto* certificate = create(vm, structure); + certificate->m_x509 = std::move(cert); + size_t size = i2d_X509(certificate->m_x509.get(), nullptr); + certificate->m_extraMemorySizeForGC = size; + vm.heap.reportExtraMemoryAllocated(certificate, size); + return certificate; +} + +String JSX509Certificate::toPEMString() const +{ + VM& vm = globalObject()->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto bio = view().toPEM(); + if (!bio) { + return String(); + } + + return toWTFString(bio); +} + +void JSX509Certificate::destroy(JSCell* cell) +{ + static_cast(cell)->~JSX509Certificate(); +} + +JSX509Certificate::~JSX509Certificate() +{ +} + +template +void JSX509Certificate::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSX509Certificate* thisObject = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + + thisObject->m_subject.visit(visitor); + thisObject->m_issuer.visit(visitor); + thisObject->m_validFrom.visit(visitor); + thisObject->m_validTo.visit(visitor); + thisObject->m_serialNumber.visit(visitor); + thisObject->m_fingerprint.visit(visitor); + thisObject->m_fingerprint256.visit(visitor); + thisObject->m_fingerprint512.visit(visitor); + thisObject->m_raw.visit(visitor); + thisObject->m_infoAccess.visit(visitor); + thisObject->m_subjectAltName.visit(visitor); + thisObject->m_publicKey.visit(visitor); + visitor.reportExtraMemoryVisited(thisObject->m_extraMemorySizeForGC); +} + +DEFINE_VISIT_CHILDREN(JSX509Certificate); + +size_t JSX509Certificate::estimatedSize(JSCell* cell, VM& vm) +{ + JSX509Certificate* thisObject = jsCast(cell); + size_t size = i2d_X509(thisObject->m_x509.get(), nullptr); + return Base::estimatedSize(cell, vm) + size; +} + +void JSX509Certificate::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) +{ + Base::analyzeHeap(cell, analyzer); +} + +JSC::Structure* JSX509Certificate::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +// Convert an X509_NAME* into a JavaScript object. +// Each entry of the name is converted into a property of the object. +// The property value may be a single string or an array of strings. +template +static JSObject* GetX509NameObject(JSGlobalObject* globalObject, const X509* cert) +{ + X509_NAME* name = get_name(cert); + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (!name) + return nullptr; + + int cnt = X509_NAME_entry_count(name); + if (cnt < 0) + return nullptr; + + // Create object with null prototype to match Node.js behavior + JSObject* result = constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure()); + RETURN_IF_EXCEPTION(scope, nullptr); + + for (int i = 0; i < cnt; i++) { + X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, i); + if (!entry) + continue; + + ASN1_OBJECT* obj = X509_NAME_ENTRY_get_object(entry); + ASN1_STRING* str = X509_NAME_ENTRY_get_data(entry); + if (!obj || !str) + continue; + + // Convert the ASN1_OBJECT to a string key + String key; + int nid = OBJ_obj2nid(obj); + if (nid != NID_undef) { + const char* sn = OBJ_nid2sn(nid); + if (sn) + key = String::fromUTF8(sn); + } + if (key.isEmpty()) { + char buf[80]; + if (OBJ_obj2txt(buf, sizeof(buf), obj, 1) >= 0) + key = String::fromUTF8(buf); + } + if (key.isEmpty()) + continue; + + // Convert the ASN1_STRING to a string value + unsigned char* value_str = nullptr; + int value_str_size = ASN1_STRING_to_UTF8(&value_str, str); + if (value_str_size < 0) + continue; + + ncrypto::DataPointer free_value_str(value_str, value_str_size); + JSValue jsvalue = jsString(vm, String::fromUTF8(std::span(reinterpret_cast(value_str), value_str_size))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Check if this key already exists + JSValue existing = result->getIfPropertyExists(globalObject, Identifier::fromString(vm, key)); + + RETURN_IF_EXCEPTION(scope, nullptr); + if (existing) { + JSArray* array = jsDynamicCast(existing); + if (!array) { + array = JSArray::tryCreate(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 2); + if (!array) { + throwOutOfMemoryError(globalObject, scope); + return nullptr; + } + array->putDirectIndex(globalObject, 0, existing); + array->putDirectIndex(globalObject, 1, jsvalue); + result->putDirect(vm, Identifier::fromString(vm, key), array, 0); + } else { + array->putDirectIndex(globalObject, array->length(), jsvalue); + } + } else { + // First occurrence of this key + result->putDirect(vm, Identifier::fromString(vm, key), jsvalue); + } + RETURN_IF_EXCEPTION(scope, nullptr); + } + + return result; +} + +JSValue JSX509Certificate::computeSubject(ncrypto::X509View view, JSGlobalObject* globalObject, bool legacy) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* cert = view.get(); + if (!cert) + return jsUndefined(); + + if (!legacy) { + auto bio = view.getSubject(); + if (!bio) { + throwCryptoOperationFailed(globalObject, scope); + return jsUndefined(); + } + return jsString(vm, toWTFString(bio)); + } + + // For legacy mode, convert to object format + X509_NAME* name = X509_get_subject_name(cert); + if (!name) + return jsUndefined(); + + JSObject* obj = GetX509NameObject(globalObject, cert); + if (!obj) + return jsUndefined(); + + return obj; +} + +JSValue JSX509Certificate::computeIssuer(ncrypto::X509View view, JSGlobalObject* globalObject, bool legacy) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto bio = view.getIssuer(); + if (!bio) { + throwCryptoOperationFailed(globalObject, scope); + return jsEmptyString(vm); + } + + if (!legacy) { + return jsString(vm, toWTFString(bio)); + } + + return GetX509NameObject(globalObject, view.get()); +} + +JSString* JSX509Certificate::computeValidFrom(ncrypto::X509View view, JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto bio = view.getValidFrom(); + if (!bio) { + throwCryptoOperationFailed(globalObject, scope); + return jsEmptyString(vm); + } + + return jsString(vm, toWTFString(bio)); +} + +JSString* JSX509Certificate::computeValidTo(ncrypto::X509View view, JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto bio = view.getValidTo(); + if (!bio) { + throwCryptoOperationFailed(globalObject, scope); + return jsEmptyString(vm); + } + + return jsString(vm, toWTFString(bio)); +} + +JSString* JSX509Certificate::computeSerialNumber(ncrypto::X509View view, JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto serial = view.getSerialNumber(); + if (!serial) { + throwCryptoOperationFailed(globalObject, scope); + return jsEmptyString(vm); + } + + return jsString(vm, String::fromUTF8(std::span(static_cast(serial.get()), serial.size()))); +} + +JSString* JSX509Certificate::computeFingerprint(ncrypto::X509View view, JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto fingerprint = view.getFingerprint(EVP_sha1()); + if (!fingerprint) { + throwCryptoOperationFailed(globalObject, scope); + return jsEmptyString(vm); + } + + return jsString(vm, fingerprint.value()); +} + +JSString* JSX509Certificate::computeFingerprint256(ncrypto::X509View view, JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto fingerprint = view.getFingerprint(EVP_sha256()); + if (!fingerprint) { + throwCryptoOperationFailed(globalObject, scope); + return jsEmptyString(vm); + } + + return jsString(vm, fingerprint.value()); +} + +JSString* JSX509Certificate::computeFingerprint512(ncrypto::X509View view, JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto fingerprint = view.getFingerprint(EVP_sha512()); + if (!fingerprint) { + throwCryptoOperationFailed(globalObject, scope); + return jsEmptyString(vm); + } + + return jsString(vm, fingerprint.value()); +} + +JSUint8Array* JSX509Certificate::computeRaw(ncrypto::X509View view, JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto bio = view.toDER(); + if (!bio) { + throwCryptoOperationFailed(globalObject, scope); + return nullptr; + } + auto bio_ptr = bio.release(); + BUF_MEM* bptr = nullptr; + BIO_get_mem_ptr(bio_ptr, &bptr); + + Ref buffer = JSC::ArrayBuffer::createFromBytes(std::span(reinterpret_cast(bptr->data), bptr->length), createSharedTask([](void* data) { + ncrypto::BIOPointer free_me(static_cast(data)); + })); + return Bun::createBuffer(globalObject, WTFMove(buffer)); +} + +bool JSX509Certificate::computeIsCA(ncrypto::X509View view, JSGlobalObject* globalObject) +{ + return view.isCA(); +} + +static bool handleMatchResult(JSGlobalObject* globalObject, ASCIILiteral errorMessage, JSC::ThrowScope& scope, ncrypto::X509View::CheckMatch result) +{ + switch (result) { + case ncrypto::X509View::CheckMatch::INVALID_NAME: + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_VALUE, errorMessage); + return false; + case ncrypto::X509View::CheckMatch::NO_MATCH: + return false; + case ncrypto::X509View::CheckMatch::MATCH: + return true; + default: { + throwCryptoOperationFailed(globalObject, scope); + return false; + } + } +} + +bool JSX509Certificate::checkHost(JSGlobalObject* globalObject, std::span name, uint32_t flags) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto result = view().checkHost(name, flags); + return handleMatchResult(globalObject, "Invalid name"_s, scope, result); +} + +bool JSX509Certificate::checkEmail(JSGlobalObject* globalObject, std::span email, uint32_t flags) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto result = view().checkEmail(email, flags); + return handleMatchResult(globalObject, "Invalid email"_s, scope, result); +} + +bool JSX509Certificate::checkIP(JSGlobalObject* globalObject, std::span ip) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto result = view().checkIp(ip, 0); + return handleMatchResult(globalObject, "Invalid IP address"_s, scope, result); +} + +bool JSX509Certificate::checkIssued(JSGlobalObject* globalObject, JSX509Certificate* issuer) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (!issuer) + return false; + + return view().isIssuedBy(issuer->view()); +} + +JSString* JSX509Certificate::subject() +{ + return m_subject.get(this); +} +JSString* JSX509Certificate::issuer() +{ + return m_issuer.get(this); +} +JSString* JSX509Certificate::validFrom() +{ + return m_validFrom.get(this); +} +JSString* JSX509Certificate::validTo() +{ + return m_validTo.get(this); +} +JSString* JSX509Certificate::serialNumber() +{ + return m_serialNumber.get(this); +} +JSString* JSX509Certificate::fingerprint() +{ + return m_fingerprint.get(this); +} +JSString* JSX509Certificate::fingerprint256() +{ + return m_fingerprint256.get(this); +} +JSString* JSX509Certificate::fingerprint512() +{ + return m_fingerprint512.get(this); +} +JSUint8Array* JSX509Certificate::raw() +{ + return m_raw.get(this); +} +JSString* JSX509Certificate::infoAccess() +{ + return m_infoAccess.get(this); +} +JSString* JSX509Certificate::subjectAltName() +{ + return m_subjectAltName.get(this); +} + +JSValue JSX509Certificate::publicKey() +{ + return m_publicKey.get(this); +} + +bool JSX509Certificate::checkPrivateKey(JSGlobalObject* globalObject, EVP_PKEY* pkey) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (!pkey) + return false; + + return view().checkPrivateKey(ncrypto::EVPKeyPointer(pkey)); +} + +bool JSX509Certificate::verify(JSGlobalObject* globalObject, EVP_PKEY* pkey) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (!pkey) + return false; + + return view().checkPublicKey(ncrypto::EVPKeyPointer(pkey)); +} + +// This one doesn't depend on a JSX509Certificate object +JSC::JSObject* JSX509Certificate::toLegacyObject(ncrypto::X509View view, JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* cert = view.get(); + + if (!cert) + return nullptr; + + JSC::JSObject* object = constructEmptyObject(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Helper function to convert JSValue to undefined if empty/null + auto valueOrUndefined = [&](JSValue value) -> JSValue { + if (value.isEmpty() || value.isNull() || (value.isString() && value.toString(globalObject)->length() == 0)) + return jsUndefined(); + return value; + }; + + // Set subject + object->putDirect(vm, Identifier::fromString(vm, "subject"_s), valueOrUndefined(computeSubject(view, globalObject, true))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set issuer + object->putDirect(vm, Identifier::fromString(vm, "issuer"_s), valueOrUndefined(computeIssuer(view, globalObject, true))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set subjectaltname + object->putDirect(vm, Identifier::fromString(vm, "subjectaltname"_s), valueOrUndefined(computeSubjectAltName(view, globalObject))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set infoAccess + object->putDirect(vm, Identifier::fromString(vm, "infoAccess"_s), valueOrUndefined(computeInfoAccess(view, globalObject, true))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set modulus and exponent for RSA keys + EVP_PKEY* pkey = X509_get0_pubkey(cert); + if (pkey) { + switch (EVP_PKEY_base_id(pkey)) { + case EVP_PKEY_RSA: { + const RSA* rsa = EVP_PKEY_get0_RSA(pkey); + if (rsa) { + const BIGNUM* n; + const BIGNUM* e; + RSA_get0_key(rsa, &n, &e, nullptr); + + // Convert modulus to string + auto bio = ncrypto::BIOPointer::New(n); + if (bio) { + object->putDirect(vm, Identifier::fromString(vm, "modulus"_s), jsString(vm, toWTFString(bio))); + RETURN_IF_EXCEPTION(scope, nullptr); + } + + // Convert exponent to string + uint64_t exponent_word = static_cast(ncrypto::BignumPointer::GetWord(e)); + auto bio_e = ncrypto::BIOPointer::NewMem(); + if (bio_e) { + BIO_printf(bio_e.get(), "0x%" PRIx64, exponent_word); + object->putDirect(vm, Identifier::fromString(vm, "exponent"_s), jsString(vm, toWTFString(bio_e))); + RETURN_IF_EXCEPTION(scope, nullptr); + } + + // Set bits + object->putDirect(vm, Identifier::fromString(vm, "bits"_s), jsNumber(ncrypto::BignumPointer::GetBitCount(n))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set pubkey + int size = i2d_RSA_PUBKEY(rsa, nullptr); + if (size > 0) { + auto* buffer = Bun::createUninitializedBuffer(globalObject, static_cast(size)); + RETURN_IF_EXCEPTION(scope, nullptr); + uint8_t* data = buffer->typedVector(); + i2d_RSA_PUBKEY(rsa, &data); + object->putDirect(vm, Identifier::fromString(vm, "pubkey"_s), buffer); + } + } + break; + } + case EVP_PKEY_EC: { + const EC_KEY* ec = EVP_PKEY_get0_EC_KEY(pkey); + if (ec) { + const EC_GROUP* group = EC_KEY_get0_group(ec); + if (group) { + // Set bits + int bits = EC_GROUP_order_bits(group); + if (bits > 0) { + object->putDirect(vm, Identifier::fromString(vm, "bits"_s), jsNumber(bits)); + RETURN_IF_EXCEPTION(scope, nullptr); + } + + // Add pubkey field for EC keys + const EC_POINT* point = EC_KEY_get0_public_key(ec); + if (point) { + point_conversion_form_t form = EC_KEY_get_conv_form(ec); + size_t size = EC_POINT_point2oct(group, point, form, nullptr, 0, nullptr); + if (size > 0) { + auto* buffer = Bun::createUninitializedBuffer(globalObject, size); + RETURN_IF_EXCEPTION(scope, nullptr); + uint8_t* data = buffer->typedVector(); + size_t result_size = EC_POINT_point2oct(group, point, form, data, size, nullptr); + if (result_size == size) { + object->putDirect(vm, Identifier::fromString(vm, "pubkey"_s), buffer); + } + } + } + + // Set curve info + int nid = EC_GROUP_get_curve_name(group); + if (nid != 0) { + const char* sn = OBJ_nid2sn(nid); + if (sn) { + object->putDirect(vm, Identifier::fromString(vm, "asn1Curve"_s), jsString(vm, String::fromUTF8(sn))); + RETURN_IF_EXCEPTION(scope, nullptr); + } + + const char* nist = EC_curve_nid2nist(nid); + if (nist) { + object->putDirect(vm, Identifier::fromString(vm, "nistCurve"_s), jsString(vm, String::fromUTF8(nist))); + RETURN_IF_EXCEPTION(scope, nullptr); + } + } + } + } + break; + } + } + } + + // Set validFrom + object->putDirect(vm, Identifier::fromString(vm, "valid_from"_s), valueOrUndefined(computeValidFrom(view, globalObject))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set validTo + object->putDirect(vm, Identifier::fromString(vm, "valid_to"_s), valueOrUndefined(computeValidTo(view, globalObject))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set fingerprints + object->putDirect(vm, Identifier::fromString(vm, "fingerprint"_s), valueOrUndefined(computeFingerprint(view, globalObject))); + RETURN_IF_EXCEPTION(scope, nullptr); + + object->putDirect(vm, Identifier::fromString(vm, "fingerprint256"_s), valueOrUndefined(computeFingerprint256(view, globalObject))); + RETURN_IF_EXCEPTION(scope, nullptr); + + object->putDirect(vm, Identifier::fromString(vm, "fingerprint512"_s), valueOrUndefined(computeFingerprint512(view, globalObject))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set keyUsage + object->putDirect(vm, Identifier::fromString(vm, "ext_key_usage"_s), getKeyUsage(view, globalObject)); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set serialNumber + object->putDirect(vm, Identifier::fromString(vm, "serialNumber"_s), valueOrUndefined(computeSerialNumber(view, globalObject))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set raw + object->putDirect(vm, Identifier::fromString(vm, "raw"_s), computeRaw(view, globalObject)); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set CA flag + object->putDirect(vm, Identifier::fromString(vm, "ca"_s), jsBoolean(computeIsCA(view, globalObject))); + RETURN_IF_EXCEPTION(scope, nullptr); + + return object; +} + +// This one DOES depend on a JSX509Certificate object +// This implementation re-uses the cached values from the JSX509Certificate object getters +// saving memory. +JSC::JSObject* JSX509Certificate::toLegacyObject(JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* cert = view().get(); + + if (!cert) + return nullptr; + + JSC::JSObject* object = constructEmptyObject(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Helper function to convert JSValue to undefined if empty/null + auto valueOrUndefined = [&](JSValue value) -> JSValue { + if (value.isEmpty() || value.isNull() || (value.isString() && value.toString(globalObject)->length() == 0)) + return jsUndefined(); + return value; + }; + + // Set subject + object->putDirect(vm, Identifier::fromString(vm, "subject"_s), valueOrUndefined(computeSubject(view(), globalObject, true))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set issuer + object->putDirect(vm, Identifier::fromString(vm, "issuer"_s), valueOrUndefined(computeIssuer(view(), globalObject, true))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set subjectaltname + object->putDirect(vm, Identifier::fromString(vm, "subjectaltname"_s), valueOrUndefined(subjectAltName())); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set infoAccess + object->putDirect(vm, Identifier::fromString(vm, "infoAccess"_s), valueOrUndefined(computeInfoAccess(view(), globalObject, true))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set modulus and exponent for RSA keys + EVP_PKEY* pkey = X509_get0_pubkey(cert); + if (pkey) { + switch (EVP_PKEY_base_id(pkey)) { + case EVP_PKEY_RSA: { + const RSA* rsa = EVP_PKEY_get0_RSA(pkey); + if (rsa) { + const BIGNUM* n; + const BIGNUM* e; + RSA_get0_key(rsa, &n, &e, nullptr); + + // Convert modulus to string + auto bio = ncrypto::BIOPointer::New(n); + if (bio) { + object->putDirect(vm, Identifier::fromString(vm, "modulus"_s), jsString(vm, toWTFString(bio))); + RETURN_IF_EXCEPTION(scope, nullptr); + } + + // Convert exponent to string + uint64_t exponent_word = static_cast(ncrypto::BignumPointer::GetWord(e)); + auto bio_e = ncrypto::BIOPointer::NewMem(); + if (bio_e) { + BIO_printf(bio_e.get(), "0x%" PRIx64, exponent_word); + object->putDirect(vm, Identifier::fromString(vm, "exponent"_s), jsString(vm, toWTFString(bio_e))); + RETURN_IF_EXCEPTION(scope, nullptr); + } + + // Set bits + object->putDirect(vm, Identifier::fromString(vm, "bits"_s), jsNumber(ncrypto::BignumPointer::GetBitCount(n))); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set pubkey + int size = i2d_RSA_PUBKEY(rsa, nullptr); + if (size > 0) { + auto* buffer = Bun::createUninitializedBuffer(globalObject, static_cast(size)); + RETURN_IF_EXCEPTION(scope, nullptr); + uint8_t* data = buffer->typedVector(); + i2d_RSA_PUBKEY(rsa, &data); + object->putDirect(vm, Identifier::fromString(vm, "pubkey"_s), buffer); + } + } + break; + } + case EVP_PKEY_EC: { + const EC_KEY* ec = EVP_PKEY_get0_EC_KEY(pkey); + if (ec) { + const EC_GROUP* group = EC_KEY_get0_group(ec); + if (group) { + // Set bits + int bits = EC_GROUP_order_bits(group); + if (bits > 0) { + object->putDirect(vm, Identifier::fromString(vm, "bits"_s), jsNumber(bits)); + RETURN_IF_EXCEPTION(scope, nullptr); + } + + // Add pubkey field for EC keys + const EC_POINT* point = EC_KEY_get0_public_key(ec); + if (point) { + point_conversion_form_t form = EC_KEY_get_conv_form(ec); + size_t size = EC_POINT_point2oct(group, point, form, nullptr, 0, nullptr); + if (size > 0) { + auto* buffer = Bun::createUninitializedBuffer(globalObject, size); + RETURN_IF_EXCEPTION(scope, nullptr); + uint8_t* data = buffer->typedVector(); + size_t result_size = EC_POINT_point2oct(group, point, form, data, size, nullptr); + if (result_size == size) { + object->putDirect(vm, Identifier::fromString(vm, "pubkey"_s), buffer); + } + } + } + + // Set curve info + int nid = EC_GROUP_get_curve_name(group); + if (nid != 0) { + const char* sn = OBJ_nid2sn(nid); + if (sn) { + object->putDirect(vm, Identifier::fromString(vm, "asn1Curve"_s), jsString(vm, String::fromUTF8(sn))); + RETURN_IF_EXCEPTION(scope, nullptr); + } + + const char* nist = EC_curve_nid2nist(nid); + if (nist) { + object->putDirect(vm, Identifier::fromString(vm, "nistCurve"_s), jsString(vm, String::fromUTF8(nist))); + RETURN_IF_EXCEPTION(scope, nullptr); + } + } + } + } + break; + } + } + } + + // Set validFrom + object->putDirect(vm, Identifier::fromString(vm, "valid_from"_s), valueOrUndefined(validFrom())); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set validTo + object->putDirect(vm, Identifier::fromString(vm, "valid_to"_s), valueOrUndefined(validTo())); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set fingerprints + object->putDirect(vm, Identifier::fromString(vm, "fingerprint"_s), valueOrUndefined(fingerprint())); + RETURN_IF_EXCEPTION(scope, nullptr); + + object->putDirect(vm, Identifier::fromString(vm, "fingerprint256"_s), valueOrUndefined(fingerprint256())); + RETURN_IF_EXCEPTION(scope, nullptr); + + object->putDirect(vm, Identifier::fromString(vm, "fingerprint512"_s), valueOrUndefined(fingerprint512())); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set keyUsage + object->putDirect(vm, Identifier::fromString(vm, "ext_key_usage"_s), getKeyUsage(globalObject)); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set serialNumber + object->putDirect(vm, Identifier::fromString(vm, "serialNumber"_s), valueOrUndefined(serialNumber())); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set raw + object->putDirect(vm, Identifier::fromString(vm, "raw"_s), raw()); + RETURN_IF_EXCEPTION(scope, nullptr); + + // Set CA flag + object->putDirect(vm, Identifier::fromString(vm, "ca"_s), jsBoolean(computeIsCA(view(), globalObject))); + RETURN_IF_EXCEPTION(scope, nullptr); + + return object; +} + +JSValue JSX509Certificate::computePublicKey(ncrypto::X509View view, JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto result = view.getPublicKey(); + if (!result) { + throwBoringSSLError(vm, scope, globalObject, result.error.value_or(0)); + return {}; + } + + RefPtr key = toCryptoKey(result.value.release()); + if (!key) { + throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INVALID_STATE, "Failed to convert public key to CryptoKey"_s); + return {}; + } + + return toJSNewlyCreated(globalObject, defaultGlobalObject(globalObject), key.releaseNonNull()); +} + +JSValue JSX509Certificate::computeInfoAccess(ncrypto::X509View view, JSGlobalObject* globalObject, bool legacy) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto bio = view.getInfoAccess(); + if (!bio) { + return jsEmptyString(vm); + } + String info = toWTFString(bio); + if (!legacy) { + return jsString(vm, info); + } + + // InfoAccess is always an array, even when a single element is present. + JSObject* object = constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure()); + + // Go through each newline + unsigned substring_start = 0; + while (substring_start < info.length()) { + auto key_index = info.find(':', substring_start); + + if (key_index == notFound) { + break; + } + auto line_end = info.find('\n', key_index); + unsigned value_start = key_index + 1; + String key = info.substringSharingImpl(substring_start, key_index - substring_start); + String value = line_end == notFound ? info.substringSharingImpl(value_start) : info.substringSharingImpl(value_start, line_end - value_start); + Identifier identifier = Identifier::fromString(vm, key); + + if (identifier.isNull()) { + continue; + } + JSValue existingValue = object->getIfPropertyExists(globalObject, identifier); + RETURN_IF_EXCEPTION(scope, {}); + if (existingValue) { + JSArray* array = jsCast(existingValue); + array->push(globalObject, jsString(vm, value)); + } else { + JSArray* array = constructEmptyArray(globalObject, static_cast(nullptr), 1); + array->putDirectIndex(globalObject, 0, jsString(vm, value)); + object->putDirect(vm, identifier, array); + } + + if (line_end == notFound) { + break; + } + substring_start = line_end + 1; + } + + return object; +} + +JSString* JSX509Certificate::computeSubjectAltName(ncrypto::X509View view, JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto bio = view.getSubjectAltName(); + if (!bio) { + return jsEmptyString(vm); + } + + return jsString(vm, toWTFString(bio)); +} + +JSValue JSX509Certificate::getKeyUsage(ncrypto::X509View view, JSGlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto keyUsage = view.getKeyUsage(); + if (!keyUsage) { + return jsUndefined(); + } + + JSArray* array = JSArray::tryCreate(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 0); + if (!array) { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + + int count = sk_ASN1_OBJECT_num(keyUsage.get()); + char buf[256]; + + int j = 0; + for (int i = 0; i < count; i++) { + if (OBJ_obj2txt(buf, sizeof(buf), sk_ASN1_OBJECT_value(keyUsage.get(), i), 1) >= 0) { + array->putDirectIndex(globalObject, j++, jsString(vm, String::fromUTF8(buf))); + } + } + + return array; +} + +void setupX509CertificateClassStructure(LazyClassStructure::Initializer& init) +{ + auto* prototypeStructure = JSX509CertificatePrototype::createStructure(init.vm, init.global, init.global->objectPrototype()); + auto* prototype = JSX509CertificatePrototype::create(init.vm, init.global, prototypeStructure); + + auto* constructorStructure = JSX509CertificateConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()); + + auto* constructor = JSX509CertificateConstructor::create(init.vm, init.global, constructorStructure, prototype); + + auto* structure = JSX509Certificate::createStructure(init.vm, init.global, prototype); + init.setPrototype(prototype); + init.setStructure(structure); + init.setConstructor(constructor); +} + +extern "C" EncodedJSValue Bun__X509__toJSLegacyEncoding(X509* cert, JSGlobalObject* globalObject) +{ + ncrypto::X509View view(cert); + return JSValue::encode(JSX509Certificate::toLegacyObject(view, globalObject)); +} +extern "C" EncodedJSValue Bun__X509__toJS(X509* cert, JSGlobalObject* globalObject) +{ + ncrypto::X509Pointer cert_ptr(cert); + auto* zigGlobalObject = defaultGlobalObject(globalObject); + return JSValue::encode(JSX509Certificate::create(zigGlobalObject->vm(), zigGlobalObject->m_JSX509CertificateClassStructure.get(zigGlobalObject), globalObject, WTFMove(cert_ptr))); +} + +JSC_DEFINE_HOST_FUNCTION(jsIsX509Certificate, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + JSValue value = callFrame->argument(0); + if (!value.isCell()) + return JSValue::encode(jsBoolean(false)); + return JSValue::encode(jsBoolean(value.asCell()->inherits(JSX509Certificate::info()))); +} + +} // namespace Bun diff --git a/src/bun.js/bindings/JSX509Certificate.h b/src/bun.js/bindings/JSX509Certificate.h new file mode 100644 index 0000000000..3014c7f6b3 --- /dev/null +++ b/src/bun.js/bindings/JSX509Certificate.h @@ -0,0 +1,156 @@ +#pragma once + +#include "root.h" + +#include "BunClientData.h" +#include "ncrypto.h" +#include "headers-handwritten.h" + +#include +#include +#include +#include +#include + +namespace Zig { +class GlobalObject; +} + +namespace Bun { + +JSC_DECLARE_HOST_FUNCTION(jsIsX509Certificate); + +using namespace JSC; + +class JSX509Certificate final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr bool needsDestruction = true; + + // The underlying X509 certificate + ncrypto::X509Pointer m_x509; + + ncrypto::X509View view() const + { + return m_x509.view(); + } + + // Lazily computed certificate data + LazyProperty m_subject; + LazyProperty m_issuer; + LazyProperty m_validFrom; + LazyProperty m_validTo; + LazyProperty m_serialNumber; + LazyProperty m_fingerprint; + LazyProperty m_fingerprint256; + LazyProperty m_fingerprint512; + LazyProperty m_raw; + LazyProperty m_subjectAltName; + LazyProperty m_infoAccess; + LazyProperty m_publicKey; + + JSString* subject(); + JSString* issuer(); + JSString* validFrom(); + JSString* validTo(); + JSString* serialNumber(); + JSString* fingerprint(); + JSString* fingerprint256(); + JSString* fingerprint512(); + JSUint8Array* raw(); + JSString* infoAccess(); + JSString* subjectAltName(); + JSValue publicKey(); + + // Certificate validation methods + bool checkHost(JSGlobalObject*, std::span, uint32_t flags); + bool checkEmail(JSGlobalObject*, std::span, uint32_t flags); + bool checkIP(JSGlobalObject*, std::span); + bool checkIssued(JSGlobalObject*, JSX509Certificate* issuer); + bool checkPrivateKey(JSGlobalObject*, EVP_PKEY* pkey); + bool verify(JSGlobalObject*, EVP_PKEY* pkey); + JSC::JSObject* toLegacyObject(JSGlobalObject*); + static JSObject* toLegacyObject(ncrypto::X509View view, JSGlobalObject*); + + // Certificate data access methods + static JSValue getKeyUsage(ncrypto::X509View view, JSGlobalObject*); + EVP_PKEY* getPublicKey(JSGlobalObject* globalObject); + JSValue getKeyUsage(JSGlobalObject* globalObject) { return JSX509Certificate::getKeyUsage(view(), globalObject); } + + static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm); + + static void destroy(JSC::JSCell*); + + ~JSX509Certificate(); + + void finishCreation(JSC::VM& vm); + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSValue prototype); + + static JSX509Certificate* create( + JSC::VM& vm, + JSC::Structure* structure); + + static JSX509Certificate* create( + JSC::VM& vm, + JSC::Structure* structure, + JSC::JSGlobalObject* globalObject, + std::span data); + + static JSX509Certificate* create( + JSC::VM& vm, + JSC::Structure* structure, + JSC::JSGlobalObject* globalObject, + ncrypto::X509Pointer&& cert); + + static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + + template + static void visitChildren(JSCell*, Visitor&); + + DECLARE_INFO; + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForJSX509Certificate.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSX509Certificate = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSX509Certificate.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSX509Certificate = std::forward(space); }); + } + + static JSValue computeSubject(ncrypto::X509View view, JSGlobalObject*, bool legacy); + static JSValue computeIssuer(ncrypto::X509View view, JSGlobalObject*, bool legacy); + static JSString* computeValidFrom(ncrypto::X509View view, JSGlobalObject*); + static JSString* computeValidTo(ncrypto::X509View view, JSGlobalObject*); + static JSString* computeSerialNumber(ncrypto::X509View view, JSGlobalObject*); + static JSString* computeFingerprint(ncrypto::X509View view, JSGlobalObject*); + static JSString* computeFingerprint256(ncrypto::X509View view, JSGlobalObject*); + static JSString* computeFingerprint512(ncrypto::X509View view, JSGlobalObject*); + static JSUint8Array* computeRaw(ncrypto::X509View view, JSGlobalObject*); + static bool computeIsCA(ncrypto::X509View view, JSGlobalObject*); + static JSValue computeInfoAccess(ncrypto::X509View view, JSGlobalObject*, bool legacy); + static JSString* computeSubjectAltName(ncrypto::X509View view, JSGlobalObject*); + static JSValue computePublicKey(ncrypto::X509View view, JSGlobalObject*); + + JSX509Certificate(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + { + } + + // Convert the certificate to PEM format + String toPEMString() const; + +private: + uint16_t m_extraMemorySizeForGC = 0; +}; + +void setupX509CertificateClassStructure(LazyClassStructure::Initializer& init); + +} // namespace Bun diff --git a/src/bun.js/bindings/JSX509CertificateConstructor.cpp b/src/bun.js/bindings/JSX509CertificateConstructor.cpp new file mode 100644 index 0000000000..04b7fc1fb8 --- /dev/null +++ b/src/bun.js/bindings/JSX509CertificateConstructor.cpp @@ -0,0 +1,11 @@ +#include "root.h" +#include "JSX509CertificateConstructor.h" +#include "JSX509Certificate.h" +#include "ZigGlobalObject.h" +#include + +namespace Bun { + +using namespace JSC; + +} // namespace Bun diff --git a/src/bun.js/bindings/JSX509CertificateConstructor.h b/src/bun.js/bindings/JSX509CertificateConstructor.h new file mode 100644 index 0000000000..ee740d0ee1 --- /dev/null +++ b/src/bun.js/bindings/JSX509CertificateConstructor.h @@ -0,0 +1,15 @@ +#pragma once + +#include "root.h" +#include +#include + +namespace Zig { +class GlobalObject; +} + +namespace Bun { + +using namespace JSC; + +} // namespace Bun diff --git a/src/bun.js/bindings/JSX509CertificatePrototype.cpp b/src/bun.js/bindings/JSX509CertificatePrototype.cpp new file mode 100644 index 0000000000..caa127f364 --- /dev/null +++ b/src/bun.js/bindings/JSX509CertificatePrototype.cpp @@ -0,0 +1,752 @@ + + +#include "root.h" + +#include "JSDOMExceptionHandling.h" +#include "ZigGlobalObject.h" +#include "ncrypto.h" +#include "JSX509Certificate.h" +#include "JSX509CertificatePrototype.h" +#include "ErrorCode.h" + +#include +#include +#include +#include "BunString.h" +#include "webcrypto/JSCryptoKey.h" +#include "webcrypto/CryptoKeyEC.h" +#include "webcrypto/CryptoKeyRSA.h" +#include "webcrypto/CryptoKeyOKP.h" +#include "webcrypto/CryptoKeyAES.h" +#include "wtf/DateMath.h" +#include "AsymmetricKeyValue.h" +#include + +namespace Bun { + +using namespace JSC; + +static JSC_DECLARE_HOST_FUNCTION(jsX509CertificateProtoFuncCheckEmail); +static JSC_DECLARE_HOST_FUNCTION(jsX509CertificateProtoFuncCheckHost); +static JSC_DECLARE_HOST_FUNCTION(jsX509CertificateProtoFuncCheckIP); +static JSC_DECLARE_HOST_FUNCTION(jsX509CertificateProtoFuncCheckIssued); +static JSC_DECLARE_HOST_FUNCTION(jsX509CertificateProtoFuncCheckPrivateKey); +static JSC_DECLARE_HOST_FUNCTION(jsX509CertificateProtoFuncToJSON); +static JSC_DECLARE_HOST_FUNCTION(jsX509CertificateProtoFuncToLegacyObject); +static JSC_DECLARE_HOST_FUNCTION(jsX509CertificateProtoFuncToString); +static JSC_DECLARE_HOST_FUNCTION(jsX509CertificateProtoFuncVerify); + +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_ca); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_fingerprint); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_fingerprint256); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_fingerprint512); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_subject); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_subjectAltName); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_infoAccess); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_keyUsage); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_issuer); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_issuerCertificate); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_publicKey); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_raw); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_serialNumber); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_validFrom); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_validTo); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_validFromDate); +static JSC_DECLARE_CUSTOM_GETTER(jsX509CertificateGetter_validToDate); + +static const HashTableValue JSX509CertificatePrototypeTableValues[] = { + { "ca"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_ca, 0 } }, + { "checkEmail"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsX509CertificateProtoFuncCheckEmail, 2 } }, + { "checkHost"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsX509CertificateProtoFuncCheckHost, 2 } }, + { "checkIP"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsX509CertificateProtoFuncCheckIP, 1 } }, + { "checkIssued"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsX509CertificateProtoFuncCheckIssued, 1 } }, + { "checkPrivateKey"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsX509CertificateProtoFuncCheckPrivateKey, 1 } }, + { "fingerprint"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_fingerprint, 0 } }, + { "fingerprint256"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_fingerprint256, 0 } }, + { "fingerprint512"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_fingerprint512, 0 } }, + { "infoAccess"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_infoAccess, 0 } }, + { "issuer"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_issuer, 0 } }, + { "issuerCertificate"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_issuerCertificate, 0 } }, + { "keyUsage"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_keyUsage, 0 } }, + { "publicKey"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_publicKey, 0 } }, + { "raw"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_raw, 0 } }, + { "serialNumber"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_serialNumber, 0 } }, + { "subject"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_subject, 0 } }, + { "subjectAltName"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_subjectAltName, 0 } }, + { "toJSON"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsX509CertificateProtoFuncToJSON, 0 } }, + { "toLegacyObject"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsX509CertificateProtoFuncToLegacyObject, 0 } }, + { "toString"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsX509CertificateProtoFuncToString, 0 } }, + { "validFrom"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_validFrom, 0 } }, + { "validFromDate"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessorOrValue), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_validFromDate, 0 } }, + { "validTo"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_validTo, 0 } }, + { "validToDate"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessorOrValue), NoIntrinsic, { HashTableValue::GetterSetterType, jsX509CertificateGetter_validToDate, 0 } }, + { "verify"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsX509CertificateProtoFuncVerify, 1 } }, +}; + +const ClassInfo JSX509CertificatePrototype::s_info = { "X509Certificate"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSX509CertificatePrototype) }; + +void JSX509CertificatePrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSX509Certificate::info(), JSX509CertificatePrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +JSC_DEFINE_HOST_FUNCTION(jsX509CertificateProtoFuncToString, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "toString"_s); + return {}; + } + + // Convert the certificate to PEM format and return it + String pemString = thisObject->toPEMString(); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsString(vm, pemString)); +} + +// function getFlags(options = kEmptyObject) { +// validateObject(options, 'options'); +// const { +// subject = 'default', // Can be 'default', 'always', or 'never' +// wildcards = true, +// partialWildcards = true, +// multiLabelWildcards = false, +// singleLabelSubdomains = false, +// } = { ...options }; +// let flags = 0; +// validateString(subject, 'options.subject'); +// validateBoolean(wildcards, 'options.wildcards'); +// validateBoolean(partialWildcards, 'options.partialWildcards'); +// validateBoolean(multiLabelWildcards, 'options.multiLabelWildcards'); +// validateBoolean(singleLabelSubdomains, 'options.singleLabelSubdomains'); +// switch (subject) { +// case 'default': /* Matches OpenSSL's default, no flags. */ break; +// case 'always': flags |= X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT; break; +// case 'never': flags |= X509_CHECK_FLAG_NEVER_CHECK_SUBJECT; break; +// default: +// throw new ERR_INVALID_ARG_VALUE('options.subject', subject); +// } +// if (!wildcards) flags |= X509_CHECK_FLAG_NO_WILDCARDS; +// if (!partialWildcards) flags |= X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS; +// if (multiLabelWildcards) flags |= X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS; +// if (singleLabelSubdomains) flags |= X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS; +// return flags; +// } +static uint32_t getFlags(JSC::VM& vm, JSGlobalObject* globalObject, JSC::ThrowScope& scope, JSValue options) +{ + if (options.isUndefined()) + return 0; + + JSObject* object = options.getObject(); + RETURN_IF_EXCEPTION(scope, {}); + if (!object) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "options must be an object"_s); + return 0; + } + + JSValue subject = object->get(globalObject, Identifier::fromString(vm, String("subject"_s))); + RETURN_IF_EXCEPTION(scope, {}); + + JSValue wildcards = object->get(globalObject, Identifier::fromString(vm, String("wildcards"_s))); + RETURN_IF_EXCEPTION(scope, {}); + + JSValue partialWildcards = object->get(globalObject, Identifier::fromString(vm, String("partialWildcards"_s))); + RETURN_IF_EXCEPTION(scope, {}); + + JSValue multiLabelWildcards = object->get(globalObject, Identifier::fromString(vm, String("multiLabelWildcards"_s))); + RETURN_IF_EXCEPTION(scope, {}); + + JSValue singleLabelSubdomains = object->get(globalObject, Identifier::fromString(vm, String("singleLabelSubdomains"_s))); + RETURN_IF_EXCEPTION(scope, {}); + + uint32_t flags = 0; + bool any = false; + + if (!subject.isUndefined()) { + any = true; + if (!subject.isString()) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "subject must be a string"_s); + return 0; + } + + auto subjectString = subject.toString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto view = subjectString->view(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + if (view == "always"_s) { + flags |= X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT; + } else if (view == "never"_s) { + flags |= X509_CHECK_FLAG_NEVER_CHECK_SUBJECT; + } else if (view == "default"_s) { + // Matches OpenSSL's default, no flags. + } else { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_VALUE, "subject must be 'always' or 'never'"_s); + return 0; + } + } + + if (!wildcards.isUndefined()) { + any = true; + if (!wildcards.isBoolean()) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "wildcards must be a boolean"_s); + return 0; + } + + if (!wildcards.asBoolean()) + flags |= X509_CHECK_FLAG_NO_WILDCARDS; + } + + if (!partialWildcards.isUndefined()) { + any = true; + if (!partialWildcards.isBoolean()) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "partialWildcards must be a boolean"_s); + return 0; + } + + if (!partialWildcards.asBoolean()) + flags |= X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS; + } + + if (!multiLabelWildcards.isUndefined()) { + any = true; + if (!multiLabelWildcards.isBoolean()) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "multiLabelWildcards must be a boolean"_s); + return 0; + } + + if (multiLabelWildcards.asBoolean()) + flags |= X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS; + } + + if (!singleLabelSubdomains.isUndefined()) { + any = true; + if (!singleLabelSubdomains.isBoolean()) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "singleLabelSubdomains must be a boolean"_s); + return 0; + } + if (singleLabelSubdomains.asBoolean()) + flags |= X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS; + } + + if (!any) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "options must have at least one property"_s); + return 0; + } + + return flags; +} + +JSC_DEFINE_HOST_FUNCTION(jsX509CertificateProtoFuncCheckEmail, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "checkEmail"_s); + return {}; + } + + JSValue arg0 = callFrame->argument(0); + if (!arg0.isUndefined()) { + if (!arg0.isString()) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "email must be a string"_s); + return {}; + } + } + + auto emailString = arg0.toString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto view = emailString->view(globalObject); + + uint32_t flags = getFlags(vm, globalObject, scope, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + + Bun::UTF8View emailView(view); + + if (!thisObject->checkEmail(globalObject, emailView.span(), flags)) + return JSValue::encode(jsUndefined()); + return JSValue::encode(emailString); +} + +JSC_DEFINE_HOST_FUNCTION(jsX509CertificateProtoFuncCheckHost, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "checkHost"_s); + return {}; + } + + JSValue arg0 = callFrame->argument(0); + if (!arg0.isUndefined()) { + if (!arg0.isString()) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "host must be a string"_s); + return {}; + } + } + + uint32_t flags = getFlags(vm, globalObject, scope, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + + auto hostString = arg0.toString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto view = hostString->view(globalObject); + + Bun::UTF8View hostView(view); + + if (!thisObject->checkHost(globalObject, hostView.span(), flags)) + return JSValue::encode(jsUndefined()); + return JSValue::encode(hostString); +} + +JSC_DEFINE_HOST_FUNCTION(jsX509CertificateProtoFuncCheckIP, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "checkIP"_s); + return {}; + } + + JSValue arg0 = callFrame->argument(0); + if (!arg0.isUndefined()) { + if (!arg0.isString()) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "ip must be a string"_s); + return {}; + } + } + + auto ipString = arg0.toString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto view = ipString->view(globalObject); + WTF::CString ip = view->utf8(); + + // ignore flags + // uint32_t flags = getFlags(vm, globalObject, scope, callFrame->argument(1)); + // RETURN_IF_EXCEPTION(scope, {}); + + if (!thisObject->checkIP(globalObject, ip.spanIncludingNullTerminator())) + return JSValue::encode(jsUndefined()); + return JSValue::encode(ipString); +} + +JSC_DEFINE_HOST_FUNCTION(jsX509CertificateProtoFuncCheckIssued, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) + return throwVMError(globalObject, scope, createError(globalObject, ErrorCode::ERR_INVALID_THIS, "checkIssued called on incompatible receiver"_s)); + + JSX509Certificate* issuer = jsDynamicCast(callFrame->argument(0)); + if (!issuer) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "issuer must be a JSX509Certificate"_s); + return {}; + } + + if (!thisObject->checkIssued(globalObject, issuer)) + return JSValue::encode(jsUndefined()); + return JSValue::encode(issuer); +} + +JSC_DEFINE_HOST_FUNCTION(jsX509CertificateProtoFuncCheckPrivateKey, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) + return throwVMError(globalObject, scope, createError(globalObject, ErrorCode::ERR_INVALID_THIS, "checkPrivateKey called on incompatible receiver"_s)); + + JSValue privateKey = callFrame->argument(0); + RETURN_IF_EXCEPTION(scope, {}); + + WebCore::JSCryptoKey* key = WebCore::JSCryptoKey::fromJS(globalObject, privateKey); + if (!key) + return throwVMError(globalObject, scope, createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, "Private key must be a valid CryptoKey"_s)); + + auto& wrapped = key->wrapped(); + + if (wrapped.type() != WebCore::CryptoKeyType::Private) + return throwVMError(globalObject, scope, createError(globalObject, ErrorCode::ERR_INVALID_ARG_VALUE, "CryptoKey must be a private key"_s)); + + bool isValid = false; + switch (wrapped.keyClass()) { + case WebCore::CryptoKeyClass::RSA: + isValid = thisObject->checkPrivateKey(globalObject, downcast(wrapped).platformKey()); + break; + case WebCore::CryptoKeyClass::EC: + isValid = thisObject->checkPrivateKey(globalObject, downcast(wrapped).platformKey()); + break; + default: + break; + } + + if (!isValid) + return JSValue::encode(jsUndefined()); + + return JSValue::encode(privateKey); +} + +JSC_DEFINE_HOST_FUNCTION(jsX509CertificateProtoFuncToJSON, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) + return throwVMError(globalObject, scope, createError(globalObject, ErrorCode::ERR_INVALID_THIS, "toJSON called on incompatible receiver"_s)); + + // There's no standardized JSON encoding for X509 certs so we + // fallback to providing the PEM encoding as a string. + return JSValue::encode(jsString(vm, thisObject->toPEMString())); +} + +JSC_DEFINE_HOST_FUNCTION(jsX509CertificateProtoFuncToLegacyObject, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "toLegacyObject"_s); + return {}; + } + + return JSValue::encode(thisObject->toLegacyObject(globalObject)); +} + +static JSValue undefinedIfEmpty(JSString* value) +{ + if (!value || value->length() == 0) + return jsUndefined(); + return value; +} + +static JSValue undefinedIfEmpty(JSUint8Array* value) +{ + if (!value || value->length() == 0) + return jsUndefined(); + return value; +} + +JSC_DEFINE_HOST_FUNCTION(jsX509CertificateProtoFuncVerify, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "verify"_s); + return {}; + } + + JSObject* arg0 = callFrame->argument(0).getObject(); + if (!arg0) + return throwVMError(globalObject, scope, createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, "argument 0 must be an object"_s)); + + JSCryptoKey* key = JSCryptoKey::fromJS(globalObject, arg0); + if (!key) + return throwVMError(globalObject, scope, createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, "argument 0 must be a valid KeyObject"_s)); + + auto& wrapped = key->wrapped(); + // A Public Key can be derived from a private key, so we allow both. + // Node has ^ comment, but the test suite passes a private key. + if (wrapped.type() != CryptoKeyType::Public) { + return throwVMError(globalObject, scope, createError(globalObject, ErrorCode::ERR_INVALID_ARG_VALUE, "argument 0 must be a public key"_s)); + } + + AsymmetricKeyValue asymmetricKeyValue(wrapped); + if (!asymmetricKeyValue) + return throwVMError(globalObject, scope, createError(globalObject, ErrorCode::ERR_INVALID_ARG_VALUE, "argument 0 must be a valid public key"_s)); + + ncrypto::ClearErrorOnReturn clearErrorOnReturn; + int result = X509_verify(thisObject->view().get(), asymmetricKeyValue.key); + return JSValue::encode(jsBoolean(result == 1)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_ca, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "ca"_s); + return {}; + } + + return JSValue::encode(jsBoolean(thisObject->view().isCA())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_fingerprint, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "fingerprint"_s); + return {}; + } + + return JSValue::encode(thisObject->fingerprint()); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_fingerprint256, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "fingerprint256"_s); + return {}; + } + + return JSValue::encode(thisObject->fingerprint256()); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_fingerprint512, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "fingerprint512"_s); + return {}; + } + + return JSValue::encode(thisObject->fingerprint512()); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_subject, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "subject"_s); + return {}; + } + + return JSValue::encode(undefinedIfEmpty(thisObject->subject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_subjectAltName, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "subjectAltName"_s); + return {}; + } + + return JSValue::encode(undefinedIfEmpty(thisObject->subjectAltName())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_infoAccess, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "infoAccess"_s); + return {}; + } + + auto bio = thisObject->view().getInfoAccess(); + if (!bio) + return JSValue::encode(jsUndefined()); + + BUF_MEM* bptr = bio; + return JSValue::encode(undefinedIfEmpty(jsString(vm, String::fromUTF8(std::span(bptr->data, bptr->length))))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_keyUsage, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "keyUsage"_s); + return {}; + } + + return JSValue::encode(thisObject->getKeyUsage(globalObject)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_issuer, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "issuer"_s); + return {}; + } + + return JSValue::encode(undefinedIfEmpty(thisObject->issuer())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_issuerCertificate, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "issuerCertificate"_s); + return {}; + } + + auto issuerCert = thisObject->view().getIssuer(); + if (!issuerCert) + return JSValue::encode(jsUndefined()); + + auto bio = issuerCert.get(); + + BUF_MEM* bptr = nullptr; + BIO_get_mem_ptr(bio, &bptr); + std::span span(reinterpret_cast(bptr->data), bptr->length); + auto* zigGlobalObject = defaultGlobalObject(globalObject); + auto* structure = zigGlobalObject->m_JSX509CertificateClassStructure.get(zigGlobalObject); + auto jsIssuerCert = JSX509Certificate::create(vm, structure, globalObject, span); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsIssuerCert); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_publicKey, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "publicKey"_s); + return {}; + } + + return JSValue::encode(thisObject->publicKey()); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_raw, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "raw"_s); + return {}; + } + + return JSValue::encode(undefinedIfEmpty(thisObject->raw())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_serialNumber, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "serialNumber"_s); + return {}; + } + + return JSValue::encode(undefinedIfEmpty(thisObject->serialNumber())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_validFrom, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "validFrom"_s); + return {}; + } + + return JSValue::encode(undefinedIfEmpty(thisObject->validFrom())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_validTo, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "validTo"_s); + return {}; + } + + return JSValue::encode(undefinedIfEmpty(thisObject->validTo())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_validToDate, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "validToDate"_s); + return {}; + } + + auto* validToDate = thisObject->validTo(); + RETURN_IF_EXCEPTION(scope, {}); + auto view = validToDate->view(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + Bun::UTF8View validToDateView = Bun::UTF8View(view); + if (view->isEmpty()) + return JSValue::encode(jsUndefined()); + std::span span = { reinterpret_cast(validToDateView.span().data()), validToDateView.span().size() }; + double date = WTF::parseDate(span); + return JSValue::encode(JSC::DateInstance::create(vm, globalObject->dateStructure(), date)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_validFromDate, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSX509Certificate* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "JSX509Certificate"_s, "validFromDate"_s); + return {}; + } + + auto* validFromDate = thisObject->validFrom(); + RETURN_IF_EXCEPTION(scope, {}); + auto view = validFromDate->view(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + Bun::UTF8View validFromDateView = Bun::UTF8View(view); + if (view->isEmpty()) + return JSValue::encode(jsUndefined()); + std::span span = { reinterpret_cast(validFromDateView.span().data()), validFromDateView.span().size() }; + double date = WTF::parseDate(span); + return JSValue::encode(JSC::DateInstance::create(vm, globalObject->dateStructure(), date)); +} +} // namespace Bun diff --git a/src/bun.js/bindings/JSX509CertificatePrototype.h b/src/bun.js/bindings/JSX509CertificatePrototype.h new file mode 100644 index 0000000000..4328cdd140 --- /dev/null +++ b/src/bun.js/bindings/JSX509CertificatePrototype.h @@ -0,0 +1,49 @@ +#pragma once + +#include "root.h" +#include + +namespace JSC { +class JSGlobalObject; +class VM; +} + +namespace Bun { + +class JSX509CertificatePrototype final : public JSC::JSObject { +public: + using Base = JSC::JSObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSX509CertificatePrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) + { + JSX509CertificatePrototype* prototype = new (NotNull, allocateCell(vm)) JSX509CertificatePrototype(vm, structure); + prototype->finishCreation(vm); + return prototype; + } + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.plainObjectSpace(); + } + + DECLARE_INFO; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + structure->setMayBePrototype(true); + return structure; + } + +private: + JSX509CertificatePrototype(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; + +} // namespace Bun diff --git a/src/bun.js/bindings/KeyObject.cpp b/src/bun.js/bindings/KeyObject.cpp index ba7733b036..8d8715f8be 100644 --- a/src/bun.js/bindings/KeyObject.cpp +++ b/src/bun.js/bindings/KeyObject.cpp @@ -26,6 +26,7 @@ #include "JavaScriptCore/JSArrayBufferView.h" #include "JavaScriptCore/JSCJSValue.h" #include "JavaScriptCore/JSCast.h" +#include "ZigGlobalObject.h" #include "webcrypto/JSCryptoKey.h" #include "webcrypto/JSSubtleCrypto.h" #include "webcrypto/CryptoKeyOKP.h" @@ -58,6 +59,8 @@ #include "CryptoAlgorithmRegistry.h" #include "wtf/ForbidHeapAllocation.h" #include "wtf/Noncopyable.h" +#include "ncrypto.h" +#include "AsymmetricKeyValue.h" using namespace JSC; using namespace Bun; using JSGlobalObject = JSC::JSGlobalObject; @@ -140,11 +143,6 @@ static bool KeyObject__IsEncryptedPrivateKeyInfo(const unsigned char* data, size return len >= 1 && data[offset] != 2; } -struct AsymmetricKeyValue { - EVP_PKEY* key; - bool owned; -}; - struct AsymmetricKeyValueWithDER { EVP_PKEY* key; unsigned char* der_data; @@ -323,6 +321,7 @@ AsymmetricKeyValueWithDER KeyObject__ParsePublicKeyPEM(const char* key_pem, JSC_DEFINE_HOST_FUNCTION(KeyObject__createPrivateKey, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { + ncrypto::ClearErrorOnReturn clearErrorOnReturn; auto count = callFrame->argumentCount(); auto& vm = globalObject->vm(); @@ -861,7 +860,7 @@ static JSC::EncodedJSValue KeyObject__createPublicFromPrivate(JSC::JSGlobalObjec JSC_DEFINE_HOST_FUNCTION(KeyObject__createPublicKey, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - + ncrypto::ClearErrorOnReturn clearErrorOnReturn; auto count = callFrame->argumentCount(); auto& vm = globalObject->vm(); @@ -1255,6 +1254,7 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__createPublicKey, (JSC::JSGlobalObject * glob JSC_DEFINE_HOST_FUNCTION(KeyObject__createSecretKey, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { + ncrypto::ClearErrorOnReturn clearErrorOnReturn; JSValue bufferArg = callFrame->uncheckedArgument(0); auto& vm = lexicalGlobalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -1317,7 +1317,7 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__createSecretKey, (JSC::JSGlobalObject * lexi return {}; } -static ExceptionOr> KeyObject__GetBuffer(JSValue bufferArg) +ExceptionOr> KeyObject__GetBuffer(JSValue bufferArg) { if (!bufferArg.isCell()) { return Exception { OperationError }; @@ -1370,6 +1370,7 @@ static ExceptionOr> KeyObject__GetBuffer(JSValue bufferArg) } JSC_DEFINE_HOST_FUNCTION(KeyObject__Sign, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { + ncrypto::ClearErrorOnReturn clearErrorOnReturn; auto count = callFrame->argumentCount(); auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -1579,6 +1580,7 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__Sign, (JSC::JSGlobalObject * globalObject, J JSC_DEFINE_HOST_FUNCTION(KeyObject__Verify, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { + ncrypto::ClearErrorOnReturn clearErrorOnReturn; auto count = callFrame->argumentCount(); auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -1654,7 +1656,11 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__Verify, (JSC::JSGlobalObject * globalObject, const auto& hmac = downcast(wrapped); auto result = (customHash) ? WebCore::CryptoAlgorithmHMAC::platformVerifyWithAlgorithm(hmac, hash, signatureData, vectorData) : WebCore::CryptoAlgorithmHMAC::platformVerify(hmac, signatureData, vectorData); if (result.hasException()) { - WebCore::propagateException(*globalObject, scope, result.releaseException()); + Exception exception = result.releaseException(); + if (exception.code() == WebCore::ExceptionCode::OperationError) { + return JSValue::encode(jsBoolean(false)); + } + WebCore::propagateException(*globalObject, scope, WTFMove(exception)); return JSC::JSValue::encode(JSC::JSValue {}); } return JSC::JSValue::encode(jsBoolean(result.releaseReturnValue())); @@ -1663,7 +1669,11 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__Verify, (JSC::JSGlobalObject * globalObject, const auto& okpKey = downcast(wrapped); auto result = WebCore::CryptoAlgorithmEd25519::platformVerify(okpKey, signatureData, vectorData); if (result.hasException()) { - WebCore::propagateException(*globalObject, scope, result.releaseException()); + Exception exception = result.releaseException(); + if (exception.code() == WebCore::ExceptionCode::OperationError) { + return JSValue::encode(jsBoolean(false)); + } + WebCore::propagateException(*globalObject, scope, WTFMove(exception)); return JSC::JSValue::encode(JSC::JSValue {}); } return JSC::JSValue::encode(jsBoolean(result.releaseReturnValue())); @@ -1697,7 +1707,11 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__Verify, (JSC::JSGlobalObject * globalObject, } auto result = WebCore::CryptoAlgorithmECDSA::platformVerify(params, ec, signatureData, vectorData); if (result.hasException()) { - WebCore::propagateException(*globalObject, scope, result.releaseException()); + Exception exception = result.releaseException(); + if (exception.code() == WebCore::ExceptionCode::OperationError) { + return JSValue::encode(jsBoolean(false)); + } + WebCore::propagateException(*globalObject, scope, WTFMove(exception)); return JSC::JSValue::encode(JSC::JSValue {}); } return JSC::JSValue::encode(jsBoolean(result.releaseReturnValue())); @@ -1714,7 +1728,11 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__Verify, (JSC::JSGlobalObject * globalObject, case CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5: { auto result = (customHash) ? WebCore::CryptoAlgorithmRSASSA_PKCS1_v1_5::platformVerifyWithAlgorithm(rsa, hash, signatureData, vectorData) : CryptoAlgorithmRSASSA_PKCS1_v1_5::platformVerify(rsa, signatureData, vectorData); if (result.hasException()) { - WebCore::propagateException(*globalObject, scope, result.releaseException()); + Exception exception = result.releaseException(); + if (exception.code() == WebCore::ExceptionCode::OperationError) { + return JSValue::encode(jsBoolean(false)); + } + WebCore::propagateException(*globalObject, scope, WTFMove(exception)); return JSC::JSValue::encode(JSC::JSValue {}); } return JSC::JSValue::encode(jsBoolean(result.releaseReturnValue())); @@ -1758,7 +1776,11 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__Verify, (JSC::JSGlobalObject * globalObject, params.identifier = CryptoAlgorithmIdentifier::RSA_PSS; auto result = (customHash) ? WebCore::CryptoAlgorithmRSA_PSS::platformVerifyWithAlgorithm(params, hash, rsa, signatureData, vectorData) : CryptoAlgorithmRSA_PSS::platformVerify(params, rsa, signatureData, vectorData); if (result.hasException()) { - WebCore::propagateException(*globalObject, scope, result.releaseException()); + Exception exception = result.releaseException(); + if (exception.code() == WebCore::ExceptionCode::OperationError) { + return JSValue::encode(jsBoolean(false)); + } + WebCore::propagateException(*globalObject, scope, WTFMove(exception)); return JSC::JSValue::encode(JSC::JSValue {}); } return JSC::JSValue::encode(jsBoolean(result.releaseReturnValue())); @@ -1786,7 +1808,7 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__Verify, (JSC::JSGlobalObject * globalObject, JSC_DEFINE_HOST_FUNCTION(KeyObject__Exports, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - + ncrypto::ClearErrorOnReturn clearErrorOnReturn; auto count = callFrame->argumentCount(); auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -2371,7 +2393,7 @@ static char* bignum_to_string(const BIGNUM* bn) JSC_DEFINE_HOST_FUNCTION(KeyObject_AsymmetricKeyDetails, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { - + ncrypto::ClearErrorOnReturn clearErrorOnReturn; if (auto* key = jsDynamicCast(callFrame->argument(0))) { auto id = key->wrapped().algorithmIdentifier(); auto& vm = lexicalGlobalObject->vm(); @@ -2496,6 +2518,7 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject_AsymmetricKeyDetails, (JSC::JSGlobalObject * JSC_DEFINE_HOST_FUNCTION(KeyObject__generateKeyPairSync, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { + ncrypto::ClearErrorOnReturn clearErrorOnReturn; auto count = callFrame->argumentCount(); auto& vm = lexicalGlobalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -2674,8 +2697,7 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__generateKeyPairSync, (JSC::JSGlobalObject * } else if (namedCurve == "P-521"_s || namedCurve == "p521"_s || namedCurve == "secp521r1"_s) { namedCurve = "P-521"_s; } else { - throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "curve not supported"_s)); - return {}; + return Bun::ERR::CRYPTO_JWK_UNSUPPORTED_CURVE(scope, lexicalGlobalObject, namedCurve); } auto result = CryptoKeyEC::generatePair(CryptoAlgorithmIdentifier::ECDSA, namedCurve, true, CryptoKeyUsageSign | CryptoKeyUsageVerify); @@ -2718,6 +2740,7 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__generateKeyPairSync, (JSC::JSGlobalObject * } JSC_DEFINE_HOST_FUNCTION(KeyObject__generateKeySync, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { + ncrypto::ClearErrorOnReturn clearErrorOnReturn; auto count = callFrame->argumentCount(); auto& vm = lexicalGlobalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -2778,6 +2801,7 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__generateKeySync, (JSC::JSGlobalObject * lexi JSC_DEFINE_HOST_FUNCTION(KeyObject__AsymmetricKeyType, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { + ncrypto::ClearErrorOnReturn clearErrorOnReturn; static const NeverDestroyed values[] = { MAKE_STATIC_STRING_IMPL("rsa"), MAKE_STATIC_STRING_IMPL("rsa-pss"), @@ -2833,36 +2857,66 @@ static Vector GetRawKeyFromSecret(WebCore::CryptoKey& key) } } } -static AsymmetricKeyValue GetInternalAsymmetricKey(WebCore::CryptoKey& key) +AsymmetricKeyValue::~AsymmetricKeyValue() { - auto id = key.algorithmIdentifier(); + if (key && owned) { + EVP_PKEY_free(key); + } +} + +AsymmetricKeyValue::AsymmetricKeyValue(WebCore::CryptoKey& cryptoKey) +{ + auto id = cryptoKey.algorithmIdentifier(); + owned = false; + key = nullptr; + switch (id) { case CryptoAlgorithmIdentifier::RSAES_PKCS1_v1_5: case CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5: case CryptoAlgorithmIdentifier::RSA_OAEP: case CryptoAlgorithmIdentifier::RSA_PSS: - return AsymmetricKeyValue { .key = downcast(key).platformKey(), .owned = false }; + key = downcast(cryptoKey).platformKey(); + break; case CryptoAlgorithmIdentifier::ECDSA: case CryptoAlgorithmIdentifier::ECDH: - return AsymmetricKeyValue { .key = downcast(key).platformKey(), .owned = false }; + key = downcast(cryptoKey).platformKey(); + break; case CryptoAlgorithmIdentifier::Ed25519: { - const auto& okpKey = downcast(key); + const auto& okpKey = downcast(cryptoKey); auto keyData = okpKey.exportKey(); if (okpKey.type() == CryptoKeyType::Private) { - auto* evp_key = EVP_PKEY_new_raw_private_key(okpKey.namedCurve() == CryptoKeyOKP::NamedCurve::X25519 ? EVP_PKEY_X25519 : EVP_PKEY_ED25519, nullptr, keyData.data(), keyData.size()); - return AsymmetricKeyValue { .key = evp_key, .owned = true }; + key = EVP_PKEY_new_raw_private_key(okpKey.namedCurve() == CryptoKeyOKP::NamedCurve::X25519 ? EVP_PKEY_X25519 : EVP_PKEY_ED25519, nullptr, keyData.data(), keyData.size()); + owned = true; + break; } else { auto* evp_key = EVP_PKEY_new_raw_public_key(okpKey.namedCurve() == CryptoKeyOKP::NamedCurve::X25519 ? EVP_PKEY_X25519 : EVP_PKEY_ED25519, nullptr, keyData.data(), keyData.size()); - return AsymmetricKeyValue { .key = evp_key, .owned = true }; + key = evp_key; + owned = true; + break; } } - default: - return AsymmetricKeyValue { .key = NULL, .owned = false }; + case CryptoAlgorithmIdentifier::AES_CTR: + case CryptoAlgorithmIdentifier::AES_CBC: + case CryptoAlgorithmIdentifier::AES_GCM: + case CryptoAlgorithmIdentifier::AES_CFB: + case CryptoAlgorithmIdentifier::AES_KW: + case CryptoAlgorithmIdentifier::HMAC: + case CryptoAlgorithmIdentifier::SHA_1: + case CryptoAlgorithmIdentifier::SHA_224: + case CryptoAlgorithmIdentifier::SHA_256: + case CryptoAlgorithmIdentifier::SHA_384: + case CryptoAlgorithmIdentifier::SHA_512: + case CryptoAlgorithmIdentifier::HKDF: + case CryptoAlgorithmIdentifier::PBKDF2: + case CryptoAlgorithmIdentifier::None: + key = nullptr; + break; } } JSC_DEFINE_HOST_FUNCTION(KeyObject__Equals, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { + ncrypto::ClearErrorOnReturn clearErrorOnReturn; if (auto* key = jsDynamicCast(callFrame->argument(0))) { if (auto* key2 = jsDynamicCast(callFrame->argument(1))) { auto& wrapped = key->wrapped(); @@ -2882,17 +2936,11 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__Equals, (JSC::JSGlobalObject * lexicalGlobal } return JSC::JSValue::encode(jsBoolean(CRYPTO_memcmp(keyData.data(), keyData2.data(), size) == 0)); } - auto evp_key = GetInternalAsymmetricKey(wrapped); - auto evp_key2 = GetInternalAsymmetricKey(wrapped2); + AsymmetricKeyValue first(wrapped); + AsymmetricKeyValue second(wrapped2); - int ok = !evp_key.key || !evp_key2.key ? -2 : EVP_PKEY_cmp(evp_key.key, evp_key2.key); + int ok = !first.key || !second.key ? -2 : EVP_PKEY_cmp(first.key, second.key); - if (evp_key.key && evp_key.owned) { - EVP_PKEY_free(evp_key.key); - } - if (evp_key2.key && evp_key2.owned) { - EVP_PKEY_free(evp_key2.key); - } if (ok == -2) { auto& vm = lexicalGlobalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -3159,7 +3207,7 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__publicDecrypt, (JSGlobalObject * globalObjec return doAsymmetricSign(globalObject, callFrame, false); } -JSValue createNodeCryptoBinding(Zig::GlobalObject* globalObject) +JSValue createKeyObjectBinding(Zig::GlobalObject* globalObject) { VM& vm = globalObject->vm(); auto* obj = constructEmptyObject(globalObject); @@ -3200,6 +3248,8 @@ JSValue createNodeCryptoBinding(Zig::GlobalObject* globalObject) obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "publicDecrypt"_s)), JSFunction::create(vm, globalObject, 2, "publicDecrypt"_s, KeyObject__publicDecrypt, ImplementationVisibility::Public, NoIntrinsic), 0); + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "X509Certificate"_s)), + globalObject->m_JSX509CertificateClassStructure.constructor(globalObject)); return obj; } diff --git a/src/bun.js/bindings/KeyObject.h b/src/bun.js/bindings/KeyObject.h index 924ba9223f..9c4bd99731 100644 --- a/src/bun.js/bindings/KeyObject.h +++ b/src/bun.js/bindings/KeyObject.h @@ -3,9 +3,11 @@ #include "root.h" #include "helpers.h" +#include "ExceptionOr.h" namespace WebCore { -JSC::JSValue createNodeCryptoBinding(Zig::GlobalObject* globalObject); +ExceptionOr> KeyObject__GetBuffer(JSC::JSValue bufferArg); +JSC::JSValue createKeyObjectBinding(Zig::GlobalObject* globalObject); } // namespace WebCore diff --git a/src/bun.js/bindings/NodeCrypto.cpp b/src/bun.js/bindings/NodeCrypto.cpp new file mode 100644 index 0000000000..2d68321d37 --- /dev/null +++ b/src/bun.js/bindings/NodeCrypto.cpp @@ -0,0 +1,441 @@ +#include "NodeCrypto.h" +#include "KeyObject.h" +#include "ErrorCode.h" +#include "JavaScriptCore/JSArrayBufferView.h" +#include "JavaScriptCore/JSCJSValue.h" +#include "JavaScriptCore/JSCast.h" +#include "ZigGlobalObject.h" +#include "webcrypto/JSCryptoKey.h" +#include "webcrypto/JSSubtleCrypto.h" +#include "webcrypto/CryptoKeyOKP.h" +#include "webcrypto/CryptoKeyEC.h" +#include "webcrypto/CryptoKeyRSA.h" +#include "webcrypto/CryptoKeyAES.h" +#include "webcrypto/CryptoKeyHMAC.h" +#include "webcrypto/CryptoKeyRaw.h" +#include "webcrypto/CryptoKeyUsage.h" +#include "webcrypto/JsonWebKey.h" +#include "webcrypto/JSJsonWebKey.h" +#include "JavaScriptCore/JSObject.h" +#include "JavaScriptCore/ObjectConstructor.h" +#include "headers-handwritten.h" +#include +#include +#include +#include +#include +#include "JSBuffer.h" +#include "CryptoAlgorithmHMAC.h" +#include "CryptoAlgorithmEd25519.h" +#include "CryptoAlgorithmRSA_OAEP.h" +#include "CryptoAlgorithmRSA_PSS.h" +#include "CryptoAlgorithmRSASSA_PKCS1_v1_5.h" +#include "CryptoAlgorithmECDSA.h" +#include "CryptoAlgorithmEcdsaParams.h" +#include "CryptoAlgorithmRsaOaepParams.h" +#include "CryptoAlgorithmRsaPssParams.h" +#include "CryptoAlgorithmRegistry.h" +#include "wtf/ForbidHeapAllocation.h" +#include "wtf/Noncopyable.h" +#include "ncrypto.h" +#include "AsymmetricKeyValue.h" +#include "NodeValidator.h" + +using namespace JSC; +using namespace Bun; + +namespace WebCore { + +JSC_DEFINE_HOST_FUNCTION(jsStatelessDH, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (callFrame->argumentCount() < 2) { + return Bun::ERR::INVALID_ARG_VALUE(scope, lexicalGlobalObject, "diffieHellman"_s, jsUndefined(), "requires 2 arguments"_s); + } + + auto* privateKeyObj = JSC::jsDynamicCast(callFrame->argument(0)); + auto* publicKeyObj = JSC::jsDynamicCast(callFrame->argument(1)); + + if (!privateKeyObj || !publicKeyObj) { + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "diffieHellman"_s, "CryptoKey"_s, !privateKeyObj ? callFrame->argument(0) : callFrame->argument(1)); + } + + auto& privateKey = privateKeyObj->wrapped(); + auto& publicKey = publicKeyObj->wrapped(); + + // Create AsymmetricKeyValue objects to access the EVP_PKEY pointers + WebCore::AsymmetricKeyValue ourKeyValue(privateKey); + WebCore::AsymmetricKeyValue theirKeyValue(publicKey); + + // Get the EVP_PKEY from both keys + EVP_PKEY* ourKey = ourKeyValue.key; + EVP_PKEY* theirKey = theirKeyValue.key; + + if (!ourKey || !theirKey) { + return Bun::ERR::INVALID_ARG_VALUE(scope, lexicalGlobalObject, "key"_s, jsUndefined(), "is invalid"_s); + } + + // Create EVPKeyPointers to wrap the keys + ncrypto::EVPKeyPointer ourKeyPtr(ourKey); + ncrypto::EVPKeyPointer theirKeyPtr(theirKey); + + // Use DHPointer::stateless to compute the shared secret + auto secret = ncrypto::DHPointer::stateless(ourKeyPtr, theirKeyPtr).release(); + + auto buffer = ArrayBuffer::createFromBytes({ reinterpret_cast(secret.data), secret.len }, createSharedTask([](void* p) { + OPENSSL_free(p); + })); + Zig::GlobalObject* globalObject = reinterpret_cast(lexicalGlobalObject); + auto* result = JSC::JSUint8Array::create(lexicalGlobalObject, globalObject->JSBufferSubclassStructure(), WTFMove(buffer), 0, secret.len); + if (!result) { + return Bun::ERR::INVALID_ARG_VALUE(scope, lexicalGlobalObject, "diffieHellman"_s, jsUndefined(), "failed to allocate result buffer"_s); + } + + return JSC::JSValue::encode(result); +} + +JSC_DEFINE_HOST_FUNCTION(jsECDHConvertKey, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + ncrypto::ClearErrorOnReturn clearErrorOnReturn; + + if (callFrame->argumentCount() < 3) + return throwVMError(lexicalGlobalObject, scope, "ECDH.convertKey requires 3 arguments"_s); + + auto keyBuffer = KeyObject__GetBuffer(callFrame->argument(0)); + if (keyBuffer.hasException()) + return JSValue::encode(jsUndefined()); + + if (keyBuffer.returnValue().isEmpty()) + return JSValue::encode(JSC::jsEmptyString(vm)); + + auto curveName = callFrame->argument(1).toWTFString(lexicalGlobalObject); + if (scope.exception()) + return encodedJSValue(); + + int nid = OBJ_sn2nid(curveName.utf8().data()); + if (nid == NID_undef) + return Bun::ERR::CRYPTO_INVALID_CURVE(scope, lexicalGlobalObject); + + auto group = ncrypto::ECGroupPointer::NewByCurveName(nid); + if (!group) + return throwVMError(lexicalGlobalObject, scope, "Failed to get EC_GROUP"_s); + + auto point = ncrypto::ECPointPointer::New(group); + if (!point) + return throwVMError(lexicalGlobalObject, scope, "Failed to create EC_POINT"_s); + + const unsigned char* key_data = keyBuffer.returnValue().data(); + size_t key_length = keyBuffer.returnValue().size(); + + if (!EC_POINT_oct2point(group, point, key_data, key_length, nullptr)) + return throwVMError(lexicalGlobalObject, scope, "Failed to convert Buffer to EC_POINT"_s); + + uint32_t form = callFrame->argument(2).toUInt32(lexicalGlobalObject); + if (scope.exception()) + return encodedJSValue(); + + size_t size = EC_POINT_point2oct(group, point, static_cast(form), nullptr, 0, nullptr); + if (size == 0) + return throwVMError(lexicalGlobalObject, scope, "Failed to calculate buffer size"_s); + + auto buf = ArrayBuffer::createUninitialized(size, 1); + if (!EC_POINT_point2oct(group, point, static_cast(form), reinterpret_cast(buf->data()), size, nullptr)) + return throwVMError(lexicalGlobalObject, scope, "Failed to convert EC_POINT to Buffer"_s); + + auto* result = JSC::JSUint8Array::create(lexicalGlobalObject, reinterpret_cast(lexicalGlobalObject)->JSBufferSubclassStructure(), WTFMove(buf), 0, size); + + if (!result) + return throwVMError(lexicalGlobalObject, scope, "Failed to allocate result buffer"_s); + + return JSValue::encode(result); +} + +JSC_DEFINE_HOST_FUNCTION(jsGetCurves, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + const size_t numCurves = EC_get_builtin_curves(nullptr, 0); + Vector curves(numCurves); + EC_get_builtin_curves(curves.data(), numCurves); + + JSArray* result = JSC::constructEmptyArray(lexicalGlobalObject, nullptr, numCurves); + RETURN_IF_EXCEPTION(scope, {}); + + for (size_t i = 0; i < numCurves; i++) { + const char* curveName = OBJ_nid2sn(curves[i].nid); + auto curveWTFStr = WTF::String::fromUTF8(curveName); + JSString* curveStr = JSC::jsString(vm, curveWTFStr); + result->putDirectIndex(lexicalGlobalObject, i, curveStr); + RETURN_IF_EXCEPTION(scope, {}); + } + + return JSValue::encode(result); +} + +JSC_DEFINE_HOST_FUNCTION(jsGetCiphers, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + ncrypto::MarkPopErrorOnReturn mark_pop_error_on_return; + + // Create an array to store cipher names + JSC::JSArray* result = JSC::constructEmptyArray(lexicalGlobalObject, nullptr); + RETURN_IF_EXCEPTION(scope, {}); + + struct CipherPushContext { + JSC::JSGlobalObject* globalObject; + JSC::JSArray* array; + int index; + JSC::VM& vm; + bool hasException; + }; + + CipherPushContext ctx = { + lexicalGlobalObject, + result, + 0, + vm, + false + }; + + auto callback = [](const EVP_CIPHER* cipher, const char* name, const char* /*unused*/, void* arg) { + auto* ctx = static_cast(arg); + if (ctx->hasException) + return; + + auto cipherStr = JSC::jsString(ctx->vm, String::fromUTF8(name)); + if (!ctx->array->putDirectIndex(ctx->globalObject, ctx->index++, cipherStr)) + ctx->hasException = true; + }; + + EVP_CIPHER_do_all_sorted(callback, &ctx); + + if (ctx.hasException) + return JSValue::encode({}); + + return JSValue::encode(result); +} + +JSC_DEFINE_HOST_FUNCTION(jsCertVerifySpkac, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto input = KeyObject__GetBuffer(callFrame->argument(0)); + if (input.hasException()) { + return JSValue::encode(jsUndefined()); + } + + auto buffer = input.returnValue(); + if (buffer.size() > std::numeric_limits().max()) { + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "spkac"_s, 0, std::numeric_limits().max(), jsNumber(buffer.size())); + } + + bool result = ncrypto::VerifySpkac(reinterpret_cast(buffer.data()), buffer.size()); + return JSValue::encode(JSC::jsBoolean(result)); +} + +JSC_DEFINE_HOST_FUNCTION(jsCertExportPublicKey, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto input = KeyObject__GetBuffer(callFrame->argument(0)); + if (input.hasException()) { + return JSValue::encode(jsEmptyString(vm)); + } + + auto buffer = input.returnValue(); + if (buffer.size() > std::numeric_limits().max()) { + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "spkac"_s, 0, std::numeric_limits().max(), jsNumber(buffer.size())); + } + + auto bio = ncrypto::ExportPublicKey(reinterpret_cast(buffer.data()), buffer.size()); + if (!bio) { + return JSValue::encode(jsEmptyString(vm)); + } + + char* data = nullptr; + long len = BIO_get_mem_data(bio.get(), &data); + if (len <= 0 || data == nullptr) { + return JSValue::encode(jsEmptyString(vm)); + } + + return JSValue::encode(jsString(vm, String::fromUTF8({ data, static_cast(len) }))); +} + +JSC_DEFINE_HOST_FUNCTION(jsCertExportChallenge, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto input = KeyObject__GetBuffer(callFrame->argument(0)); + if (input.hasException()) { + return JSValue::encode(jsEmptyString(vm)); + } + + auto buffer = input.returnValue(); + if (buffer.size() > std::numeric_limits().max()) { + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "spkac"_s, 0, std::numeric_limits().max(), jsNumber(buffer.size())); + } + + auto cert = ncrypto::ExportChallenge(reinterpret_cast(buffer.data()), buffer.size()); + if (!cert.data || cert.len == 0) { + return JSValue::encode(jsEmptyString(vm)); + } + + auto result = JSC::ArrayBuffer::tryCreate({ reinterpret_cast(cert.data), cert.len }); + if (!result) { + return JSValue::encode(jsEmptyString(vm)); + } + + auto* bufferResult = JSC::JSUint8Array::create(lexicalGlobalObject, reinterpret_cast(lexicalGlobalObject)->JSBufferSubclassStructure(), WTFMove(result), 0, cert.len); + + return JSValue::encode(bufferResult); +} + +JSC_DEFINE_HOST_FUNCTION(jsGetCipherInfo, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + ncrypto::MarkPopErrorOnReturn mark_pop_error_on_return; + + if (callFrame->argumentCount() < 2) { + return JSValue::encode(jsUndefined()); + } + + if (!callFrame->argument(0).isObject()) { + return JSValue::encode(jsUndefined()); + } + + JSObject* info = callFrame->argument(0).getObject(); + + // Get cipher from name or nid + ncrypto::Cipher cipher; + if (callFrame->argument(1).isString()) { + auto cipherName = callFrame->argument(1).toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + cipher = ncrypto::Cipher::FromName(cipherName.utf8().data()); + } else if (callFrame->argument(1).isInt32()) { + int nid = callFrame->argument(1).asInt32(); + cipher = ncrypto::Cipher::FromNid(nid); + } + + if (!cipher) { + return JSValue::encode(jsUndefined()); + } + + int iv_length = cipher.getIvLength(); + int key_length = cipher.getKeyLength(); + int block_length = cipher.getBlockSize(); + + // Test key and IV lengths if provided + if (callFrame->argumentCount() >= 3 && (callFrame->argument(2).isInt32() || callFrame->argument(3).isInt32())) { + auto ctx = ncrypto::CipherCtxPointer::New(); + if (!ctx.init(cipher, true)) { + return JSValue::encode(jsUndefined()); + } + + if (callFrame->argument(2).isInt32()) { + int check_len = callFrame->argument(2).asInt32(); + if (!ctx.setKeyLength(check_len)) { + return JSValue::encode(jsUndefined()); + } + key_length = check_len; + } + + if (callFrame->argument(3).isInt32()) { + int check_len = callFrame->argument(3).asInt32(); + switch (cipher.getMode()) { + case EVP_CIPH_CCM_MODE: + if (check_len < 7 || check_len > 13) + return JSValue::encode(jsUndefined()); + break; + case EVP_CIPH_GCM_MODE: + case EVP_CIPH_OCB_MODE: + if (!ctx.setIvLength(check_len)) { + return JSValue::encode(jsUndefined()); + } + break; + default: + if (check_len != iv_length) + return JSValue::encode(jsUndefined()); + } + iv_length = check_len; + } + } + + // Set mode if available + auto mode_label = cipher.getModeLabel(); + if (!mode_label.empty()) { + info->putDirect(vm, PropertyName(Identifier::fromString(vm, "mode"_s)), + jsString(vm, String::fromUTF8({ mode_label.data(), mode_label.length() }))); + RETURN_IF_EXCEPTION(scope, {}); + } + + // Set name + auto name = cipher.getName(); + info->putDirect(vm, vm.propertyNames->name, + jsString(vm, String::fromUTF8({ name.data(), name.length() }))); + RETURN_IF_EXCEPTION(scope, {}); + + // Set nid + info->putDirect(vm, PropertyName(Identifier::fromString(vm, "nid"_s)), + jsNumber(cipher.getNid())); + RETURN_IF_EXCEPTION(scope, {}); + + // Set blockSize for non-stream ciphers + if (cipher.getMode() != EVP_CIPH_STREAM_CIPHER) { + info->putDirect(vm, PropertyName(Identifier::fromString(vm, "blockSize"_s)), + jsNumber(block_length)); + RETURN_IF_EXCEPTION(scope, {}); + } + + // Set ivLength if cipher uses IV + if (iv_length != 0) { + info->putDirect(vm, PropertyName(Identifier::fromString(vm, "ivLength"_s)), + jsNumber(iv_length)); + RETURN_IF_EXCEPTION(scope, {}); + } + + // Set keyLength + info->putDirect(vm, PropertyName(Identifier::fromString(vm, "keyLength"_s)), + jsNumber(key_length)); + RETURN_IF_EXCEPTION(scope, {}); + + return JSValue::encode(info); +} + +JSValue createNodeCryptoBinding(Zig::GlobalObject* globalObject) +{ + VM& vm = globalObject->vm(); + JSObject* obj = constructEmptyObject(globalObject); + + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "statelessDH"_s)), + JSFunction::create(vm, globalObject, 2, "statelessDH"_s, jsStatelessDH, ImplementationVisibility::Public, NoIntrinsic), 0); + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "ecdhConvertKey"_s)), + JSFunction::create(vm, globalObject, 3, "ecdhConvertKey"_s, jsECDHConvertKey, ImplementationVisibility::Public, NoIntrinsic), 0); + + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "certVerifySpkac"_s)), + JSFunction::create(vm, globalObject, 1, "verifySpkac"_s, jsCertVerifySpkac, ImplementationVisibility::Public, NoIntrinsic), 1); + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "certExportPublicKey"_s)), + JSFunction::create(vm, globalObject, 1, "certExportPublicKey"_s, jsCertExportPublicKey, ImplementationVisibility::Public, NoIntrinsic), 1); + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "certExportChallenge"_s)), + JSFunction::create(vm, globalObject, 1, "certExportChallenge"_s, jsCertExportChallenge, ImplementationVisibility::Public, NoIntrinsic), 1); + + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "getCurves"_s)), + JSFunction::create(vm, globalObject, 0, "getCurves"_s, jsGetCurves, ImplementationVisibility::Public, NoIntrinsic), 0); + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "getCiphers"_s)), + JSFunction::create(vm, globalObject, 0, "getCiphers"_s, jsGetCiphers, ImplementationVisibility::Public, NoIntrinsic), 0); + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "_getCipherInfo"_s)), + JSFunction::create(vm, globalObject, 1, "_getCipherInfo"_s, jsGetCipherInfo, ImplementationVisibility::Public, NoIntrinsic), 4); + + return obj; +} + +} // namespace WebCore diff --git a/src/bun.js/bindings/NodeCrypto.h b/src/bun.js/bindings/NodeCrypto.h new file mode 100644 index 0000000000..924ba9223f --- /dev/null +++ b/src/bun.js/bindings/NodeCrypto.h @@ -0,0 +1,11 @@ + +#pragma once + +#include "root.h" +#include "helpers.h" + +namespace WebCore { + +JSC::JSValue createNodeCryptoBinding(Zig::GlobalObject* globalObject); + +} // namespace WebCore diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 0eb54881dd..016fa82433 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -159,7 +159,7 @@ #include "JSPerformanceServerTiming.h" #include "JSPerformanceResourceTiming.h" #include "JSPerformanceTiming.h" - +#include "JSX509Certificate.h" #include "JSS3File.h" #include "S3Error.h" #if ENABLE(REMOTE_INSPECTOR) @@ -2784,7 +2784,9 @@ void GlobalObject::finishCreation(VM& vm) m_http2_commongStrings.initialize(); Bun::addNodeModuleConstructorProperties(vm, this); - + m_JSX509CertificateClassStructure.initLater([](LazyClassStructure::Initializer& init) { + setupX509CertificateClassStructure(init); + }); m_lazyStackCustomGetterSetter.initLater( [](const Initializer& init) { init.set(CustomGetterSetter::create(init.vm, errorInstanceLazyStackCustomGetter, errorInstanceLazyStackCustomSetter)); @@ -3860,6 +3862,8 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->mockModule.mockWithImplementationCleanupDataStructure.visit(visitor); thisObject->mockModule.withImplementationCleanupFunction.visit(visitor); + thisObject->m_JSX509CertificateClassStructure.visit(visitor); + thisObject->m_nodeErrorCache.visit(visitor); for (auto& barrier : thisObject->m_thenables) { diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 322de6b989..a4796c3a69 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -531,6 +531,7 @@ public: LazyClassStructure m_callSiteStructure; LazyClassStructure m_JSBufferClassStructure; LazyClassStructure m_NodeVMScriptClassStructure; + LazyClassStructure m_JSX509CertificateClassStructure; /** * WARNING: You must update visitChildrenImpl() if you add a new field. diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 7081539580..27ca054ab1 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -2979,8 +2979,20 @@ pub const JSGlobalObject = opaque { argname: []const u8, value: JSValue, ) bun.JSError { - var formatter = JSC.ConsoleObject.Formatter{ .globalThis = this }; - return this.ERR_INVALID_ARG_VALUE("The \"{s}\" argument is invalid. Received {}", .{ argname, value.toFmt(&formatter) }).throw(); + const actual_string_value = try determineSpecificType(this, value); + defer actual_string_value.deref(); + return this.ERR_INVALID_ARG_VALUE("The \"{s}\" argument is invalid. Received {}", .{ argname, actual_string_value }).throw(); + } + + extern "C" fn Bun__ErrorCode__determineSpecificType(*JSGlobalObject, JSValue) String; + + pub fn determineSpecificType(global: *JSGlobalObject, value: JSValue) JSError!String { + const str = Bun__ErrorCode__determineSpecificType(global, value); + errdefer str.deref(); + if (global.hasException()) { + return error.JSError; + } + return str; } /// "The argument must be of type . Received " @@ -2990,8 +3002,9 @@ pub const JSGlobalObject = opaque { typename: []const u8, value: JSValue, ) bun.JSError { - var formatter = JSC.ConsoleObject.Formatter{ .globalThis = this }; - return this.ERR_INVALID_ARG_TYPE("The \"{s}\" argument must be of type {s}. Received {}", .{ argname, typename, value.toFmt(&formatter) }).throw(); + const actual_string_value = try determineSpecificType(this, value); + defer actual_string_value.deref(); + return this.ERR_INVALID_ARG_TYPE("The \"{s}\" argument must be of type {s}. Received {}", .{ argname, typename, actual_string_value }).throw(); } pub fn throwInvalidArgumentRangeValue( diff --git a/src/bun.js/bindings/dh-primes.h b/src/bun.js/bindings/dh-primes.h new file mode 100644 index 0000000000..deb5c80f5c --- /dev/null +++ b/src/bun.js/bindings/dh-primes.h @@ -0,0 +1,76 @@ +/* ==================================================================== + * Copyright (c) 2011 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.OpenSSL.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * licensing@OpenSSL.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.OpenSSL.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). */ + +#ifndef DEPS_NCRYPTO_DH_PRIMES_H_ +#define DEPS_NCRYPTO_DH_PRIMES_H_ + +#include + +#include +#include +#include + +extern "C" int bn_set_words(BIGNUM* bn, const BN_ULONG* words, size_t num); + +// Backporting primes that may not be supported in earlier boringssl versions. +// Intentionally keeping the existing C-style formatting. + +#define OPENSSL_ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) + +#if defined(OPENSSL_64_BIT) +#define TOBN(hi, lo) ((BN_ULONG)(hi) << 32 | (lo)) +#elif defined(OPENSSL_32_BIT) +#define TOBN(hi, lo) (lo), (hi) +#else +#error "Must define either OPENSSL_32_BIT or OPENSSL_64_BIT" +#endif +#endif // DEPS_NCRYPTO_DH_PRIMES_H_ diff --git a/src/bun.js/bindings/ncrpyto_engine.cpp b/src/bun.js/bindings/ncrpyto_engine.cpp new file mode 100644 index 0000000000..52ad42d376 --- /dev/null +++ b/src/bun.js/bindings/ncrpyto_engine.cpp @@ -0,0 +1,106 @@ +#include "ncrypto.h" + +namespace ncrypto { + +// ============================================================================ +// Engine + +#ifndef OPENSSL_NO_ENGINE +EnginePointer::EnginePointer(ENGINE* engine_, bool finish_on_exit_) + : engine(engine_) + , finish_on_exit(finish_on_exit_) +{ +} + +EnginePointer::EnginePointer(EnginePointer&& other) noexcept + : engine(other.engine) + , finish_on_exit(other.finish_on_exit) +{ + other.release(); +} + +EnginePointer::~EnginePointer() +{ + reset(); +} + +EnginePointer& EnginePointer::operator=(EnginePointer&& other) noexcept +{ + if (this == &other) return *this; + this->~EnginePointer(); + return *new (this) EnginePointer(std::move(other)); +} + +void EnginePointer::reset(ENGINE* engine_, bool finish_on_exit_) +{ + if (engine != nullptr) { + if (finish_on_exit) { + // This also does the equivalent of ENGINE_free. + ENGINE_finish(engine); + } else { + ENGINE_free(engine); + } + } + engine = engine_; + finish_on_exit = finish_on_exit_; +} + +ENGINE* EnginePointer::release() +{ + ENGINE* ret = engine; + engine = nullptr; + finish_on_exit = false; + return ret; +} + +EnginePointer EnginePointer::getEngineByName(const std::string_view name, + CryptoErrorList* errors) +{ + MarkPopErrorOnReturn mark_pop_error_on_return(errors); + EnginePointer engine(ENGINE_by_id(name.data())); + if (!engine) { + // Engine not found, try loading dynamically. + engine = EnginePointer(ENGINE_by_id("dynamic")); + if (engine) { + if (!ENGINE_ctrl_cmd_string(engine.get(), "SO_PATH", name.data(), 0) || !ENGINE_ctrl_cmd_string(engine.get(), "LOAD", nullptr, 0)) { + engine.reset(); + } + } + } + return engine; +} + +bool EnginePointer::setAsDefault(uint32_t flags, CryptoErrorList* errors) +{ + if (engine == nullptr) return false; + ClearErrorOnReturn clear_error_on_return(errors); + return ENGINE_set_default(engine, flags) != 0; +} + +bool EnginePointer::init(bool finish_on_exit) +{ + if (engine == nullptr) return false; + if (finish_on_exit) setFinishOnExit(); + return ENGINE_init(engine) == 1; +} + +EVPKeyPointer EnginePointer::loadPrivateKey(const std::string_view key_name) +{ + if (engine == nullptr) return EVPKeyPointer(); + return EVPKeyPointer( + ENGINE_load_private_key(engine, key_name.data(), nullptr, nullptr)); +} + +void EnginePointer::initEnginesOnce() +{ + static bool initialized = false; + if (!initialized) { + ENGINE_load_builtin_engines(); + ENGINE_register_all_complete(); + initialized = true; + } +} + +#endif // OPENSSL_NO_ENGINE + +} // namespace ncrypto diff --git a/src/bun.js/bindings/ncrypto.cpp b/src/bun.js/bindings/ncrypto.cpp new file mode 100644 index 0000000000..0b12ac0ea4 --- /dev/null +++ b/src/bun.js/bindings/ncrypto.cpp @@ -0,0 +1,3196 @@ +#include "root.h" +#include "wtf/text/ASCIILiteral.h" +#include "wtf/text/StringImpl.h" + +#include "ncrypto.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if OPENSSL_VERSION_MAJOR >= 3 +#include +#endif +#ifdef OPENSSL_IS_BORINGSSL +#include "dh-primes.h" +#endif // OPENSSL_IS_BORINGSSL + +namespace ncrypto { + +WTF_MAKE_ISO_ALLOCATED_IMPL(BIOPointer); +WTF_MAKE_ISO_ALLOCATED_IMPL(CipherCtxPointer); +WTF_MAKE_ISO_ALLOCATED_IMPL(EVPKeyPointer); +WTF_MAKE_ISO_ALLOCATED_IMPL(DHPointer); +WTF_MAKE_ISO_ALLOCATED_IMPL(SSLCtxPointer); +WTF_MAKE_ISO_ALLOCATED_IMPL(SSLPointer); +WTF_MAKE_ISO_ALLOCATED_IMPL(X509Pointer); + +namespace { +static constexpr int kX509NameFlagsRFC2253WithinUtf8JSON = XN_FLAG_RFC2253 & ~ASN1_STRFLGS_ESC_MSB & ~ASN1_STRFLGS_ESC_CTRL; + +bool EqualNoCase(WTF::StringView a, ASCIILiteral b) +{ + return WTF::equalIgnoringASCIICase(a, b); +} +} // namespace + +// ============================================================================ + +ClearErrorOnReturn::ClearErrorOnReturn(CryptoErrorList* errors) + : errors_(errors) +{ + ERR_clear_error(); +} + +ClearErrorOnReturn::~ClearErrorOnReturn() +{ + if (errors_ != nullptr) errors_->capture(); + ERR_clear_error(); +} + +int ClearErrorOnReturn::peekError() +{ + return ERR_peek_error(); +} + +MarkPopErrorOnReturn::MarkPopErrorOnReturn(CryptoErrorList* errors) + : errors_(errors) +{ + ERR_set_mark(); +} + +MarkPopErrorOnReturn::~MarkPopErrorOnReturn() +{ + if (errors_ != nullptr) errors_->capture(); + ERR_pop_to_mark(); +} + +int MarkPopErrorOnReturn::peekError() +{ + return ERR_peek_error(); +} + +CryptoErrorList::CryptoErrorList(CryptoErrorList::Option option) +{ + if (option == Option::CAPTURE_ON_CONSTRUCT) capture(); +} + +void CryptoErrorList::capture() +{ + unsigned long err; + while ((err = ERR_get_error()) != 0) { + char buf[256]; + ERR_error_string_n(err, buf, sizeof(buf)); + add(WTF::String::fromUTF8(buf)); + } +} + +void CryptoErrorList::add(WTF::String message) +{ + errors_.push_back(WTFMove(message)); +} + +std::optional CryptoErrorList::pop_back() +{ + if (errors_.empty()) return std::nullopt; + WTF::String message = WTFMove(errors_.back()); + errors_.pop_back(); + return message; +} + +std::optional CryptoErrorList::pop_front() +{ + if (errors_.empty()) return std::nullopt; + WTF::String message = WTFMove(errors_.front()); + errors_.pop_front(); + return message; +} + +// ============================================================================ +DataPointer DataPointer::Alloc(size_t len) +{ + return DataPointer(OPENSSL_zalloc(len), len); +} + +DataPointer::DataPointer(void* data, size_t length) + : data_(data) + , len_(length) +{ +} + +DataPointer::DataPointer(const Buffer& buffer) + : data_(buffer.data) + , len_(buffer.len) +{ +} + +DataPointer::DataPointer(DataPointer&& other) noexcept + : data_(other.data_) + , len_(other.len_) +{ + other.data_ = nullptr; + other.len_ = 0; +} + +DataPointer& DataPointer::operator=(DataPointer&& other) noexcept +{ + if (this == &other) return *this; + this->~DataPointer(); + return *new (this) DataPointer(std::move(other)); +} + +DataPointer::~DataPointer() +{ + reset(); +} + +void DataPointer::reset(void* data, size_t length) +{ + if (data_ != nullptr) { + OPENSSL_clear_free(data_, len_); + } + data_ = data; + len_ = length; +} + +void DataPointer::reset(const Buffer& buffer) +{ + reset(buffer.data, buffer.len); +} + +Buffer DataPointer::release() +{ + Buffer buf { + .data = data_, + .len = len_, + }; + data_ = nullptr; + len_ = 0; + return buf; +} + +// ============================================================================ +bool isFipsEnabled() +{ +#ifdef OPENSSL_FIPS + return FIPS_mode() == 1; +#else + return false; +#endif +} + +bool setFipsEnabled(bool enable, CryptoErrorList* errors) +{ + if (isFipsEnabled() == enable) return true; + ClearErrorOnReturn clearErrorOnReturn(errors); +#ifdef OPENSSL_FIPS + return FIPS_mode_set(enable ? 1 : 0) == 1; +#else + return false; +#endif +} + +bool testFipsEnabled() +{ +#ifdef OPENSSL_FIPS + return FIPS_selftest() == 1; +#else + return false; +#endif +} + +// ============================================================================ +// Bignum +BignumPointer::BignumPointer(BIGNUM* bignum) + : bn_(bignum) +{ +} + +BignumPointer::BignumPointer(const unsigned char* data, size_t len) + : BignumPointer(BN_bin2bn(data, len, nullptr)) +{ +} + +BignumPointer::BignumPointer(BignumPointer&& other) noexcept + : bn_(other.release()) +{ +} + +BignumPointer BignumPointer::New() +{ + return BignumPointer(BN_new()); +} + +BignumPointer BignumPointer::NewSecure() +{ + return BignumPointer(BN_secure_new()); +} + +BignumPointer& BignumPointer::operator=(BignumPointer&& other) noexcept +{ + if (this == &other) return *this; + this->~BignumPointer(); + return *new (this) BignumPointer(std::move(other)); +} + +BignumPointer::~BignumPointer() +{ + reset(); +} + +void BignumPointer::reset(BIGNUM* bn) +{ + bn_.reset(bn); +} + +void BignumPointer::reset(const unsigned char* data, size_t len) +{ + reset(BN_bin2bn(data, len, nullptr)); +} + +BIGNUM* BignumPointer::release() +{ + return bn_.release(); +} + +size_t BignumPointer::byteLength() const +{ + if (bn_ == nullptr) return 0; + return BN_num_bytes(bn_.get()); +} + +DataPointer BignumPointer::encode() const +{ + return EncodePadded(bn_.get(), byteLength()); +} + +DataPointer BignumPointer::encodePadded(size_t size) const +{ + return EncodePadded(bn_.get(), size); +} + +size_t BignumPointer::encodeInto(unsigned char* out) const +{ + if (!bn_) return 0; + return BN_bn2bin(bn_.get(), out); +} + +size_t BignumPointer::encodePaddedInto(unsigned char* out, size_t size) const +{ + if (!bn_) return 0; + return BN_bn2binpad(bn_.get(), out, size); +} + +DataPointer BignumPointer::Encode(const BIGNUM* bn) +{ + return EncodePadded(bn, bn != nullptr ? BN_num_bytes(bn) : 0); +} + +bool BignumPointer::setWord(unsigned long w) +{ // NOLINT(runtime/int) + if (!bn_) return false; + return BN_set_word(bn_.get(), w) == 1; +} + +unsigned long BignumPointer::GetWord(const BIGNUM* bn) +{ // NOLINT(runtime/int) + return BN_get_word(bn); +} + +unsigned long BignumPointer::getWord() const +{ // NOLINT(runtime/int) + if (!bn_) return 0; + return GetWord(bn_.get()); +} + +DataPointer BignumPointer::EncodePadded(const BIGNUM* bn, size_t s) +{ + if (bn == nullptr) return DataPointer(); + size_t size = std::max(s, static_cast(GetByteCount(bn))); + auto buf = DataPointer::Alloc(size); + BN_bn2binpad(bn, reinterpret_cast(buf.get()), size); + return buf; +} +size_t BignumPointer::EncodePaddedInto(const BIGNUM* bn, + unsigned char* out, + size_t size) +{ + if (bn == nullptr) return 0; + return BN_bn2binpad(bn, out, size); +} + +int BignumPointer::operator<=>(const BignumPointer& other) const noexcept +{ + if (bn_ == nullptr && other.bn_ != nullptr) return -1; + if (bn_ != nullptr && other.bn_ == nullptr) return 1; + if (bn_ == nullptr && other.bn_ == nullptr) return 0; + return BN_cmp(bn_.get(), other.bn_.get()); +} + +int BignumPointer::operator<=>(const BIGNUM* other) const noexcept +{ + if (bn_ == nullptr && other != nullptr) return -1; + if (bn_ != nullptr && other == nullptr) return 1; + if (bn_ == nullptr && other == nullptr) return 0; + return BN_cmp(bn_.get(), other); +} + +DataPointer BignumPointer::toHex() const +{ + if (!bn_) return {}; + char* hex = BN_bn2hex(bn_.get()); + if (!hex) return {}; + return DataPointer(hex, strlen(hex)); +} + +int BignumPointer::GetBitCount(const BIGNUM* bn) +{ + return BN_num_bits(bn); +} + +int BignumPointer::GetByteCount(const BIGNUM* bn) +{ + return BN_num_bytes(bn); +} + +bool BignumPointer::isZero() const +{ + return bn_ && BN_is_zero(bn_.get()); +} + +bool BignumPointer::isOne() const +{ + return bn_ && BN_is_one(bn_.get()); +} + +const BIGNUM* BignumPointer::One() +{ + return BN_value_one(); +} + +BignumPointer BignumPointer::clone() +{ + if (!bn_) return {}; + return BignumPointer(BN_dup(bn_.get())); +} + +int BignumPointer::isPrime(int nchecks, + BignumPointer::PrimeCheckCallback innerCb) const +{ + BignumCtxPointer ctx(BN_CTX_new()); + BignumGenCallbackPointer cb(nullptr); + if (innerCb != nullptr) { + cb = BignumGenCallbackPointer(BN_GENCB_new()); + if (!cb) [[unlikely]] + return -1; + BN_GENCB_set( + cb.get(), + // TODO(@jasnell): This could be refactored to allow inlining. + // Not too important right now tho. + [](int a, int b, BN_GENCB* ctx) mutable -> int { + PrimeCheckCallback& ptr = *static_cast(BN_GENCB_get_arg(ctx)); + return ptr(a, b) ? 1 : 0; + }, + &innerCb); + } + return BN_is_prime_ex(get(), nchecks, ctx.get(), cb.get()); +} + +BignumPointer BignumPointer::NewPrime(const PrimeConfig& params, + PrimeCheckCallback cb) +{ + BignumPointer prime(BN_new()); + if (!prime || !prime.generate(params, std::move(cb))) { + return {}; + } + return prime; +} + +bool BignumPointer::generate(const PrimeConfig& params, + PrimeCheckCallback innerCb) const +{ + // BN_generate_prime_ex() calls RAND_bytes_ex() internally. + // Make sure the CSPRNG is properly seeded. + (void)CSPRNG(nullptr, 0); + BignumGenCallbackPointer cb(nullptr); + if (innerCb != nullptr) { + cb = BignumGenCallbackPointer(BN_GENCB_new()); + if (!cb) [[unlikely]] + return -1; + BN_GENCB_set( + cb.get(), + [](int a, int b, BN_GENCB* ctx) mutable -> int { + PrimeCheckCallback& ptr = *static_cast(BN_GENCB_get_arg(ctx)); + return ptr(a, b) ? 1 : 0; + }, + &innerCb); + } + if (BN_generate_prime_ex(get(), + params.bits, + params.safe ? 1 : 0, + params.add.get(), + params.rem.get(), + cb.get()) + == 0) { + return false; + } + + return true; +} + +BignumPointer BignumPointer::NewSub(const BignumPointer& a, + const BignumPointer& b) +{ + BignumPointer res = New(); + if (!res) return {}; + if (!BN_sub(res.get(), a.get(), b.get())) { + return {}; + } + return res; +} + +BignumPointer BignumPointer::NewLShift(size_t length) +{ + BignumPointer res = New(); + if (!res) return {}; + if (!BN_lshift(res.get(), One(), length)) { + return {}; + } + return res; +} + +// ============================================================================ +// Utility methods + +bool CSPRNG(void* buffer, size_t length) +{ + auto buf = reinterpret_cast(buffer); + do { + if (1 == RAND_status()) { +#if OPENSSL_VERSION_MAJOR >= 3 + if (1 == RAND_bytes_ex(nullptr, buf, length, 0)) { + return true; + } +#else + while (length > INT_MAX && 1 == RAND_bytes(buf, INT_MAX)) { + buf += INT_MAX; + length -= INT_MAX; + } + if (length <= INT_MAX && 1 == RAND_bytes(buf, static_cast(length))) + return true; +#endif + } +#if OPENSSL_VERSION_MAJOR >= 3 + const auto code = ERR_peek_last_error(); + // A misconfigured OpenSSL 3 installation may report 1 from RAND_poll() + // and RAND_status() but fail in RAND_bytes() if it cannot look up + // a matching algorithm for the CSPRNG. + if (ERR_GET_LIB(code) == ERR_LIB_RAND) { + const auto reason = ERR_GET_REASON(code); + if (reason == RAND_R_ERROR_INSTANTIATING_DRBG || reason == RAND_R_UNABLE_TO_FETCH_DRBG || reason == RAND_R_UNABLE_TO_CREATE_DRBG) { + return false; + } + } +#endif + } while (1 == RAND_poll()); + + return false; +} + +int NoPasswordCallback(char* buf, int size, int rwflag, void* u) +{ + return 0; +} + +int PasswordCallback(char* buf, int size, int rwflag, void* u) +{ + auto passphrase = static_cast*>(u); + if (passphrase != nullptr) { + size_t buflen = static_cast(size); + size_t len = passphrase->len; + if (buflen < len) return -1; + memcpy(buf, reinterpret_cast(passphrase->data), len); + return len; + } + + return -1; +} + +// Algorithm: http://howardhinnant.github.io/date_algorithms.html +constexpr int days_from_epoch(int y, unsigned m, unsigned d) +{ + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); // [0, 399] + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; // [0, 365] + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096] + return era * 146097 + static_cast(doe) - 719468; +} + +// tm must be in UTC +// using time_t causes problems on 32-bit systems and windows x64. +int64_t PortableTimeGM(struct tm* t) +{ + int year = t->tm_year + 1900; + int month = t->tm_mon; + if (month > 11) { + year += month / 12; + month %= 12; + } else if (month < 0) { + int years_diff = (11 - month) / 12; + year -= years_diff; + month += 12 * years_diff; + } + int days_since_epoch = days_from_epoch(year, month + 1, t->tm_mday); + + return 60 * (60 * (24LL * static_cast(days_since_epoch) + t->tm_hour) + t->tm_min) + t->tm_sec; +} + +// ============================================================================ +// SPKAC + +bool VerifySpkac(const char* input, size_t length) +{ +#ifdef OPENSSL_IS_BORINGSSL + // OpenSSL uses EVP_DecodeBlock, which explicitly removes trailing characters, + // while BoringSSL uses EVP_DecodedLength and EVP_DecodeBase64, which do not. + // As such, we trim those characters here for compatibility. + // + // find_last_not_of can return npos, which is the maximum value of size_t. + // The + 1 will force a roll-ver to 0, which is the correct value. in that + // case. + length = std::string_view(input, length).find_last_not_of(" \n\r\t") + 1; +#endif + NetscapeSPKIPointer spki(NETSCAPE_SPKI_b64_decode(input, length)); + if (!spki) return false; + + EVPKeyPointer pkey(X509_PUBKEY_get(spki->spkac->pubkey)); + return pkey ? NETSCAPE_SPKI_verify(spki.get(), pkey.get()) > 0 : false; +} + +BIOPointer ExportPublicKey(const char* input, size_t length) +{ + BIOPointer bio(BIO_new(BIO_s_mem())); + if (!bio) return {}; + +#ifdef OPENSSL_IS_BORINGSSL + // OpenSSL uses EVP_DecodeBlock, which explicitly removes trailing characters, + // while BoringSSL uses EVP_DecodedLength and EVP_DecodeBase64, which do not. + // As such, we trim those characters here for compatibility. + length = std::string_view(input, length).find_last_not_of(" \n\r\t") + 1; +#endif + NetscapeSPKIPointer spki(NETSCAPE_SPKI_b64_decode(input, length)); + if (!spki) return {}; + + EVPKeyPointer pkey(NETSCAPE_SPKI_get_pubkey(spki.get())); + if (!pkey) return {}; + + if (PEM_write_bio_PUBKEY(bio.get(), pkey.get()) <= 0) return {}; + + return bio; +} + +Buffer ExportChallenge(const char* input, size_t length) +{ +#ifdef OPENSSL_IS_BORINGSSL + // OpenSSL uses EVP_DecodeBlock, which explicitly removes trailing characters, + // while BoringSSL uses EVP_DecodedLength and EVP_DecodeBase64, which do not. + // As such, we trim those characters here for compatibility. + length = std::string_view(input, length).find_last_not_of(" \n\r\t") + 1; +#endif + NetscapeSPKIPointer sp(NETSCAPE_SPKI_b64_decode(input, length)); + if (!sp) return {}; + + unsigned char* buf = nullptr; + int buf_size = ASN1_STRING_to_UTF8(&buf, sp->spkac->challenge); + if (buf_size >= 0) { + return { + .data = reinterpret_cast(buf), + .len = static_cast(buf_size), + }; + } + + return {}; +} + +// ============================================================================ +namespace { +enum class AltNameOption { + NONE, + UTF8, +}; + +bool IsSafeAltName(const char* name, size_t length, AltNameOption option) +{ + for (size_t i = 0; i < length; i++) { + char c = name[i]; + switch (c) { + case '"': + case '\\': + // These mess with encoding rules. + // Fall through. + case ',': + // Commas make it impossible to split the list of subject alternative + // names unambiguously, which is why we have to escape. + // Fall through. + case '\'': + // Single quotes are unlikely to appear in any legitimate values, but + // they could be used to make a value look like it was escaped (i.e., + // enclosed in single/double quotes). + return false; + default: + if (option == AltNameOption::UTF8) { + // In UTF8 strings, we require escaping for any ASCII control + // character, but NOT for non-ASCII characters. Note that all bytes of + // any code point that consists of more than a single byte have their + // MSB set. + if (static_cast(c) < ' ' || c == '\x7f') { + return false; + } + } else { + // Check if the char is a control character or non-ASCII character. + // Note that char may or may not be a signed type. Regardless, + // non-ASCII values will always be outside of this range. + if (c < ' ' || c > '~') { + return false; + } + } + } + } + return true; +} + +void PrintAltName(const BIOPointer& out, + const char* name, + size_t length, + AltNameOption option = AltNameOption::NONE, + const char* safe_prefix = nullptr) +{ + if (IsSafeAltName(name, length, option)) { + // For backward-compatibility, append "safe" names without any + // modifications. + if (safe_prefix != nullptr) { + BIO_printf(out.get(), "%s:", safe_prefix); + } + BIO_write(out.get(), name, length); + } else { + // If a name is not "safe", we cannot embed it without special + // encoding. This does not usually happen, but we don't want to hide + // it from the user either. We use JSON compatible escaping here. + BIO_write(out.get(), "\"", 1); + if (safe_prefix != nullptr) { + BIO_printf(out.get(), "%s:", safe_prefix); + } + for (size_t j = 0; j < length; j++) { + char c = static_cast(name[j]); + if (c == '\\') { + BIO_write(out.get(), "\\\\", 2); + } else if (c == '"') { + BIO_write(out.get(), "\\\"", 2); + } else if ((c >= ' ' && c != ',' && c <= '~') || (option == AltNameOption::UTF8 && (c & 0x80))) { + // Note that the above condition explicitly excludes commas, which means + // that those are encoded as Unicode escape sequences in the "else" + // block. That is not strictly necessary, and Node.js itself would parse + // it correctly either way. We only do this to account for third-party + // code that might be splitting the string at commas (as Node.js itself + // used to do). + BIO_write(out.get(), &c, 1); + } else { + // Control character or non-ASCII character. We treat everything as + // Latin-1, which corresponds to the first 255 Unicode code points. + const char hex[] = "0123456789abcdef"; + char u[] = { '\\', 'u', '0', '0', hex[(c & 0xf0) >> 4], hex[c & 0x0f] }; + BIO_write(out.get(), u, sizeof(u)); + } + } + BIO_write(out.get(), "\"", 1); + } +} + +// This function emulates the behavior of i2v_GENERAL_NAME in a safer and less +// ambiguous way. "othername:" entries use the GENERAL_NAME_print format. +bool PrintGeneralName(const BIOPointer& out, const GENERAL_NAME* gen) +{ + if (gen->type == GEN_DNS) { + ASN1_IA5STRING* name = gen->d.dNSName; + BIO_write(out.get(), "DNS:", 4); + // Note that the preferred name syntax (see RFCs 5280 and 1034) with + // wildcards is a subset of what we consider "safe", so spec-compliant DNS + // names will never need to be escaped. + PrintAltName(out, reinterpret_cast(name->data), name->length); + } else if (gen->type == GEN_EMAIL) { + ASN1_IA5STRING* name = gen->d.rfc822Name; + BIO_write(out.get(), "email:", 6); + PrintAltName(out, reinterpret_cast(name->data), name->length); + } else if (gen->type == GEN_URI) { + ASN1_IA5STRING* name = gen->d.uniformResourceIdentifier; + BIO_write(out.get(), "URI:", 4); + // The set of "safe" names was designed to include just about any URI, + // with a few exceptions, most notably URIs that contains commas (see + // RFC 2396). In other words, most legitimate URIs will not require + // escaping. + PrintAltName(out, reinterpret_cast(name->data), name->length); + } else if (gen->type == GEN_DIRNAME) { + // Earlier versions of Node.js used X509_NAME_oneline to print the X509_NAME + // object. The format was non standard and should be avoided. The use of + // X509_NAME_oneline is discouraged by OpenSSL but was required for backward + // compatibility. Conveniently, X509_NAME_oneline produced ASCII and the + // output was unlikely to contains commas or other characters that would + // require escaping. However, it SHOULD NOT produce ASCII output since an + // RFC5280 AttributeValue may be a UTF8String. + // Newer versions of Node.js have since switched to X509_NAME_print_ex to + // produce a better format at the cost of backward compatibility. The new + // format may contain Unicode characters and it is likely to contain commas, + // which require escaping. Fortunately, the recently safeguarded function + // PrintAltName handles all of that safely. + BIO_printf(out.get(), "DirName:"); + BIOPointer tmp(BIO_new(BIO_s_mem())); + NCRYPTO_ASSERT_TRUE(tmp); + if (X509_NAME_print_ex( + tmp.get(), gen->d.dirn, 0, kX509NameFlagsRFC2253WithinUtf8JSON) + < 0) { + return false; + } + char* oline = nullptr; + long n_bytes = BIO_get_mem_data(tmp.get(), &oline); // NOLINT(runtime/int) + NCRYPTO_ASSERT_TRUE(n_bytes >= 0); + PrintAltName(out, + oline, + static_cast(n_bytes), + ncrypto::AltNameOption::UTF8, + nullptr); + } else if (gen->type == GEN_IPADD) { + BIO_printf(out.get(), "IP Address:"); + const ASN1_OCTET_STRING* ip = gen->d.ip; + const unsigned char* b = ip->data; + if (ip->length == 4) { + BIO_printf(out.get(), "%d.%d.%d.%d", b[0], b[1], b[2], b[3]); + } else if (ip->length == 16) { + for (unsigned int j = 0; j < 8; j++) { + uint16_t pair = (b[2 * j] << 8) | b[2 * j + 1]; + BIO_printf(out.get(), (j == 0) ? "%X" : ":%X", pair); + } + } else { +#if OPENSSL_VERSION_MAJOR >= 3 + BIO_printf(out.get(), "", ip->length); +#else + BIO_printf(out.get(), ""); +#endif + } + } else if (gen->type == GEN_RID) { + // Unlike OpenSSL's default implementation, never print the OID as text and + // instead always print its numeric representation. + char oline[256]; + OBJ_obj2txt(oline, sizeof(oline), gen->d.rid, true); + BIO_printf(out.get(), "Registered ID:%s", oline); + } else if (gen->type == GEN_OTHERNAME) { + // The format that is used here is based on OpenSSL's implementation of + // GENERAL_NAME_print (as of OpenSSL 3.0.1). Earlier versions of Node.js + // instead produced the same format as i2v_GENERAL_NAME, which was somewhat + // awkward, especially when passed to translatePeerCertificate. + bool unicode = true; + const char* prefix = nullptr; + // OpenSSL 1.1.1 does not support othername in GENERAL_NAME_print and may + // not define these NIDs. +#if OPENSSL_VERSION_MAJOR >= 3 + int nid = OBJ_obj2nid(gen->d.otherName->type_id); + switch (nid) { + case NID_id_on_SmtpUTF8Mailbox: + prefix = "SmtpUTF8Mailbox"; + break; + case NID_XmppAddr: + prefix = "XmppAddr"; + break; + case NID_SRVName: + prefix = "SRVName"; + unicode = false; + break; + case NID_ms_upn: + prefix = "UPN"; + break; + case NID_NAIRealm: + prefix = "NAIRealm"; + break; + } +#endif // OPENSSL_VERSION_MAJOR >= 3 + int val_type = gen->d.otherName->value->type; + if (prefix == nullptr || (unicode && val_type != V_ASN1_UTF8STRING) || (!unicode && val_type != V_ASN1_IA5STRING)) { + BIO_printf(out.get(), "othername:"); + } else { + BIO_printf(out.get(), "othername:"); + if (unicode) { + auto name = gen->d.otherName->value->value.utf8string; + PrintAltName(out, + reinterpret_cast(name->data), + name->length, + AltNameOption::UTF8, + prefix); + } else { + auto name = gen->d.otherName->value->value.ia5string; + PrintAltName(out, + reinterpret_cast(name->data), + name->length, + AltNameOption::NONE, + prefix); + } + } + } else if (gen->type == GEN_X400) { + // TODO(tniessen): this is what OpenSSL does, implement properly instead + BIO_printf(out.get(), "X400Name:"); + } else if (gen->type == GEN_EDIPARTY) { + // TODO(tniessen): this is what OpenSSL does, implement properly instead + BIO_printf(out.get(), "EdiPartyName:"); + } else { + // This is safe because X509V3_EXT_d2i would have returned nullptr in this + // case already. + unreachable(); + } + + return true; +} +} // namespace + +bool SafeX509SubjectAltNamePrint(const BIOPointer& out, X509_EXTENSION* ext) +{ + [[maybe_unused]] auto ret = OBJ_obj2nid(X509_EXTENSION_get_object(ext)); + NCRYPTO_ASSERT_EQUAL(ret, NID_subject_alt_name, "unexpected extension type"); + + GENERAL_NAMES* names = static_cast(X509V3_EXT_d2i(ext)); + if (names == nullptr) return false; + + bool ok = true; + + for (int i = 0; i < sk_GENERAL_NAME_num(names); i++) { + GENERAL_NAME* gen = sk_GENERAL_NAME_value(names, i); + + if (i != 0) BIO_write(out.get(), ", ", 2); + + if (!(ok = ncrypto::PrintGeneralName(out, gen))) { + break; + } + } + sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free); + + return ok; +} + +bool SafeX509InfoAccessPrint(const BIOPointer& out, X509_EXTENSION* ext) +{ + auto ret = OBJ_obj2nid(X509_EXTENSION_get_object(ext)); + NCRYPTO_ASSERT_EQUAL(ret, NID_info_access, "unexpected extension type"); + + AUTHORITY_INFO_ACCESS* descs = static_cast(X509V3_EXT_d2i(ext)); + if (descs == nullptr) return false; + + bool ok = true; + + for (int i = 0; i < sk_ACCESS_DESCRIPTION_num(descs); i++) { + ACCESS_DESCRIPTION* desc = sk_ACCESS_DESCRIPTION_value(descs, i); + + if (i != 0) BIO_write(out.get(), "\n", 1); + + char objtmp[80]; + i2t_ASN1_OBJECT(objtmp, sizeof(objtmp), desc->method); + BIO_printf(out.get(), "%s - ", objtmp); + if (!(ok = ncrypto::PrintGeneralName(out, desc->location))) { + break; + } + } + sk_ACCESS_DESCRIPTION_pop_free(descs, ACCESS_DESCRIPTION_free); + +#if OPENSSL_VERSION_MAJOR < 3 + BIO_write(out.get(), "\n", 1); +#endif + + return ok; +} + +// ============================================================================ +// X509Pointer + +X509Pointer::X509Pointer(X509* x509) + : cert_(x509) +{ +} + +X509Pointer::X509Pointer(X509Pointer&& other) noexcept + : cert_(other.release()) +{ +} + +X509Pointer& X509Pointer::operator=(X509Pointer&& other) noexcept +{ + if (this == &other) return *this; + this->~X509Pointer(); + return *new (this) X509Pointer(std::move(other)); +} + +X509Pointer::~X509Pointer() +{ + reset(); +} + +void X509Pointer::reset(X509* x509) +{ + cert_.reset(x509); +} + +X509* X509Pointer::release() +{ + return cert_.release(); +} + +X509View X509Pointer::view() const +{ + return X509View(cert_.get()); +} + +BIOPointer X509View::toPEM() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return {}; + BIOPointer bio(BIO_new(BIO_s_mem())); + if (!bio) return {}; + if (PEM_write_bio_X509(bio.get(), const_cast(cert_)) <= 0) return {}; + return bio; +} + +BIOPointer X509View::toDER() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return {}; + BIOPointer bio(BIO_new(BIO_s_mem())); + if (!bio) return {}; + if (i2d_X509_bio(bio.get(), const_cast(cert_)) <= 0) return {}; + return bio; +} + +BIOPointer X509View::getSubject() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return {}; + BIOPointer bio(BIO_new(BIO_s_mem())); + if (!bio) return {}; + if (X509_NAME_print_ex(bio.get(), + X509_get_subject_name(cert_), + 0, + kX509NameFlagsMultiline) + <= 0) { + return {}; + } + return bio; +} + +BIOPointer X509View::getSubjectAltName() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return {}; + BIOPointer bio(BIO_new(BIO_s_mem())); + if (!bio) return {}; + int index = X509_get_ext_by_NID(cert_, NID_subject_alt_name, -1); + if (index < 0 || !SafeX509SubjectAltNamePrint(bio, X509_get_ext(cert_, index))) { + return {}; + } + return bio; +} + +BIOPointer X509View::getIssuer() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return {}; + BIOPointer bio(BIO_new(BIO_s_mem())); + if (!bio) return {}; + if (X509_NAME_print_ex( + bio.get(), X509_get_issuer_name(cert_), 0, kX509NameFlagsMultiline) + <= 0) { + return {}; + } + return bio; +} + +BIOPointer X509View::getInfoAccess() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return {}; + BIOPointer bio(BIO_new(BIO_s_mem())); + if (!bio) return {}; + int index = X509_get_ext_by_NID(cert_, NID_info_access, -1); + if (index < 0) return {}; + if (!SafeX509InfoAccessPrint(bio, X509_get_ext(cert_, index))) { + return {}; + } + return bio; +} + +BIOPointer X509View::getValidFrom() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return {}; + BIOPointer bio(BIO_new(BIO_s_mem())); + if (!bio) return {}; + ASN1_TIME_print(bio.get(), X509_get_notBefore(cert_)); + return bio; +} + +BIOPointer X509View::getValidTo() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return {}; + BIOPointer bio(BIO_new(BIO_s_mem())); + if (!bio) return {}; + ASN1_TIME_print(bio.get(), X509_get_notAfter(cert_)); + return bio; +} + +int64_t X509View::getValidToTime() const +{ + const ASN1_TIME* time = X509_get0_notAfter(cert_); + if (!time) return 0; + if (!ASN1_TIME_check(time)) return 0; + int day, sec; + if (!ASN1_TIME_diff(&day, &sec, nullptr, time)) return 0; + return (day * 24 * 60 * 60) + sec; +} + +int64_t X509View::getValidFromTime() const +{ + const ASN1_TIME* time = X509_get0_notBefore(cert_); + if (!time) return 0; + if (!ASN1_TIME_check(time)) return 0; + int day, sec; + if (!ASN1_TIME_diff(&day, &sec, nullptr, time)) return 0; + return (day * 24 * 60 * 60) + sec; +} + +DataPointer X509View::getSerialNumber() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return {}; + if (ASN1_INTEGER* serial_number = X509_get_serialNumber(const_cast(cert_))) { + if (auto bn = BignumPointer(ASN1_INTEGER_to_BN(serial_number, nullptr))) { + return bn.toHex(); + } + } + return {}; +} + +Result X509View::getPublicKey() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return Result(EVPKeyPointer {}); + auto pkey = EVPKeyPointer(X509_get_pubkey(const_cast(cert_))); + if (!pkey) return Result(ERR_get_error()); + return pkey; +} + +StackOfASN1 X509View::getKeyUsage() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return {}; + return StackOfASN1(static_cast( + X509_get_ext_d2i(cert_, NID_ext_key_usage, nullptr, nullptr))); +} + +bool X509View::isCA() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return false; + return X509_check_ca(const_cast(cert_)) == 1; +} + +bool X509View::isIssuedBy(const X509View& issuer) const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr || issuer.cert_ == nullptr) return false; + return X509_check_issued(const_cast(issuer.cert_), + const_cast(cert_)) + == X509_V_OK; +} + +bool X509View::checkPrivateKey(const EVPKeyPointer& pkey) const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr || pkey == nullptr) return false; + return X509_check_private_key(const_cast(cert_), pkey.get()) == 1; +} + +bool X509View::checkPublicKey(const EVPKeyPointer& pkey) const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr || pkey == nullptr) return false; + return X509_verify(const_cast(cert_), pkey.get()) == 1; +} + +X509View::CheckMatch X509View::checkHost(std::span host, + int flags, + DataPointer* peerName) const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return CheckMatch::NO_MATCH; + char* peername; + switch (X509_check_host( + const_cast(cert_), host.data(), host.size(), flags, &peername)) { + case 0: + return CheckMatch::NO_MATCH; + case 1: { + if (peername != nullptr) { + DataPointer name(peername, strlen(peername)); + if (peerName != nullptr) *peerName = std::move(name); + } + return CheckMatch::MATCH; + } + case -2: + return CheckMatch::INVALID_NAME; + default: + return CheckMatch::OPERATION_FAILED; + } +} + +X509View::CheckMatch X509View::checkEmail(std::span email, int flags) const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return CheckMatch::NO_MATCH; + switch (X509_check_email( + const_cast(cert_), email.data(), email.size(), flags)) { + case 0: + return CheckMatch::NO_MATCH; + case 1: + return CheckMatch::MATCH; + case -2: + return CheckMatch::INVALID_NAME; + default: + return CheckMatch::OPERATION_FAILED; + } +} + +X509View::CheckMatch X509View::checkIp(std::span ip, int flags) const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return CheckMatch::NO_MATCH; + switch (X509_check_ip_asc( + const_cast(cert_), ip.data(), flags)) { + case 0: + return CheckMatch::NO_MATCH; + case 1: + return CheckMatch::MATCH; + case -2: + return CheckMatch::INVALID_NAME; + default: + return CheckMatch::OPERATION_FAILED; + } +} + +X509View X509View::From(const SSLPointer& ssl) +{ + ClearErrorOnReturn clear_error_on_return; + if (!ssl) return {}; + return X509View(SSL_get_certificate(ssl.get())); +} + +X509View X509View::From(const SSLCtxPointer& ctx) +{ + ClearErrorOnReturn clear_error_on_return; + if (!ctx) return {}; + return X509View(SSL_CTX_get0_certificate(ctx.get())); +} + +std::optional X509View::getFingerprint(const EVP_MD* method) const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr || method == nullptr) return std::nullopt; + + unsigned int len = 0; + unsigned char fingerprint[EVP_MAX_MD_SIZE]; + + if (X509_digest(cert_, method, fingerprint, &len) != 1) { + return std::nullopt; + } + + WTF::StringBuilder builder; + static constexpr char hex[] = "0123456789ABCDEF"; + for (unsigned int i = 0; i < len; i++) { + if (i > 0) builder.append(':'); + builder.append(hex[(fingerprint[i] & 0xf0) >> 4]); + builder.append(hex[fingerprint[i] & 0x0f]); + } + return builder.toString(); +} + +X509Pointer X509View::clone() const +{ + ClearErrorOnReturn clear_error_on_return; + if (!cert_) return {}; + return X509Pointer(X509_dup(const_cast(cert_))); +} + +Result X509Pointer::Parse( + Buffer buffer) +{ + ClearErrorOnReturn clearErrorOnReturn; + BIOPointer bio(BIO_new_mem_buf(buffer.data, buffer.len)); + if (!bio) return Result(ERR_get_error()); + + X509Pointer pem( + PEM_read_bio_X509_AUX(bio.get(), nullptr, NoPasswordCallback, nullptr)); + if (pem) return Result(std::move(pem)); + BIO_reset(bio.get()); + + X509Pointer der(d2i_X509_bio(bio.get(), nullptr)); + if (der) return Result(std::move(der)); + + return Result(ERR_get_error()); +} + +X509Pointer X509Pointer::IssuerFrom(const SSLPointer& ssl, + const X509View& view) +{ + return IssuerFrom(SSL_get_SSL_CTX(ssl.get()), view); +} + +X509Pointer X509Pointer::IssuerFrom(const SSL_CTX* ctx, const X509View& cert) +{ + X509_STORE* store = SSL_CTX_get_cert_store(ctx); + DeleteFnPtr store_ctx( + X509_STORE_CTX_new()); + X509Pointer result; + X509* issuer; + if (store_ctx.get() != nullptr && X509_STORE_CTX_init(store_ctx.get(), store, nullptr, nullptr) == 1 && X509_STORE_CTX_get1_issuer(&issuer, store_ctx.get(), cert.get()) == 1) { + result.reset(issuer); + } + return result; +} + +X509Pointer X509Pointer::PeerFrom(const SSLPointer& ssl) +{ + return X509Pointer(SSL_get_peer_certificate(ssl.get())); +} + +// When adding or removing errors below, please also update the list in the API +// documentation. See the "OpenSSL Error Codes" section of doc/api/errors.md +// Also *please* update the respective section in doc/api/tls.md as well +ASCIILiteral X509Pointer::ErrorCode(int32_t err) +{ + switch (err) { +#define V(name, msg) \ + case X509_V_ERR_##name: \ + return msg##_s; + V(UNABLE_TO_GET_ISSUER_CERT, "unable to get issuer certificate") + V(UNABLE_TO_GET_CRL, "unable to get certificate CRL") + V(UNABLE_TO_DECRYPT_CERT_SIGNATURE, "unable to decrypt certificate's signature") + V(UNABLE_TO_DECRYPT_CRL_SIGNATURE, "unable to decrypt CRL's signature") + V(UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY, "unable to decode issuer public key") + V(CERT_SIGNATURE_FAILURE, "certificate signature failure") + V(CRL_SIGNATURE_FAILURE, "CRL signature failure") + V(CERT_NOT_YET_VALID, "certificate is not yet valid") + V(CERT_HAS_EXPIRED, "certificate has expired") + V(CRL_NOT_YET_VALID, "CRL is not yet valid") + V(CRL_HAS_EXPIRED, "CRL has expired") + V(ERROR_IN_CERT_NOT_BEFORE_FIELD, "format error in certificate's notBefore field") + V(ERROR_IN_CERT_NOT_AFTER_FIELD, "format error in certificate's notAfter field") + V(ERROR_IN_CRL_LAST_UPDATE_FIELD, "format error in CRL's lastUpdate field") + V(ERROR_IN_CRL_NEXT_UPDATE_FIELD, "format error in CRL's nextUpdate field") + V(OUT_OF_MEM, "out of memory") + V(DEPTH_ZERO_SELF_SIGNED_CERT, "self signed certificate") + V(SELF_SIGNED_CERT_IN_CHAIN, "self signed certificate in certificate chain") + V(UNABLE_TO_GET_ISSUER_CERT_LOCALLY, "unable to get local issuer certificate") + V(UNABLE_TO_VERIFY_LEAF_SIGNATURE, "unable to verify the first certificate") + V(CERT_CHAIN_TOO_LONG, "certificate chain too long") + V(CERT_REVOKED, "certificate revoked") + V(INVALID_CA, "invalid CA certificate") + V(PATH_LENGTH_EXCEEDED, "path length constraint exceeded") + V(INVALID_PURPOSE, "unsupported certificate purpose") + V(CERT_UNTRUSTED, "certificate not trusted") + V(CERT_REJECTED, "certificate rejected") + V(HOSTNAME_MISMATCH, "Hostname mismatch") + V(EMAIL_MISMATCH, "Email address mismatch") + V(IP_ADDRESS_MISMATCH, "IP address mismatch") +#undef V + } + return ""_s; +} + +std::optional X509Pointer::ErrorReason(int32_t err) +{ + switch (err) { +#define V(name, msg) \ + case X509_V_ERR_##name: \ + return msg##_s; + V(HOSTNAME_MISMATCH, "Hostname does not match certificate") + V(EMAIL_MISMATCH, "Email address does not match certificate") + V(IP_ADDRESS_MISMATCH, "IP address does not match certificate") +#undef V + } + return std::nullopt; +} + +// ============================================================================ +// BIOPointer + +BIOPointer::BIOPointer(BIO* bio) + : bio_(bio) +{ +} + +BIOPointer::BIOPointer(BIOPointer&& other) noexcept + : bio_(other.release()) +{ +} + +BIOPointer& BIOPointer::operator=(BIOPointer&& other) noexcept +{ + if (this == &other) return *this; + this->~BIOPointer(); + return *new (this) BIOPointer(std::move(other)); +} + +BIOPointer::~BIOPointer() +{ + reset(); +} + +void BIOPointer::reset(BIO* bio) +{ + bio_.reset(bio); +} + +BIO* BIOPointer::release() +{ + return bio_.release(); +} + +bool BIOPointer::resetBio() const +{ + if (!bio_) return 0; + return BIO_reset(bio_.get()) == 1; +} + +BIOPointer BIOPointer::NewMem() +{ + return BIOPointer(BIO_new(BIO_s_mem())); +} + +BIOPointer BIOPointer::NewSecMem() +{ +#ifndef OPENSSL_IS_BORINGSSL + return BIOPointer(BIO_new(BIO_s_secmem())); +#else + return BIOPointer(BIO_new(BIO_s_mem())); +#endif +} + +BIOPointer BIOPointer::New(const BIO_METHOD* method) +{ + return BIOPointer(BIO_new(method)); +} + +BIOPointer BIOPointer::New(const void* data, size_t len) +{ + return BIOPointer(BIO_new_mem_buf(data, len)); +} + +BIOPointer BIOPointer::NewFile(WTF::StringView filename, + WTF::StringView mode) +{ + auto utf8 = filename.utf8(); + auto modeUtf8 = mode.utf8(); + return BIOPointer(BIO_new_file(utf8.data(), modeUtf8.data())); +} + +BIOPointer BIOPointer::NewFp(FILE* fd, int close_flag) +{ + return BIOPointer(BIO_new_fp(fd, close_flag)); +} + +BIOPointer BIOPointer::New(const BIGNUM* bn) +{ + auto res = NewMem(); + if (!res || !BN_print(res.get(), bn)) return {}; + return res; +} + +int BIOPointer::Write(BIOPointer* bio, std::span message) +{ + if (bio == nullptr || !*bio) return 0; + return BIO_write(bio->get(), message.data(), message.size()); +} + +int BIOPointer::Write(BIOPointer* bio, WTF::StringView message) +{ + auto utf8 = message.utf8(); + return Write(bio, utf8.span()); +} + +// ============================================================================ +// DHPointer + +DHPointer::DHPointer(DH* dh) + : dh_(dh) +{ +} + +DHPointer::DHPointer(DHPointer&& other) noexcept + : dh_(other.release()) +{ +} + +DHPointer& DHPointer::operator=(DHPointer&& other) noexcept +{ + if (this == &other) return *this; + this->~DHPointer(); + return *new (this) DHPointer(std::move(other)); +} + +DHPointer::~DHPointer() +{ + reset(); +} + +void DHPointer::reset(DH* dh) +{ + dh_.reset(dh); +} + +DH* DHPointer::release() +{ + return dh_.release(); +} + +BignumPointer DHPointer::FindGroup(WTF::StringView name, + FindGroupOption option) +{ +#define V(n, p) \ + if (EqualNoCase(name, n)) return BignumPointer(p(nullptr)); + if (option != FindGroupOption::NO_SMALL_PRIMES) { +#ifndef OPENSSL_IS_BORINGSSL + V("modp1"_s, BN_get_rfc2409_prime_768); + V("modp2"_s, BN_get_rfc2409_prime_1024); +#endif + V("modp5"_s, BN_get_rfc3526_prime_1536); + } + V("modp14"_s, BN_get_rfc3526_prime_2048); + V("modp15"_s, BN_get_rfc3526_prime_3072); + V("modp16"_s, BN_get_rfc3526_prime_4096); + V("modp17"_s, BN_get_rfc3526_prime_6144); + V("modp18"_s, BN_get_rfc3526_prime_8192); +#undef V + return {}; +} + +BignumPointer DHPointer::GetStandardGenerator() +{ + auto bn = BignumPointer::New(); + if (!bn) return {}; + if (!bn.setWord(DH_GENERATOR_2)) return {}; + return bn; +} + +DHPointer DHPointer::FromGroup(WTF::StringView name, + FindGroupOption option) +{ + auto group = FindGroup(name, option); + if (!group) return {}; // Unable to find the named group. + + auto generator = GetStandardGenerator(); + if (!generator) return {}; // Unable to create the generator. + + return New(std::move(group), std::move(generator)); +} + +DHPointer DHPointer::New(BignumPointer&& p, BignumPointer&& g) +{ + if (!p || !g) return {}; + + DHPointer dh(DH_new()); + if (!dh) return {}; + + if (DH_set0_pqg(dh.get(), p.get(), nullptr, g.get()) != 1) return {}; + + // If the call above is successful, the DH object takes ownership of the + // BIGNUMs, so we must release them here. + p.release(); + g.release(); + + return dh; +} + +DHPointer DHPointer::New(size_t bits, unsigned int generator) +{ + DHPointer dh(DH_new()); + if (!dh) return {}; + + if (DH_generate_parameters_ex(dh.get(), bits, generator, nullptr) != 1) { + return {}; + } + + return dh; +} + +DHPointer::CheckResult DHPointer::check() +{ + ClearErrorOnReturn clearErrorOnReturn; + if (!dh_) return DHPointer::CheckResult::NONE; + int codes = 0; + if (DH_check(dh_.get(), &codes) != 1) + return DHPointer::CheckResult::CHECK_FAILED; + return static_cast(codes); +} + +DHPointer::CheckPublicKeyResult DHPointer::checkPublicKey( + const BignumPointer& pub_key) +{ + ClearErrorOnReturn clearErrorOnReturn; + if (!pub_key || !dh_) return DHPointer::CheckPublicKeyResult::CHECK_FAILED; + int codes = 0; + if (DH_check_pub_key(dh_.get(), pub_key.get(), &codes) != 1) + return DHPointer::CheckPublicKeyResult::CHECK_FAILED; +#ifndef OPENSSL_IS_BORINGSSL + if (codes & DH_CHECK_PUBKEY_TOO_SMALL) { + return DHPointer::CheckPublicKeyResult::TOO_SMALL; + } else if (codes & DH_CHECK_PUBKEY_TOO_SMALL) { + return DHPointer::CheckPublicKeyResult::TOO_LARGE; + } +#endif + if (codes != 0) { + return DHPointer::CheckPublicKeyResult::INVALID; + } + return CheckPublicKeyResult::NONE; +} + +DataPointer DHPointer::getPrime() const +{ + if (!dh_) return {}; + const BIGNUM* p; + DH_get0_pqg(dh_.get(), &p, nullptr, nullptr); + return BignumPointer::Encode(p); +} + +DataPointer DHPointer::getGenerator() const +{ + if (!dh_) return {}; + const BIGNUM* g; + DH_get0_pqg(dh_.get(), nullptr, nullptr, &g); + return BignumPointer::Encode(g); +} + +DataPointer DHPointer::getPublicKey() const +{ + if (!dh_) return {}; + const BIGNUM* pub_key; + DH_get0_key(dh_.get(), &pub_key, nullptr); + return BignumPointer::Encode(pub_key); +} + +DataPointer DHPointer::getPrivateKey() const +{ + if (!dh_) return {}; + const BIGNUM* pvt_key; + DH_get0_key(dh_.get(), nullptr, &pvt_key); + return BignumPointer::Encode(pvt_key); +} + +DataPointer DHPointer::generateKeys() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (!dh_) return {}; + + // Key generation failed + if (!DH_generate_key(dh_.get())) return {}; + + return getPublicKey(); +} + +size_t DHPointer::size() const +{ + if (!dh_) return 0; + return DH_size(dh_.get()); +} + +DataPointer DHPointer::computeSecret(const BignumPointer& peer) const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (!dh_ || !peer) return {}; + + auto dp = DataPointer::Alloc(size()); + if (!dp) return {}; + + int size = DH_compute_key(static_cast(dp.get()), peer.get(), dh_.get()); + if (size < 0) return {}; + + // The size of the computed key can be smaller than the size of the DH key. + // We want to make sure that the key is correctly padded. + if (static_cast(size) < dp.size()) { + const size_t padding = dp.size() - size; + uint8_t* data = static_cast(dp.get()); + memmove(data + padding, data, size); + memset(data, 0, padding); + } + + return dp; +} + +bool DHPointer::setPublicKey(BignumPointer&& key) +{ + if (!dh_) return false; + if (DH_set0_key(dh_.get(), key.get(), nullptr) == 1) { + key.release(); + return true; + } + return false; +} + +bool DHPointer::setPrivateKey(BignumPointer&& key) +{ + if (!dh_) return false; + if (DH_set0_key(dh_.get(), nullptr, key.get()) == 1) { + key.release(); + return true; + } + return false; +} + +DataPointer DHPointer::stateless(const EVPKeyPointer& ourKey, + const EVPKeyPointer& theirKey) +{ + size_t out_size; + if (!ourKey || !theirKey) return {}; + + EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(ourKey.get(), nullptr)); + if (!ctx || EVP_PKEY_derive_init(ctx.get()) <= 0 || EVP_PKEY_derive_set_peer(ctx.get(), theirKey.get()) <= 0 || EVP_PKEY_derive(ctx.get(), nullptr, &out_size) <= 0) { + return {}; + } + + if (out_size == 0) return {}; + + auto out = DataPointer::Alloc(out_size); + if (EVP_PKEY_derive( + ctx.get(), reinterpret_cast(out.get()), &out_size) + <= 0) { + return {}; + } + + if (out_size < out.size()) { + const size_t padding = out.size() - out_size; + uint8_t* data = static_cast(out.get()); + memmove(data + padding, data, out_size); + memset(data, 0, padding); + } + + return out; +} + +// ============================================================================ +// KDF + +const EVP_MD* getDigestByName(const std::string_view name) +{ + return EVP_get_digestbyname(name.data()); +} + +bool checkHkdfLength(const EVP_MD* md, size_t length) +{ + // HKDF-Expand computes up to 255 HMAC blocks, each having as many bits as + // the output of the hash function. 255 is a hard limit because HKDF appends + // an 8-bit counter to each HMAC'd message, starting at 1. + static constexpr size_t kMaxDigestMultiplier = 255; + size_t max_length = EVP_MD_size(md) * kMaxDigestMultiplier; + if (length > max_length) return false; + return true; +} + +DataPointer hkdf(const EVP_MD* md, + const Buffer& key, + const Buffer& info, + const Buffer& salt, + size_t length) +{ + ClearErrorOnReturn clearErrorOnReturn; + + if (!checkHkdfLength(md, length) || info.len > INT_MAX || salt.len > INT_MAX) { + return {}; + } + + EVPKeyCtxPointer ctx = EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr)); + if (!ctx || !EVP_PKEY_derive_init(ctx.get()) || !EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md) || !EVP_PKEY_CTX_add1_hkdf_info(ctx.get(), info.data, info.len)) { + return {}; + } + + std::string_view actual_salt; + static const char default_salt[EVP_MAX_MD_SIZE] = { 0 }; + if (salt.len > 0) { + actual_salt = { reinterpret_cast(salt.data), salt.len }; + } else { + actual_salt = { default_salt, static_cast(EVP_MD_size(md)) }; + } + + // We do not use EVP_PKEY_HKDF_MODE_EXTRACT_AND_EXPAND because and instead + // implement the extraction step ourselves because EVP_PKEY_derive does not + // handle zero-length keys, which are required for Web Crypto. + // TODO(jasnell): Once OpenSSL 1.1.1 support is dropped completely, and once + // BoringSSL is confirmed to support it, wen can hopefully drop this and use + // EVP_KDF directly which does support zero length keys. + unsigned char pseudorandom_key[EVP_MAX_MD_SIZE]; + unsigned pseudorandom_key_len = sizeof(pseudorandom_key); + + if (HMAC(md, + actual_salt.data(), + actual_salt.size(), + key.data, + key.len, + pseudorandom_key, + &pseudorandom_key_len) + == nullptr) { + return {}; + } + if (!EVP_PKEY_CTX_hkdf_mode(ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) || !EVP_PKEY_CTX_set1_hkdf_key(ctx.get(), pseudorandom_key, pseudorandom_key_len)) { + return {}; + } + + auto buf = DataPointer::Alloc(length); + if (!buf) return {}; + + if (EVP_PKEY_derive( + ctx.get(), static_cast(buf.get()), &length) + <= 0) { + return {}; + } + + return buf; +} + +bool checkScryptParams(uint64_t N, uint64_t r, uint64_t p, uint64_t maxmem) +{ + return EVP_PBE_scrypt(nullptr, 0, nullptr, 0, N, r, p, maxmem, nullptr, 0) == 1; +} + +DataPointer scrypt(const Buffer& pass, + const Buffer& salt, + uint64_t N, + uint64_t r, + uint64_t p, + uint64_t maxmem, + size_t length) +{ + ClearErrorOnReturn clearErrorOnReturn; + + if (pass.len > INT_MAX || salt.len > INT_MAX) { + return {}; + } + + auto dp = DataPointer::Alloc(length); + if (dp && EVP_PBE_scrypt(pass.data, pass.len, salt.data, salt.len, N, r, p, maxmem, reinterpret_cast(dp.get()), length)) { + return dp; + } + + return {}; +} + +DataPointer pbkdf2(const EVP_MD* md, + const Buffer& pass, + const Buffer& salt, + uint32_t iterations, + size_t length) +{ + ClearErrorOnReturn clearErrorOnReturn; + + if (pass.len > INT_MAX || salt.len > INT_MAX || length > INT_MAX) { + return {}; + } + + auto dp = DataPointer::Alloc(length); + if (dp && PKCS5_PBKDF2_HMAC(pass.data, pass.len, salt.data, salt.len, iterations, md, length, reinterpret_cast(dp.get()))) { + return dp; + } + + return {}; +} + +// ============================================================================ + +EVPKeyPointer::PrivateKeyEncodingConfig::PrivateKeyEncodingConfig( + const PrivateKeyEncodingConfig& other) + : PrivateKeyEncodingConfig( + other.output_key_object, other.format, other.type) +{ + cipher = other.cipher; + if (other.passphrase.has_value()) { + auto& otherPassphrase = other.passphrase.value(); + auto newPassphrase = DataPointer::Alloc(otherPassphrase.size()); + memcpy(newPassphrase.get(), otherPassphrase.get(), otherPassphrase.size()); + passphrase = std::move(newPassphrase); + } +} + +EVPKeyPointer::AsymmetricKeyEncodingConfig::AsymmetricKeyEncodingConfig( + bool output_key_object, PKFormatType format, PKEncodingType type) + : output_key_object(output_key_object) + , format(format) + , type(type) +{ +} + +EVPKeyPointer::PrivateKeyEncodingConfig& +EVPKeyPointer::PrivateKeyEncodingConfig::operator=( + const PrivateKeyEncodingConfig& other) +{ + if (this == &other) return *this; + this->~PrivateKeyEncodingConfig(); + return *new (this) PrivateKeyEncodingConfig(other); +} + +EVPKeyPointer EVPKeyPointer::New() +{ + return EVPKeyPointer(EVP_PKEY_new()); +} + +EVPKeyPointer EVPKeyPointer::NewRawPublic( + int id, const Buffer& data) +{ + if (id == 0) return {}; + return EVPKeyPointer( + EVP_PKEY_new_raw_public_key(id, nullptr, data.data, data.len)); +} + +EVPKeyPointer EVPKeyPointer::NewRawPrivate( + int id, const Buffer& data) +{ + if (id == 0) return {}; + return EVPKeyPointer( + EVP_PKEY_new_raw_private_key(id, nullptr, data.data, data.len)); +} + +EVPKeyPointer::EVPKeyPointer(EVP_PKEY* pkey) + : pkey_(pkey) +{ +} + +EVPKeyPointer::EVPKeyPointer(EVPKeyPointer&& other) noexcept + : pkey_(other.release()) +{ +} + +EVPKeyPointer& EVPKeyPointer::operator=(EVPKeyPointer&& other) noexcept +{ + if (this == &other) return *this; + this->~EVPKeyPointer(); + return *new (this) EVPKeyPointer(std::move(other)); +} + +EVPKeyPointer::~EVPKeyPointer() +{ + reset(); +} + +void EVPKeyPointer::reset(EVP_PKEY* pkey) +{ + pkey_.reset(pkey); +} + +EVP_PKEY* EVPKeyPointer::release() +{ + return pkey_.release(); +} + +int EVPKeyPointer::id(const EVP_PKEY* key) +{ + if (key == nullptr) return 0; + return EVP_PKEY_id(key); +} + +int EVPKeyPointer::base_id(const EVP_PKEY* key) +{ + if (key == nullptr) return 0; + return EVP_PKEY_base_id(key); +} + +int EVPKeyPointer::id() const +{ + return id(get()); +} + +int EVPKeyPointer::base_id() const +{ + return base_id(get()); +} + +int EVPKeyPointer::bits() const +{ + if (get() == nullptr) return 0; + return EVP_PKEY_bits(get()); +} + +size_t EVPKeyPointer::size() const +{ + if (get() == nullptr) return 0; + return EVP_PKEY_size(get()); +} + +EVPKeyCtxPointer EVPKeyPointer::newCtx() const +{ + if (!pkey_) return {}; + return EVPKeyCtxPointer(EVP_PKEY_CTX_new(get(), nullptr)); +} + +size_t EVPKeyPointer::rawPublicKeySize() const +{ + if (!pkey_) return 0; + size_t len = 0; + if (EVP_PKEY_get_raw_public_key(get(), nullptr, &len) == 1) return len; + return 0; +} + +size_t EVPKeyPointer::rawPrivateKeySize() const +{ + if (!pkey_) return 0; + size_t len = 0; + if (EVP_PKEY_get_raw_private_key(get(), nullptr, &len) == 1) return len; + return 0; +} + +DataPointer EVPKeyPointer::rawPublicKey() const +{ + if (!pkey_) return {}; + if (auto data = DataPointer::Alloc(rawPublicKeySize())) { + const Buffer buf = data; + size_t len = data.size(); + if (EVP_PKEY_get_raw_public_key(get(), buf.data, &len) != 1) return {}; + return data; + } + return {}; +} + +DataPointer EVPKeyPointer::rawPrivateKey() const +{ + if (!pkey_) return {}; + if (auto data = DataPointer::Alloc(rawPrivateKeySize())) { + const Buffer buf = data; + size_t len = data.size(); + if (EVP_PKEY_get_raw_private_key(get(), buf.data, &len) != 1) return {}; + return data; + } + return {}; +} + +BIOPointer EVPKeyPointer::derPublicKey() const +{ + if (!pkey_) return {}; + auto bio = BIOPointer::NewMem(); + if (!bio) return {}; + if (!i2d_PUBKEY_bio(bio.get(), get())) return {}; + return bio; +} + +bool EVPKeyPointer::assign(const ECKeyPointer& eckey) +{ + if (!pkey_ || !eckey) return {}; + return EVP_PKEY_assign_EC_KEY(pkey_.get(), eckey.get()); +} + +bool EVPKeyPointer::set(const ECKeyPointer& eckey) +{ + if (!pkey_ || !eckey) return false; + return EVP_PKEY_set1_EC_KEY(pkey_.get(), eckey); +} + +EVPKeyPointer::operator const EC_KEY*() const +{ + if (!pkey_) return nullptr; + return EVP_PKEY_get0_EC_KEY(pkey_.get()); +} + +namespace { +EVPKeyPointer::ParseKeyResult TryParsePublicKeyInner(const BIOPointer& bp, + const char* name, + auto&& parse) +{ + if (!bp.resetBio()) { + return EVPKeyPointer::ParseKeyResult(EVPKeyPointer::PKParseError::FAILED); + } + unsigned char* der_data; + long der_len; // NOLINT(runtime/int) + + // This skips surrounding data and decodes PEM to DER. + { + MarkPopErrorOnReturn mark_pop_error_on_return; + if (PEM_bytes_read_bio( + &der_data, &der_len, nullptr, name, bp.get(), nullptr, nullptr) + != 1) + return EVPKeyPointer::ParseKeyResult( + EVPKeyPointer::PKParseError::NOT_RECOGNIZED); + } + DataPointer data(der_data, der_len); + + // OpenSSL might modify the pointer, so we need to make a copy before parsing. + const unsigned char* p = der_data; + EVPKeyPointer pkey(parse(&p, der_len)); + if (!pkey) + return EVPKeyPointer::ParseKeyResult(EVPKeyPointer::PKParseError::FAILED); + return EVPKeyPointer::ParseKeyResult(std::move(pkey)); +} + +constexpr bool IsASN1Sequence(const unsigned char* data, + size_t size, + size_t* data_offset, + size_t* data_size) +{ + if (size < 2 || data[0] != 0x30) return false; + + if (data[1] & 0x80) { + // Long form. + size_t n_bytes = data[1] & ~0x80; + if (n_bytes + 2 > size || n_bytes > sizeof(size_t)) return false; + size_t length = 0; + for (size_t i = 0; i < n_bytes; i++) + length = (length << 8) | data[i + 2]; + *data_offset = 2 + n_bytes; + *data_size = std::min(size - 2 - n_bytes, length); + } else { + // Short form. + *data_offset = 2; + *data_size = std::min(size - 2, data[1]); + } + + return true; +} + +constexpr bool IsEncryptedPrivateKeyInfo( + const Buffer& buffer) +{ + // Both PrivateKeyInfo and EncryptedPrivateKeyInfo start with a SEQUENCE. + if (buffer.len == 0 || buffer.data == nullptr) return false; + size_t offset, len; + if (!IsASN1Sequence(buffer.data, buffer.len, &offset, &len)) return false; + + // A PrivateKeyInfo sequence always starts with an integer whereas an + // EncryptedPrivateKeyInfo starts with an AlgorithmIdentifier. + return len >= 1 && buffer.data[offset] != 2; +} + +} // namespace + +bool EVPKeyPointer::IsRSAPrivateKey(const Buffer& buffer) +{ + // Both RSAPrivateKey and RSAPublicKey structures start with a SEQUENCE. + size_t offset, len; + if (!IsASN1Sequence(buffer.data, buffer.len, &offset, &len)) return false; + + // An RSAPrivateKey sequence always starts with a single-byte integer whose + // value is either 0 or 1, whereas an RSAPublicKey starts with the modulus + // (which is the product of two primes and therefore at least 4), so we can + // decide the type of the structure based on the first three bytes of the + // sequence. + return len >= 3 && buffer.data[offset] == 2 && buffer.data[offset + 1] == 1 && !(buffer.data[offset + 2] & 0xfe); +} + +EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKeyPEM( + const Buffer& buffer) +{ + auto bp = BIOPointer::New(buffer.data, buffer.len); + if (!bp) return ParseKeyResult(PKParseError::FAILED); + + // Try parsing as SubjectPublicKeyInfo (SPKI) first. + if (auto ret = TryParsePublicKeyInner( + bp, + "PUBLIC KEY", + [](const unsigned char** p, long l) { // NOLINT(runtime/int) + return d2i_PUBKEY(nullptr, p, l); + })) { + return ret; + } + + // Maybe it is PKCS#1. + if (auto ret = TryParsePublicKeyInner( + bp, + "RSA PUBLIC KEY", + [](const unsigned char** p, long l) { // NOLINT(runtime/int) + return d2i_PublicKey(EVP_PKEY_RSA, nullptr, p, l); + })) { + return ret; + } + + // X.509 fallback. + if (auto ret = TryParsePublicKeyInner( + bp, + "CERTIFICATE", + [](const unsigned char** p, long l) { // NOLINT(runtime/int) + X509Pointer x509(d2i_X509(nullptr, p, l)); + return x509 ? X509_get_pubkey(x509.get()) : nullptr; + })) { + return ret; + }; + + return ParseKeyResult(PKParseError::NOT_RECOGNIZED); +} + +EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKey( + const PublicKeyEncodingConfig& config, + const Buffer& buffer) +{ + if (config.format == PKFormatType::PEM) { + return TryParsePublicKeyPEM(buffer); + } + + if (config.format != PKFormatType::DER) { + return ParseKeyResult(PKParseError::FAILED); + } + + const unsigned char* start = buffer.data; + + EVP_PKEY* key = nullptr; + + if (config.type == PKEncodingType::PKCS1 && (key = d2i_PublicKey(EVP_PKEY_RSA, nullptr, &start, buffer.len))) { + return EVPKeyPointer::ParseKeyResult(EVPKeyPointer(key)); + } + + if (config.type == PKEncodingType::SPKI && (key = d2i_PUBKEY(nullptr, &start, buffer.len))) { + return EVPKeyPointer::ParseKeyResult(EVPKeyPointer(key)); + } + + return ParseKeyResult(PKParseError::FAILED); +} + +namespace { +Buffer GetPassphrase( + const EVPKeyPointer::PrivateKeyEncodingConfig& config) +{ + Buffer pass { + // OpenSSL will not actually dereference this pointer, so it can be any + // non-null pointer. We cannot assert that directly, which is why we + // intentionally use a pointer that will likely cause a segmentation fault + // when dereferenced. + .data = reinterpret_cast(-1), + .len = 0, + }; + if (config.passphrase.has_value()) { + auto& passphrase = config.passphrase.value(); + // The pass.data can't be a nullptr, even if the len is zero or else + // openssl will prompt for a password and we really don't want that. + if (passphrase.get() != nullptr) { + pass.data = static_cast(passphrase.get()); + } + pass.len = passphrase.size(); + } + return pass; +} +} // namespace + +EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey( + const PrivateKeyEncodingConfig& config, + const Buffer& buffer) +{ + static constexpr auto keyOrError = [](EVPKeyPointer pkey, + bool had_passphrase = false) { + if (int err = ERR_peek_error()) { + if (ERR_GET_LIB(err) == ERR_LIB_PEM && ERR_GET_REASON(err) == PEM_R_BAD_PASSWORD_READ && !had_passphrase) { + return ParseKeyResult(PKParseError::NEED_PASSPHRASE); + } + return ParseKeyResult(PKParseError::FAILED, err); + } + if (!pkey) return ParseKeyResult(PKParseError::FAILED); + return ParseKeyResult(std::move(pkey)); + }; + + auto bio = BIOPointer::New(buffer); + if (!bio) return ParseKeyResult(PKParseError::FAILED); + + auto passphrase = GetPassphrase(config); + + if (config.format == PKFormatType::PEM) { + auto key = PEM_read_bio_PrivateKey( + bio.get(), + nullptr, + PasswordCallback, + config.passphrase.has_value() ? &passphrase : nullptr); + return keyOrError(EVPKeyPointer(key), config.passphrase.has_value()); + } + + if (config.format != PKFormatType::DER) { + return ParseKeyResult(PKParseError::FAILED); + } + + switch (config.type) { + case PKEncodingType::PKCS1: { + auto key = d2i_PrivateKey_bio(bio.get(), nullptr); + return keyOrError(EVPKeyPointer(key)); + } + case PKEncodingType::PKCS8: { + if (IsEncryptedPrivateKeyInfo(buffer)) { + auto key = d2i_PKCS8PrivateKey_bio( + bio.get(), + nullptr, + PasswordCallback, + config.passphrase.has_value() ? &passphrase : nullptr); + return keyOrError(EVPKeyPointer(key), config.passphrase.has_value()); + } + + PKCS8Pointer p8inf(d2i_PKCS8_PRIV_KEY_INFO_bio(bio.get(), nullptr)); + if (!p8inf) { + return ParseKeyResult(PKParseError::FAILED, ERR_peek_error()); + } + return keyOrError(EVPKeyPointer(EVP_PKCS82PKEY(p8inf.get()))); + } + case PKEncodingType::SEC1: { + auto key = d2i_PrivateKey_bio(bio.get(), nullptr); + return keyOrError(EVPKeyPointer(key)); + } + default: { + return ParseKeyResult(PKParseError::FAILED, ERR_peek_error()); + } + }; +} + +Result EVPKeyPointer::writePrivateKey( + const PrivateKeyEncodingConfig& config) const +{ + if (config.format == PKFormatType::JWK) { + return Result(false); + } + + auto bio = BIOPointer::NewMem(); + if (!bio) { + return Result(false); + } + + auto passphrase = GetPassphrase(config); + MarkPopErrorOnReturn mark_pop_error_on_return; + bool err; + + switch (config.type) { + case PKEncodingType::PKCS1: { + // PKCS1 is only permitted for RSA keys. + if (id() != EVP_PKEY_RSA) return Result(false); + +#if OPENSSL_VERSION_MAJOR >= 3 + const RSA* rsa = EVP_PKEY_get0_RSA(get()); +#else + RSA* rsa = EVP_PKEY_get0_RSA(get()); +#endif + switch (config.format) { + case PKFormatType::PEM: { + err = PEM_write_bio_RSAPrivateKey( + bio.get(), + rsa, + config.cipher, + reinterpret_cast(passphrase.data), + passphrase.len, + nullptr, + nullptr) + != 1; + break; + } + case PKFormatType::DER: { + // Encoding PKCS1 as DER. This variation does not permit encryption. + err = i2d_RSAPrivateKey_bio(bio.get(), rsa) != 1; + break; + } + default: { + // Should never get here. + return Result(false); + } + } + break; + } + case PKEncodingType::PKCS8: { + switch (config.format) { + case PKFormatType::PEM: { + // Encode PKCS#8 as PEM. + err = PEM_write_bio_PKCS8PrivateKey(bio.get(), + get(), + config.cipher, + passphrase.data, + passphrase.len, + nullptr, + nullptr) + != 1; + break; + } + case PKFormatType::DER: { + err = i2d_PKCS8PrivateKey_bio(bio.get(), + get(), + config.cipher, + passphrase.data, + passphrase.len, + nullptr, + nullptr) + != 1; + break; + } + default: { + // Should never get here. + return Result(false); + } + } + break; + } + case PKEncodingType::SEC1: { + // SEC1 is only permitted for EC keys + if (id() != EVP_PKEY_EC) return Result(false); + +#if OPENSSL_VERSION_MAJOR >= 3 + const EC_KEY* ec = EVP_PKEY_get0_EC_KEY(get()); +#else + EC_KEY* ec = EVP_PKEY_get0_EC_KEY(get()); +#endif + switch (config.format) { + case PKFormatType::PEM: { + err = PEM_write_bio_ECPrivateKey( + bio.get(), + ec, + config.cipher, + reinterpret_cast(passphrase.data), + passphrase.len, + nullptr, + nullptr) + != 1; + break; + } + case PKFormatType::DER: { + // Encoding SEC1 as DER. This variation does not permit encryption. + err = i2d_ECPrivateKey_bio(bio.get(), ec) != 1; + break; + } + default: { + // Should never get here. + return Result(false); + } + } + break; + } + default: { + // Not a valid private key encoding + return Result(false); + } + } + + if (err) { + // Failed to encode the private key. + return Result(false, + mark_pop_error_on_return.peekError()); + } + + return bio; +} + +Result EVPKeyPointer::writePublicKey( + const ncrypto::EVPKeyPointer::PublicKeyEncodingConfig& config) const +{ + auto bio = BIOPointer::NewMem(); + if (!bio) return Result(false); + + MarkPopErrorOnReturn mark_pop_error_on_return; + + if (config.type == ncrypto::EVPKeyPointer::PKEncodingType::PKCS1) { + // PKCS#1 is only valid for RSA keys. +#if OPENSSL_VERSION_MAJOR >= 3 + const RSA* rsa = EVP_PKEY_get0_RSA(get()); +#else + RSA* rsa = EVP_PKEY_get0_RSA(get()); +#endif + if (config.format == ncrypto::EVPKeyPointer::PKFormatType::PEM) { + // Encode PKCS#1 as PEM. + if (PEM_write_bio_RSAPublicKey(bio.get(), rsa) != 1) { + return Result(false, + mark_pop_error_on_return.peekError()); + } + return bio; + } + + // Encode PKCS#1 as DER. + if (i2d_RSAPublicKey_bio(bio.get(), rsa) != 1) { + return Result(false, + mark_pop_error_on_return.peekError()); + } + return bio; + } + + if (config.format == ncrypto::EVPKeyPointer::PKFormatType::PEM) { + // Encode SPKI as PEM. + if (PEM_write_bio_PUBKEY(bio.get(), get()) != 1) { + return Result(false, + mark_pop_error_on_return.peekError()); + } + return bio; + } + + // Encode SPKI as DER. + if (i2d_PUBKEY_bio(bio.get(), get()) != 1) { + return Result(false, + mark_pop_error_on_return.peekError()); + } + return bio; +} + +// ============================================================================ + +SSLPointer::SSLPointer(SSL* ssl) + : ssl_(ssl) +{ +} + +SSLPointer::SSLPointer(SSLPointer&& other) noexcept + : ssl_(other.release()) +{ +} + +SSLPointer& SSLPointer::operator=(SSLPointer&& other) noexcept +{ + if (this == &other) return *this; + this->~SSLPointer(); + return *new (this) SSLPointer(std::move(other)); +} + +SSLPointer::~SSLPointer() +{ + reset(); +} + +void SSLPointer::reset(SSL* ssl) +{ + ssl_.reset(ssl); +} + +SSL* SSLPointer::release() +{ + return ssl_.release(); +} + +SSLPointer SSLPointer::New(const SSLCtxPointer& ctx) +{ + if (!ctx) return {}; + return SSLPointer(SSL_new(ctx.get())); +} + +void SSLPointer::getCiphers(WTF::Function&& cb) const +{ + if (!ssl_) return; + STACK_OF(SSL_CIPHER)* ciphers = SSL_get_ciphers(get()); + + // TLSv1.3 ciphers aren't listed by EVP. There are only 5, we could just + // document them, but since there are only 5, easier to just add them manually + // and not have to explain their absence in the API docs. They are lower-cased + // because the docs say they will be. + static constexpr ASCIILiteral TLS13_CIPHERS[] = { + "tls_aes_256_gcm_sha384"_s, + "tls_chacha20_poly1305_sha256"_s, + "tls_aes_128_gcm_sha256"_s, + "tls_aes_128_ccm_8_sha256"_s, + "tls_aes_128_ccm_sha256"_s + }; + + const int n = sk_SSL_CIPHER_num(ciphers); + + for (int i = 0; i < n; ++i) { + const SSL_CIPHER* cipher = sk_SSL_CIPHER_value(ciphers, i); + const char* name = SSL_CIPHER_get_name(cipher); + WTF::String str = WTF::String::fromUTF8(name); + cb(str); + } + + for (unsigned i = 0; i < 5; ++i) { + WTF::String str = WTF::String(TLS13_CIPHERS[i]); + cb(str); + } +} + +bool SSLPointer::setSession(const SSLSessionPointer& session) +{ + if (!session || !ssl_) return false; + return SSL_set_session(get(), session.get()) == 1; +} + +bool SSLPointer::setSniContext(const SSLCtxPointer& ctx) const +{ + if (!ctx) return false; + auto x509 = ncrypto::X509View::From(ctx); + if (!x509) return false; + EVP_PKEY* pkey = SSL_CTX_get0_privatekey(ctx.get()); + STACK_OF(X509) * chain; + int err = SSL_CTX_get0_chain_certs(ctx.get(), &chain); + if (err == 1) err = SSL_use_certificate(get(), x509); + if (err == 1) err = SSL_use_PrivateKey(get(), pkey); + if (err == 1 && chain != nullptr) err = SSL_set1_chain(get(), chain); + return err == 1; +} + +std::optional SSLPointer::verifyPeerCertificate() const +{ + if (!ssl_) return std::nullopt; + if (X509Pointer::PeerFrom(*this)) { + return SSL_get_verify_result(get()); + } + + const SSL_CIPHER* curr_cipher = SSL_get_current_cipher(get()); + const SSL_SESSION* sess = SSL_get_session(get()); + // Allow no-cert for PSK authentication in TLS1.2 and lower. + // In TLS1.3 check that session was reused because TLS1.3 PSK + // looks like session resumption. + if (SSL_CIPHER_get_auth_nid(curr_cipher) == NID_auth_psk || (SSL_SESSION_get_protocol_version(sess) == TLS1_3_VERSION && SSL_session_reused(get()))) { + return X509_V_OK; + } + + return std::nullopt; +} + +WTF::StringView SSLPointer::getClientHelloAlpn() const +{ + if (ssl_ == nullptr) return {}; + // BoringSSL doesn't have SSL_client_hello_get0_ext + // We'll need to use the early callback mechanism instead + return {}; +} + +WTF::StringView SSLPointer::getClientHelloServerName() const +{ + if (ssl_ == nullptr) return {}; + // BoringSSL doesn't have SSL_client_hello_get0_ext + // We'll need to use the early callback mechanism instead + return {}; +} + +std::optional SSLPointer::GetServerName(const SSL* ssl) +{ + if (ssl == nullptr) return std::nullopt; + auto res = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); + if (res == nullptr) return std::nullopt; + return WTF::String::fromUTF8(res); +} + +std::optional SSLPointer::getServerName() const +{ + if (!ssl_) return std::nullopt; + return GetServerName(get()); +} + +X509View SSLPointer::getCertificate() const +{ + if (!ssl_) return {}; + ClearErrorOnReturn clear_error_on_return; + return ncrypto::X509View(SSL_get_certificate(get())); +} + +const SSL_CIPHER* SSLPointer::getCipher() const +{ + if (!ssl_) return nullptr; + return SSL_get_current_cipher(get()); +} + +bool SSLPointer::isServer() const +{ + return SSL_is_server(get()) != 0; +} + +EVPKeyPointer SSLPointer::getPeerTempKey() const +{ + if (!ssl_) return {}; + EVP_PKEY* raw_key = nullptr; +#ifndef OPENSSL_IS_BORINGSSL + if (!SSL_get_peer_tmp_key(get(), &raw_key)) return {}; +#else + if (!SSL_get_server_tmp_key(get(), &raw_key)) return {}; +#endif + return EVPKeyPointer(raw_key); +} + +SSLCtxPointer::SSLCtxPointer(SSL_CTX* ctx) + : ctx_(ctx) +{ +} + +SSLCtxPointer::SSLCtxPointer(SSLCtxPointer&& other) noexcept + : ctx_(other.release()) +{ +} + +SSLCtxPointer& SSLCtxPointer::operator=(SSLCtxPointer&& other) noexcept +{ + if (this == &other) return *this; + this->~SSLCtxPointer(); + return *new (this) SSLCtxPointer(std::move(other)); +} + +SSLCtxPointer::~SSLCtxPointer() +{ + reset(); +} + +void SSLCtxPointer::reset(SSL_CTX* ctx) +{ + ctx_.reset(ctx); +} + +void SSLCtxPointer::reset(const SSL_METHOD* method) +{ + ctx_.reset(SSL_CTX_new(method)); +} + +SSL_CTX* SSLCtxPointer::release() +{ + return ctx_.release(); +} + +SSLCtxPointer SSLCtxPointer::NewServer() +{ + return SSLCtxPointer(SSL_CTX_new(TLS_server_method())); +} + +SSLCtxPointer SSLCtxPointer::NewClient() +{ + return SSLCtxPointer(SSL_CTX_new(TLS_client_method())); +} + +SSLCtxPointer SSLCtxPointer::New(const SSL_METHOD* method) +{ + return SSLCtxPointer(SSL_CTX_new(method)); +} + +bool SSLCtxPointer::setGroups(const char* groups) +{ + return SSL_CTX_set1_groups_list(get(), groups) == 1; +} + +// ============================================================================ + +const Cipher Cipher::FromName(const char* name) +{ + return Cipher(EVP_get_cipherbyname(name)); +} + +const Cipher Cipher::FromNid(int nid) +{ + return Cipher(EVP_get_cipherbynid(nid)); +} + +const Cipher Cipher::FromCtx(const CipherCtxPointer& ctx) +{ + return Cipher(EVP_CIPHER_CTX_cipher(ctx.get())); +} + +int Cipher::getMode() const +{ + if (!cipher_) return 0; + return EVP_CIPHER_mode(cipher_); +} + +int Cipher::getIvLength() const +{ + if (!cipher_) return 0; + return EVP_CIPHER_iv_length(cipher_); +} + +int Cipher::getKeyLength() const +{ + if (!cipher_) return 0; + return EVP_CIPHER_key_length(cipher_); +} + +int Cipher::getBlockSize() const +{ + if (!cipher_) return 0; + return EVP_CIPHER_block_size(cipher_); +} + +int Cipher::getNid() const +{ + if (!cipher_) return 0; + return EVP_CIPHER_nid(cipher_); +} + +std::string_view Cipher::getModeLabel() const +{ + if (!cipher_) return {}; + switch (getMode()) { + case EVP_CIPH_CCM_MODE: + return "ccm"; + case EVP_CIPH_CFB_MODE: + return "cfb"; + case EVP_CIPH_CBC_MODE: + return "cbc"; + case EVP_CIPH_CTR_MODE: + return "ctr"; + case EVP_CIPH_ECB_MODE: + return "ecb"; + case EVP_CIPH_GCM_MODE: + return "gcm"; + case EVP_CIPH_OCB_MODE: + return "ocb"; + case EVP_CIPH_OFB_MODE: + return "ofb"; + case EVP_CIPH_WRAP_MODE: + return "wrap"; + case EVP_CIPH_XTS_MODE: + return "xts"; + case EVP_CIPH_STREAM_CIPHER: + return "stream"; + } + return "{unknown}"; +} + +std::string_view Cipher::getName() const +{ + if (!cipher_) return {}; + // OBJ_nid2sn(EVP_CIPHER_nid(cipher)) is used here instead of + // EVP_CIPHER_name(cipher) for compatibility with BoringSSL. + return OBJ_nid2sn(getNid()); +} + +bool Cipher::isSupportedAuthenticatedMode() const +{ + switch (getMode()) { + case EVP_CIPH_CCM_MODE: + case EVP_CIPH_GCM_MODE: +#ifndef OPENSSL_NO_OCB + case EVP_CIPH_OCB_MODE: +#endif + return true; + case EVP_CIPH_STREAM_CIPHER: + return getNid() == NID_chacha20_poly1305; + default: + return false; + } +} + +// ============================================================================ + +CipherCtxPointer CipherCtxPointer::New() +{ + auto ret = CipherCtxPointer(EVP_CIPHER_CTX_new()); + if (!ret) return {}; + EVP_CIPHER_CTX_init(ret.get()); + return ret; +} + +CipherCtxPointer::CipherCtxPointer(EVP_CIPHER_CTX* ctx) + : ctx_(ctx) +{ +} + +CipherCtxPointer::CipherCtxPointer(CipherCtxPointer&& other) noexcept + : ctx_(other.release()) +{ +} + +CipherCtxPointer& CipherCtxPointer::operator=( + CipherCtxPointer&& other) noexcept +{ + if (this == &other) return *this; + this->~CipherCtxPointer(); + return *new (this) CipherCtxPointer(std::move(other)); +} + +CipherCtxPointer::~CipherCtxPointer() +{ + reset(); +} + +void CipherCtxPointer::reset(EVP_CIPHER_CTX* ctx) +{ + ctx_.reset(ctx); +} + +EVP_CIPHER_CTX* CipherCtxPointer::release() +{ + return ctx_.release(); +} + +void CipherCtxPointer::setFlags(int flags) +{ + if (!ctx_) return; + EVP_CIPHER_CTX_set_flags(ctx_.get(), flags); +} + +bool CipherCtxPointer::setKeyLength(size_t length) +{ + if (!ctx_) return false; + return EVP_CIPHER_CTX_set_key_length(ctx_.get(), length); +} + +bool CipherCtxPointer::setIvLength(size_t length) +{ + if (!ctx_) return false; + return EVP_CIPHER_CTX_ctrl( + ctx_.get(), EVP_CTRL_AEAD_SET_IVLEN, length, nullptr); +} + +bool CipherCtxPointer::setAeadTag(const Buffer& tag) +{ + if (!ctx_) return false; + return EVP_CIPHER_CTX_ctrl( + ctx_.get(), EVP_CTRL_AEAD_SET_TAG, tag.len, const_cast(tag.data)); +} + +bool CipherCtxPointer::setAeadTagLength(size_t length) +{ + if (!ctx_) return false; + return EVP_CIPHER_CTX_ctrl( + ctx_.get(), EVP_CTRL_AEAD_SET_TAG, length, nullptr); +} + +bool CipherCtxPointer::setPadding(bool padding) +{ + if (!ctx_) return false; + return EVP_CIPHER_CTX_set_padding(ctx_.get(), padding); +} + +int CipherCtxPointer::getBlockSize() const +{ + if (!ctx_) return 0; + return EVP_CIPHER_CTX_block_size(ctx_.get()); +} + +int CipherCtxPointer::getMode() const +{ + if (!ctx_) return 0; + return EVP_CIPHER_CTX_mode(ctx_.get()); +} + +int CipherCtxPointer::getNid() const +{ + if (!ctx_) return 0; + return EVP_CIPHER_CTX_nid(ctx_.get()); +} + +bool CipherCtxPointer::init(const Cipher& cipher, + bool encrypt, + const unsigned char* key, + const unsigned char* iv) +{ + if (!ctx_) return false; + return EVP_CipherInit_ex( + ctx_.get(), cipher, nullptr, key, iv, encrypt ? 1 : 0) + == 1; +} + +bool CipherCtxPointer::update(const Buffer& in, + unsigned char* out, + int* out_len, + bool finalize) +{ + if (!ctx_) return false; + if (!finalize) { + return EVP_CipherUpdate(ctx_.get(), out, out_len, in.data, in.len) == 1; + } + return EVP_CipherFinal_ex(ctx_.get(), out, out_len) == 1; +} + +bool CipherCtxPointer::getAeadTag(size_t len, unsigned char* out) +{ + if (!ctx_) return false; + return EVP_CIPHER_CTX_ctrl(ctx_.get(), EVP_CTRL_AEAD_GET_TAG, len, out); +} + +// ============================================================================ + +ECDSASigPointer::ECDSASigPointer() + : sig_(nullptr) +{ +} +ECDSASigPointer::ECDSASigPointer(ECDSA_SIG* sig) + : sig_(sig) +{ + if (sig_) { + ECDSA_SIG_get0(sig_.get(), &pr_, &ps_); + } +} +ECDSASigPointer::ECDSASigPointer(ECDSASigPointer&& other) noexcept + : sig_(other.release()) +{ + if (sig_) { + ECDSA_SIG_get0(sig_.get(), &pr_, &ps_); + } +} + +ECDSASigPointer& ECDSASigPointer::operator=(ECDSASigPointer&& other) noexcept +{ + sig_.reset(other.release()); + if (sig_) { + ECDSA_SIG_get0(sig_.get(), &pr_, &ps_); + } + return *this; +} + +ECDSASigPointer::~ECDSASigPointer() +{ + reset(); +} + +void ECDSASigPointer::reset(ECDSA_SIG* sig) +{ + sig_.reset(); + pr_ = nullptr; + ps_ = nullptr; +} + +ECDSA_SIG* ECDSASigPointer::release() +{ + pr_ = nullptr; + ps_ = nullptr; + return sig_.release(); +} + +ECDSASigPointer ECDSASigPointer::New() +{ + return ECDSASigPointer(ECDSA_SIG_new()); +} + +ECDSASigPointer ECDSASigPointer::Parse(const Buffer& sig) +{ + const unsigned char* ptr = sig.data; + return ECDSASigPointer(d2i_ECDSA_SIG(nullptr, &ptr, sig.len)); +} + +bool ECDSASigPointer::setParams(BignumPointer&& r, BignumPointer&& s) +{ + if (!sig_) return false; + return ECDSA_SIG_set0(sig_.get(), r.release(), s.release()); +} + +Buffer ECDSASigPointer::encode() const +{ + if (!sig_) + return { + .data = nullptr, + .len = 0, + }; + Buffer buf; + buf.len = i2d_ECDSA_SIG(sig_.get(), &buf.data); + return buf; +} + +// ============================================================================ + +ECGroupPointer::ECGroupPointer() + : group_(nullptr) +{ +} + +ECGroupPointer::ECGroupPointer(EC_GROUP* group) + : group_(group) +{ +} + +ECGroupPointer::ECGroupPointer(ECGroupPointer&& other) noexcept + : group_(other.release()) +{ +} + +ECGroupPointer& ECGroupPointer::operator=(ECGroupPointer&& other) noexcept +{ + group_.reset(other.release()); + return *this; +} + +ECGroupPointer::~ECGroupPointer() +{ + reset(); +} + +void ECGroupPointer::reset(EC_GROUP* group) +{ + group_.reset(); +} + +EC_GROUP* ECGroupPointer::release() +{ + return group_.release(); +} + +ECGroupPointer ECGroupPointer::NewByCurveName(int nid) +{ + return ECGroupPointer(EC_GROUP_new_by_curve_name(nid)); +} + +// ============================================================================ + +ECPointPointer::ECPointPointer() + : point_(nullptr) +{ +} + +ECPointPointer::ECPointPointer(EC_POINT* point) + : point_(point) +{ +} + +ECPointPointer::ECPointPointer(ECPointPointer&& other) noexcept + : point_(other.release()) +{ +} + +ECPointPointer& ECPointPointer::operator=(ECPointPointer&& other) noexcept +{ + point_.reset(other.release()); + return *this; +} + +ECPointPointer::~ECPointPointer() +{ + reset(); +} + +void ECPointPointer::reset(EC_POINT* point) +{ + point_.reset(point); +} + +EC_POINT* ECPointPointer::release() +{ + return point_.release(); +} + +ECPointPointer ECPointPointer::New(const EC_GROUP* group) +{ + return ECPointPointer(EC_POINT_new(group)); +} + +bool ECPointPointer::setFromBuffer(const Buffer& buffer, + const EC_GROUP* group) +{ + if (!point_) return false; + return EC_POINT_oct2point( + group, point_.get(), buffer.data, buffer.len, nullptr); +} + +bool ECPointPointer::mul(const EC_GROUP* group, const BIGNUM* priv_key) +{ + if (!point_) return false; + return EC_POINT_mul(group, point_.get(), priv_key, nullptr, nullptr, nullptr); +} + +// ============================================================================ + +ECKeyPointer::ECKeyPointer() + : key_(nullptr) +{ +} + +ECKeyPointer::ECKeyPointer(EC_KEY* key) + : key_(key) +{ +} + +ECKeyPointer::ECKeyPointer(ECKeyPointer&& other) noexcept + : key_(other.release()) +{ +} + +ECKeyPointer& ECKeyPointer::operator=(ECKeyPointer&& other) noexcept +{ + key_.reset(other.release()); + return *this; +} + +ECKeyPointer::~ECKeyPointer() +{ + reset(); +} + +void ECKeyPointer::reset(EC_KEY* key) +{ + key_.reset(key); +} + +EC_KEY* ECKeyPointer::release() +{ + return key_.release(); +} + +ECKeyPointer ECKeyPointer::clone() const +{ + if (!key_) return {}; + return ECKeyPointer(EC_KEY_dup(key_.get())); +} + +bool ECKeyPointer::generate() +{ + if (!key_) return false; + return EC_KEY_generate_key(key_.get()); +} + +bool ECKeyPointer::setPublicKey(const ECPointPointer& pub) +{ + if (!key_) return false; + return EC_KEY_set_public_key(key_.get(), pub.get()) == 1; +} + +bool ECKeyPointer::setPublicKeyRaw(const BignumPointer& x, + const BignumPointer& y) +{ + if (!key_) return false; + return EC_KEY_set_public_key_affine_coordinates( + key_.get(), x.get(), y.get()) + == 1; +} + +bool ECKeyPointer::setPrivateKey(const BignumPointer& priv) +{ + if (!key_) return false; + return EC_KEY_set_private_key(key_.get(), priv.get()) == 1; +} + +const BIGNUM* ECKeyPointer::getPrivateKey() const +{ + if (!key_) return nullptr; + return GetPrivateKey(key_.get()); +} + +const BIGNUM* ECKeyPointer::GetPrivateKey(const EC_KEY* key) +{ + return EC_KEY_get0_private_key(key); +} + +const EC_POINT* ECKeyPointer::getPublicKey() const +{ + if (!key_) return nullptr; + return GetPublicKey(key_.get()); +} + +const EC_POINT* ECKeyPointer::GetPublicKey(const EC_KEY* key) +{ + return EC_KEY_get0_public_key(key); +} + +const EC_GROUP* ECKeyPointer::getGroup() const +{ + if (!key_) return nullptr; + return GetGroup(key_.get()); +} + +const EC_GROUP* ECKeyPointer::GetGroup(const EC_KEY* key) +{ + return EC_KEY_get0_group(key); +} + +int ECKeyPointer::GetGroupName(const EC_KEY* key) +{ + const EC_GROUP* group = GetGroup(key); + return group ? EC_GROUP_get_curve_name(group) : 0; +} + +bool ECKeyPointer::Check(const EC_KEY* key) +{ + return EC_KEY_check_key(key) == 1; +} + +bool ECKeyPointer::checkKey() const +{ + if (!key_) return false; + return Check(key_.get()); +} + +ECKeyPointer ECKeyPointer::NewByCurveName(int nid) +{ + return ECKeyPointer(EC_KEY_new_by_curve_name(nid)); +} + +ECKeyPointer ECKeyPointer::New(const EC_GROUP* group) +{ + auto ptr = ECKeyPointer(EC_KEY_new()); + if (!ptr) return {}; + if (!EC_KEY_set_group(ptr.get(), group)) return {}; + return ptr; +} + +} // namespace ncrypto diff --git a/src/bun.js/bindings/ncrypto.h b/src/bun.js/bindings/ncrypto.h new file mode 100644 index 0000000000..82651af467 --- /dev/null +++ b/src/bun.js/bindings/ncrypto.h @@ -0,0 +1,1119 @@ +#pragma once + +#include "root.h" + +#ifdef ASSERT_ENABLED +#define NCRYPTO_DEVELOPMENT_CHECKS 1 +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef OPENSSL_NO_ENGINE +#include +#endif // !OPENSSL_NO_ENGINE +// The FIPS-related functions are only available +// when the OpenSSL itself was compiled with FIPS support. +#if defined(OPENSSL_FIPS) && OPENSSL_VERSION_MAJOR < 3 +#include +#endif // OPENSSL_FIPS + +#if OPENSSL_VERSION_MAJOR >= 3 +#define OSSL3_CONST const +#else +#define OSSL3_CONST +#endif + +#ifdef __GNUC__ +#define NCRYPTO_MUST_USE_RESULT __attribute__((warn_unused_result)) +#else +#define NCRYPTO_MUST_USE_RESULT +#endif + +namespace ncrypto { + +// ============================================================================ +// Utility macros + +#if NCRYPTO_DEVELOPMENT_CHECKS +#define NCRYPTO_STR(x) #x +#define NCRYPTO_REQUIRE(EXPR) ASSERT_WITH_MESSAGE(EXPR, "Assertion failed") +#define NCRYPTO_FAIL(MESSAGE) ASSERT_WITH_MESSAGE(false, MESSAGE) +#define NCRYPTO_ASSERT_EQUAL(LHS, RHS, MESSAGE) ASSERT_WITH_MESSAGE(LHS == RHS, MESSAGE) +#define NCRYPTO_ASSERT_TRUE(COND) ASSERT_WITH_MESSAGE(COND, NCRYPTO_STR(COND)) +#else +#define NCRYPTO_FAIL(MESSAGE) +#define NCRYPTO_ASSERT_EQUAL(LHS, RHS, MESSAGE) +#define NCRYPTO_ASSERT_TRUE(COND) +#endif + +#define NCRYPTO_DISALLOW_COPY(Name) \ + Name(const Name&) = delete; \ + Name& operator=(const Name&) = delete; +#define NCRYPTO_DISALLOW_MOVE(Name) \ + Name(Name&&) = delete; \ + Name& operator=(Name&&) = delete; +#define NCRYPTO_DISALLOW_COPY_AND_MOVE(Name) \ + NCRYPTO_DISALLOW_COPY(Name) \ + NCRYPTO_DISALLOW_MOVE(Name) +#define NCRYPTO_DISALLOW_NEW_DELETE() \ + void* operator new(size_t) = delete; \ + void operator delete(void*) = delete; + +[[noreturn]] inline void unreachable() +{ +#ifdef __GNUC__ + __builtin_unreachable(); +#elif defined(_MSC_VER) + __assume(false); +#else +#endif +} + +static constexpr int kX509NameFlagsMultiline = ASN1_STRFLGS_ESC_2253 | ASN1_STRFLGS_ESC_CTRL | ASN1_STRFLGS_UTF8_CONVERT | XN_FLAG_SEP_MULTILINE | XN_FLAG_FN_SN; + +// ============================================================================ +// Error handling utilities + +// Capture the current OpenSSL Error Stack. The stack will be ordered such +// that the error currently at the top of the stack is at the end of the +// list and the error at the bottom of the stack is at the beginning. +class CryptoErrorList final { +public: + enum class Option { NONE, + CAPTURE_ON_CONSTRUCT }; + CryptoErrorList(Option option = Option::CAPTURE_ON_CONSTRUCT); + + void capture(); + + // Add an error message to the end of the stack. + void add(WTF::String message); + + inline const WTF::String& peek_back() const { return errors_.back(); } + inline size_t size() const { return errors_.size(); } + inline bool empty() const { return errors_.empty(); } + + inline auto begin() const noexcept { return errors_.begin(); } + inline auto end() const noexcept { return errors_.end(); } + inline auto rbegin() const noexcept { return errors_.rbegin(); } + inline auto rend() const noexcept { return errors_.rend(); } + + std::optional pop_back(); + std::optional pop_front(); + +private: + std::list errors_; +}; + +// Forcibly clears the error stack on destruction. This stops stale errors +// from popping up later in the lifecycle of crypto operations where they +// would cause spurious failures. It is a rather blunt method, though, and +// ERR_clear_error() isn't necessarily cheap. +// +// If created with a pointer to a CryptoErrorList, the current OpenSSL error +// stack will be captured before clearing the error. +class ClearErrorOnReturn final { +public: + ClearErrorOnReturn(CryptoErrorList* errors = nullptr); + ~ClearErrorOnReturn(); + NCRYPTO_DISALLOW_COPY_AND_MOVE(ClearErrorOnReturn) + NCRYPTO_DISALLOW_NEW_DELETE() + + int peekError(); + +private: + CryptoErrorList* errors_; +}; + +// Pop errors from OpenSSL's error stack that were added between when this +// was constructed and destructed. +// +// If created with a pointer to a CryptoErrorList, the current OpenSSL error +// stack will be captured before resetting the error to the mark. +class MarkPopErrorOnReturn final { +public: + MarkPopErrorOnReturn(CryptoErrorList* errors = nullptr); + ~MarkPopErrorOnReturn(); + NCRYPTO_DISALLOW_COPY_AND_MOVE(MarkPopErrorOnReturn) + NCRYPTO_DISALLOW_NEW_DELETE() + + int peekError(); + +private: + CryptoErrorList* errors_; +}; + +// TODO(@jasnell): Eventually replace with std::expected when we are able to +// bump up to c++23. +template +struct Result final { + const bool has_value; + T value; + std::optional error = std::nullopt; + std::optional openssl_error = std::nullopt; + Result(T&& value) + : has_value(true) + , value(std::move(value)) + { + } + Result(E&& error, std::optional openssl_error = std::nullopt) + : has_value(false) + , error(std::move(error)) + , openssl_error(std::move(openssl_error)) + { + } + inline operator bool() const { return has_value; } +}; + +// ============================================================================ +// Various smart pointer aliases for OpenSSL types. + +template +struct FunctionDeleter { + void operator()(T* pointer) const { function(pointer); } + typedef std::unique_ptr Pointer; +}; + +template +using DeleteFnPtr = typename FunctionDeleter::Pointer; + +using BignumCtxPointer = DeleteFnPtr; +using BignumGenCallbackPointer = DeleteFnPtr; +using DSAPointer = DeleteFnPtr; +using DSASigPointer = DeleteFnPtr; +// using ECDSASigPointer = DeleteFnPtr; +// using ECPointer = DeleteFnPtr; +// using ECGroupPointer = DeleteFnPtr; +// using ECKeyPointer = DeleteFnPtr; +// using ECPointPointer = DeleteFnPtr; +using EVPKeyCtxPointer = DeleteFnPtr; +using EVPMDCtxPointer = DeleteFnPtr; +using HMACCtxPointer = DeleteFnPtr; +using NetscapeSPKIPointer = DeleteFnPtr; +using PKCS8Pointer = DeleteFnPtr; +using RSAPointer = DeleteFnPtr; +using SSLSessionPointer = DeleteFnPtr; + +class CipherCtxPointer; +class ECKeyPointer; + +struct StackOfXASN1Deleter { + void operator()(STACK_OF(ASN1_OBJECT) * p) const + { + sk_ASN1_OBJECT_pop_free(p, ASN1_OBJECT_free); + } +}; +using StackOfASN1 = std::unique_ptr; + +// An unowned, unmanaged pointer to a buffer of data. +template +struct Buffer { + T* data = nullptr; + size_t len = 0; +}; + +class Cipher final { +public: + Cipher() = default; + Cipher(const EVP_CIPHER* cipher) + : cipher_(cipher) + { + } + Cipher(const Cipher&) = default; + Cipher& operator=(const Cipher&) = default; + inline Cipher& operator=(const EVP_CIPHER* cipher) + { + cipher_ = cipher; + return *this; + } + NCRYPTO_DISALLOW_MOVE(Cipher) + + inline const EVP_CIPHER* get() const { return cipher_; } + inline operator const EVP_CIPHER*() const { return cipher_; } + inline operator bool() const { return cipher_ != nullptr; } + + int getNid() const; + int getMode() const; + int getIvLength() const; + int getKeyLength() const; + int getBlockSize() const; + std::string_view getModeLabel() const; + std::string_view getName() const; + + bool isSupportedAuthenticatedMode() const; + + static const Cipher FromName(const char* name); + static const Cipher FromNid(int nid); + static const Cipher FromCtx(const CipherCtxPointer& ctx); + +private: + const EVP_CIPHER* cipher_ = nullptr; +}; + +// A managed pointer to a buffer of data. When destroyed the underlying +// buffer will be freed. +class DataPointer final { +public: + static DataPointer Alloc(size_t len); + + DataPointer() = default; + explicit DataPointer(void* data, size_t len); + explicit DataPointer(const Buffer& buffer); + DataPointer(DataPointer&& other) noexcept; + DataPointer& operator=(DataPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(DataPointer) + ~DataPointer(); + + inline bool operator==(std::nullptr_t) noexcept { return data_ == nullptr; } + inline operator bool() const { return data_ != nullptr; } + inline void* get() const noexcept { return data_; } + inline size_t size() const noexcept { return len_; } + void reset(void* data = nullptr, size_t len = 0); + void reset(const Buffer& buffer); + + // Releases ownership of the underlying data buffer. It is the caller's + // responsibility to ensure the buffer is appropriately freed. + Buffer release(); + + // Returns a Buffer struct that is a view of the underlying data. + template + inline operator const Buffer() const + { + return { + .data = static_cast(data_), + .len = len_, + }; + } + +private: + void* data_ = nullptr; + size_t len_ = 0; +}; + +class BIOPointer final { + WTF_MAKE_ISO_ALLOCATED(BIOPointer); + +public: + static BIOPointer NewMem(); + static BIOPointer NewSecMem(); + static BIOPointer New(const BIO_METHOD* method); + static BIOPointer New(const void* data, size_t len); + static BIOPointer New(const BIGNUM* bn); + static BIOPointer NewFile(WTF::StringView filename, WTF::StringView mode); + static BIOPointer NewFp(FILE* fd, int flags); + + template + static BIOPointer New(const Buffer& buf) + { + return New(buf.data, buf.len); + } + + BIOPointer() = default; + BIOPointer(std::nullptr_t) + : bio_(nullptr) + { + } + explicit BIOPointer(BIO* bio); + BIOPointer(BIOPointer&& other) noexcept; + BIOPointer& operator=(BIOPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(BIOPointer) + ~BIOPointer(); + + inline bool operator==(std::nullptr_t) noexcept { return bio_ == nullptr; } + inline operator bool() const { return bio_ != nullptr; } + inline BIO* get() const noexcept { return bio_.get(); } + + inline operator BUF_MEM*() const + { + BUF_MEM* mem = nullptr; + if (!bio_) return mem; + BIO_get_mem_ptr(bio_.get(), &mem); + return mem; + } + + inline operator BIO*() const { return bio_.get(); } + + void reset(BIO* bio = nullptr); + BIO* release(); + + bool resetBio() const; + + static int Write(BIOPointer* bio, WTF::StringView message); + static int Write(BIOPointer* bio, std::span message); + + template + static void Printf(BIOPointer* bio, const char* format, Args... args) + { + if (bio == nullptr || !*bio) return; + BIO_printf(bio->get(), format, std::forward(args...)); + } + +private: + mutable DeleteFnPtr bio_; +}; + +class BignumPointer final { +public: + BignumPointer() = default; + explicit BignumPointer(BIGNUM* bignum); + explicit BignumPointer(const unsigned char* data, size_t len); + BignumPointer(BignumPointer&& other) noexcept; + BignumPointer& operator=(BignumPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(BignumPointer) + ~BignumPointer(); + + int operator<=>(const BignumPointer& other) const noexcept; + int operator<=>(const BIGNUM* other) const noexcept; + inline operator bool() const { return bn_ != nullptr; } + inline BIGNUM* get() const noexcept { return bn_.get(); } + void reset(BIGNUM* bn = nullptr); + void reset(const unsigned char* data, size_t len); + BIGNUM* release(); + + bool isZero() const; + bool isOne() const; + + bool setWord(unsigned long w); // NOLINT(runtime/int) + unsigned long getWord() const; // NOLINT(runtime/int) + + size_t byteLength() const; + + DataPointer toHex() const; + DataPointer encode() const; + DataPointer encodePadded(size_t size) const; + size_t encodeInto(unsigned char* out) const; + size_t encodePaddedInto(unsigned char* out, size_t size) const; + + using PrimeCheckCallback = std::function; + int isPrime(int checks, + PrimeCheckCallback cb = defaultPrimeCheckCallback) const; + struct PrimeConfig { + int bits; + bool safe = false; + const BignumPointer& add; + const BignumPointer& rem; + }; + + static BignumPointer NewPrime( + const PrimeConfig& params, + PrimeCheckCallback cb = defaultPrimeCheckCallback); + + bool generate(const PrimeConfig& params, + PrimeCheckCallback cb = defaultPrimeCheckCallback) const; + + static BignumPointer New(); + static BignumPointer NewSecure(); + static BignumPointer NewSub(const BignumPointer& a, const BignumPointer& b); + static BignumPointer NewLShift(size_t length); + + static DataPointer Encode(const BIGNUM* bn); + static DataPointer EncodePadded(const BIGNUM* bn, size_t size); + static size_t EncodePaddedInto(const BIGNUM* bn, + unsigned char* out, + size_t size); + static int GetBitCount(const BIGNUM* bn); + static int GetByteCount(const BIGNUM* bn); + static unsigned long GetWord(const BIGNUM* bn); // NOLINT(runtime/int) + static const BIGNUM* One(); + + BignumPointer clone(); + +private: + DeleteFnPtr bn_; + + static bool defaultPrimeCheckCallback(int, int) { return 1; } +}; + +class CipherCtxPointer final { + WTF_MAKE_ISO_ALLOCATED(CipherCtxPointer); + +public: + static CipherCtxPointer New(); + + CipherCtxPointer() = default; + explicit CipherCtxPointer(EVP_CIPHER_CTX* ctx); + CipherCtxPointer(CipherCtxPointer&& other) noexcept; + CipherCtxPointer& operator=(CipherCtxPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(CipherCtxPointer) + ~CipherCtxPointer(); + + inline bool operator==(std::nullptr_t) const noexcept + { + return ctx_ == nullptr; + } + inline operator bool() const { return ctx_ != nullptr; } + inline EVP_CIPHER_CTX* get() const { return ctx_.get(); } + inline operator EVP_CIPHER_CTX*() const { return ctx_.get(); } + void reset(EVP_CIPHER_CTX* ctx = nullptr); + EVP_CIPHER_CTX* release(); + + void setFlags(int flags); + bool setKeyLength(size_t length); + bool setIvLength(size_t length); + bool setAeadTag(const Buffer& tag); + bool setAeadTagLength(size_t length); + bool setPadding(bool padding); + bool init(const Cipher& cipher, + bool encrypt, + const unsigned char* key = nullptr, + const unsigned char* iv = nullptr); + + int getBlockSize() const; + int getMode() const; + int getNid() const; + + bool update(const Buffer& in, + unsigned char* out, + int* out_len, + bool finalize = false); + bool getAeadTag(size_t len, unsigned char* out); + +private: + DeleteFnPtr ctx_; +}; + +class EVPKeyPointer final { + WTF_MAKE_ISO_ALLOCATED(EVPKeyPointer); + +public: + static EVPKeyPointer New(); + static EVPKeyPointer NewRawPublic(int id, + const Buffer& data); + static EVPKeyPointer NewRawPrivate(int id, + const Buffer& data); + + enum class PKEncodingType { + // RSAPublicKey / RSAPrivateKey according to PKCS#1. + PKCS1, + // PrivateKeyInfo or EncryptedPrivateKeyInfo according to PKCS#8. + PKCS8, + // SubjectPublicKeyInfo according to X.509. + SPKI, + // ECPrivateKey according to SEC1. + SEC1, + }; + + enum class PKFormatType { + DER, + PEM, + JWK, + }; + + enum class PKParseError { NOT_RECOGNIZED, + NEED_PASSPHRASE, + FAILED }; + using ParseKeyResult = Result; + + struct AsymmetricKeyEncodingConfig { + bool output_key_object = false; + PKFormatType format = PKFormatType::DER; + PKEncodingType type = PKEncodingType::PKCS8; + AsymmetricKeyEncodingConfig() = default; + AsymmetricKeyEncodingConfig(bool output_key_object, + PKFormatType format, + PKEncodingType type); + AsymmetricKeyEncodingConfig(const AsymmetricKeyEncodingConfig&) = default; + AsymmetricKeyEncodingConfig& operator=(const AsymmetricKeyEncodingConfig&) = default; + }; + using PublicKeyEncodingConfig = AsymmetricKeyEncodingConfig; + + struct PrivateKeyEncodingConfig : public AsymmetricKeyEncodingConfig { + const EVP_CIPHER* cipher = nullptr; + std::optional passphrase = std::nullopt; + PrivateKeyEncodingConfig() = default; + PrivateKeyEncodingConfig(bool output_key_object, + PKFormatType format, + PKEncodingType type) + : AsymmetricKeyEncodingConfig(output_key_object, format, type) + { + } + PrivateKeyEncodingConfig(const PrivateKeyEncodingConfig&); + PrivateKeyEncodingConfig& operator=(const PrivateKeyEncodingConfig&); + }; + + static ParseKeyResult TryParsePublicKey( + const PublicKeyEncodingConfig& config, + const Buffer& buffer); + + static ParseKeyResult TryParsePublicKeyPEM( + const Buffer& buffer); + + static ParseKeyResult TryParsePrivateKey( + const PrivateKeyEncodingConfig& config, + const Buffer& buffer); + + EVPKeyPointer() = default; + explicit EVPKeyPointer(EVP_PKEY* pkey); + EVPKeyPointer(EVPKeyPointer&& other) noexcept; + EVPKeyPointer& operator=(EVPKeyPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(EVPKeyPointer) + ~EVPKeyPointer(); + + bool assign(const ECKeyPointer& eckey); + bool set(const ECKeyPointer& eckey); + operator const EC_KEY*() const; + + inline bool operator==(std::nullptr_t) const noexcept + { + return pkey_ == nullptr; + } + inline operator bool() const { return pkey_ != nullptr; } + inline EVP_PKEY* get() const { return pkey_.get(); } + void reset(EVP_PKEY* pkey = nullptr); + EVP_PKEY* release(); + + static int id(const EVP_PKEY* key); + static int base_id(const EVP_PKEY* key); + + int id() const; + int base_id() const; + int bits() const; + size_t size() const; + + size_t rawPublicKeySize() const; + size_t rawPrivateKeySize() const; + DataPointer rawPublicKey() const; + DataPointer rawPrivateKey() const; + BIOPointer derPublicKey() const; + + Result writePrivateKey( + const PrivateKeyEncodingConfig& config) const; + Result writePublicKey( + const PublicKeyEncodingConfig& config) const; + + EVPKeyCtxPointer newCtx() const; + + static bool IsRSAPrivateKey(const Buffer& buffer); + +private: + DeleteFnPtr pkey_; +}; + +class DHPointer final { + WTF_MAKE_ISO_ALLOCATED(DHPointer); + +public: + enum class FindGroupOption { + NONE, + // There are known and documented security issues with prime groups smaller + // than 2048 bits. When the NO_SMALL_PRIMES option is set, these small prime + // groups will not be supported. + NO_SMALL_PRIMES, + }; + + static BignumPointer GetStandardGenerator(); + + static BignumPointer FindGroup( + WTF::StringView name, + FindGroupOption option = FindGroupOption::NONE); + static DHPointer FromGroup(WTF::StringView name, + FindGroupOption option = FindGroupOption::NONE); + + static DHPointer New(BignumPointer&& p, BignumPointer&& g); + static DHPointer New(size_t bits, unsigned int generator); + + DHPointer() = default; + explicit DHPointer(DH* dh); + DHPointer(DHPointer&& other) noexcept; + DHPointer& operator=(DHPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(DHPointer) + ~DHPointer(); + + inline bool operator==(std::nullptr_t) noexcept { return dh_ == nullptr; } + inline operator bool() const { return dh_ != nullptr; } + inline DH* get() const { return dh_.get(); } + void reset(DH* dh = nullptr); + DH* release(); + + enum class CheckResult { + NONE, + P_NOT_PRIME = DH_CHECK_P_NOT_PRIME, + P_NOT_SAFE_PRIME = DH_CHECK_P_NOT_SAFE_PRIME, + UNABLE_TO_CHECK_GENERATOR = DH_UNABLE_TO_CHECK_GENERATOR, + NOT_SUITABLE_GENERATOR = DH_NOT_SUITABLE_GENERATOR, + Q_NOT_PRIME = DH_CHECK_Q_NOT_PRIME, +#ifndef OPENSSL_IS_BORINGSSL + INVALID_Q = DH_CHECK_INVALID_Q_VALUE, + INVALID_J = DH_CHECK_INVALID_J_VALUE, +#endif + CHECK_FAILED = 512, + }; + CheckResult check(); + + enum class CheckPublicKeyResult { + NONE, +#ifndef OPENSSL_IS_BORINGSSL + TOO_SMALL = DH_R_CHECK_PUBKEY_TOO_SMALL, + TOO_LARGE = DH_R_CHECK_PUBKEY_TOO_LARGE, +#endif + INVALID = DH_R_INVALID_PUBKEY, + CHECK_FAILED = 512, + }; + // Check to see if the given public key is suitable for this DH instance. + CheckPublicKeyResult checkPublicKey(const BignumPointer& pub_key); + + DataPointer getPrime() const; + DataPointer getGenerator() const; + DataPointer getPublicKey() const; + DataPointer getPrivateKey() const; + DataPointer generateKeys() const; + DataPointer computeSecret(const BignumPointer& peer) const; + + bool setPublicKey(BignumPointer&& key); + bool setPrivateKey(BignumPointer&& key); + + size_t size() const; + + static DataPointer stateless(const EVPKeyPointer& ourKey, + const EVPKeyPointer& theirKey); + +private: + DeleteFnPtr dh_; +}; + +struct StackOfX509Deleter { + void operator()(STACK_OF(X509) * p) const { sk_X509_pop_free(p, X509_free); } +}; +using StackOfX509 = std::unique_ptr; + +class X509Pointer; +class X509View; + +class SSLCtxPointer final { + WTF_MAKE_ISO_ALLOCATED(SSLCtxPointer); + +public: + SSLCtxPointer() = default; + explicit SSLCtxPointer(SSL_CTX* ctx); + SSLCtxPointer(SSLCtxPointer&& other) noexcept; + SSLCtxPointer& operator=(SSLCtxPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(SSLCtxPointer) + ~SSLCtxPointer(); + + inline bool operator==(std::nullptr_t) const noexcept + { + return ctx_ == nullptr; + } + inline operator bool() const { return ctx_ != nullptr; } + inline SSL_CTX* get() const { return ctx_.get(); } + void reset(SSL_CTX* ctx = nullptr); + void reset(const SSL_METHOD* method); + SSL_CTX* release(); + + bool setGroups(const char* groups); + void setStatusCallback(auto callback) + { + if (!ctx_) return; + SSL_CTX_set_tlsext_status_cb(get(), callback); + SSL_CTX_set_tlsext_status_arg(get(), nullptr); + } + + static SSLCtxPointer NewServer(); + static SSLCtxPointer NewClient(); + static SSLCtxPointer New(const SSL_METHOD* method = TLS_method()); + +private: + DeleteFnPtr ctx_; +}; + +class SSLPointer final { + WTF_MAKE_ISO_ALLOCATED(SSLPointer); + +public: + SSLPointer() = default; + explicit SSLPointer(SSL* ssl); + SSLPointer(SSLPointer&& other) noexcept; + SSLPointer& operator=(SSLPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(SSLPointer) + ~SSLPointer(); + + inline bool operator==(std::nullptr_t) noexcept { return ssl_ == nullptr; } + inline operator bool() const { return ssl_ != nullptr; } + inline SSL* get() const { return ssl_.get(); } + inline operator SSL*() const { return ssl_.get(); } + void reset(SSL* ssl = nullptr); + SSL* release(); + + bool setSession(const SSLSessionPointer& session); + bool setSniContext(const SSLCtxPointer& ctx) const; + + WTF::StringView getClientHelloAlpn() const; + WTF::StringView getClientHelloServerName() const; + + std::optional getServerName() const; + X509View getCertificate() const; + EVPKeyPointer getPeerTempKey() const; + const SSL_CIPHER* getCipher() const; + bool isServer() const; + + std::optional verifyPeerCertificate() const; + + void getCiphers(WTF::Function&& cb) const; + + static SSLPointer New(const SSLCtxPointer& ctx); + static std::optional GetServerName(const SSL* ssl); + +private: + DeleteFnPtr ssl_; +}; + +class X509View final { +public: + static X509View From(const SSLPointer& ssl); + static X509View From(const SSLCtxPointer& ctx); + + X509View() = default; + inline explicit X509View(const X509* cert) + : cert_(cert) + { + } + X509View(const X509View& other) = default; + X509View& operator=(const X509View& other) = default; + NCRYPTO_DISALLOW_MOVE(X509View) + + inline X509* get() const { return const_cast(cert_); } + inline operator X509*() const { return const_cast(cert_); } + inline operator const X509*() const { return cert_; } + + inline bool operator==(std::nullptr_t) noexcept { return cert_ == nullptr; } + inline operator bool() const { return cert_ != nullptr; } + + BIOPointer toPEM() const; + BIOPointer toDER() const; + + BIOPointer getSubject() const; + BIOPointer getSubjectAltName() const; + BIOPointer getIssuer() const; + BIOPointer getInfoAccess() const; + BIOPointer getValidFrom() const; + BIOPointer getValidTo() const; + int64_t getValidFromTime() const; + int64_t getValidToTime() const; + DataPointer getSerialNumber() const; + Result getPublicKey() const; + StackOfASN1 getKeyUsage() const; + + bool isCA() const; + bool isIssuedBy(const X509View& other) const; + bool checkPrivateKey(const EVPKeyPointer& pkey) const; + bool checkPublicKey(const EVPKeyPointer& pkey) const; + + std::optional getFingerprint(const EVP_MD* method) const; + + X509Pointer clone() const; + + enum class CheckMatch { + NO_MATCH, + MATCH, + INVALID_NAME, + OPERATION_FAILED, + }; + CheckMatch checkHost(std::span host, + int flags, + DataPointer* peerName = nullptr) const; + CheckMatch checkEmail(std::span email, int flags) const; + CheckMatch checkIp(std::span ip, int flags) const; + +private: + const X509* cert_ = nullptr; +}; + +class X509Pointer final { + WTF_MAKE_ISO_ALLOCATED(X509Pointer); + +public: + static Result Parse(Buffer buffer); + static X509Pointer IssuerFrom(const SSLPointer& ssl, const X509View& view); + static X509Pointer IssuerFrom(const SSL_CTX* ctx, const X509View& view); + static X509Pointer PeerFrom(const SSLPointer& ssl); + + X509Pointer() = default; + explicit X509Pointer(X509* cert); + X509Pointer(X509Pointer&& other) noexcept; + X509Pointer& operator=(X509Pointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(X509Pointer) + ~X509Pointer(); + + inline bool operator==(std::nullptr_t) noexcept { return cert_ == nullptr; } + inline operator bool() const { return cert_ != nullptr; } + inline X509* get() const { return cert_.get(); } + inline operator X509*() const { return cert_.get(); } + inline operator const X509*() const { return cert_.get(); } + void reset(X509* cert = nullptr); + X509* release(); + + X509View view() const; + operator X509View() const { return view(); } + + static ASCIILiteral ErrorCode(int32_t err); + static std::optional ErrorReason(int32_t err); + +private: + DeleteFnPtr cert_; +}; + +class ECDSASigPointer final { + WTF_MAKE_ISO_ALLOCATED(ECDSASigPointer); + +public: + explicit ECDSASigPointer(); + explicit ECDSASigPointer(ECDSA_SIG* sig); + ECDSASigPointer(ECDSASigPointer&& other) noexcept; + ECDSASigPointer& operator=(ECDSASigPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(ECDSASigPointer) + ~ECDSASigPointer(); + + inline bool operator==(std::nullptr_t) noexcept { return sig_ == nullptr; } + inline operator bool() const { return sig_ != nullptr; } + inline ECDSA_SIG* get() const { return sig_.get(); } + inline operator ECDSA_SIG*() const { return sig_.get(); } + void reset(ECDSA_SIG* sig = nullptr); + ECDSA_SIG* release(); + + static ECDSASigPointer New(); + static ECDSASigPointer Parse(const Buffer& buffer); + + inline const BIGNUM* r() const { return pr_; } + inline const BIGNUM* s() const { return ps_; } + + bool setParams(BignumPointer&& r, BignumPointer&& s); + + Buffer encode() const; + +private: + DeleteFnPtr sig_; + const BIGNUM* pr_ = nullptr; + const BIGNUM* ps_ = nullptr; +}; + +class ECGroupPointer final { + WTF_MAKE_ISO_ALLOCATED(ECGroupPointer); + +public: + explicit ECGroupPointer(); + explicit ECGroupPointer(EC_GROUP* group); + ECGroupPointer(ECGroupPointer&& other) noexcept; + ECGroupPointer& operator=(ECGroupPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(ECGroupPointer) + ~ECGroupPointer(); + + inline bool operator==(std::nullptr_t) noexcept { return group_ == nullptr; } + inline operator bool() const { return group_ != nullptr; } + inline EC_GROUP* get() const { return group_.get(); } + inline operator EC_GROUP*() const { return group_.get(); } + void reset(EC_GROUP* group = nullptr); + EC_GROUP* release(); + + static ECGroupPointer NewByCurveName(int nid); + +private: + DeleteFnPtr group_; +}; + +class ECPointPointer final { + WTF_MAKE_ISO_ALLOCATED(ECPointPointer); + +public: + ECPointPointer(); + explicit ECPointPointer(EC_POINT* point); + ECPointPointer(ECPointPointer&& other) noexcept; + ECPointPointer& operator=(ECPointPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(ECPointPointer) + ~ECPointPointer(); + + inline bool operator==(std::nullptr_t) noexcept { return point_ == nullptr; } + inline operator bool() const { return point_ != nullptr; } + inline EC_POINT* get() const { return point_.get(); } + inline operator EC_POINT*() const { return point_.get(); } + void reset(EC_POINT* point = nullptr); + EC_POINT* release(); + + bool setFromBuffer(const Buffer& buffer, + const EC_GROUP* group); + bool mul(const EC_GROUP* group, const BIGNUM* priv_key); + + static ECPointPointer New(const EC_GROUP* group); + +private: + DeleteFnPtr point_; +}; + +class ECKeyPointer final { + WTF_MAKE_ISO_ALLOCATED(ECKeyPointer); + +public: + ECKeyPointer(); + explicit ECKeyPointer(EC_KEY* key); + ECKeyPointer(ECKeyPointer&& other) noexcept; + ECKeyPointer& operator=(ECKeyPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(ECKeyPointer) + ~ECKeyPointer(); + + inline bool operator==(std::nullptr_t) noexcept { return key_ == nullptr; } + inline operator bool() const { return key_ != nullptr; } + inline EC_KEY* get() const { return key_.get(); } + inline operator EC_KEY*() const { return key_.get(); } + void reset(EC_KEY* key = nullptr); + EC_KEY* release(); + + ECKeyPointer clone() const; + bool setPrivateKey(const BignumPointer& priv); + bool setPublicKey(const ECPointPointer& pub); + bool setPublicKeyRaw(const BignumPointer& x, const BignumPointer& y); + bool generate(); + bool checkKey() const; + + const EC_GROUP* getGroup() const; + const BIGNUM* getPrivateKey() const; + const EC_POINT* getPublicKey() const; + + static ECKeyPointer New(const EC_GROUP* group); + static ECKeyPointer NewByCurveName(int nid); + + static const EC_POINT* GetPublicKey(const EC_KEY* key); + static const BIGNUM* GetPrivateKey(const EC_KEY* key); + static const EC_GROUP* GetGroup(const EC_KEY* key); + static int GetGroupName(const EC_KEY* key); + static bool Check(const EC_KEY* key); + +private: + DeleteFnPtr key_; +}; + +#ifndef OPENSSL_NO_ENGINE +class EnginePointer final { +public: + EnginePointer() = default; + + explicit EnginePointer(ENGINE* engine_, bool finish_on_exit = false); + EnginePointer(EnginePointer&& other) noexcept; + EnginePointer& operator=(EnginePointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(EnginePointer) + ~EnginePointer(); + + inline operator bool() const { return engine != nullptr; } + inline ENGINE* get() { return engine; } + inline void setFinishOnExit() { finish_on_exit = true; } + + void reset(ENGINE* engine_ = nullptr, bool finish_on_exit_ = false); + + bool setAsDefault(uint32_t flags, CryptoErrorList* errors = nullptr); + bool init(bool finish_on_exit = false); + EVPKeyPointer loadPrivateKey(WTF::StringView key_name); + + // Release ownership of the ENGINE* pointer. + ENGINE* release(); + + // Retrieve an OpenSSL Engine instance by name. If the name does not + // identify a valid named engine, the returned EnginePointer will be + // empty. + static EnginePointer getEngineByName(WTF::StringView name, + CryptoErrorList* errors = nullptr); + + // Call once when initializing OpenSSL at startup for the process. + static void initEnginesOnce(); + +private: + ENGINE* engine = nullptr; + bool finish_on_exit = false; +}; +#endif // !OPENSSL_NO_ENGINE + +// ============================================================================ +// FIPS +bool isFipsEnabled(); + +bool setFipsEnabled(bool enabled, CryptoErrorList* errors); + +bool testFipsEnabled(); + +// ============================================================================ +// Various utilities + +bool CSPRNG(void* buffer, size_t length) NCRYPTO_MUST_USE_RESULT; + +// This callback is used to avoid the default passphrase callback in OpenSSL +// which will typically prompt for the passphrase. The prompting is designed +// for the OpenSSL CLI, but works poorly for some environments like Node.js +// because it involves synchronous interaction with the controlling terminal, +// something we never want, and use this function to avoid it. +int NoPasswordCallback(char* buf, int size, int rwflag, void* u); + +int PasswordCallback(char* buf, int size, int rwflag, void* u); + +bool SafeX509SubjectAltNamePrint(const BIOPointer& out, X509_EXTENSION* ext); +bool SafeX509InfoAccessPrint(const BIOPointer& out, X509_EXTENSION* ext); + +// ============================================================================ +// SPKAC + +bool VerifySpkac(const char* input, size_t length); +BIOPointer ExportPublicKey(const char* input, size_t length); + +// The caller takes ownership of the returned Buffer +Buffer ExportChallenge(const char* input, size_t length); + +// ============================================================================ +// KDF + +const EVP_MD* getDigestByName(const std::string_view name); + +// Verify that the specified HKDF output length is valid for the given digest. +// The maximum length for HKDF output for a given digest is 255 times the +// hash size for the given digest algorithm. +bool checkHkdfLength(const EVP_MD* md, size_t length); + +DataPointer hkdf(const EVP_MD* md, + const Buffer& key, + const Buffer& info, + const Buffer& salt, + size_t length); + +bool checkScryptParams(uint64_t N, uint64_t r, uint64_t p, uint64_t maxmem); + +DataPointer scrypt(const Buffer& pass, + const Buffer& salt, + uint64_t N, + uint64_t r, + uint64_t p, + uint64_t maxmem, + size_t length); + +DataPointer pbkdf2(const EVP_MD* md, + const Buffer& pass, + const Buffer& salt, + uint32_t iterations, + size_t length); + +// ============================================================================ +// Version metadata +#define NCRYPTO_VERSION "0.0.1" + +enum { + NCRYPTO_VERSION_MAJOR = 0, + NCRYPTO_VERSION_MINOR = 0, + NCRYPTO_VERSION_REVISION = 1, +}; + +} // namespace ncrypto diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 2dffbe8465..e5d24cb951 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -60,6 +60,7 @@ public: std::unique_ptr m_clientSubspaceForNodeVMGlobalObject; std::unique_ptr m_clientSubspaceForJSS3Bucket; std::unique_ptr m_clientSubspaceForJSS3File; + std::unique_ptr m_clientSubspaceForJSX509Certificate; #include "ZigGeneratedClasses+DOMClientIsoSubspaces.h" /* --- bun --- */ diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index 5af65d80ac..bc995592e7 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -60,7 +60,7 @@ public: std::unique_ptr m_subspaceForNodeVMGlobalObject; std::unique_ptr m_subspaceForJSS3Bucket; std::unique_ptr m_subspaceForJSS3File; - + std::unique_ptr m_subspaceForJSX509Certificate; #include "ZigGeneratedClasses+DOMIsoSubspaces.h" /*-- BUN --*/ diff --git a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp index c749770c33..7bb59bdfb1 100644 --- a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp +++ b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp @@ -104,8 +104,11 @@ #include #include +#include "ZigGlobalObject.h" #include "blob.h" #include "ZigGeneratedClasses.h" +#include "JSX509Certificate.h" +#include "ncrypto.h" #if USE(CG) #include @@ -233,6 +236,7 @@ enum SerializationTag { Bun__BlobTag = 254, // bun types start at 254 and decrease with each addition + Bun__X509CertificateTag = 253, ErrorTag = 255 }; @@ -1612,14 +1616,6 @@ private: // return true; // } - // write bun types - if (auto _cloneable = StructuredCloneableSerialize::fromJS(value)) { - StructuredCloneableSerialize cloneable = WTFMove(_cloneable.value()); - write(cloneable.tag); - cloneable.write(this, m_lexicalGlobalObject); - return true; - } - // if (auto* blob = JSBlob::toWrapped(vm, obj)) { // write(BlobTag); // m_blobHandles.append(blob->handle().isolatedCopy()); @@ -1929,6 +1925,40 @@ private: } #endif + // write bun types + if (auto _cloneable = StructuredCloneableSerialize::fromJS(value)) { + StructuredCloneableSerialize cloneable = WTFMove(_cloneable.value()); + write(cloneable.tag); + cloneable.write(this, m_lexicalGlobalObject); + return true; + } + + if (auto* x509 = jsDynamicCast(obj)) { + write(Bun__X509CertificateTag); + X509* cert = x509->m_x509.get(); + + // Get the size needed for the DER encoding + int size = i2d_X509(cert, nullptr); + if (size <= 0) + return false; + + Vector der; + der.reserveInitialCapacity(size); + der.grow(size); + + // Get pointer to where we should write + unsigned char* der_ptr = der.begin(); + + // Write the DER encoding + if (i2d_X509(cert, &der_ptr) != size) { + return false; + } + + write(der); + + return true; + } + return false; } // Any other types are expected to serialize as null. @@ -4368,6 +4398,36 @@ private: // return getJSValue(bitmap); // } + JSValue readX509Certificate() + { + Vector buffer; + + if (!read(buffer)) { + fail(); + return JSValue(); + } + + if (buffer.size() == 0) { + return Bun::JSX509Certificate::create(m_lexicalGlobalObject->vm(), defaultGlobalObject(m_globalObject)->m_JSX509CertificateClassStructure.get(m_globalObject)); + } + ncrypto::ClearErrorOnReturn clear_error_on_return; + X509* ptr = nullptr; + const uint8_t* data = buffer.data(); + + auto cert = d2i_X509(&ptr, &data, buffer.size()); + if (!cert) { + fail(); + return JSValue(); + } + + auto cert_ptr = ncrypto::X509Pointer(cert); + auto* domGlobalObject = defaultGlobalObject(m_globalObject); + auto* cert_obj = Bun::JSX509Certificate::create(m_lexicalGlobalObject->vm(), domGlobalObject->m_JSX509CertificateClassStructure.get(domGlobalObject), m_globalObject, WTFMove(cert_ptr)); + m_gcBuffer.appendWithCrashOnOverflow(cert_obj); + + return cert_obj; + } + JSValue readDOMException() { CachedStringRef message; @@ -4923,6 +4983,9 @@ private: case DOMExceptionTag: return readDOMException(); + case Bun__X509CertificateTag: + return readX509Certificate(); + default: m_ptr--; // Push the tag back return JSValue(); diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithm.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithm.cpp index 3c958bdfca..f6350fe65d 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithm.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithm.cpp @@ -34,57 +34,57 @@ namespace WebCore { void CryptoAlgorithm::encrypt(const CryptoAlgorithmParameters&, Ref&&, Vector&&, VectorCallback&&, ExceptionCallback&& exceptionCallback, ScriptExecutionContext&, WorkQueue&) { - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); } void CryptoAlgorithm::decrypt(const CryptoAlgorithmParameters&, Ref&&, Vector&&, VectorCallback&&, ExceptionCallback&& exceptionCallback, ScriptExecutionContext&, WorkQueue&) { - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); } void CryptoAlgorithm::sign(const CryptoAlgorithmParameters&, Ref&&, Vector&&, VectorCallback&&, ExceptionCallback&& exceptionCallback, ScriptExecutionContext&, WorkQueue&) { - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); } void CryptoAlgorithm::verify(const CryptoAlgorithmParameters&, Ref&&, Vector&&, Vector&&, BoolCallback&&, ExceptionCallback&& exceptionCallback, ScriptExecutionContext&, WorkQueue&) { - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); } void CryptoAlgorithm::digest(Vector&&, VectorCallback&&, ExceptionCallback&& exceptionCallback, ScriptExecutionContext&, WorkQueue&) { - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); } void CryptoAlgorithm::generateKey(const CryptoAlgorithmParameters&, bool, CryptoKeyUsageBitmap, KeyOrKeyPairCallback&&, ExceptionCallback&& exceptionCallback, ScriptExecutionContext&) { - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); } void CryptoAlgorithm::deriveBits(const CryptoAlgorithmParameters&, Ref&&, size_t, VectorCallback&&, ExceptionCallback&& exceptionCallback, ScriptExecutionContext&, WorkQueue&) { - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); } void CryptoAlgorithm::importKey(CryptoKeyFormat, KeyData&&, const CryptoAlgorithmParameters&, bool, CryptoKeyUsageBitmap, KeyCallback&&, ExceptionCallback&& exceptionCallback) { - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); } void CryptoAlgorithm::exportKey(CryptoKeyFormat, Ref&&, KeyDataCallback&&, ExceptionCallback&& exceptionCallback) { - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); } void CryptoAlgorithm::wrapKey(Ref&&, Vector&&, VectorCallback&&, ExceptionCallback&& exceptionCallback) { - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); } void CryptoAlgorithm::unwrapKey(Ref&&, Vector&&, VectorCallback&&, ExceptionCallback&& exceptionCallback) { - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); } ExceptionOr CryptoAlgorithm::getKeyLength(const CryptoAlgorithmParameters&) @@ -102,7 +102,7 @@ static void dispatchAlgorithmOperation(WorkQueue& workQueue, ScriptExecutionCont ScriptExecutionContext::postTaskTo(contextIdentifier, [result = crossThreadCopy(WTFMove(result)), callback = WTFMove(callback), exceptionCallback = WTFMove(exceptionCallback)](auto& context) mutable { context.unrefEventLoop(); if (result.hasException()) { - exceptionCallback(result.releaseException().code()); + exceptionCallback(result.releaseException().code(), ""_s); return; } callback(result.releaseReturnValue()); diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithm.h b/src/bun.js/bindings/webcrypto/CryptoAlgorithm.h index 40dbb0ad61..29964198d1 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithm.h +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithm.h @@ -60,7 +60,7 @@ public: // FIXME: https://bugs.webkit.org/show_bug.cgi?id=169395 using VectorCallback = Function&)>; using VoidCallback = Function; - using ExceptionCallback = Function; + using ExceptionCallback = Function; using KeyDataCallback = Function; virtual void encrypt(const CryptoAlgorithmParameters&, Ref&&, Vector&&, VectorCallback&&, ExceptionCallback&&, ScriptExecutionContext&, WorkQueue&); diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_CBC.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_CBC.cpp index c13df346e5..585ed77c2f 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_CBC.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_CBC.cpp @@ -63,7 +63,7 @@ void CryptoAlgorithmAES_CBC::encrypt(const CryptoAlgorithmParameters& parameters auto& aesParameters = downcast(parameters); if (aesParameters.ivVector().size() != IVSIZE) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, "algorithm.iv must contain exactly 16 bytes"_s); return; } @@ -79,7 +79,7 @@ void CryptoAlgorithmAES_CBC::decrypt(const CryptoAlgorithmParameters& parameters auto& aesParameters = downcast(parameters); if (aesParameters.ivVector().size() != IVSIZE) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, "algorithm.iv must contain exactly 16 bytes"_s); return; } @@ -94,13 +94,13 @@ void CryptoAlgorithmAES_CBC::generateKey(const CryptoAlgorithmParameters& parame const auto& aesParameters = downcast(parameters); if (usagesAreInvalidForCryptoAlgorithmAES_CBC(usages)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } auto result = CryptoKeyAES::generate(CryptoAlgorithmIdentifier::AES_CBC, aesParameters.length, extractable, usages); if (!result) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -112,7 +112,7 @@ void CryptoAlgorithmAES_CBC::importKey(CryptoKeyFormat format, KeyData&& data, c using namespace CryptoAlgorithmAES_CBCInternal; if (usagesAreInvalidForCryptoAlgorithmAES_CBC(usages)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } @@ -137,11 +137,11 @@ void CryptoAlgorithmAES_CBC::importKey(CryptoKeyFormat format, KeyData&& data, c break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } if (!result) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -154,7 +154,7 @@ void CryptoAlgorithmAES_CBC::exportKey(CryptoKeyFormat format, Ref&& const auto& aesKey = downcast(key.get()); if (aesKey.key().isEmpty()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -182,7 +182,7 @@ void CryptoAlgorithmAES_CBC::exportKey(CryptoKeyFormat format, Ref&& break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_CFB.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_CFB.cpp index c5d802cfd1..34b7afe92b 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_CFB.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_CFB.cpp @@ -63,7 +63,7 @@ void CryptoAlgorithmAES_CFB::encrypt(const CryptoAlgorithmParameters& parameters auto& aesParameters = downcast(parameters); if (aesParameters.ivVector().size() != IVSIZE) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, "algorithm.iv must contain exactly 16 bytes"_s); return; } @@ -79,7 +79,7 @@ void CryptoAlgorithmAES_CFB::decrypt(const CryptoAlgorithmParameters& parameters auto& aesParameters = downcast(parameters); if (aesParameters.ivVector().size() != IVSIZE) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, "algorithm.iv must contain exactly 16 bytes"_s); return; } @@ -94,13 +94,13 @@ void CryptoAlgorithmAES_CFB::generateKey(const CryptoAlgorithmParameters& parame const auto& aesParameters = downcast(parameters); if (usagesAreInvalidForCryptoAlgorithmAES_CFB(usages)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } auto result = CryptoKeyAES::generate(CryptoAlgorithmIdentifier::AES_CFB, aesParameters.length, extractable, usages); if (!result) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -112,7 +112,7 @@ void CryptoAlgorithmAES_CFB::importKey(CryptoKeyFormat format, KeyData&& data, c using namespace CryptoAlgorithmAES_CFBInternal; if (usagesAreInvalidForCryptoAlgorithmAES_CFB(usages)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } @@ -137,11 +137,11 @@ void CryptoAlgorithmAES_CFB::importKey(CryptoKeyFormat format, KeyData&& data, c break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } if (!result) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -154,7 +154,7 @@ void CryptoAlgorithmAES_CFB::exportKey(CryptoKeyFormat format, Ref&& const auto& aesKey = downcast(key.get()); if (aesKey.key().isEmpty()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -182,7 +182,7 @@ void CryptoAlgorithmAES_CFB::exportKey(CryptoKeyFormat format, Ref&& break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_CTR.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_CTR.cpp index 09e397a9bb..b9fe723e5c 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_CTR.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_CTR.cpp @@ -74,7 +74,7 @@ void CryptoAlgorithmAES_CTR::encrypt(const CryptoAlgorithmParameters& parameters { auto& aesParameters = downcast(parameters); if (!parametersAreValid(aesParameters)) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -88,7 +88,7 @@ void CryptoAlgorithmAES_CTR::decrypt(const CryptoAlgorithmParameters& parameters { auto& aesParameters = downcast(parameters); if (!parametersAreValid(aesParameters)) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -103,13 +103,13 @@ void CryptoAlgorithmAES_CTR::generateKey(const CryptoAlgorithmParameters& parame const auto& aesParameters = downcast(parameters); if (usagesAreInvalidForCryptoAlgorithmAES_CTR(usages)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } auto result = CryptoKeyAES::generate(CryptoAlgorithmIdentifier::AES_CTR, aesParameters.length, extractable, usages); if (!result) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -121,7 +121,7 @@ void CryptoAlgorithmAES_CTR::importKey(CryptoKeyFormat format, KeyData&& data, c using namespace CryptoAlgorithmAES_CTRInternal; if (usagesAreInvalidForCryptoAlgorithmAES_CTR(usages)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } @@ -146,11 +146,11 @@ void CryptoAlgorithmAES_CTR::importKey(CryptoKeyFormat format, KeyData&& data, c break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } if (!result) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -163,7 +163,7 @@ void CryptoAlgorithmAES_CTR::exportKey(CryptoKeyFormat format, Ref&& const auto& aesKey = downcast(key.get()); if (aesKey.key().isEmpty()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -191,7 +191,7 @@ void CryptoAlgorithmAES_CTR::exportKey(CryptoKeyFormat format, Ref&& break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_GCM.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_GCM.cpp index 2dcbbb7b81..af4b982fa7 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_GCM.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_GCM.cpp @@ -79,22 +79,22 @@ void CryptoAlgorithmAES_GCM::encrypt(const CryptoAlgorithmParameters& parameters #if CPU(ADDRESS64) if (plainText.size() > PlainTextMaxLength) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } if (aesParameters.ivVector().size() > UINT64_MAX) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } if (aesParameters.additionalDataVector().size() > UINT64_MAX) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } #endif aesParameters.tagLength = aesParameters.tagLength ? aesParameters.tagLength : DefaultTagLength; if (!tagLengthIsValid(*(aesParameters.tagLength))) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, makeString(aesParameters.tagLength.value(), " is not a valid AES-GCM tag length"_s)); return; } @@ -112,21 +112,21 @@ void CryptoAlgorithmAES_GCM::decrypt(const CryptoAlgorithmParameters& parameters aesParameters.tagLength = aesParameters.tagLength ? aesParameters.tagLength : DefaultTagLength; if (!tagLengthIsValid(*(aesParameters.tagLength))) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, makeString(aesParameters.tagLength.value(), " is not a valid AES-GCM tag length"_s)); return; } if (cipherText.size() < *(aesParameters.tagLength) / 8) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, "The provided data is too small"_s); return; } #if CPU(ADDRESS64) if (aesParameters.ivVector().size() > UINT64_MAX) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } if (aesParameters.additionalDataVector().size() > UINT64_MAX) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } #endif @@ -142,13 +142,13 @@ void CryptoAlgorithmAES_GCM::generateKey(const CryptoAlgorithmParameters& parame const auto& aesParameters = downcast(parameters); if (usagesAreInvalidForCryptoAlgorithmAES_GCM(usages)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } auto result = CryptoKeyAES::generate(CryptoAlgorithmIdentifier::AES_GCM, aesParameters.length, extractable, usages); if (!result) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -160,7 +160,7 @@ void CryptoAlgorithmAES_GCM::importKey(CryptoKeyFormat format, KeyData&& data, c using namespace CryptoAlgorithmAES_GCMInternal; if (usagesAreInvalidForCryptoAlgorithmAES_GCM(usages)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } @@ -185,11 +185,11 @@ void CryptoAlgorithmAES_GCM::importKey(CryptoKeyFormat format, KeyData&& data, c break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } if (!result) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -202,7 +202,7 @@ void CryptoAlgorithmAES_GCM::exportKey(CryptoKeyFormat format, Ref&& const auto& aesKey = downcast(key.get()); if (aesKey.key().isEmpty()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -230,7 +230,7 @@ void CryptoAlgorithmAES_GCM::exportKey(CryptoKeyFormat format, Ref&& break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_KW.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_KW.cpp index adc65b6455..c748c89254 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_KW.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmAES_KW.cpp @@ -58,13 +58,13 @@ CryptoAlgorithmIdentifier CryptoAlgorithmAES_KW::identifier() const void CryptoAlgorithmAES_KW::generateKey(const CryptoAlgorithmParameters& parameters, bool extractable, CryptoKeyUsageBitmap usages, KeyOrKeyPairCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext&) { if (usagesAreInvalidForCryptoAlgorithmAES_KW(usages)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } auto result = CryptoKeyAES::generate(CryptoAlgorithmIdentifier::AES_KW, downcast(parameters).length, extractable, usages); if (!result) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -76,7 +76,7 @@ void CryptoAlgorithmAES_KW::importKey(CryptoKeyFormat format, KeyData&& data, co using namespace CryptoAlgorithmAES_KWInternal; if (usagesAreInvalidForCryptoAlgorithmAES_KW(usages)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } @@ -100,11 +100,11 @@ void CryptoAlgorithmAES_KW::importKey(CryptoKeyFormat format, KeyData&& data, co break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } if (!result) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -117,7 +117,7 @@ void CryptoAlgorithmAES_KW::exportKey(CryptoKeyFormat format, Ref&& k const auto& aesKey = downcast(key.get()); if (aesKey.key().isEmpty()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -145,7 +145,7 @@ void CryptoAlgorithmAES_KW::exportKey(CryptoKeyFormat format, Ref&& k break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } @@ -155,13 +155,13 @@ void CryptoAlgorithmAES_KW::exportKey(CryptoKeyFormat format, Ref&& k void CryptoAlgorithmAES_KW::wrapKey(Ref&& key, Vector&& data, VectorCallback&& callback, ExceptionCallback&& exceptionCallback) { if (data.size() % 8) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } auto result = platformWrapKey(downcast(key.get()), WTFMove(data)); if (result.hasException()) { - exceptionCallback(result.releaseException().code()); + exceptionCallback(result.releaseException().code(), ""_s); return; } @@ -172,7 +172,7 @@ void CryptoAlgorithmAES_KW::unwrapKey(Ref&& key, Vector&& da { auto result = platformUnwrapKey(downcast(key.get()), WTFMove(data)); if (result.hasException()) { - exceptionCallback(result.releaseException().code()); + exceptionCallback(result.releaseException().code(), ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmECDH.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmECDH.cpp index 05be9bb885..a26290f7aa 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmECDH.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmECDH.cpp @@ -50,13 +50,13 @@ void CryptoAlgorithmECDH::generateKey(const CryptoAlgorithmParameters& parameter const auto& ecParameters = downcast(parameters); if (usages & (CryptoKeyUsageEncrypt | CryptoKeyUsageDecrypt | CryptoKeyUsageSign | CryptoKeyUsageVerify | CryptoKeyUsageWrapKey | CryptoKeyUsageUnwrapKey)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } auto result = CryptoKeyEC::generatePair(CryptoAlgorithmIdentifier::ECDH, ecParameters.namedCurve, extractable, usages); if (result.hasException()) { - exceptionCallback(result.releaseException().code()); + exceptionCallback(result.releaseException().code(), ""_s); return; } @@ -71,28 +71,28 @@ void CryptoAlgorithmECDH::deriveBits(const CryptoAlgorithmParameters& parameters auto& ecParameters = downcast(parameters); if (baseKey->type() != CryptoKey::Type::Private) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } ASSERT(ecParameters.publicKey); if (ecParameters.publicKey->type() != CryptoKey::Type::Public) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } if (baseKey->algorithmIdentifier() != ecParameters.publicKey->algorithmIdentifier()) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } auto& ecBaseKey = downcast(baseKey.get()); auto& ecPublicKey = downcast(*(ecParameters.publicKey.get())); if (ecBaseKey.namedCurve() != ecPublicKey.namedCurve()) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } auto unifiedCallback = [callback = WTFMove(callback), exceptionCallback = WTFMove(exceptionCallback)](std::optional>&& derivedKey, size_t length) mutable { if (!derivedKey) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } if (!length) { @@ -101,7 +101,7 @@ void CryptoAlgorithmECDH::deriveBits(const CryptoAlgorithmParameters& parameters } auto lengthInBytes = std::ceil(length / 8.); if (lengthInBytes > (*derivedKey).size()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } (*derivedKey).shrink(lengthInBytes); @@ -136,12 +136,12 @@ void CryptoAlgorithmECDH::importKey(CryptoKeyFormat format, KeyData&& data, cons } isUsagesAllowed = isUsagesAllowed || !usages; if (!isUsagesAllowed) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } if (usages && !key.use.isNull() && key.use != "enc"_s) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -150,28 +150,28 @@ void CryptoAlgorithmECDH::importKey(CryptoKeyFormat format, KeyData&& data, cons } case CryptoKeyFormat::Raw: if (usages) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } result = CryptoKeyEC::importRaw(ecParameters.identifier, ecParameters.namedCurve, WTFMove(std::get>(data)), extractable, usages); break; case CryptoKeyFormat::Spki: if (usages) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } result = CryptoKeyEC::importSpki(ecParameters.identifier, ecParameters.namedCurve, WTFMove(std::get>(data)), extractable, usages); break; case CryptoKeyFormat::Pkcs8: if (usages && (usages ^ CryptoKeyUsageDeriveKey) && (usages ^ CryptoKeyUsageDeriveBits) && (usages ^ (CryptoKeyUsageDeriveKey | CryptoKeyUsageDeriveBits))) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } result = CryptoKeyEC::importPkcs8(ecParameters.identifier, ecParameters.namedCurve, WTFMove(std::get>(data)), extractable, usages); break; } if (!result) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -183,7 +183,7 @@ void CryptoAlgorithmECDH::exportKey(CryptoKeyFormat format, Ref&& key const auto& ecKey = downcast(key.get()); if (!ecKey.keySizeInBits()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -192,7 +192,7 @@ void CryptoAlgorithmECDH::exportKey(CryptoKeyFormat format, Ref&& key case CryptoKeyFormat::Jwk: { auto jwk = ecKey.exportJwk(); if (jwk.hasException()) { - exceptionCallback(jwk.releaseException().code()); + exceptionCallback(jwk.releaseException().code(), ""_s); return; } result = jwk.releaseReturnValue(); @@ -201,7 +201,7 @@ void CryptoAlgorithmECDH::exportKey(CryptoKeyFormat format, Ref&& key case CryptoKeyFormat::Raw: { auto raw = ecKey.exportRaw(); if (raw.hasException()) { - exceptionCallback(raw.releaseException().code()); + exceptionCallback(raw.releaseException().code(), ""_s); return; } result = raw.releaseReturnValue(); @@ -210,7 +210,7 @@ void CryptoAlgorithmECDH::exportKey(CryptoKeyFormat format, Ref&& key case CryptoKeyFormat::Spki: { auto spki = ecKey.exportSpki(); if (spki.hasException()) { - exceptionCallback(spki.releaseException().code()); + exceptionCallback(spki.releaseException().code(), ""_s); return; } result = spki.releaseReturnValue(); @@ -219,7 +219,7 @@ void CryptoAlgorithmECDH::exportKey(CryptoKeyFormat format, Ref&& key case CryptoKeyFormat::Pkcs8: { auto pkcs8 = ecKey.exportPkcs8(); if (pkcs8.hasException()) { - exceptionCallback(pkcs8.releaseException().code()); + exceptionCallback(pkcs8.releaseException().code(), ""_s); return; } result = pkcs8.releaseReturnValue(); diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmECDSA.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmECDSA.cpp index dbc0006203..4744b65003 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmECDSA.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmECDSA.cpp @@ -58,7 +58,7 @@ CryptoAlgorithmIdentifier CryptoAlgorithmECDSA::identifier() const void CryptoAlgorithmECDSA::sign(const CryptoAlgorithmParameters& parameters, Ref&& key, Vector&& data, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (key->type() != CryptoKeyType::Private) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } @@ -71,7 +71,7 @@ void CryptoAlgorithmECDSA::sign(const CryptoAlgorithmParameters& parameters, Ref void CryptoAlgorithmECDSA::verify(const CryptoAlgorithmParameters& parameters, Ref&& key, Vector&& signature, Vector&& data, BoolCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (key->type() != CryptoKeyType::Public) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } @@ -86,13 +86,13 @@ void CryptoAlgorithmECDSA::generateKey(const CryptoAlgorithmParameters& paramete const auto& ecParameters = downcast(parameters); if (usages & (CryptoKeyUsageEncrypt | CryptoKeyUsageDecrypt | CryptoKeyUsageDeriveKey | CryptoKeyUsageDeriveBits | CryptoKeyUsageWrapKey | CryptoKeyUsageUnwrapKey)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } auto result = CryptoKeyEC::generatePair(CryptoAlgorithmIdentifier::ECDSA, ecParameters.namedCurve, extractable, usages); if (result.hasException()) { - exceptionCallback(result.releaseException().code()); + exceptionCallback(result.releaseException().code(), ""_s); return; } @@ -113,11 +113,11 @@ void CryptoAlgorithmECDSA::importKey(CryptoKeyFormat format, KeyData&& data, con JsonWebKey key = WTFMove(std::get(data)); if (usages && ((!key.d.isNull() && (usages ^ CryptoKeyUsageSign)) || (key.d.isNull() && (usages ^ CryptoKeyUsageVerify)))) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } if (usages && !key.use.isNull() && key.use != "sig"_s) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -129,7 +129,7 @@ void CryptoAlgorithmECDSA::importKey(CryptoKeyFormat format, KeyData&& data, con if (key.crv == P521) isMatched = key.alg.isNull() || key.alg == ALG512; if (!isMatched) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -138,28 +138,28 @@ void CryptoAlgorithmECDSA::importKey(CryptoKeyFormat format, KeyData&& data, con } case CryptoKeyFormat::Raw: if (usages && (usages ^ CryptoKeyUsageVerify)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } result = CryptoKeyEC::importRaw(ecParameters.identifier, ecParameters.namedCurve, WTFMove(std::get>(data)), extractable, usages); break; case CryptoKeyFormat::Spki: if (usages && (usages ^ CryptoKeyUsageVerify)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } result = CryptoKeyEC::importSpki(ecParameters.identifier, ecParameters.namedCurve, WTFMove(std::get>(data)), extractable, usages); break; case CryptoKeyFormat::Pkcs8: if (usages && (usages ^ CryptoKeyUsageSign)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } result = CryptoKeyEC::importPkcs8(ecParameters.identifier, ecParameters.namedCurve, WTFMove(std::get>(data)), extractable, usages); break; } if (!result) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -171,7 +171,7 @@ void CryptoAlgorithmECDSA::exportKey(CryptoKeyFormat format, Ref&& ke const auto& ecKey = downcast(key.get()); if (!ecKey.keySizeInBits()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -180,7 +180,7 @@ void CryptoAlgorithmECDSA::exportKey(CryptoKeyFormat format, Ref&& ke case CryptoKeyFormat::Jwk: { auto jwk = ecKey.exportJwk(); if (jwk.hasException()) { - exceptionCallback(jwk.releaseException().code()); + exceptionCallback(jwk.releaseException().code(), ""_s); return; } result = jwk.releaseReturnValue(); @@ -189,7 +189,7 @@ void CryptoAlgorithmECDSA::exportKey(CryptoKeyFormat format, Ref&& ke case CryptoKeyFormat::Raw: { auto raw = ecKey.exportRaw(); if (raw.hasException()) { - exceptionCallback(raw.releaseException().code()); + exceptionCallback(raw.releaseException().code(), ""_s); return; } result = raw.releaseReturnValue(); @@ -198,7 +198,7 @@ void CryptoAlgorithmECDSA::exportKey(CryptoKeyFormat format, Ref&& ke case CryptoKeyFormat::Spki: { auto spki = ecKey.exportSpki(); if (spki.hasException()) { - exceptionCallback(spki.releaseException().code()); + exceptionCallback(spki.releaseException().code(), ""_s); return; } result = spki.releaseReturnValue(); @@ -207,7 +207,7 @@ void CryptoAlgorithmECDSA::exportKey(CryptoKeyFormat format, Ref&& ke case CryptoKeyFormat::Pkcs8: { auto pkcs8 = ecKey.exportPkcs8(); if (pkcs8.hasException()) { - exceptionCallback(pkcs8.releaseException().code()); + exceptionCallback(pkcs8.releaseException().code(), ""_s); return; } result = pkcs8.releaseReturnValue(); diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmEd25519.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmEd25519.cpp index b09968213f..2fbfa01a35 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmEd25519.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmEd25519.cpp @@ -71,13 +71,13 @@ Ref CryptoAlgorithmEd25519::create() void CryptoAlgorithmEd25519::generateKey(const CryptoAlgorithmParameters&, bool extractable, CryptoKeyUsageBitmap usages, KeyOrKeyPairCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext&) { if (usages & (CryptoKeyUsageEncrypt | CryptoKeyUsageDecrypt | CryptoKeyUsageDeriveKey | CryptoKeyUsageDeriveBits | CryptoKeyUsageWrapKey | CryptoKeyUsageUnwrapKey)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } auto result = CryptoKeyOKP::generatePair(CryptoAlgorithmIdentifier::Ed25519, CryptoKeyOKP::NamedCurve::Ed25519, extractable, usages); if (result.hasException()) { - exceptionCallback(result.releaseException().code()); + exceptionCallback(result.releaseException().code(), ""_s); return; } @@ -90,7 +90,7 @@ void CryptoAlgorithmEd25519::generateKey(const CryptoAlgorithmParameters&, bool void CryptoAlgorithmEd25519::sign(const CryptoAlgorithmParameters&, Ref&& key, Vector&& data, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (key->type() != CryptoKeyType::Private) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } dispatchOperationInWorkQueue(workQueue, context, WTFMove(callback), WTFMove(exceptionCallback), @@ -102,7 +102,7 @@ void CryptoAlgorithmEd25519::sign(const CryptoAlgorithmParameters&, Ref&& key, Vector&& signature, Vector&& data, BoolCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (key->type() != CryptoKeyType::Public) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } dispatchOperationInWorkQueue(workQueue, context, WTFMove(callback), WTFMove(exceptionCallback), @@ -118,11 +118,11 @@ void CryptoAlgorithmEd25519::importKey(CryptoKeyFormat format, KeyData&& data, c case CryptoKeyFormat::Jwk: { JsonWebKey key = WTFMove(std::get(data)); if (usages && ((!key.d.isNull() && (usages ^ CryptoKeyUsageSign)) || (key.d.isNull() && (usages ^ CryptoKeyUsageVerify)))) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } if (usages && !key.use.isNull() && key.use != "sig"_s) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } result = CryptoKeyOKP::importJwk(CryptoAlgorithmIdentifier::Ed25519, CryptoKeyOKP::NamedCurve::Ed25519, WTFMove(key), extractable, usages); @@ -130,28 +130,28 @@ void CryptoAlgorithmEd25519::importKey(CryptoKeyFormat format, KeyData&& data, c } case CryptoKeyFormat::Raw: if (usages && (usages ^ CryptoKeyUsageVerify)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } result = CryptoKeyOKP::importRaw(CryptoAlgorithmIdentifier::Ed25519, CryptoKeyOKP::NamedCurve::Ed25519, WTFMove(std::get>(data)), extractable, usages); break; case CryptoKeyFormat::Spki: if (usages && (usages ^ CryptoKeyUsageVerify)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } result = CryptoKeyOKP::importSpki(CryptoAlgorithmIdentifier::Ed25519, CryptoKeyOKP::NamedCurve::Ed25519, WTFMove(std::get>(data)), extractable, usages); break; case CryptoKeyFormat::Pkcs8: if (usages && (usages ^ CryptoKeyUsageSign)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } result = CryptoKeyOKP::importPkcs8(CryptoAlgorithmIdentifier::Ed25519, CryptoKeyOKP::NamedCurve::Ed25519, WTFMove(std::get>(data)), extractable, usages); break; } if (!result) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } callback(*result); @@ -161,7 +161,7 @@ void CryptoAlgorithmEd25519::exportKey(CryptoKeyFormat format, Ref&& { const auto& okpKey = downcast(key.get()); if (!okpKey.keySizeInBits()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } KeyData result; @@ -169,7 +169,7 @@ void CryptoAlgorithmEd25519::exportKey(CryptoKeyFormat format, Ref&& case CryptoKeyFormat::Jwk: { auto jwk = okpKey.exportJwk(); if (jwk.hasException()) { - exceptionCallback(jwk.releaseException().code()); + exceptionCallback(jwk.releaseException().code(), ""_s); return; } result = jwk.releaseReturnValue(); @@ -178,7 +178,7 @@ void CryptoAlgorithmEd25519::exportKey(CryptoKeyFormat format, Ref&& case CryptoKeyFormat::Raw: { auto raw = okpKey.exportRaw(); if (raw.hasException()) { - exceptionCallback(raw.releaseException().code()); + exceptionCallback(raw.releaseException().code(), ""_s); return; } result = raw.releaseReturnValue(); @@ -187,7 +187,7 @@ void CryptoAlgorithmEd25519::exportKey(CryptoKeyFormat format, Ref&& case CryptoKeyFormat::Spki: { auto spki = okpKey.exportSpki(); if (spki.hasException()) { - exceptionCallback(spki.releaseException().code()); + exceptionCallback(spki.releaseException().code(), ""_s); return; } result = spki.releaseReturnValue(); @@ -196,7 +196,7 @@ void CryptoAlgorithmEd25519::exportKey(CryptoKeyFormat format, Ref&& case CryptoKeyFormat::Pkcs8: { auto pkcs8 = okpKey.exportPkcs8(); if (pkcs8.hasException()) { - exceptionCallback(pkcs8.releaseException().code()); + exceptionCallback(pkcs8.releaseException().code(), ""_s); return; } result = pkcs8.releaseReturnValue(); diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmHKDF.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmHKDF.cpp index b45e6aa8a2..a8aafe83e0 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmHKDF.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmHKDF.cpp @@ -48,7 +48,7 @@ CryptoAlgorithmIdentifier CryptoAlgorithmHKDF::identifier() const void CryptoAlgorithmHKDF::deriveBits(const CryptoAlgorithmParameters& parameters, Ref&& baseKey, size_t length, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (!length || length % 8) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -61,15 +61,15 @@ void CryptoAlgorithmHKDF::deriveBits(const CryptoAlgorithmParameters& parameters void CryptoAlgorithmHKDF::importKey(CryptoKeyFormat format, KeyData&& data, const CryptoAlgorithmParameters& parameters, bool extractable, CryptoKeyUsageBitmap usages, KeyCallback&& callback, ExceptionCallback&& exceptionCallback) { if (format != CryptoKeyFormat::Raw) { - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } if (usages & (CryptoKeyUsageEncrypt | CryptoKeyUsageDecrypt | CryptoKeyUsageSign | CryptoKeyUsageVerify | CryptoKeyUsageWrapKey | CryptoKeyUsageUnwrapKey)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } if (extractable) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmHMAC.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmHMAC.cpp index 58eaf1b0d8..d5226cc6b6 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmHMAC.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmHMAC.cpp @@ -78,18 +78,18 @@ void CryptoAlgorithmHMAC::generateKey(const CryptoAlgorithmParameters& parameter const auto& hmacParameters = downcast(parameters); if (usagesAreInvalidForCryptoAlgorithmHMAC(usages)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } if (hmacParameters.length && !hmacParameters.length.value()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } auto result = CryptoKeyHMAC::generate(hmacParameters.length.value_or(0), hmacParameters.hashIdentifier, extractable, usages); if (!result) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -103,7 +103,7 @@ void CryptoAlgorithmHMAC::importKey(CryptoKeyFormat format, KeyData&& data, cons const auto& hmacParameters = downcast(parameters); if (usagesAreInvalidForCryptoAlgorithmHMAC(usages)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } @@ -134,11 +134,11 @@ void CryptoAlgorithmHMAC::importKey(CryptoKeyFormat format, KeyData&& data, cons break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } if (!result) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -151,7 +151,7 @@ void CryptoAlgorithmHMAC::exportKey(CryptoKeyFormat format, Ref&& key const auto& hmacKey = downcast(key.get()); if (hmacKey.key().isEmpty()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -185,7 +185,7 @@ void CryptoAlgorithmHMAC::exportKey(CryptoKeyFormat format, Ref&& key break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmPBKDF2.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmPBKDF2.cpp index 268c051d9f..52d9736695 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmPBKDF2.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmPBKDF2.cpp @@ -48,7 +48,7 @@ CryptoAlgorithmIdentifier CryptoAlgorithmPBKDF2::identifier() const void CryptoAlgorithmPBKDF2::deriveBits(const CryptoAlgorithmParameters& parameters, Ref&& baseKey, size_t length, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (!length || length % 8) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -61,15 +61,15 @@ void CryptoAlgorithmPBKDF2::deriveBits(const CryptoAlgorithmParameters& paramete void CryptoAlgorithmPBKDF2::importKey(CryptoKeyFormat format, KeyData&& data, const CryptoAlgorithmParameters& parameters, bool extractable, CryptoKeyUsageBitmap usages, KeyCallback&& callback, ExceptionCallback&& exceptionCallback) { if (format != CryptoKeyFormat::Raw) { - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } if (usages & (CryptoKeyUsageEncrypt | CryptoKeyUsageDecrypt | CryptoKeyUsageSign | CryptoKeyUsageVerify | CryptoKeyUsageWrapKey | CryptoKeyUsageUnwrapKey)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } if (extractable) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSAES_PKCS1_v1_5.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSAES_PKCS1_v1_5.cpp index 9eace13625..691ad5f4e2 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSAES_PKCS1_v1_5.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSAES_PKCS1_v1_5.cpp @@ -50,7 +50,7 @@ CryptoAlgorithmIdentifier CryptoAlgorithmRSAES_PKCS1_v1_5::identifier() const void CryptoAlgorithmRSAES_PKCS1_v1_5::encrypt(const CryptoAlgorithmParameters&, Ref&& key, Vector&& plainText, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (key->type() != CryptoKeyType::Public) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } @@ -63,7 +63,7 @@ void CryptoAlgorithmRSAES_PKCS1_v1_5::encrypt(const CryptoAlgorithmParameters&, void CryptoAlgorithmRSAES_PKCS1_v1_5::decrypt(const CryptoAlgorithmParameters&, Ref&& key, Vector&& cipherText, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (key->type() != CryptoKeyType::Private) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } @@ -78,7 +78,7 @@ void CryptoAlgorithmRSAES_PKCS1_v1_5::generateKey(const CryptoAlgorithmParameter const auto& rsaParameters = downcast(parameters); if (usages & (CryptoKeyUsageSign | CryptoKeyUsageVerify | CryptoKeyUsageDeriveKey | CryptoKeyUsageDeriveBits | CryptoKeyUsageWrapKey | CryptoKeyUsageUnwrapKey)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } @@ -88,7 +88,7 @@ void CryptoAlgorithmRSAES_PKCS1_v1_5::generateKey(const CryptoAlgorithmParameter capturedCallback(WTFMove(pair)); }; auto failureCallback = [capturedCallback = WTFMove(exceptionCallback)]() { - capturedCallback(OperationError); + capturedCallback(OperationError, ""_s); }; // Notice: CryptoAlgorithmIdentifier::SHA_1 is just a placeholder. It should not have any effect. CryptoKeyRSA::generatePair(CryptoAlgorithmIdentifier::RSAES_PKCS1_v1_5, CryptoAlgorithmIdentifier::SHA_1, false, rsaParameters.modulusLength, rsaParameters.publicExponentVector(), extractable, usages, WTFMove(keyPairCallback), WTFMove(failureCallback), &context); @@ -101,15 +101,15 @@ void CryptoAlgorithmRSAES_PKCS1_v1_5::importKey(CryptoKeyFormat format, KeyData& case CryptoKeyFormat::Jwk: { JsonWebKey key = WTFMove(std::get(data)); if (usages && ((!key.d.isNull() && (usages ^ CryptoKeyUsageDecrypt)) || (key.d.isNull() && (usages ^ CryptoKeyUsageEncrypt)))) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } if (usages && !key.use.isNull() && key.use != "enc"_s) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } if (!key.alg.isNull() && key.alg != ALG) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } result = CryptoKeyRSA::importJwk(parameters.identifier, std::nullopt, WTFMove(key), extractable, usages); @@ -117,7 +117,7 @@ void CryptoAlgorithmRSAES_PKCS1_v1_5::importKey(CryptoKeyFormat format, KeyData& } case CryptoKeyFormat::Spki: { if (usages && (usages ^ CryptoKeyUsageEncrypt)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } result = CryptoKeyRSA::importSpki(parameters.identifier, std::nullopt, WTFMove(std::get>(data)), extractable, usages); @@ -125,18 +125,18 @@ void CryptoAlgorithmRSAES_PKCS1_v1_5::importKey(CryptoKeyFormat format, KeyData& } case CryptoKeyFormat::Pkcs8: { if (usages && (usages ^ CryptoKeyUsageDecrypt)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } result = CryptoKeyRSA::importPkcs8(parameters.identifier, std::nullopt, WTFMove(std::get>(data)), extractable, usages); break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } if (!result) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -148,7 +148,7 @@ void CryptoAlgorithmRSAES_PKCS1_v1_5::exportKey(CryptoKeyFormat format, Ref(key.get()); if (!rsaKey.keySizeInBits()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -163,7 +163,7 @@ void CryptoAlgorithmRSAES_PKCS1_v1_5::exportKey(CryptoKeyFormat format, Ref&& key, Vector&& data, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (key->type() != CryptoKeyType::Private) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } @@ -70,7 +70,7 @@ void CryptoAlgorithmRSASSA_PKCS1_v1_5::sign(const CryptoAlgorithmParameters&, Re void CryptoAlgorithmRSASSA_PKCS1_v1_5::verify(const CryptoAlgorithmParameters&, Ref&& key, Vector&& signature, Vector&& data, BoolCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (key->type() != CryptoKeyType::Public) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } @@ -85,7 +85,7 @@ void CryptoAlgorithmRSASSA_PKCS1_v1_5::generateKey(const CryptoAlgorithmParamete const auto& rsaParameters = downcast(parameters); if (usages & (CryptoKeyUsageDecrypt | CryptoKeyUsageEncrypt | CryptoKeyUsageDeriveKey | CryptoKeyUsageDeriveBits | CryptoKeyUsageWrapKey | CryptoKeyUsageUnwrapKey)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } @@ -95,7 +95,7 @@ void CryptoAlgorithmRSASSA_PKCS1_v1_5::generateKey(const CryptoAlgorithmParamete capturedCallback(WTFMove(pair)); }; auto failureCallback = [capturedCallback = WTFMove(exceptionCallback)]() { - capturedCallback(OperationError); + capturedCallback(OperationError, ""_s); }; CryptoKeyRSA::generatePair(CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5, rsaParameters.hashIdentifier, true, rsaParameters.modulusLength, rsaParameters.publicExponentVector(), extractable, usages, WTFMove(keyPairCallback), WTFMove(failureCallback), &context); } @@ -112,11 +112,11 @@ void CryptoAlgorithmRSASSA_PKCS1_v1_5::importKey(CryptoKeyFormat format, KeyData JsonWebKey key = WTFMove(std::get(data)); if (usages && ((!key.d.isNull() && (usages ^ CryptoKeyUsageSign)) || (key.d.isNull() && (usages ^ CryptoKeyUsageVerify)))) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } if (usages && !key.use.isNull() && key.use != "sig"_s) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -141,7 +141,7 @@ void CryptoAlgorithmRSASSA_PKCS1_v1_5::importKey(CryptoKeyFormat format, KeyData break; } if (!isMatched) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -150,7 +150,7 @@ void CryptoAlgorithmRSASSA_PKCS1_v1_5::importKey(CryptoKeyFormat format, KeyData } case CryptoKeyFormat::Spki: { if (usages && (usages ^ CryptoKeyUsageVerify)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } // FIXME: @@ -159,7 +159,7 @@ void CryptoAlgorithmRSASSA_PKCS1_v1_5::importKey(CryptoKeyFormat format, KeyData } case CryptoKeyFormat::Pkcs8: { if (usages && (usages ^ CryptoKeyUsageSign)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } // FIXME: @@ -167,11 +167,11 @@ void CryptoAlgorithmRSASSA_PKCS1_v1_5::importKey(CryptoKeyFormat format, KeyData break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } if (!result) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -184,7 +184,7 @@ void CryptoAlgorithmRSASSA_PKCS1_v1_5::exportKey(CryptoKeyFormat format, Ref(key.get()); if (!rsaKey.keySizeInBits()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -217,7 +217,7 @@ void CryptoAlgorithmRSASSA_PKCS1_v1_5::exportKey(CryptoKeyFormat format, Ref&& key, Vector&& plainText, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (key->type() != CryptoKeyType::Public) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } @@ -72,7 +72,7 @@ void CryptoAlgorithmRSA_OAEP::encrypt(const CryptoAlgorithmParameters& parameter void CryptoAlgorithmRSA_OAEP::decrypt(const CryptoAlgorithmParameters& parameters, Ref&& key, Vector&& cipherText, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (key->type() != CryptoKeyType::Private) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } @@ -87,7 +87,7 @@ void CryptoAlgorithmRSA_OAEP::generateKey(const CryptoAlgorithmParameters& param const auto& rsaParameters = downcast(parameters); if (usages & (CryptoKeyUsageSign | CryptoKeyUsageVerify | CryptoKeyUsageDeriveKey | CryptoKeyUsageDeriveBits)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } @@ -97,7 +97,7 @@ void CryptoAlgorithmRSA_OAEP::generateKey(const CryptoAlgorithmParameters& param capturedCallback(WTFMove(pair)); }; auto failureCallback = [capturedCallback = WTFMove(exceptionCallback)]() { - capturedCallback(OperationError); + capturedCallback(OperationError, ""_s); }; CryptoKeyRSA::generatePair(CryptoAlgorithmIdentifier::RSA_OAEP, rsaParameters.hashIdentifier, true, rsaParameters.modulusLength, rsaParameters.publicExponentVector(), extractable, usages, WTFMove(keyPairCallback), WTFMove(failureCallback), &context); } @@ -125,12 +125,12 @@ void CryptoAlgorithmRSA_OAEP::importKey(CryptoKeyFormat format, KeyData&& data, } isUsagesAllowed = isUsagesAllowed || !usages; if (!isUsagesAllowed) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } if (usages && !key.use.isNull() && key.use != "enc"_s) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -155,7 +155,7 @@ void CryptoAlgorithmRSA_OAEP::importKey(CryptoKeyFormat format, KeyData&& data, break; } if (!isMatched) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -164,7 +164,7 @@ void CryptoAlgorithmRSA_OAEP::importKey(CryptoKeyFormat format, KeyData&& data, } case CryptoKeyFormat::Spki: { if (usages && (usages ^ CryptoKeyUsageEncrypt) && (usages ^ CryptoKeyUsageWrapKey) && (usages ^ (CryptoKeyUsageEncrypt | CryptoKeyUsageWrapKey))) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } // FIXME: @@ -173,7 +173,7 @@ void CryptoAlgorithmRSA_OAEP::importKey(CryptoKeyFormat format, KeyData&& data, } case CryptoKeyFormat::Pkcs8: { if (usages && (usages ^ CryptoKeyUsageDecrypt) && (usages ^ CryptoKeyUsageUnwrapKey) && (usages ^ (CryptoKeyUsageDecrypt | CryptoKeyUsageUnwrapKey))) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } // FIXME: @@ -181,11 +181,11 @@ void CryptoAlgorithmRSA_OAEP::importKey(CryptoKeyFormat format, KeyData&& data, break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } if (!result) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -198,7 +198,7 @@ void CryptoAlgorithmRSA_OAEP::exportKey(CryptoKeyFormat format, Ref&& const auto& rsaKey = downcast(key.get()); if (!rsaKey.keySizeInBits()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -232,7 +232,7 @@ void CryptoAlgorithmRSA_OAEP::exportKey(CryptoKeyFormat format, Ref&& // FIXME: auto spki = rsaKey.exportSpki(); if (spki.hasException()) { - exceptionCallback(spki.releaseException().code()); + exceptionCallback(spki.releaseException().code(), ""_s); return; } result = spki.releaseReturnValue(); @@ -242,14 +242,14 @@ void CryptoAlgorithmRSA_OAEP::exportKey(CryptoKeyFormat format, Ref&& // FIXME: auto pkcs8 = rsaKey.exportPkcs8(); if (pkcs8.hasException()) { - exceptionCallback(pkcs8.releaseException().code()); + exceptionCallback(pkcs8.releaseException().code(), ""_s); return; } result = pkcs8.releaseReturnValue(); break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSA_PSS.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSA_PSS.cpp index 2d3f5d9313..11a851ff4a 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSA_PSS.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSA_PSS.cpp @@ -59,7 +59,7 @@ CryptoAlgorithmIdentifier CryptoAlgorithmRSA_PSS::identifier() const void CryptoAlgorithmRSA_PSS::sign(const CryptoAlgorithmParameters& parameters, Ref&& key, Vector&& data, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (key->type() != CryptoKeyType::Private) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } @@ -72,7 +72,7 @@ void CryptoAlgorithmRSA_PSS::sign(const CryptoAlgorithmParameters& parameters, R void CryptoAlgorithmRSA_PSS::verify(const CryptoAlgorithmParameters& parameters, Ref&& key, Vector&& signature, Vector&& data, BoolCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) { if (key->type() != CryptoKeyType::Public) { - exceptionCallback(InvalidAccessError); + exceptionCallback(InvalidAccessError, ""_s); return; } @@ -87,7 +87,7 @@ void CryptoAlgorithmRSA_PSS::generateKey(const CryptoAlgorithmParameters& parame const auto& rsaParameters = downcast(parameters); if (usages & (CryptoKeyUsageDecrypt | CryptoKeyUsageEncrypt | CryptoKeyUsageDeriveKey | CryptoKeyUsageDeriveBits | CryptoKeyUsageWrapKey | CryptoKeyUsageUnwrapKey)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } @@ -97,7 +97,7 @@ void CryptoAlgorithmRSA_PSS::generateKey(const CryptoAlgorithmParameters& parame capturedCallback(WTFMove(pair)); }; auto failureCallback = [capturedCallback = WTFMove(exceptionCallback)]() { - capturedCallback(OperationError); + capturedCallback(OperationError, ""_s); }; CryptoKeyRSA::generatePair(CryptoAlgorithmIdentifier::RSA_PSS, rsaParameters.hashIdentifier, true, rsaParameters.modulusLength, rsaParameters.publicExponentVector(), extractable, usages, WTFMove(keyPairCallback), WTFMove(failureCallback), &context); } @@ -114,11 +114,11 @@ void CryptoAlgorithmRSA_PSS::importKey(CryptoKeyFormat format, KeyData&& data, c JsonWebKey key = WTFMove(std::get(data)); if (usages && ((!key.d.isNull() && (usages ^ CryptoKeyUsageSign)) || (key.d.isNull() && (usages ^ CryptoKeyUsageVerify)))) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } if (usages && !key.use.isNull() && key.use != "sig"_s) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -143,7 +143,7 @@ void CryptoAlgorithmRSA_PSS::importKey(CryptoKeyFormat format, KeyData&& data, c break; } if (!isMatched) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -152,7 +152,7 @@ void CryptoAlgorithmRSA_PSS::importKey(CryptoKeyFormat format, KeyData&& data, c } case CryptoKeyFormat::Spki: { if (usages && (usages ^ CryptoKeyUsageVerify)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } // FIXME: @@ -161,7 +161,7 @@ void CryptoAlgorithmRSA_PSS::importKey(CryptoKeyFormat format, KeyData&& data, c } case CryptoKeyFormat::Pkcs8: { if (usages && (usages ^ CryptoKeyUsageSign)) { - exceptionCallback(SyntaxError); + exceptionCallback(SyntaxError, ""_s); return; } // FIXME: @@ -169,11 +169,11 @@ void CryptoAlgorithmRSA_PSS::importKey(CryptoKeyFormat format, KeyData&& data, c break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } if (!result) { - exceptionCallback(DataError); + exceptionCallback(DataError, ""_s); return; } @@ -186,7 +186,7 @@ void CryptoAlgorithmRSA_PSS::exportKey(CryptoKeyFormat format, Ref&& const auto& rsaKey = downcast(key.get()); if (!rsaKey.keySizeInBits()) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } @@ -219,7 +219,7 @@ void CryptoAlgorithmRSA_PSS::exportKey(CryptoKeyFormat format, Ref&& case CryptoKeyFormat::Spki: { auto spki = rsaKey.exportSpki(); if (spki.hasException()) { - exceptionCallback(spki.releaseException().code()); + exceptionCallback(spki.releaseException().code(), ""_s); return; } result = spki.releaseReturnValue(); @@ -228,14 +228,14 @@ void CryptoAlgorithmRSA_PSS::exportKey(CryptoKeyFormat format, Ref&& case CryptoKeyFormat::Pkcs8: { auto pkcs8 = rsaKey.exportPkcs8(); if (pkcs8.hasException()) { - exceptionCallback(pkcs8.releaseException().code()); + exceptionCallback(pkcs8.releaseException().code(), ""_s); return; } result = pkcs8.releaseReturnValue(); break; } default: - exceptionCallback(NotSupportedError); + exceptionCallback(NotSupportedError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA1.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA1.cpp index 8a2a8829fa..e2691e87a0 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA1.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA1.cpp @@ -47,7 +47,7 @@ void CryptoAlgorithmSHA1::digest(Vector&& message, VectorCallback&& cal { auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_1); if (!digest) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA224.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA224.cpp index 3591215bf1..9333c304ad 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA224.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA224.cpp @@ -47,7 +47,7 @@ void CryptoAlgorithmSHA224::digest(Vector&& message, VectorCallback&& c { auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_224); if (!digest) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA256.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA256.cpp index c9bad917bb..c04dfc790c 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA256.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA256.cpp @@ -47,7 +47,7 @@ void CryptoAlgorithmSHA256::digest(Vector&& message, VectorCallback&& c { auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256); if (!digest) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA384.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA384.cpp index 890d317b94..0297099f98 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA384.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA384.cpp @@ -47,7 +47,7 @@ void CryptoAlgorithmSHA384::digest(Vector&& message, VectorCallback&& c { auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_384); if (!digest) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA512.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA512.cpp index 38a9fbe18a..edb1f8b492 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA512.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA512.cpp @@ -47,7 +47,7 @@ void CryptoAlgorithmSHA512::digest(Vector&& message, VectorCallback&& c { auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_512); if (!digest) { - exceptionCallback(OperationError); + exceptionCallback(OperationError, ""_s); return; } diff --git a/src/bun.js/bindings/webcrypto/CryptoKey.cpp b/src/bun.js/bindings/webcrypto/CryptoKey.cpp index 7902dd5d90..dc7f55af8b 100644 --- a/src/bun.js/bindings/webcrypto/CryptoKey.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoKey.cpp @@ -32,7 +32,10 @@ #include "WebCoreOpaqueRoot.h" #include #include - +#include +#include "CryptoKeyRSA.h" +#include "CryptoKeyEC.h" +#include "CryptoKeyHMAC.h" namespace WebCore { CryptoKey::CryptoKey(CryptoAlgorithmIdentifier algorithmIdentifier, Type type, bool extractable, CryptoKeyUsageBitmap usages) diff --git a/src/bun.js/bindings/webcrypto/CryptoKey.h b/src/bun.js/bindings/webcrypto/CryptoKey.h index 066ff3de1e..57a0725e36 100644 --- a/src/bun.js/bindings/webcrypto/CryptoKey.h +++ b/src/bun.js/bindings/webcrypto/CryptoKey.h @@ -25,8 +25,6 @@ #pragma once -#if ENABLE(WEB_CRYPTO) - #include "CryptoAesKeyAlgorithm.h" #include "CryptoAlgorithmIdentifier.h" #include "CryptoEcKeyAlgorithm.h" @@ -101,5 +99,3 @@ WebCoreOpaqueRoot root(CryptoKey*); return key.keyClass() == WebCore::KeyClass; \ } \ SPECIALIZE_TYPE_TRAITS_END() - -#endif // ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/JSCryptoKey.cpp b/src/bun.js/bindings/webcrypto/JSCryptoKey.cpp index ff2140d0d7..45887d719a 100644 --- a/src/bun.js/bindings/webcrypto/JSCryptoKey.cpp +++ b/src/bun.js/bindings/webcrypto/JSCryptoKey.cpp @@ -174,6 +174,28 @@ static const HashTableValue JSCryptoKeyPrototypeTableValues[] = { const ClassInfo JSCryptoKeyPrototype::s_info = { "CryptoKey"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCryptoKeyPrototype) }; +JSCryptoKey* JSCryptoKey::fromJS(JSGlobalObject* globalObject, JSValue value) +{ + if (value.inherits()) { + return jsCast(value); + } + + JSObject* object = value.getObject(); + if (!object) { + return nullptr; + } + + auto& vm = globalObject->vm(); + + auto& names = WebCore::builtinNames(vm); + + if (auto nativeValue = object->getIfPropertyExists(globalObject, names.bunNativePtrPrivateName())) { + return jsDynamicCast(nativeValue); + } + + return nullptr; +} + void JSCryptoKeyPrototype::finishCreation(VM& vm) { Base::finishCreation(vm); diff --git a/src/bun.js/bindings/webcrypto/JSCryptoKey.h b/src/bun.js/bindings/webcrypto/JSCryptoKey.h index af09c01381..2dcf45538f 100644 --- a/src/bun.js/bindings/webcrypto/JSCryptoKey.h +++ b/src/bun.js/bindings/webcrypto/JSCryptoKey.h @@ -44,6 +44,8 @@ public: static CryptoKey* toWrapped(JSC::VM&, JSC::JSValue); static void destroy(JSC::JSCell*); + static JSCryptoKey* fromJS(JSGlobalObject* globalObject, JSValue value); + DECLARE_INFO; static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) diff --git a/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp b/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp index 92780d31ad..7120ed30a7 100644 --- a/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp +++ b/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp @@ -67,6 +67,7 @@ #include #include #include +#include "ErrorCode.h" namespace WebCore { using namespace JSC; @@ -378,7 +379,10 @@ static inline JSC::EncodedJSValue jsSubtleCryptoPrototypeFunction_digestBody(JSC RETURN_IF_EXCEPTION(throwScope, {}); EnsureStillAliveScope argument1 = callFrame->uncheckedArgument(1); auto data = convert>(*lexicalGlobalObject, argument1.value()); - RETURN_IF_EXCEPTION(throwScope, {}); + if (UNLIKELY(throwScope.exception())) { + throwScope.clearException(); + return Bun::ERR::INVALID_ARG_TYPE(throwScope, lexicalGlobalObject, "data"_s, "ArrayBuffer, Buffer, TypedArray, or DataView"_s, argument1.value()); + } RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS>(*lexicalGlobalObject, *castedThis->globalObject(), throwScope, [&]() -> decltype(auto) { return impl.digest(*jsCast(lexicalGlobalObject), WTFMove(algorithm), WTFMove(data), WTFMove(promise)); }))); } diff --git a/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp b/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp index 961b714f65..cf0c2ae915 100644 --- a/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp +++ b/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp @@ -443,8 +443,12 @@ static CryptoKeyUsageBitmap toCryptoKeyUsageBitmap(const Vector& } // Maybe we want more specific error messages? -static void rejectWithException(Ref&& passedPromise, ExceptionCode ec) +static void rejectWithException(Ref&& passedPromise, ExceptionCode ec, const String& msg) { + if (!msg.isEmpty()) { + passedPromise->reject(ec, msg); + return; + } switch (ec) { case NotSupportedError: passedPromise->reject(ec, "The algorithm is not supported"_s); @@ -613,9 +617,9 @@ void SubtleCrypto::encrypt(JSC::JSGlobalObject& state, AlgorithmIdentifier&& alg if (auto promise = getPromise(index, weakThis)) fulfillPromiseWithArrayBuffer(promise.releaseNonNull(), cipherText.data(), cipherText.size()); }; - auto exceptionCallback = [index, weakThis](ExceptionCode ec) mutable { + auto exceptionCallback = [index, weakThis](ExceptionCode ec, const String& msg) mutable { if (auto promise = getPromise(index, weakThis)) - rejectWithException(promise.releaseNonNull(), ec); + rejectWithException(promise.releaseNonNull(), ec, msg); }; algorithm->encrypt(*params, key, WTFMove(data), WTFMove(callback), WTFMove(exceptionCallback), *scriptExecutionContext(), m_workQueue); @@ -653,9 +657,9 @@ void SubtleCrypto::decrypt(JSC::JSGlobalObject& state, AlgorithmIdentifier&& alg if (auto promise = getPromise(index, weakThis)) fulfillPromiseWithArrayBuffer(promise.releaseNonNull(), plainText.data(), plainText.size()); }; - auto exceptionCallback = [index, weakThis](ExceptionCode ec) mutable { + auto exceptionCallback = [index, weakThis](ExceptionCode ec, const String& msg) mutable { if (auto promise = getPromise(index, weakThis)) - rejectWithException(promise.releaseNonNull(), ec); + rejectWithException(promise.releaseNonNull(), ec, msg); }; algorithm->decrypt(*params, key, WTFMove(data), WTFMove(callback), WTFMove(exceptionCallback), *scriptExecutionContext(), m_workQueue); @@ -691,9 +695,9 @@ void SubtleCrypto::sign(JSC::JSGlobalObject& state, AlgorithmIdentifier&& algori if (auto promise = getPromise(index, weakThis)) fulfillPromiseWithArrayBuffer(promise.releaseNonNull(), signature.data(), signature.size()); }; - auto exceptionCallback = [index, weakThis](ExceptionCode ec) mutable { + auto exceptionCallback = [index, weakThis](ExceptionCode ec, const String& msg) mutable { if (auto promise = getPromise(index, weakThis)) - rejectWithException(promise.releaseNonNull(), ec); + rejectWithException(promise.releaseNonNull(), ec, msg); }; algorithm->sign(*params, key, WTFMove(data), WTFMove(callback), WTFMove(exceptionCallback), *scriptExecutionContext(), m_workQueue); @@ -730,9 +734,9 @@ void SubtleCrypto::verify(JSC::JSGlobalObject& state, AlgorithmIdentifier&& algo if (auto promise = getPromise(index, weakThis)) promise->resolve(result); }; - auto exceptionCallback = [index, weakThis](ExceptionCode ec) mutable { + auto exceptionCallback = [index, weakThis](ExceptionCode ec, const String& msg) mutable { if (auto promise = getPromise(index, weakThis)) - rejectWithException(promise.releaseNonNull(), ec); + rejectWithException(promise.releaseNonNull(), ec, msg); }; algorithm->verify(*params, key, WTFMove(signature), WTFMove(data), WTFMove(callback), WTFMove(exceptionCallback), *scriptExecutionContext(), m_workQueue); @@ -742,7 +746,7 @@ void SubtleCrypto::digest(JSC::JSGlobalObject& state, AlgorithmIdentifier&& algo { auto paramsOrException = normalizeCryptoAlgorithmParameters(state, WTFMove(algorithmIdentifier), Operations::Digest); if (paramsOrException.hasException()) { - promise->reject(paramsOrException.releaseException()); + promise->reject(paramsOrException.releaseException().code(), "Unrecognized algorithm name"_s); return; } auto params = paramsOrException.releaseReturnValue(); @@ -758,9 +762,9 @@ void SubtleCrypto::digest(JSC::JSGlobalObject& state, AlgorithmIdentifier&& algo if (auto promise = getPromise(index, weakThis)) fulfillPromiseWithArrayBuffer(promise.releaseNonNull(), digest.data(), digest.size()); }; - auto exceptionCallback = [index, weakThis](ExceptionCode ec) mutable { + auto exceptionCallback = [index, weakThis](ExceptionCode ec, const String& msg) mutable { if (auto promise = getPromise(index, weakThis)) - rejectWithException(promise.releaseNonNull(), ec); + rejectWithException(promise.releaseNonNull(), ec, msg); }; algorithm->digest(WTFMove(data), WTFMove(callback), WTFMove(exceptionCallback), *scriptExecutionContext(), m_workQueue); @@ -788,23 +792,23 @@ void SubtleCrypto::generateKey(JSC::JSGlobalObject& state, AlgorithmIdentifier&& keyOrKeyPair, [&promise](RefPtr& key) { if ((key->type() == CryptoKeyType::Private || key->type() == CryptoKeyType::Secret) && !key->usagesBitmap()) { - rejectWithException(promise.releaseNonNull(), SyntaxError); + rejectWithException(promise.releaseNonNull(), SyntaxError, ""_s); return; } promise->resolve>(*key); }, [&promise](CryptoKeyPair& keyPair) { if (!keyPair.privateKey->usagesBitmap()) { - rejectWithException(promise.releaseNonNull(), SyntaxError); + rejectWithException(promise.releaseNonNull(), SyntaxError, ""_s); return; } promise->resolve>(keyPair); }); } }; - auto exceptionCallback = [index, weakThis](ExceptionCode ec) mutable { + auto exceptionCallback = [index, weakThis](ExceptionCode ec, const String& msg) mutable { if (auto promise = getPromise(index, weakThis)) - rejectWithException(promise.releaseNonNull(), ec); + rejectWithException(promise.releaseNonNull(), ec, msg); }; // The 26 January 2017 version of the specification suggests we should perform the following task asynchronously @@ -869,22 +873,22 @@ void SubtleCrypto::deriveKey(JSC::JSGlobalObject& state, AlgorithmIdentifier&& a auto callback = [index, weakThis](CryptoKey& key) mutable { if (auto promise = getPromise(index, weakThis)) { if ((key.type() == CryptoKeyType::Private || key.type() == CryptoKeyType::Secret) && !key.usagesBitmap()) { - rejectWithException(promise.releaseNonNull(), SyntaxError); + rejectWithException(promise.releaseNonNull(), SyntaxError, ""_s); return; } promise->resolve>(key); } }; - auto exceptionCallback = [index, weakThis](ExceptionCode ec) mutable { + auto exceptionCallback = [index, weakThis](ExceptionCode ec, const String& msg) mutable { if (auto promise = getPromise(index, weakThis)) - rejectWithException(promise.releaseNonNull(), ec); + rejectWithException(promise.releaseNonNull(), ec, msg); }; importAlgorithm->importKey(SubtleCrypto::KeyFormat::Raw, WTFMove(data), *importParams, extractable, keyUsagesBitmap, WTFMove(callback), WTFMove(exceptionCallback)); }; - auto exceptionCallback = [index, weakThis](ExceptionCode ec) mutable { + auto exceptionCallback = [index, weakThis](ExceptionCode ec, const String& msg) mutable { if (auto promise = getPromise(index, weakThis)) - rejectWithException(promise.releaseNonNull(), ec); + rejectWithException(promise.releaseNonNull(), ec, msg); }; algorithm->deriveBits(*params, baseKey, length, WTFMove(callback), WTFMove(exceptionCallback), *scriptExecutionContext(), m_workQueue); @@ -918,9 +922,9 @@ void SubtleCrypto::deriveBits(JSC::JSGlobalObject& state, AlgorithmIdentifier&& if (auto promise = getPromise(index, weakThis)) fulfillPromiseWithArrayBuffer(promise.releaseNonNull(), derivedKey.data(), derivedKey.size()); }; - auto exceptionCallback = [index, weakThis](ExceptionCode ec) mutable { + auto exceptionCallback = [index, weakThis](ExceptionCode ec, const String& msg) mutable { if (auto promise = getPromise(index, weakThis)) - rejectWithException(promise.releaseNonNull(), ec); + rejectWithException(promise.releaseNonNull(), ec, msg); }; algorithm->deriveBits(*params, baseKey, length, WTFMove(callback), WTFMove(exceptionCallback), *scriptExecutionContext(), m_workQueue); @@ -952,15 +956,15 @@ void SubtleCrypto::importKey(JSC::JSGlobalObject& state, KeyFormat format, KeyDa auto callback = [index, weakThis](CryptoKey& key) mutable { if (auto promise = getPromise(index, weakThis)) { if ((key.type() == CryptoKeyType::Private || key.type() == CryptoKeyType::Secret) && !key.usagesBitmap()) { - rejectWithException(promise.releaseNonNull(), SyntaxError); + rejectWithException(promise.releaseNonNull(), SyntaxError, ""_s); return; } promise->resolve>(key); } }; - auto exceptionCallback = [index, weakThis](ExceptionCode ec) mutable { + auto exceptionCallback = [index, weakThis](ExceptionCode ec, const String& msg) mutable { if (auto promise = getPromise(index, weakThis)) - rejectWithException(promise.releaseNonNull(), ec); + rejectWithException(promise.releaseNonNull(), ec, msg); }; // The 11 December 2014 version of the specification suggests we should perform the following task asynchronously: @@ -1003,9 +1007,9 @@ void SubtleCrypto::exportKey(KeyFormat format, CryptoKey& key, Refresolve>(key); } }; - auto exceptionCallback = [index, weakThis](ExceptionCode ec) mutable { + auto exceptionCallback = [index, weakThis](ExceptionCode ec, const String& msg) mutable { if (auto promise = getPromise(index, weakThis)) - rejectWithException(promise.releaseNonNull(), ec); + rejectWithException(promise.releaseNonNull(), ec, msg); }; // The following operation should be performed synchronously. @@ -1211,9 +1215,9 @@ void SubtleCrypto::unwrapKey(JSC::JSGlobalObject& state, KeyFormat format, Buffe } } }; - auto exceptionCallback = [index, weakThis](ExceptionCode ec) mutable { + auto exceptionCallback = [index, weakThis](ExceptionCode ec, const String& msg) mutable { if (auto promise = getPromise(index, weakThis)) - rejectWithException(promise.releaseNonNull(), ec); + rejectWithException(promise.releaseNonNull(), ec, msg); }; if (!isDecryption) { diff --git a/src/bun.js/node/node_crypto_binding.zig b/src/bun.js/node/node_crypto_binding.zig index 2605f64fb8..ebdf65a76d 100644 --- a/src/bun.js/node/node_crypto_binding.zig +++ b/src/bun.js/node/node_crypto_binding.zig @@ -40,7 +40,7 @@ fn randomInt(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSE } fn pbkdf2(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments = callframe.arguments_old(5); + const arguments = callframe.arguments_old(6); const data = try PBKDF2.fromJS(globalThis, arguments.slice(), true); diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index c31674b085..de2350021f 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -1533,13 +1533,13 @@ pub const FileSystemFlags = enum(Mode) { if (value.isInt32()) { const int: i32 = value.asInt32(); if (int < min or int > max) { - return global.ERR_OUT_OF_RANGE(comptime std.fmt.comptimePrint("mode is out of range: >= {d} && <= {d}", .{ min, max }), .{}).throw(); + return global.ERR_OUT_OF_RANGE(comptime std.fmt.comptimePrint("mode is out of range: >= {d} and <= {d}", .{ min, max }), .{}).throw(); } return @enumFromInt(int); } else { const float = value.asNumber(); if (std.math.isNan(float) or std.math.isInf(float) or float < min or float > max) { - return global.ERR_OUT_OF_RANGE(comptime std.fmt.comptimePrint("mode is out of range: >= {d} && <= {d}", .{ min, max }), .{}).throw(); + return global.ERR_OUT_OF_RANGE(comptime std.fmt.comptimePrint("mode is out of range: >= {d} and <= {d}", .{ min, max }), .{}).throw(); } return @enumFromInt(@as(i32, @intFromFloat(float))); } diff --git a/src/bun.js/node/util/validators.zig b/src/bun.js/node/util/validators.zig index a4f37d968d..a657aa5f25 100644 --- a/src/bun.js/node/util/validators.zig +++ b/src/bun.js/node/util/validators.zig @@ -65,7 +65,7 @@ pub fn validateInteger(globalThis: *JSGlobalObject, value: JSValue, comptime nam const num = value.asInt52(); if (num < min or num > max) { - return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} && <= {d}. Received {}", name_args ++ .{ min, max, num }); + return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} and <= {d}. Received {}", name_args ++ .{ min, max, num }); } return num; } @@ -85,7 +85,7 @@ pub fn validateInt32(globalThis: *JSGlobalObject, value: JSValue, comptime name_ // Use floating point comparison here to ensure values out of i32 range get caught instead of clamp/truncated. if (num < @as(f64, @floatFromInt(min)) or num > @as(f64, @floatFromInt(max))) { var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; - return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} && <= {d}. Received {}", name_args ++ .{ min, max, value.toFmt(&formatter) }); + return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} and <= {d}. Received {}", name_args ++ .{ min, max, value.toFmt(&formatter) }); } return @intFromFloat(num); } @@ -103,7 +103,7 @@ pub fn validateUint32(globalThis: *JSGlobalObject, value: JSValue, comptime name const max: i64 = @intCast(std.math.maxInt(u32)); if (num < min or num > max) { var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; - return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} && <= {d}. Received {}", name_args ++ .{ min, max, value.toFmt(&formatter) }); + return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} and <= {d}. Received {}", name_args ++ .{ min, max, value.toFmt(&formatter) }); } return @truncate(@as(u63, @intCast(num))); } @@ -130,7 +130,7 @@ pub fn validateNumber(globalThis: *JSGlobalObject, value: JSValue, comptime name } if (!valid) { if (min != null and max != null) { - return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} && <= {d}. Received {s}", name_args ++ .{ min, max, value }); + return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d} and <= {d}. Received {s}", name_args ++ .{ min, max, value }); } else if (min != null) { return throwRangeError(globalThis, "The value of \"" ++ name_fmt ++ "\" is out of range. It must be >= {d}. Received {s}", name_args ++ .{ max, value }); } else { diff --git a/src/bun.js/webcore.zig b/src/bun.js/webcore.zig index 78b9346228..36446d2dce 100644 --- a/src/bun.js/webcore.zig +++ b/src/bun.js/webcore.zig @@ -714,7 +714,7 @@ pub const Crypto = struct { } pub fn constructor(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!*Crypto { - return globalThis.throw("Crypto is not constructable", .{}); + return JSC.Error.ERR_ILLEGAL_CONSTRUCTOR.throw(globalThis, "Crypto is not constructable", .{}); } pub export fn CryptoObject__create(globalThis: *JSC.JSGlobalObject) JSC.JSValue { diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 3b0ba8c285..9190b800bc 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -1522,8 +1522,8 @@ pub const Fetch = struct { const cert = certificate_info.cert; var cert_ptr = cert.ptr; if (BoringSSL.d2i_X509(null, &cert_ptr, @intCast(cert.len))) |x509| { - defer BoringSSL.X509_free(x509); const globalObject = this.global_this; + defer x509.free(); const js_cert = X509.toJS(x509, globalObject) catch |err| { switch (err) { error.JSError => {}, diff --git a/src/deps/boringssl.translated.zig b/src/deps/boringssl.translated.zig index 6ee215f90e..60460ab810 100644 --- a/src/deps/boringssl.translated.zig +++ b/src/deps/boringssl.translated.zig @@ -154,7 +154,20 @@ pub const struct_X509_crl_st = opaque {}; pub const X509_CRL = struct_X509_crl_st; pub const struct_X509_extension_st = opaque {}; pub const X509_EXTENSION = struct_X509_extension_st; -pub const struct_x509_st = opaque {}; +pub const struct_x509_st = opaque { + pub fn dup(this: *X509) ?*X509 { + return X509_dup(this); + } + + pub fn ref(this: *X509) *X509 { + _ = X509_up_ref(this); + return this; + } + + pub fn free(this: *X509) void { + X509_free(this); + } +}; pub const X509 = struct_x509_st; pub const CRYPTO_refcount_t = u32; pub const struct_openssl_method_common_st = extern struct { diff --git a/src/fmt.zig b/src/fmt.zig index 532abb7a99..e946f2ae56 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -1767,22 +1767,28 @@ fn NewOutOfRangeFormatter(comptime T: type) type { min: i64 = std.math.maxInt(i64), max: i64 = std.math.maxInt(i64), field_name: []const u8, + msg: []const u8 = "", pub fn format(self: @This(), comptime _: []const u8, _: fmt.FormatOptions, writer: anytype) !void { try writer.writeAll("The value of \""); try writer.writeAll(self.field_name); - try writer.writeAll("\" is out of range. It "); + try writer.writeAll("\" is out of range. It must be "); + const min = self.min; const max = self.max; + const msg = self.msg; if (min != std.math.maxInt(i64) and max != std.math.maxInt(i64)) { - try std.fmt.format(writer, "must be >= {d} && <= {d}.", .{ min, max }); + try std.fmt.format(writer, ">= {d} and <= {d}.", .{ min, max }); } else if (min != std.math.maxInt(i64)) { - try std.fmt.format(writer, "must be >= {d}.", .{min}); + try std.fmt.format(writer, ">= {d}.", .{min}); } else if (max != std.math.maxInt(i64)) { - try std.fmt.format(writer, "must be <= {d}.", .{max}); + try std.fmt.format(writer, "<= {d}.", .{max}); + } else if (msg.len > 0) { + try writer.writeAll(msg); + try writer.writeByte('.'); } else { - try writer.writeAll("must be within the range of values for type "); + try writer.writeAll("within the range of values for type "); try writer.writeAll(comptime @typeName(T)); try writer.writeAll("."); } @@ -1816,10 +1822,11 @@ pub const OutOfRangeOptions = struct { min: i64 = std.math.maxInt(i64), max: i64 = std.math.maxInt(i64), field_name: []const u8, + msg: []const u8 = "", }; pub fn outOfRange(value: anytype, options: OutOfRangeOptions) OutOfRangeFormatter(@TypeOf(value)) { - return .{ .value = value, .min = options.min, .max = options.max, .field_name = options.field_name }; + return .{ .value = value, .min = options.min, .max = options.max, .field_name = options.field_name, .msg = options.msg }; } /// esbuild has an 8 character truncation of a base32 encoded bytes. this diff --git a/src/js/internal/crypto/x509.ts b/src/js/internal/crypto/x509.ts new file mode 100644 index 0000000000..6a2f5c8cbd --- /dev/null +++ b/src/js/internal/crypto/x509.ts @@ -0,0 +1,3 @@ +const isX509Certificate = $newCppFunction("JSX509Certificate.cpp", "jsIsX509Certificate", 1); + +export { isX509Certificate }; diff --git a/src/js/node/crypto.ts b/src/js/node/crypto.ts index 13ba2beb95..5198bd1239 100644 --- a/src/js/node/crypto.ts +++ b/src/js/node/crypto.ts @@ -4,6 +4,7 @@ var __getOwnPropNames = Object.getOwnPropertyNames; const StreamModule = require("node:stream"); const BufferModule = require("node:buffer"); const StringDecoder = require("node:string_decoder").StringDecoder; +const StringPrototypeToLowerCase = String.prototype.toLowerCase; const { CryptoHasher } = Bun; const { symmetricKeySize, @@ -22,7 +23,22 @@ const { privateDecrypt, privateEncrypt, publicDecrypt, -} = $cpp("KeyObject.cpp", "createNodeCryptoBinding"); + X509Certificate, +} = $cpp("KeyObject.cpp", "createKeyObjectBinding"); + +const { + statelessDH, + ecdhConvertKey, + getCurves, + certVerifySpkac, + certExportPublicKey, + certExportChallenge, + getCiphers, + _getCipherInfo, +} = $cpp("NodeCrypto.cpp", "createNodeCryptoBinding"); + +const { POINT_CONVERSION_COMPRESSED, POINT_CONVERSION_HYBRID, POINT_CONVERSION_UNCOMPRESSED } = + $processBindingConstants.crypto; const { randomInt: _randomInt, @@ -30,6 +46,53 @@ const { pbkdf2Sync: pbkdf2Sync_, } = $zig("node_crypto_binding.zig", "createNodeCryptoBindingZig"); +const { validateObject, validateString, validateInt32 } = require("internal/validators"); + +function verifySpkac(spkac, encoding) { + return certVerifySpkac(getArrayBufferOrView(spkac, "spkac", encoding)); +} +function exportPublicKey(spkac, encoding) { + return certExportPublicKey(getArrayBufferOrView(spkac, "spkac", encoding)); +} +function exportChallenge(spkac, encoding) { + return certExportChallenge(getArrayBufferOrView(spkac, "spkac", encoding)); +} + +function Certificate(): void { + if (!(this instanceof Certificate)) { + return new Certificate(); + } + + this.verifySpkac = verifySpkac; + this.exportPublicKey = exportPublicKey; + this.exportChallenge = exportChallenge; +} +Certificate.prototype = {}; +Certificate.verifySpkac = verifySpkac; +Certificate.exportPublicKey = exportPublicKey; +Certificate.exportChallenge = exportChallenge; + +function getCipherInfo(nameOrNid, options) { + if (typeof nameOrNid !== "string" && typeof nameOrNid !== "number") { + throw $ERR_INVALID_ARG_TYPE("nameOrNid", ["string", "number"], nameOrNid); + } + if (typeof nameOrNid === "number") validateInt32(nameOrNid, "nameOrNid"); + let keyLength, ivLength; + if (options !== undefined) { + validateObject(options, "options"); + ({ keyLength, ivLength } = options); + if (keyLength !== undefined) validateInt32(keyLength, "options.keyLength"); + if (ivLength !== undefined) validateInt32(ivLength, "options.ivLength"); + } + + const ret = _getCipherInfo({}, nameOrNid, keyLength, ivLength); + if (ret !== undefined) { + ret.name &&= ret.name; + ret.type &&= StringPrototypeToLowerCase.$call(ret.type); + } + return ret; +} + function randomInt(min, max, callback) { if (max == null) { max = min; @@ -1620,7 +1683,12 @@ var require_algos = __commonJS({ }, }); function pbkdf2(password, salt, iterations, keylen, digest, callback) { - const promise = pbkdf2_(password, salt, iterations, keylen, digest); + if (typeof digest === "function") { + callback = digest; + digest = undefined; + } + + const promise = pbkdf2_(password, salt, iterations, keylen, digest, callback); if (callback) { promise.then( result => callback(null, result), @@ -3081,11 +3149,7 @@ var require_decrypter = __commonJS({ var require_browser5 = __commonJS({ "node_modules/browserify-aes/browser.js"(exports) { var ciphers = require_encrypter(), - deciphers = require_decrypter(), - modes = require_list(); - function getCiphers() { - return Object.keys(modes); - } + deciphers = require_decrypter(); exports.createCipher = exports.Cipher = ciphers.createCipher; exports.createCipheriv = exports.Cipheriv = ciphers.createCipheriv; exports.createDecipher = exports.Decipher = deciphers.createDecipher; @@ -3160,9 +3224,6 @@ var require_browser6 = __commonJS({ if (desModes[suite]) return new DES({ key, iv, mode: suite, decrypt: !0 }); throw new TypeError("invalid suite type"); } - function getCiphers() { - return Object.keys(desModes).concat(aes.getCiphers()); - } exports.createCipher = exports.Cipher = createCipher; exports.createCipheriv = exports.Cipheriv = createCipheriv; exports.createDecipher = exports.Decipher = createDecipher; @@ -5514,6 +5575,39 @@ var require_browser7 = __commonJS({ } exports.DiffieHellmanGroup = exports.createDiffieHellmanGroup = exports.getDiffieHellman = getDiffieHellman; exports.createDiffieHellman = exports.DiffieHellman = createDiffieHellman; + + exports.diffieHellman = function diffieHellman(options) { + validateObject(options); + + const { privateKey, publicKey } = options; + + if (!(privateKey instanceof KeyObject)) { + throw $ERR_INVALID_ARG_VALUE("options.privateKey", privateKey); + } + + if (!(publicKey instanceof KeyObject)) { + throw $ERR_INVALID_ARG_VALUE("options.publicKey", publicKey); + } + + if (privateKey.type !== "private") { + throw $ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(privateKey.type, "private"); + } + + const publicKeyType = publicKey.type; + if (publicKeyType !== "public" && publicKeyType !== "private") { + throw $ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(publicKeyType, "private or public"); + } + + const privateType = privateKey.asymmetricKeyType; + const publicType = publicKey.asymmetricKeyType; + if (privateType !== publicType || !["dh", "ec", "x448", "x25519"].includes(privateType)) { + throw $ERR_CRYPTO_INCOMPATIBLE_KEY( + `Incompatible key types for Diffie-Hellman: ${privateType} and ${publicType}`, + ); + } + + return statelessDH(privateKey.$bunNativePtr, publicKey.$bunNativePtr); + }; }, }); @@ -5896,7 +5990,7 @@ var require_base = __commonJS({ return res; } else if ((bytes[0] === 2 || bytes[0] === 3) && bytes.length - 1 === len) return this.pointFromX(bytes.slice(1, 1 + len), bytes[0] === 3); - throw new Error("Unknown point format"); + throw $ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY("Public key is not valid for specified curve"); }; BasePoint.prototype.encodeCompressed = function (enc) { return this.encode(enc, !0); @@ -8914,8 +9008,7 @@ var require_signature = __commonJS({ function Signature(options, enc) { if (options instanceof Signature) return options; this._importDER(options, enc) || - (assert(options.r && options.s, "Signature without r or s"), - (this.r = new BN(options.r, 16)), + ((this.r = new BN(options.r, 16)), (this.s = new BN(options.s, 16)), options.recoveryParam === void 0 ? (this.recoveryParam = null) : (this.recoveryParam = options.recoveryParam)); } @@ -9212,7 +9305,7 @@ var require_signature2 = __commonJS({ R: sig.slice(0, eddsa.encodingLength), S: sig.slice(eddsa.encodingLength), }), - assert(sig.R && sig.S, "Signature without R or S"), + // assert(sig.R && sig.S, "Signature without R or S"), eddsa.isPoint(sig.R) && (this._R = sig.R), sig.S instanceof BN && (this._S = sig.S), (this._Rencoded = Array.isArray(sig.R) ? sig.R : sig.Rencoded), @@ -11355,9 +11448,6 @@ var require_browser9 = __commonJS({ "node_modules/create-ecdh/browser.js"(exports, module) { var elliptic = require_elliptic(), BN = require_bn6(); - module.exports = function (curve) { - return new ECDH(curve); - }; var aliases = { secp256k1: { name: "secp256k1", @@ -11431,6 +11521,29 @@ var require_browser9 = __commonJS({ var _priv = new BN(priv); return (_priv = _priv.toString(16)), (this.keys = this.curve.genKeyPair()), this.keys._importPrivate(_priv), this; }; + function getFormat(format) { + if (format) { + if (format === "compressed") return POINT_CONVERSION_COMPRESSED; + if (format === "hybrid") return POINT_CONVERSION_HYBRID; + if (format !== "uncompressed") throw $ERR_CRYPTO_ECDH_INVALID_FORMAT("Invalid ECDH format: " + format); + } + return POINT_CONVERSION_UNCOMPRESSED; + } + function encode(buffer, encoding) { + if (encoding && encoding !== "buffer") buffer = buffer.toString(encoding); + return buffer; + } + ECDH.convertKey = function convertKey(key, curve, inEnc, outEnc, format) { + validateString(curve, "curve"); + key = getArrayBufferOrView(key, "key", inEnc); + const f = getFormat(format); + const convertedKey = ecdhConvertKey(key, curve, f); + return encode(convertedKey, outEnc); + }; + module.exports.ECDH = ECDH; + module.exports.createECDH = function (curve) { + return new ECDH(curve); + }; function formatReturnValue(bn, enc, len) { Array.isArray(bn) || (bn = bn.toArray()); var buf = new Buffer(bn); @@ -11523,7 +11636,7 @@ var require_crypto_browserify2 = __commonJS({ exports.createDecipher = aes.createDecipher; exports.Decipheriv = aes.Decipheriv; exports.createDecipheriv = aes.createDecipheriv; - exports.getCiphers = aes.getCiphers; + exports.getCiphers = getCiphers; exports.listCiphers = aes.listCiphers; var dh = require_browser7(); exports.DiffieHellmanGroup = dh.DiffieHellmanGroup; @@ -11531,12 +11644,15 @@ var require_crypto_browserify2 = __commonJS({ exports.getDiffieHellman = dh.getDiffieHellman; exports.createDiffieHellman = dh.createDiffieHellman; exports.DiffieHellman = dh.DiffieHellman; + exports.diffieHellman = dh.diffieHellman; var sign = require_browser8(); exports.createSign = sign.createSign; exports.Sign = sign.Sign; exports.createVerify = sign.createVerify; exports.Verify = sign.Verify; - exports.createECDH = require_browser9(); + const ecdh = require_browser9(); + exports.ECDH = ecdh.ECDH; + exports.createECDH = ecdh.createECDH; exports.getRandomValues = values => crypto.getRandomValues(values); var rf = require_browser11(); exports.randomFill = rf.randomFill; @@ -11606,26 +11722,6 @@ timingSafeEqual && value: "::bunternal::", })); -const harcoded_curves = [ - "p192", - "p224", - "p256", - "p384", - "p521", - "curve25519", - "ed25519", - "secp256k1", - "secp224r1", - "prime256v1", - "prime192v1", - "secp384r1", - "secp521r1", -]; - -function getCurves() { - return harcoded_curves; -} - class KeyObject { // we use $bunNativePtr so that util.types.isKeyObject can detect it $bunNativePtr = undefined; @@ -12045,12 +12141,13 @@ crypto_exports.getRandomValues = getRandomValues; crypto_exports.randomUUID = randomUUID; crypto_exports.randomInt = randomInt; crypto_exports.getCurves = getCurves; +crypto_exports.getCipherInfo = getCipherInfo; crypto_exports.scrypt = scrypt; crypto_exports.scryptSync = scryptSync; crypto_exports.timingSafeEqual = timingSafeEqual; crypto_exports.webcrypto = webcrypto; crypto_exports.subtle = _subtle; -crypto_exports.X509Certificate = require_certificate().X509Certificate; - +crypto_exports.X509Certificate = X509Certificate; +crypto_exports.Certificate = Certificate; export default crypto_exports; /*! safe-buffer. MIT License. Feross Aboukhadijeh */ diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 2e7e28e7e1..927a2dab05 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -1079,304 +1079,320 @@ function createConnection(port, host, connectListener) { const connect = createConnection; -class Server extends EventEmitter { - [bunSocketServerConnections] = 0; - [bunSocketServerOptions]; - maxConnections = 0; - _handle = null; - - constructor(options, connectionListener) { - super(); - - if (typeof options === "function") { - connectionListener = options; - options = {}; - } else if (options == null || typeof options === "object") { - options = { ...options }; - } else { - throw new Error("bun-net-polyfill: invalid arguments"); - } - const { maxConnections } = options; - this.maxConnections = Number.isSafeInteger(maxConnections) && maxConnections > 0 ? maxConnections : 0; - - options.connectionListener = connectionListener; - this[bunSocketServerOptions] = options; +function Server(options, connectionListener): void { + if (!(this instanceof Server)) { + return new Server(options, connectionListener); } - get listening() { + EventEmitter.$apply(this, []); + + this[bunSocketServerConnections] = 0; + this[bunSocketServerOptions] = undefined; + this.maxConnections = 0; + this._handle = null; + + if (typeof options === "function") { + connectionListener = options; + options = {}; + } else if (options == null || typeof options === "object") { + options = { ...options }; + } else { + throw new Error("bun-net-polyfill: invalid arguments"); + } + const { maxConnections } = options; + this.maxConnections = Number.isSafeInteger(maxConnections) && maxConnections > 0 ? maxConnections : 0; + + options.connectionListener = connectionListener; + this[bunSocketServerOptions] = options; +} +$toClass(Server, "Server", EventEmitter); + +Object.defineProperty(Server.prototype, "listening", { + get() { return !!this._handle; - } + }, +}); - ref() { - this._handle?.ref(); - return this; - } +Server.prototype.ref = function () { + this._handle?.ref(); + return this; +}; - unref() { - this._handle?.unref(); - return this; - } +Server.prototype.unref = function () { + this._handle?.unref(); + return this; +}; - close(callback) { - if (typeof callback === "function") { - if (!this._handle) { - this.once("close", function close() { - callback($ERR_SERVER_NOT_RUNNING()); - }); - } else { - this.once("close", callback); - } - } - - if (this._handle) { - this._handle.stop(false); - this._handle = null; - } - - this._emitCloseIfDrained(); - - return this; - } - - [Symbol.asyncDispose]() { - const { resolve, reject, promise } = Promise.withResolvers(); - this.close(function (err, ...args) { - if (err) reject(err); - else resolve(...args); - }); - return promise; - } - - _emitCloseIfDrained() { - if (this._handle || this[bunSocketServerConnections] > 0) { - return; - } - process.nextTick(() => { - this.emit("close"); - }); - } - - address() { - const server = this._handle; - if (server) { - const unix = server.unix; - if (unix) { - return unix; - } - - //TODO: fix adress when host is passed - let address = server.hostname; - const type = isIP(address); - const port = server.port; - if (typeof port === "number") { - return { - port, - address, - family: type ? `IPv${type}` : undefined, - }; - } - if (type) { - return { - address, - family: type ? `IPv${type}` : undefined, - }; - } - - return address; - } - return null; - } - - getConnections(callback) { - if (typeof callback === "function") { - //in Bun case we will never error on getConnections - //node only errors if in the middle of the couting the server got disconnected, what never happens in Bun - //if disconnected will only pass null as well and 0 connected - callback(null, this._handle ? this[bunSocketServerConnections] : 0); - } - return this; - } - - listen(port, hostname, onListen) { - let backlog; - let path; - let exclusive = false; - let allowHalfOpen = false; - let reusePort = false; - let ipv6Only = false; - //port is actually path - if (typeof port === "string") { - if (Number.isSafeInteger(hostname)) { - if (hostname > 0) { - //hostname is backlog - backlog = hostname; - } - } else if (typeof hostname === "function") { - //hostname is callback - onListen = hostname; - } - - path = port; - hostname = undefined; - port = undefined; +Server.prototype.close = function (callback) { + if (typeof callback === "function") { + if (!this._handle) { + this.once("close", function close() { + callback($ERR_SERVER_NOT_RUNNING()); + }); } else { - if (typeof hostname === "function") { - onListen = hostname; - hostname = undefined; + this.once("close", callback); + } + } + + if (this._handle) { + this._handle.stop(false); + this._handle = null; + } + + this._emitCloseIfDrained(); + + return this; +}; + +Server.prototype[Symbol.asyncDispose] = function () { + const { resolve, reject, promise } = Promise.withResolvers(); + this.close(function (err, ...args) { + if (err) reject(err); + else resolve(...args); + }); + return promise; +}; + +Server.prototype._emitCloseIfDrained = function () { + if (this._handle || this[bunSocketServerConnections] > 0) { + return; + } + process.nextTick(() => { + this.emit("close"); + }); +}; + +Server.prototype.address = function () { + const server = this._handle; + if (server) { + const unix = server.unix; + if (unix) { + return unix; + } + + //TODO: fix adress when host is passed + let address = server.hostname; + const type = isIP(address); + const port = server.port; + if (typeof port === "number") { + return { + port, + address, + family: type ? `IPv${type}` : undefined, + }; + } + if (type) { + return { + address, + family: type ? `IPv${type}` : undefined, + }; + } + + return address; + } + return null; +}; + +Server.prototype.getConnections = function (callback) { + if (typeof callback === "function") { + //in Bun case we will never error on getConnections + //node only errors if in the middle of the couting the server got disconnected, what never happens in Bun + //if disconnected will only pass null as well and 0 connected + callback(null, this._handle ? this[bunSocketServerConnections] : 0); + } + return this; +}; + +Server.prototype.listen = function (port, hostname, onListen) { + let backlog; + let path; + let exclusive = false; + let allowHalfOpen = false; + let reusePort = false; + let ipv6Only = false; + //port is actually path + if (typeof port === "string") { + if (Number.isSafeInteger(hostname)) { + if (hostname > 0) { + //hostname is backlog + backlog = hostname; } + } else if (typeof hostname === "function") { + //hostname is callback + onListen = hostname; + } - if (typeof port === "function") { - onListen = port; - port = 0; - } else if (typeof port === "object") { - const options = port; - options.signal?.addEventListener("abort", () => this.close()); + path = port; + hostname = undefined; + port = undefined; + } else { + if (typeof hostname === "function") { + onListen = hostname; + hostname = undefined; + } - hostname = options.host; - exclusive = options.exclusive; - path = options.path; - port = options.port; - ipv6Only = options.ipv6Only; - allowHalfOpen = options.allowHalfOpen; - reusePort = options.reusePort; + if (typeof port === "function") { + onListen = port; + port = 0; + } else if (typeof port === "object") { + const options = port; + options.signal?.addEventListener("abort", () => this.close()); - const isLinux = process.platform === "linux"; + hostname = options.host; + exclusive = options.exclusive; + path = options.path; + port = options.port; + ipv6Only = options.ipv6Only; + allowHalfOpen = options.allowHalfOpen; + reusePort = options.reusePort; - if (!Number.isSafeInteger(port) || port < 0) { - if (path) { - const isAbstractPath = path.startsWith("\0"); - if (isLinux && isAbstractPath && (options.writableAll || options.readableAll)) { - const message = `The argument 'options' can not set readableAll or writableAll to true when path is abstract unix socket. Received ${JSON.stringify(options)}`; + const isLinux = process.platform === "linux"; - const error = new TypeError(message); - error.code = "ERR_INVALID_ARG_VALUE"; - throw error; - } - - hostname = path; - port = undefined; - } else { - let message = 'The argument \'options\' must have the property "port" or "path"'; - try { - message = `${message}. Received ${JSON.stringify(options)}`; - } catch {} + if (!Number.isSafeInteger(port) || port < 0) { + if (path) { + const isAbstractPath = path.startsWith("\0"); + if (isLinux && isAbstractPath && (options.writableAll || options.readableAll)) { + const message = `The argument 'options' can not set readableAll or writableAll to true when path is abstract unix socket. Received ${JSON.stringify(options)}`; const error = new TypeError(message); error.code = "ERR_INVALID_ARG_VALUE"; throw error; } - } else if (!Number.isSafeInteger(port) || port < 0) { - port = 0; + + hostname = path; + port = undefined; + } else { + let message = 'The argument \'options\' must have the property "port" or "path"'; + try { + message = `${message}. Received ${JSON.stringify(options)}`; + } catch {} + + const error = new TypeError(message); + error.code = "ERR_INVALID_ARG_VALUE"; + throw error; } - - // port - // host - // path Will be ignored if port is specified. See Identifying paths for IPC connections. - // backlog Common parameter of server.listen() functions. - // exclusive Default: false - // readableAll For IPC servers makes the pipe readable for all users. Default: false. - // writableAll For IPC servers makes the pipe writable for all users. Default: false. - // ipv6Only For TCP servers, setting ipv6Only to true will disable dual-stack support, i.e., binding to host :: won't make 0.0.0.0 be bound. Default: false. - // signal An AbortSignal that may be used to close a listening server. - - if (typeof options.callback === "function") onListen = options?.callback; } else if (!Number.isSafeInteger(port) || port < 0) { port = 0; } - hostname = hostname || "::"; - } - try { - var tls = undefined; - var TLSSocketClass = undefined; - const bunTLS = this[bunTlsSymbol]; - const options = this[bunSocketServerOptions]; - let contexts: Map | null = null; - if (typeof bunTLS === "function") { - [tls, TLSSocketClass] = bunTLS.$call(this, port, hostname, false); - options.servername = tls.serverName; - options.InternalSocketClass = TLSSocketClass; - contexts = tls.contexts; - if (!tls.requestCert) { - tls.rejectUnauthorized = false; - } - } else { - options.InternalSocketClass = SocketClass; - } + // port + // host + // path Will be ignored if port is specified. See Identifying paths for IPC connections. + // backlog Common parameter of server.listen() functions. + // exclusive Default: false + // readableAll For IPC servers makes the pipe readable for all users. Default: false. + // writableAll For IPC servers makes the pipe writable for all users. Default: false. + // ipv6Only For TCP servers, setting ipv6Only to true will disable dual-stack support, i.e., binding to host :: won't make 0.0.0.0 be bound. Default: false. + // signal An AbortSignal that may be used to close a listening server. - listenInCluster( - this, - null, - port, - 4, - backlog, - undefined, - exclusive, - ipv6Only, - allowHalfOpen, - reusePort, - undefined, - undefined, - path, - hostname, - tls, - contexts, - onListen, - ); - } catch (err) { - setTimeout(emitErrorNextTick, 1, this, err); + if (typeof options.callback === "function") onListen = options?.callback; + } else if (!Number.isSafeInteger(port) || port < 0) { + port = 0; } - return this; + hostname = hostname || "::"; } - [kRealListen](path, port, hostname, exclusive, ipv6Only, allowHalfOpen, reusePort, tls, contexts, onListen) { - if (path) { - this._handle = Bun.listen({ - unix: path, - tls, - allowHalfOpen: allowHalfOpen || this[bunSocketServerOptions]?.allowHalfOpen || false, - reusePort: reusePort || this[bunSocketServerOptions]?.reusePort || false, - ipv6Only: ipv6Only || this[bunSocketServerOptions]?.ipv6Only || false, - exclusive: exclusive || this[bunSocketServerOptions]?.exclusive || false, - socket: SocketClass[bunSocketServerHandlers], - }); + try { + var tls = undefined; + var TLSSocketClass = undefined; + const bunTLS = this[bunTlsSymbol]; + const options = this[bunSocketServerOptions]; + let contexts: Map | null = null; + if (typeof bunTLS === "function") { + [tls, TLSSocketClass] = bunTLS.$call(this, port, hostname, false); + options.servername = tls.serverName; + options.InternalSocketClass = TLSSocketClass; + contexts = tls.contexts; + if (!tls.requestCert) { + tls.rejectUnauthorized = false; + } } else { - this._handle = Bun.listen({ - port, - hostname, - tls, - allowHalfOpen: allowHalfOpen || this[bunSocketServerOptions]?.allowHalfOpen || false, - reusePort: reusePort || this[bunSocketServerOptions]?.reusePort || false, - ipv6Only: ipv6Only || this[bunSocketServerOptions]?.ipv6Only || false, - exclusive: exclusive || this[bunSocketServerOptions]?.exclusive || false, - socket: SocketClass[bunSocketServerHandlers], - }); + options.InternalSocketClass = SocketClass; } - //make this instance available on handlers - this._handle.data = this; + listenInCluster( + this, + null, + port, + 4, + backlog, + undefined, + exclusive, + ipv6Only, + allowHalfOpen, + reusePort, + undefined, + undefined, + path, + hostname, + tls, + contexts, + onListen, + ); + } catch (err) { + setTimeout(emitErrorNextTick, 1, this, err); + } + return this; +}; - if (contexts) { - for (const [name, context] of contexts) { - addServerName(this._handle, name, context); - } +Server.prototype[kRealListen] = function ( + path, + port, + hostname, + exclusive, + ipv6Only, + allowHalfOpen, + reusePort, + tls, + contexts, + onListen, +) { + if (path) { + this._handle = Bun.listen({ + unix: path, + tls, + allowHalfOpen: allowHalfOpen || this[bunSocketServerOptions]?.allowHalfOpen || false, + reusePort: reusePort || this[bunSocketServerOptions]?.reusePort || false, + ipv6Only: ipv6Only || this[bunSocketServerOptions]?.ipv6Only || false, + exclusive: exclusive || this[bunSocketServerOptions]?.exclusive || false, + socket: SocketClass[bunSocketServerHandlers], + }); + } else { + this._handle = Bun.listen({ + port, + hostname, + tls, + allowHalfOpen: allowHalfOpen || this[bunSocketServerOptions]?.allowHalfOpen || false, + reusePort: reusePort || this[bunSocketServerOptions]?.reusePort || false, + ipv6Only: ipv6Only || this[bunSocketServerOptions]?.ipv6Only || false, + exclusive: exclusive || this[bunSocketServerOptions]?.exclusive || false, + socket: SocketClass[bunSocketServerHandlers], + }); + } + + //make this instance available on handlers + this._handle.data = this; + + if (contexts) { + for (const [name, context] of contexts) { + addServerName(this._handle, name, context); } - - // We must schedule the emitListeningNextTick() only after the next run of - // the event loop's IO queue. Otherwise, the server may not actually be listening - // when the 'listening' event is emitted. - // - // That leads to all sorts of confusion. - // - // process.nextTick() is not sufficient because it will run before the IO queue. - setTimeout(emitListeningNextTick, 1, this, onListen?.bind(this)); } - getsockname(out) { - out.port = this.address().port; - return out; - } -} + // We must schedule the emitListeningNextTick() only after the next run of + // the event loop's IO queue. Otherwise, the server may not actually be listening + // when the 'listening' event is emitted. + // + // That leads to all sorts of confusion. + // + // process.nextTick() is not sufficient because it will run before the IO queue. + setTimeout(emitListeningNextTick, 1, this, onListen?.bind(this)); +}; + +Server.prototype.getsockname = function (out) { + out.port = this.address().port; + return out; +}; function emitErrorNextTick(self, error) { self.emit("error", error); diff --git a/src/js/node/tls.ts b/src/js/node/tls.ts index 403b09a5c5..5dae2b5cd0 100644 --- a/src/js/node/tls.ts +++ b/src/js/node/tls.ts @@ -8,6 +8,12 @@ const { Server: NetServer, [Symbol.for("::bunternal::")]: InternalTCPSocket } = const { rootCertificates, canonicalizeIP } = $cpp("NodeTLS.cpp", "createNodeTLSBinding"); +const { + ERR_TLS_CERT_ALTNAME_INVALID, + ERR_TLS_CERT_ALTNAME_FORMAT, + ERR_TLS_SNI_FROM_SERVER, +} = require("internal/errors"); + const SymbolReplace = Symbol.replace; const RegExpPrototypeSymbolReplace = RegExp.prototype[SymbolReplace]; const RegExpPrototypeExec = RegExp.prototype.exec; @@ -133,9 +139,7 @@ function splitEscapedAltNames(altNames) { 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.code = "ERR_TLS_CERT_ALTNAME_FORMAT"; - throw error; + throw $ERR_TLS_CERT_ALTNAME_FORMAT("Invalid subject alternative name string"); } currentToken += JSON.parse(match[0]); offset = nextQuote + match[0].length; @@ -202,8 +206,7 @@ function checkServerIdentity(hostname, cert) { 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"; + let error = $ERR_TLS_CERT_ALTNAME_INVALID(`Hostname/IP does not match certificate's altnames: ${reason}`); error.reason = reason; error.host = hostname; error.cert = cert; @@ -280,28 +283,6 @@ function createSecureContext(options) { // 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.$call(/([^\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; } @@ -468,9 +449,7 @@ const TLSSocket = (function (InternalTLSSocket) { 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; + throw $ERR_TLS_SNI_FROM_SERVER("Cannot issue SNI from a TLS server-side socket"); } // 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; @@ -497,10 +476,10 @@ const TLSSocket = (function (InternalTLSSocket) { } } getPeerX509Certificate() { - throw Error("Not implented in Bun yet"); + return this._handle?.getPeerX509Certificate?.(); } getX509Certificate() { - throw Error("Not implented in Bun yet"); + return this._handle?.getX509Certificate?.(); } [buntls](port, host) { @@ -519,23 +498,27 @@ const TLSSocket = (function (InternalTLSSocket) { ); let CLIENT_RENEG_LIMIT = 3, CLIENT_RENEG_WINDOW = 600; -class Server extends NetServer { - key; - cert; - ca; - passphrase; - secureOptions; - _rejectUnauthorized = rejectUnauthorizedDefault; - _requestCert; - servername; - ALPNProtocols; - #contexts: Map | null = null; - constructor(options, secureConnectionListener) { - super(options, secureConnectionListener); - this.setSecureContext(options); +function Server(options, secureConnectionListener): void { + if (!(this instanceof Server)) { + return new Server(options, secureConnectionListener); } - addContext(hostname: string, context: typeof InternalSecureContext | object) { + + NetServer.$apply(this, [options, secureConnectionListener]); + + this.key = undefined; + this.cert = undefined; + this.ca = undefined; + this.passphrase = undefined; + this.secureOptions = undefined; + this._rejectUnauthorized = rejectUnauthorizedDefault; + this._requestCert = undefined; + this.servername = undefined; + this.ALPNProtocols = undefined; + + let contexts: Map | null = null; + + this.addContext = function (hostname, context) { if (typeof hostname !== "string") { throw new TypeError("hostname must be a string"); } @@ -545,11 +528,12 @@ class Server extends NetServer { if (this._handle) { addServerName(this._handle, hostname, context); } else { - if (!this.#contexts) this.#contexts = new Map(); - this.#contexts.set(hostname, context as typeof InternalSecureContext); + if (!contexts) contexts = new Map(); + contexts.set(hostname, context); } - } - setSecureContext(options) { + }; + + this.setSecureContext = function (options) { if (options instanceof InternalSecureContext) { options = options.context; } @@ -618,17 +602,17 @@ class Server extends NetServer { this._rejectUnauthorized = rejectUnauthorized; } else this._rejectUnauthorized = rejectUnauthorizedDefault; } - } + }; - getTicketKeys() { + Server.prototype.getTicketKeys = function () { throw Error("Not implented in Bun yet"); - } + }; - setTicketKeys() { + Server.prototype.setTicketKeys = function () { throw Error("Not implented in Bun yet"); - } + }; - [buntls](port, host, isClient) { + this[buntls] = function (port, host, isClient) { return [ { serverName: this.servername || host || "localhost", @@ -642,12 +626,15 @@ class Server extends NetServer { ALPNProtocols: this.ALPNProtocols, clientRenegotiationLimit: CLIENT_RENEG_LIMIT, clientRenegotiationWindow: CLIENT_RENEG_WINDOW, - contexts: this.#contexts, + contexts: contexts, }, SocketClass, ]; - } + }; + + this.setSecureContext(options); } +$toClass(Server, "Server", NetServer); function createServer(options, connectionListener) { return new Server(options, connectionListener); diff --git a/test/js/bun/http/bun-connect-x509.test.ts b/test/js/bun/http/bun-connect-x509.test.ts new file mode 100644 index 0000000000..b81b966de7 --- /dev/null +++ b/test/js/bun/http/bun-connect-x509.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from "bun:test"; +import * as harness from "harness"; +import type { Socket } from "bun"; +describe("bun.connect", () => { + test("should have peer x509 certificate", async () => { + const defer = Promise.withResolvers(); + using socket = await Bun.connect({ + hostname: "example.com", + port: 443, + tls: true, + socket: { + open(socket: Socket) {}, + close() {}, + handshake(socket: Socket) { + defer.resolve(socket); + }, + data() {}, + drain() {}, + }, + }); + await defer.promise; + const x509: import("node:crypto").X509Certificate = socket.getPeerX509Certificate(); + expect(x509.checkHost("example.com")).toBe("example.com"); + }); + + test("should have x509 certificate", async () => { + const defer = Promise.withResolvers(); + const listener = await Bun.listen({ + hostname: "localhost", + port: 0, + tls: harness.tls, + socket: { + open(socket: Socket) {}, + close() {}, + handshake(socket: Socket) { + defer.resolve(socket); + }, + data() {}, + drain() {}, + }, + }); + + const defer2 = Promise.withResolvers(); + await Bun.connect({ + hostname: listener.hostname, + port: listener.port, + tls: harness.tls, + socket: { + open(socket: Socket) {}, + close() {}, + handshake(socket: Socket) { + defer2.resolve(socket); + }, + data() {}, + drain() {}, + }, + }); + using server = await defer.promise; + using client = await defer2.promise; + function check() { + const x509: import("node:crypto").X509Certificate = server.getX509Certificate(); + const peerX509: import("node:crypto").X509Certificate = client.getPeerX509Certificate(); + expect(x509.checkHost("localhost")).toBe("localhost"); + expect(peerX509.checkHost("localhost")).toBe("localhost"); + } + check(); + Bun.gc(true); + + // GC test: + for (let i = 0; i < 1000; i++) { + server.getX509Certificate(); + client.getPeerX509Certificate(); + if (i % 100 === 0 && i > 0) { + Bun.gc(true); + } + } + + Bun.gc(true); + listener.stop(); + }); +}); diff --git a/test/js/bun/http/proxy.test.js b/test/js/bun/http/proxy.test.js index 6faa0f4f07..19ca789e6f 100644 --- a/test/js/bun/http/proxy.test.js +++ b/test/js/bun/http/proxy.test.js @@ -71,9 +71,9 @@ beforeAll(() => { }); afterAll(() => { - server.stop(); - proxy.stop(); - auth_proxy.stop(); + server.stop(true); + proxy.stop(true); + auth_proxy.stop(true); }); const test = process.env.PROXY_URL ? it : it.skip; @@ -178,13 +178,13 @@ it.each([ const path = `${tmpdir()}/bun-test-http-proxy-env-${Date.now()}.ts`; fs.writeFileSync(path, 'await fetch("https://example.com");'); - const { stdout, stderr, exitCode } = Bun.spawnSync({ + const { stderr, exitCode } = Bun.spawnSync({ cmd: [bunExe(), "run", path], env: { http_proxy: http_proxy, https_proxy: https_proxy, }, - stdout: "pipe", + stdout: "inherit", stderr: "pipe", }); diff --git a/test/js/node/crypto/pbkdf2.test.ts b/test/js/node/crypto/pbkdf2.test.ts index 5d06fb1db7..585b6554cf 100644 --- a/test/js/node/crypto/pbkdf2.test.ts +++ b/test/js/node/crypto/pbkdf2.test.ts @@ -1,4 +1,5 @@ const crypto = require("crypto"); +const common = require("../test/common"); import { describe, expect, jest, test } from "bun:test"; function testPBKDF2_(password, salt, iterations, keylen, expected) { @@ -72,13 +73,13 @@ describe("invalid inputs", () => { for (let input of ["test", [], true, undefined, null]) { test(`${input} is invalid`, () => { expect(() => crypto.pbkdf2("pass", "salt", input, 8, "sha256")).toThrow( - `The "iteration count" argument must be of type integer. Received ${input}`, + `The "iterations" argument must be of type number.${common.invalidArgTypeHelper(input)}`, ); }); } test(`{} is invalid`, () => { expect(() => crypto.pbkdf2("pass", "salt", {}, 8, "sha256")).toThrow( - `The "iteration count" argument must be of type integer. Received {}`, + `The "iterations" argument must be of type number.${common.invalidArgTypeHelper({})}`, ); }); @@ -97,7 +98,7 @@ describe("invalid inputs", () => { }); expect(() => { crypto.pbkdf2("password", "salt", 1, input, "sha256", outer); - }).toThrow("keylen must be > 0 and < 2147483647"); + }).toThrow(`The value of "keylen" is out of range. It must be >= 0 and <= 2147483647. Received ${input}`); expect(outer).not.toHaveBeenCalled(); }); }); @@ -113,20 +114,22 @@ describe("invalid inputs", () => { thrown = e as Error; } expect(thrown.code).toBe("ERR_CRYPTO_INVALID_DIGEST"); - expect(thrown.message).toBe('Unsupported algorithm "md55"'); + expect(thrown.message).toBe("Invalid digest: md55"); }); }); [Infinity, -Infinity, NaN].forEach(input => { test(`${input} keylen`, () => { expect(() => crypto.pbkdf2("password", "salt", 1, input, "sha256")).toThrow( - `The \"keylen\" argument must be of type integer. Received ${input}`, + `The value of "keylen" is out of range. It must be an integer. Received ${input}`, ); }); }); [-1, 2147483648, 4294967296].forEach(input => { test(`${input} keylen`, () => { - expect(() => crypto.pbkdf2("password", "salt", 1, input, "sha256")).toThrow("keylen must be > 0 and < 2147483647"); + expect(() => crypto.pbkdf2("password", "salt", 1, input, "sha256")).toThrow( + `The value of "keylen" is out of range. It must be >= 0 and <= 2147483647. Received ${input}`, + ); }); }); diff --git a/test/js/node/dns/node-dns.test.js b/test/js/node/dns/node-dns.test.js index 48977c9ef3..44924c3abd 100644 --- a/test/js/node/dns/node-dns.test.js +++ b/test/js/node/dns/node-dns.test.js @@ -423,7 +423,7 @@ describe("test invalid arguments", () => { }).toThrow("Expected address to be a non-empty string for 'lookupService'."); expect(() => { dns.lookupService("google.com", 443, (err, hostname, service) => {}); - }).toThrow('The "address" argument is invalid. Received google.com'); + }).toThrow('The "address" argument is invalid. Received type string ("google.com")'); }); }); diff --git a/test/js/node/test/common/index.js b/test/js/node/test/common/index.js index 78ea989be5..c07c74094d 100644 --- a/test/js/node/test/common/index.js +++ b/test/js/node/test/common/index.js @@ -65,6 +65,9 @@ const opensslVersionNumber = (major = 0, minor = 0, patch = 0) => { return (major << 28) | (minor << 20) | (patch << 4); }; +// https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/patches/node/fix_crypto_tests_to_run_with_bssl.patch#L21 +const openSSLIsBoringSSL = process.versions.boringssl !== undefined; + let OPENSSL_VERSION_NUMBER; const hasOpenSSL = (major = 0, minor = 0, patch = 0) => { if (!hasCrypto) return false; @@ -1108,6 +1111,7 @@ const common = { mustNotMutateObjectDeep, mustSucceed, nodeProcessAborted, + openSSLIsBoringSSL, PIPE, parseTestFlags, platformTimeout, diff --git a/test/js/node/test/parallel/test-crypto-aes-wrap.js b/test/js/node/test/parallel/test-crypto-aes-wrap.js new file mode 100644 index 0000000000..9fe1b02eb2 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-aes-wrap.js @@ -0,0 +1,68 @@ +/* +Skipped test +https://github.com/electron/electron/blob/e57b69f106ae9c53a527038db4e8222692fa0ce7/script/node-disabled-tests.json#L10 + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +const test = [ + { + algorithm: 'aes128-wrap', + key: 'b26f309fbe57e9b3bb6ae5ef31d54450', + iv: '3fd838af4093d749', + text: '12345678123456781234567812345678' + }, + { + algorithm: 'id-aes128-wrap-pad', + key: 'b26f309fbe57e9b3bb6ae5ef31d54450', + iv: '3fd838af', + text: '12345678123456781234567812345678123' + }, + { + algorithm: 'aes192-wrap', + key: '40978085d68091f7dfca0d7dfc7a5ee76d2cc7f2f345a304', + iv: '3fd838af4093d749', + text: '12345678123456781234567812345678' + }, + { + algorithm: 'id-aes192-wrap-pad', + key: '40978085d68091f7dfca0d7dfc7a5ee76d2cc7f2f345a304', + iv: '3fd838af', + text: '12345678123456781234567812345678123' + }, + { + algorithm: 'aes256-wrap', + key: '29c9eab5ed5ad44134a1437fe2e673b4d88a5b7c72e68454fea08721392b7323', + iv: '3fd838af4093d749', + text: '12345678123456781234567812345678' + }, + { + algorithm: 'id-aes256-wrap-pad', + key: '29c9eab5ed5ad44134a1437fe2e673b4d88a5b7c72e68454fea08721392b7323', + iv: '3fd838af', + text: '12345678123456781234567812345678123' + }, +]; + +test.forEach((data) => { + const cipher = crypto.createCipheriv( + data.algorithm, + Buffer.from(data.key, 'hex'), + Buffer.from(data.iv, 'hex')); + const ciphertext = cipher.update(data.text, 'utf8'); + + const decipher = crypto.createDecipheriv( + data.algorithm, + Buffer.from(data.key, 'hex'), + Buffer.from(data.iv, 'hex')); + const msg = decipher.update(ciphertext, 'buffer', 'utf8'); + + assert.strictEqual(msg, data.text, `${data.algorithm} test case failed`); +}); + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-authenticated-stream.js b/test/js/node/test/parallel/test-crypto-authenticated-stream.js new file mode 100644 index 0000000000..815d18abe9 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-authenticated-stream.js @@ -0,0 +1,147 @@ +/* +Skipped test +https://github.com/electron/electron/blob/e57b69f106ae9c53a527038db4e8222692fa0ce7/script/node-disabled-tests.json#L12 + +'use strict'; +// Refs: https://github.com/nodejs/node/issues/31733 +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const fs = require('fs'); +const stream = require('stream'); +const tmpdir = require('../common/tmpdir'); + +class Sink extends stream.Writable { + constructor() { + super(); + this.chunks = []; + } + + _write(chunk, encoding, cb) { + this.chunks.push(chunk); + cb(); + } +} + +function direct(config) { + const { cipher, key, iv, aad, authTagLength, plaintextLength } = config; + const expected = Buffer.alloc(plaintextLength); + + const c = crypto.createCipheriv(cipher, key, iv, { authTagLength }); + c.setAAD(aad, { plaintextLength }); + const ciphertext = Buffer.concat([c.update(expected), c.final()]); + + const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength }); + d.setAAD(aad, { plaintextLength }); + d.setAuthTag(c.getAuthTag()); + const actual = Buffer.concat([d.update(ciphertext), d.final()]); + + assert.deepStrictEqual(expected, actual); +} + +function mstream(config) { + const { cipher, key, iv, aad, authTagLength, plaintextLength } = config; + const expected = Buffer.alloc(plaintextLength); + + const c = crypto.createCipheriv(cipher, key, iv, { authTagLength }); + c.setAAD(aad, { plaintextLength }); + + const plain = new stream.PassThrough(); + const crypt = new Sink(); + const chunks = crypt.chunks; + plain.pipe(c).pipe(crypt); + plain.end(expected); + + crypt.on('close', common.mustCall(() => { + const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength }); + d.setAAD(aad, { plaintextLength }); + d.setAuthTag(c.getAuthTag()); + + const crypt = new stream.PassThrough(); + const plain = new Sink(); + crypt.pipe(d).pipe(plain); + for (const chunk of chunks) crypt.write(chunk); + crypt.end(); + + plain.on('close', common.mustCall(() => { + const actual = Buffer.concat(plain.chunks); + assert.deepStrictEqual(expected, actual); + })); + })); +} + +function fstream(config) { + const count = fstream.count++; + const filename = (name) => tmpdir.resolve(`${name}${count}`); + + const { cipher, key, iv, aad, authTagLength, plaintextLength } = config; + const expected = Buffer.alloc(plaintextLength); + fs.writeFileSync(filename('a'), expected); + + const c = crypto.createCipheriv(cipher, key, iv, { authTagLength }); + c.setAAD(aad, { plaintextLength }); + + const plain = fs.createReadStream(filename('a')); + const crypt = fs.createWriteStream(filename('b')); + plain.pipe(c).pipe(crypt); + + // Observation: 'close' comes before 'end' on |c|, which definitely feels + // wrong. Switching to `c.on('end', ...)` doesn't fix the test though. + crypt.on('close', common.mustCall(() => { + // Just to drive home the point that decryption does actually work: + // reading the file synchronously, then decrypting it, works. + { + const ciphertext = fs.readFileSync(filename('b')); + const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength }); + d.setAAD(aad, { plaintextLength }); + d.setAuthTag(c.getAuthTag()); + const actual = Buffer.concat([d.update(ciphertext), d.final()]); + assert.deepStrictEqual(expected, actual); + } + + const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength }); + d.setAAD(aad, { plaintextLength }); + d.setAuthTag(c.getAuthTag()); + + const crypt = fs.createReadStream(filename('b')); + const plain = fs.createWriteStream(filename('c')); + crypt.pipe(d).pipe(plain); + + plain.on('close', common.mustCall(() => { + const actual = fs.readFileSync(filename('c')); + assert.deepStrictEqual(expected, actual); + })); + })); +} +fstream.count = 0; + +function test(config) { + direct(config); + mstream(config); + fstream(config); +} + +tmpdir.refresh(); + +test({ + cipher: 'aes-128-ccm', + aad: Buffer.alloc(1), + iv: Buffer.alloc(8), + key: Buffer.alloc(16), + authTagLength: 16, + plaintextLength: 32768, +}); + +test({ + cipher: 'aes-128-ccm', + aad: Buffer.alloc(1), + iv: Buffer.alloc(8), + key: Buffer.alloc(16), + authTagLength: 16, + plaintextLength: 32769, +}); + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-authenticated.js b/test/js/node/test/parallel/test-crypto-authenticated.js new file mode 100644 index 0000000000..b318e4cacd --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-authenticated.js @@ -0,0 +1,709 @@ +/* +Skipped test +https://github.com/electron/electron/blob/e57b69f106ae9c53a527038db4e8222692fa0ce7/script/node-disabled-tests.json#L11 + +// 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. +// Flags: --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { inspect } = require('util'); +const fixtures = require('../common/fixtures'); + +// +// Test authenticated encryption modes. +// +// !NEVER USE STATIC IVs IN REAL LIFE! +// + +const TEST_CASES = require(fixtures.path('aead-vectors.js')); + +const errMessages = { + auth: / auth/, + state: / state/, + FIPS: /not supported in FIPS mode/, + length: /Invalid initialization vector/, + authTagLength: /Invalid authentication tag length/ +}; + +const ciphers = crypto.getCiphers(); + +for (const test of TEST_CASES) { + if (!ciphers.includes(test.algo)) { + common.printSkipMessage(`unsupported ${test.algo} test`); + continue; + } + + if (common.hasFipsCrypto && test.iv.length < 24) { + common.printSkipMessage('IV len < 12 bytes unsupported in FIPS mode'); + continue; + } + + const isCCM = /^aes-(128|192|256)-ccm$/.test(test.algo); + const isOCB = /^aes-(128|192|256)-ocb$/.test(test.algo); + + let options; + if (isCCM || isOCB) + options = { authTagLength: test.tag.length / 2 }; + + const inputEncoding = test.plainIsHex ? 'hex' : 'ascii'; + + let aadOptions; + if (isCCM) { + aadOptions = { + plaintextLength: Buffer.from(test.plain, inputEncoding).length + }; + } + + { + const encrypt = crypto.createCipheriv(test.algo, + Buffer.from(test.key, 'hex'), + Buffer.from(test.iv, 'hex'), + options); + + if (test.aad) + encrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); + + let hex = encrypt.update(test.plain, inputEncoding, 'hex'); + hex += encrypt.final('hex'); + + const auth_tag = encrypt.getAuthTag(); + // Only test basic encryption run if output is marked as tampered. + if (!test.tampered) { + assert.strictEqual(hex, test.ct); + assert.strictEqual(auth_tag.toString('hex'), test.tag); + } + } + + { + if (isCCM && common.hasFipsCrypto) { + assert.throws(() => { + crypto.createDecipheriv(test.algo, + Buffer.from(test.key, 'hex'), + Buffer.from(test.iv, 'hex'), + options); + }, errMessages.FIPS); + } else { + const decrypt = crypto.createDecipheriv(test.algo, + Buffer.from(test.key, 'hex'), + Buffer.from(test.iv, 'hex'), + options); + decrypt.setAuthTag(Buffer.from(test.tag, 'hex')); + if (test.aad) + decrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); + + const outputEncoding = test.plainIsHex ? 'hex' : 'ascii'; + + let msg = decrypt.update(test.ct, 'hex', outputEncoding); + if (!test.tampered) { + msg += decrypt.final(outputEncoding); + assert.strictEqual(msg, test.plain); + } else { + // Assert that final throws if input data could not be verified! + assert.throws(function() { decrypt.final('hex'); }, errMessages.auth); + } + } + } + + { + // Trying to get tag before inputting all data: + const encrypt = crypto.createCipheriv(test.algo, + Buffer.from(test.key, 'hex'), + Buffer.from(test.iv, 'hex'), + options); + encrypt.update('blah', 'ascii'); + assert.throws(function() { encrypt.getAuthTag(); }, errMessages.state); + } + + { + // Trying to create cipher with incorrect IV length + assert.throws(function() { + crypto.createCipheriv( + test.algo, + Buffer.from(test.key, 'hex'), + Buffer.alloc(0) + ); + }, errMessages.length); + } +} + +// Non-authenticating mode: +{ + const encrypt = + crypto.createCipheriv('aes-128-cbc', + 'ipxp9a6i1Mb4USb4', + '6fKjEjR3Vl30EUYC'); + encrypt.update('blah', 'ascii'); + encrypt.final(); + assert.throws(() => encrypt.getAuthTag(), errMessages.state); + assert.throws(() => encrypt.setAAD(Buffer.from('123', 'ascii')), + errMessages.state); +} + +// GCM only supports specific authentication tag lengths, invalid lengths should +// throw. +{ + for (const length of [0, 1, 2, 6, 9, 10, 11, 17]) { + assert.throws(() => { + const decrypt = crypto.createDecipheriv('aes-128-gcm', + 'FxLKsqdmv0E9xrQh', + 'qkuZpJWCewa6Szih'); + decrypt.setAuthTag(Buffer.from('1'.repeat(length))); + }, { + name: 'TypeError', + message: /Invalid authentication tag length/ + }); + + assert.throws(() => { + crypto.createCipheriv('aes-256-gcm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6Szih', + { + authTagLength: length + }); + }, { + name: 'TypeError', + message: /Invalid authentication tag length/ + }); + + assert.throws(() => { + crypto.createDecipheriv('aes-256-gcm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6Szih', + { + authTagLength: length + }); + }, { + name: 'TypeError', + message: /Invalid authentication tag length/ + }); + } +} + +// Test that GCM can produce shorter authentication tags than 16 bytes. +{ + const fullTag = '1debb47b2c91ba2cea16fad021703070'; + for (const [authTagLength, e] of [[undefined, 16], [12, 12], [4, 4]]) { + const cipher = crypto.createCipheriv('aes-256-gcm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6Szih', { + authTagLength + }); + cipher.setAAD(Buffer.from('abcd')); + cipher.update('01234567', 'hex'); + cipher.final(); + const tag = cipher.getAuthTag(); + assert.strictEqual(tag.toString('hex'), fullTag.slice(0, 2 * e)); + } +} + +// Test that users can manually restrict the GCM tag length to a single value. +{ + const decipher = crypto.createDecipheriv('aes-256-gcm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6Szih', { + authTagLength: 8 + }); + + assert.throws(() => { + // This tag would normally be allowed. + decipher.setAuthTag(Buffer.from('1'.repeat(12))); + }, { + name: 'TypeError', + message: /Invalid authentication tag length/ + }); + + // The Decipher object should be left intact. + decipher.setAuthTag(Buffer.from('445352d3ff85cf94', 'hex')); + const text = Buffer.concat([ + decipher.update('3a2a3647', 'hex'), + decipher.final(), + ]); + assert.strictEqual(text.toString('utf8'), 'node'); +} + +// Test that create(De|C)ipher(iv)? throws if the mode is CCM and an invalid +// authentication tag length has been specified. +{ + for (const authTagLength of [-1, true, false, NaN, 5.5]) { + assert.throws(() => { + crypto.createCipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.authTagLength' is invalid. " + + `Received ${inspect(authTagLength)}` + }); + + assert.throws(() => { + crypto.createDecipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.authTagLength' is invalid. " + + `Received ${inspect(authTagLength)}` + }); + } + + // The following values will not be caught by the JS layer and thus will not + // use the default error codes. + for (const authTagLength of [0, 1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 18]) { + assert.throws(() => { + crypto.createCipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength + }); + }, errMessages.authTagLength); + + if (!common.hasFipsCrypto) { + assert.throws(() => { + crypto.createDecipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength + }); + }, errMessages.authTagLength); + } + } +} + +// Test that create(De|C)ipher(iv)? throws if the mode is CCM or OCB and no +// authentication tag has been specified. +{ + for (const mode of ['ccm', 'ocb']) { + assert.throws(() => { + crypto.createCipheriv(`aes-256-${mode}`, + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S'); + }, { + message: `authTagLength required for aes-256-${mode}` + }); + + // CCM decryption and create(De|C)ipher are unsupported in FIPS mode. + if (!common.hasFipsCrypto) { + assert.throws(() => { + crypto.createDecipheriv(`aes-256-${mode}`, + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S'); + }, { + message: `authTagLength required for aes-256-${mode}` + }); + } + } +} + +// Test that setAAD throws if an invalid plaintext length has been specified. +{ + const cipher = crypto.createCipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength: 10 + }); + + for (const plaintextLength of [-1, true, false, NaN, 5.5]) { + assert.throws(() => { + cipher.setAAD(Buffer.from('0123456789', 'hex'), { plaintextLength }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.plaintextLength' is invalid. " + + `Received ${inspect(plaintextLength)}` + }); + } +} + +// Test that setAAD and update throw if the plaintext is too long. +{ + for (const ivLength of [13, 12]) { + const maxMessageSize = (1 << (8 * (15 - ivLength))) - 1; + const key = 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8'; + const cipher = () => crypto.createCipheriv('aes-256-ccm', key, + '0'.repeat(ivLength), + { + authTagLength: 10 + }); + + assert.throws(() => { + cipher().setAAD(Buffer.alloc(0), { + plaintextLength: maxMessageSize + 1 + }); + }, /Invalid message length$/); + + const msg = Buffer.alloc(maxMessageSize + 1); + assert.throws(() => { + cipher().update(msg); + }, /Invalid message length/); + + const c = cipher(); + c.setAAD(Buffer.alloc(0), { + plaintextLength: maxMessageSize + }); + c.update(msg.slice(1)); + } +} + +// Test that setAAD throws if the mode is CCM and the plaintext length has not +// been specified. +{ + assert.throws(() => { + const cipher = crypto.createCipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength: 10 + }); + cipher.setAAD(Buffer.from('0123456789', 'hex')); + }, /options\.plaintextLength required for CCM mode with AAD/); + + if (!common.hasFipsCrypto) { + assert.throws(() => { + const cipher = crypto.createDecipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength: 10 + }); + cipher.setAAD(Buffer.from('0123456789', 'hex')); + }, /options\.plaintextLength required for CCM mode with AAD/); + } +} + +// Test that final() throws in CCM mode when no authentication tag is provided. +{ + if (!common.hasFipsCrypto) { + const key = Buffer.from('1ed2233fa2223ef5d7df08546049406c', 'hex'); + const iv = Buffer.from('7305220bca40d4c90e1791e9', 'hex'); + const ct = Buffer.from('8beba09d4d4d861f957d51c0794f4abf8030848e', 'hex'); + const decrypt = crypto.createDecipheriv('aes-128-ccm', key, iv, { + authTagLength: 10 + }); + // Normally, we would do this: + // decrypt.setAuthTag(Buffer.from('0d9bcd142a94caf3d1dd', 'hex')); + assert.throws(() => { + decrypt.setAAD(Buffer.from('63616c76696e', 'hex'), { + plaintextLength: ct.length + }); + decrypt.update(ct); + decrypt.final(); + }, errMessages.state); + } +} + +// Test that setAuthTag does not throw in GCM mode when called after setAAD. +{ + const key = Buffer.from('1ed2233fa2223ef5d7df08546049406c', 'hex'); + const iv = Buffer.from('579d9dfde9cd93d743da1ceaeebb86e4', 'hex'); + const decrypt = crypto.createDecipheriv('aes-128-gcm', key, iv); + decrypt.setAAD(Buffer.from('0123456789', 'hex')); + decrypt.setAuthTag(Buffer.from('1bb9253e250b8069cde97151d7ef32d9', 'hex')); + assert.strictEqual(decrypt.update('807022', 'hex', 'hex'), 'abcdef'); + assert.strictEqual(decrypt.final('hex'), ''); +} + +// Test that an IV length of 11 does not overflow max_message_size_. +{ + const key = 'x'.repeat(16); + const iv = Buffer.from('112233445566778899aabb', 'hex'); + const options = { authTagLength: 8 }; + const encrypt = crypto.createCipheriv('aes-128-ccm', key, iv, options); + encrypt.update('boom'); // Should not throw 'Message exceeds maximum size'. + encrypt.final(); +} + +// Test that the authentication tag can be set at any point before calling +// final() in GCM or OCB mode. +{ + const plain = Buffer.from('Hello world', 'utf8'); + const key = Buffer.from('0123456789abcdef', 'utf8'); + const iv = Buffer.from('0123456789ab', 'utf8'); + + for (const mode of ['gcm', 'ocb']) { + for (const authTagLength of mode === 'gcm' ? [undefined, 8] : [8]) { + const cipher = crypto.createCipheriv(`aes-128-${mode}`, key, iv, { + authTagLength + }); + const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]); + const authTag = cipher.getAuthTag(); + + for (const authTagBeforeUpdate of [true, false]) { + const decipher = crypto.createDecipheriv(`aes-128-${mode}`, key, iv, { + authTagLength + }); + if (authTagBeforeUpdate) { + decipher.setAuthTag(authTag); + } + const resultUpdate = decipher.update(ciphertext); + if (!authTagBeforeUpdate) { + decipher.setAuthTag(authTag); + } + const resultFinal = decipher.final(); + const result = Buffer.concat([resultUpdate, resultFinal]); + assert(result.equals(plain)); + } + } + } +} + +// Test that setAuthTag can only be called once. +{ + const plain = Buffer.from('Hello world', 'utf8'); + const key = Buffer.from('0123456789abcdef', 'utf8'); + const iv = Buffer.from('0123456789ab', 'utf8'); + const opts = { authTagLength: 8 }; + + for (const mode of ['gcm', 'ccm', 'ocb']) { + const cipher = crypto.createCipheriv(`aes-128-${mode}`, key, iv, opts); + const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]); + const tag = cipher.getAuthTag(); + + const decipher = crypto.createDecipheriv(`aes-128-${mode}`, key, iv, opts); + decipher.setAuthTag(tag); + assert.throws(() => { + decipher.setAuthTag(tag); + }, errMessages.state); + // Decryption should still work. + const plaintext = Buffer.concat([ + decipher.update(ciphertext), + decipher.final(), + ]); + assert(plain.equals(plaintext)); + } +} + + +// Test chacha20-poly1305 rejects invalid IV lengths of 13, 14, 15, and 16 (a +// length of 17 or greater was already rejected). +// - https://www.openssl.org/news/secadv/20190306.txt +{ + // Valid extracted from TEST_CASES, check that it detects IV tampering. + const valid = { + algo: 'chacha20-poly1305', + key: '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f', + iv: '070000004041424344454647', + plain: '4c616469657320616e642047656e746c656d656e206f662074686520636c6173' + + '73206f66202739393a204966204920636f756c64206f6666657220796f75206f' + + '6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73' + + '637265656e20776f756c642062652069742e', + plainIsHex: true, + aad: '50515253c0c1c2c3c4c5c6c7', + ct: 'd31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5' + + 'a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e06' + + '0b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fa' + + 'b324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d265' + + '86cec64b6116', + tag: '1ae10b594f09e26a7e902ecbd0600691', + tampered: false, + }; + + // Invalid IV lengths should be detected: + // - 12 and below are valid. + // - 13-16 are not detected as invalid by some OpenSSL versions. + check(13); + check(14); + check(15); + check(16); + // - 17 and above were always detected as invalid by OpenSSL. + check(17); + + function check(ivLength) { + const prefix = ivLength - valid.iv.length / 2; + assert.throws(() => crypto.createCipheriv( + valid.algo, + Buffer.from(valid.key, 'hex'), + Buffer.from(H(prefix) + valid.iv, 'hex') + ), errMessages.length, `iv length ${ivLength} was not rejected`); + + function H(length) { return '00'.repeat(length); } + } +} + +{ + // CCM cipher without data should not crash, see https://github.com/nodejs/node/issues/38035. + const algo = 'aes-128-ccm'; + const key = Buffer.alloc(16); + const iv = Buffer.alloc(12); + const opts = { authTagLength: 10 }; + + for (const cipher of [ + crypto.createCipheriv(algo, key, iv, opts), + ]) { + assert.throws(() => { + cipher.final(); + }, common.hasOpenSSL3 ? { + code: 'ERR_OSSL_TAG_NOT_SET' + } : { + message: /Unsupported state/ + }); + } +} + +{ + const key = Buffer.alloc(32); + const iv = Buffer.alloc(12); + + for (const authTagLength of [0, 17]) { + assert.throws(() => { + crypto.createCipheriv('chacha20-poly1305', key, iv, { authTagLength }); + }, { + code: 'ERR_CRYPTO_INVALID_AUTH_TAG', + message: errMessages.authTagLength + }); + } +} + +// ChaCha20-Poly1305 should respect the authTagLength option and should not +// require the authentication tag before calls to update() during decryption. +{ + const key = Buffer.alloc(32); + const iv = Buffer.alloc(12); + + for (let authTagLength = 1; authTagLength <= 16; authTagLength++) { + const cipher = + crypto.createCipheriv('chacha20-poly1305', key, iv, { authTagLength }); + const ciphertext = Buffer.concat([cipher.update('foo'), cipher.final()]); + const authTag = cipher.getAuthTag(); + assert.strictEqual(authTag.length, authTagLength); + + // The decipher operation should reject all authentication tags other than + // that of the expected length. + for (let other = 1; other <= 16; other++) { + const decipher = crypto.createDecipheriv('chacha20-poly1305', key, iv, { + authTagLength: other + }); + // ChaCha20 is a stream cipher so we do not need to call final() to obtain + // the full plaintext. + const plaintext = decipher.update(ciphertext); + assert.strictEqual(plaintext.toString(), 'foo'); + if (other === authTagLength) { + // The authentication tag length is as expected and the tag itself is + // correct, so this should work. + decipher.setAuthTag(authTag); + decipher.final(); + } else { + // The authentication tag that we are going to pass to setAuthTag is + // either too short or too long. If other < authTagLength, the + // authentication tag is still correct, but it should still be rejected + // because its security assurance is lower than expected. + assert.throws(() => { + decipher.setAuthTag(authTag); + }, { + code: 'ERR_CRYPTO_INVALID_AUTH_TAG', + message: `Invalid authentication tag length: ${authTagLength}` + }); + } + } + } +} + +// ChaCha20-Poly1305 should default to an authTagLength of 16. When encrypting, +// this matches the behavior of GCM ciphers. When decrypting, however, it is +// stricter than GCM in that it only allows authentication tags that are exactly +// 16 bytes long, whereas, when no authTagLength was specified, GCM would accept +// shorter tags as long as their length was valid according to NIST SP 800-38D. +// For ChaCha20-Poly1305, we intentionally deviate from that because there are +// no recommended or approved authentication tag lengths below 16 bytes. +{ + const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { + return algo === 'chacha20-poly1305' && tampered === false; + }); + assert.strictEqual(rfcTestCases.length, 1); + + const [testCase] = rfcTestCases; + const key = Buffer.from(testCase.key, 'hex'); + const iv = Buffer.from(testCase.iv, 'hex'); + const aad = Buffer.from(testCase.aad, 'hex'); + + for (const opt of [ + undefined, + { authTagLength: undefined }, + { authTagLength: 16 }, + ]) { + const cipher = crypto.createCipheriv('chacha20-poly1305', key, iv, opt); + const ciphertext = Buffer.concat([ + cipher.setAAD(aad).update(testCase.plain, 'hex'), + cipher.final(), + ]); + const authTag = cipher.getAuthTag(); + + assert.strictEqual(ciphertext.toString('hex'), testCase.ct); + assert.strictEqual(authTag.toString('hex'), testCase.tag); + + const decipher = crypto.createDecipheriv('chacha20-poly1305', key, iv, opt); + const plaintext = Buffer.concat([ + decipher.setAAD(aad).update(ciphertext), + decipher.setAuthTag(authTag).final(), + ]); + + assert.strictEqual(plaintext.toString('hex'), testCase.plain); + } +} + +// https://github.com/nodejs/node/issues/45874 +{ + const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { + return algo === 'chacha20-poly1305' && tampered === false; + }); + assert.strictEqual(rfcTestCases.length, 1); + + const [testCase] = rfcTestCases; + const key = Buffer.from(testCase.key, 'hex'); + const iv = Buffer.from(testCase.iv, 'hex'); + const aad = Buffer.from(testCase.aad, 'hex'); + const opt = { authTagLength: 16 }; + + const cipher = crypto.createCipheriv('chacha20-poly1305', key, iv, opt); + const ciphertext = Buffer.concat([ + cipher.setAAD(aad).update(testCase.plain, 'hex'), + cipher.final(), + ]); + const authTag = cipher.getAuthTag(); + + assert.strictEqual(ciphertext.toString('hex'), testCase.ct); + assert.strictEqual(authTag.toString('hex'), testCase.tag); + + const decipher = crypto.createDecipheriv('chacha20-poly1305', key, iv, opt); + decipher.setAAD(aad).update(ciphertext); + + assert.throws(() => { + decipher.final(); + }, /Unsupported state or unable to authenticate data/); +} + +*/ diff --git a/test/js/node/test/parallel/test-crypto-certificate.js b/test/js/node/test/parallel/test-crypto-certificate.js new file mode 100644 index 0000000000..da932c608d --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-certificate.js @@ -0,0 +1,127 @@ +// 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 assert = require('assert'); +const crypto = require('crypto'); +const { Certificate } = crypto; +const fixtures = require('../common/fixtures'); + +// Test Certificates +const spkacValid = fixtures.readKey('rsa_spkac.spkac'); +const spkacChallenge = 'this-is-a-challenge'; +const spkacFail = fixtures.readKey('rsa_spkac_invalid.spkac'); +const spkacPublicPem = fixtures.readKey('rsa_public.pem'); + +function copyArrayBuffer(buf) { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +} + +function checkMethods(certificate) { + + /* spkacValid has a md5 based signature which is not allowed in boringssl + https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/patches/node/fix_crypto_tests_to_run_with_bssl.patch#L77 + assert.strictEqual(certificate.verifySpkac(spkacValid), true); + assert.strictEqual(certificate.verifySpkac(spkacFail), false); + */ + + assert.strictEqual( + stripLineEndings(certificate.exportPublicKey(spkacValid).toString('utf8')), + stripLineEndings(spkacPublicPem.toString('utf8')) + ); + assert.strictEqual(certificate.exportPublicKey(spkacFail), ''); + + assert.strictEqual( + certificate.exportChallenge(spkacValid).toString('utf8'), + spkacChallenge + ); + assert.strictEqual(certificate.exportChallenge(spkacFail), ''); + + /* spkacValid has a md5 based signature which is not allowed in boringssl + https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/patches/node/fix_crypto_tests_to_run_with_bssl.patch#L88 + const ab = copyArrayBuffer(spkacValid); + assert.strictEqual(certificate.verifySpkac(ab), true); + assert.strictEqual(certificate.verifySpkac(new Uint8Array(ab)), true); + assert.strictEqual(certificate.verifySpkac(new DataView(ab)), true); + */ +} + +{ + // Test maximum size of input buffer + let buf; + let skip = false; + try { + buf = Buffer.alloc(2 ** 31); + } catch { + // The allocation may fail on some systems. That is expected due + // to architecture and memory constraints. If it does, go ahead + // and skip this test. + skip = true; + } + if (!skip) { + assert.throws( + () => Certificate.verifySpkac(buf), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws( + () => Certificate.exportChallenge(buf), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws( + () => Certificate.exportPublicKey(buf), { + code: 'ERR_OUT_OF_RANGE' + }); + } +} + +{ + // Test instance methods + checkMethods(new Certificate()); +} + +{ + // Test static methods + checkMethods(Certificate); +} + +function stripLineEndings(obj) { + return obj.replace(/\n/g, ''); +} + +// Direct call Certificate() should return instance +assert(Certificate() instanceof Certificate); + +[1, {}, [], Infinity, true, undefined, null].forEach((val) => { + assert.throws( + () => Certificate.verifySpkac(val), + { code: 'ERR_INVALID_ARG_TYPE' } + ); +}); + +[1, {}, [], Infinity, true, undefined, null].forEach((val) => { + const errObj = { code: 'ERR_INVALID_ARG_TYPE' }; + assert.throws(() => Certificate.exportPublicKey(val), errObj); + assert.throws(() => Certificate.exportChallenge(val), errObj); +}); diff --git a/test/js/node/test/parallel/test-crypto-des3-wrap.js b/test/js/node/test/parallel/test-crypto-des3-wrap.js new file mode 100644 index 0000000000..6648881bf6 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-des3-wrap.js @@ -0,0 +1,31 @@ +/* +Skipped test +https://github.com/electron/electron/blob/e57b69f106ae9c53a527038db4e8222692fa0ce7/script/node-disabled-tests.json#L13 + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +// Test case for des-ede3 wrap/unwrap. des3-wrap needs extra 2x blocksize +// then plaintext to store ciphertext. +const test = { + key: Buffer.from('3c08e25be22352910671cfe4ba3652b1220a8a7769b490ba', 'hex'), + iv: Buffer.alloc(0), + plaintext: '32|RmVZZkFUVmpRRkp0TmJaUm56ZU9qcnJkaXNNWVNpTTU*|iXmckfRWZBG' + + 'WWELweCBsThSsfUHLeRe0KCsK8ooHgxie0zOINpXxfZi/oNG7uq9JWFVCk70gfzQH8ZU' + + 'JjAfaFg**' +}; + +const cipher = crypto.createCipheriv('des3-wrap', test.key, test.iv); +const ciphertext = cipher.update(test.plaintext, 'utf8'); + +const decipher = crypto.createDecipheriv('des3-wrap', test.key, test.iv); +const msg = decipher.update(ciphertext, 'buffer', 'utf8'); + +assert.strictEqual(msg, test.plaintext); + +*/ diff --git a/test/js/node/test/parallel/test-crypto-dh-curves.js b/test/js/node/test/parallel/test-crypto-dh-curves.js new file mode 100644 index 0000000000..81a469c226 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-dh-curves.js @@ -0,0 +1,191 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +// Second OAKLEY group, see +// https://github.com/nodejs/node-v0.x-archive/issues/2338 and +// https://xml2rfc.tools.ietf.org/public/rfc/html/rfc2412.html#anchor49 +const p = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' + + '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' + + '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' + + 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF'; +crypto.createDiffieHellman(p, 'hex'); + +// Confirm DH_check() results are exposed for optional examination. +const bad_dh = crypto.createDiffieHellman('02', 'hex'); +assert.notStrictEqual(bad_dh.verifyError, 0); + +const availableCurves = new Set(crypto.getCurves()); +const availableHashes = new Set(crypto.getHashes()); + +// Oakley curves do not clean up ERR stack, it was causing unexpected failure +// when accessing other OpenSSL APIs afterwards. +if (availableCurves.has('Oakley-EC2N-3')) { + crypto.createECDH('Oakley-EC2N-3'); + crypto.createHash('sha256'); +} + +// Test ECDH +if (availableCurves.has('prime256v1') && availableCurves.has('secp256k1')) { + const ecdh1 = crypto.createECDH('prime256v1'); + const ecdh2 = crypto.createECDH('prime256v1'); + const key1 = ecdh1.generateKeys(); + const key2 = ecdh2.generateKeys('hex'); + const secret1 = ecdh1.computeSecret(key2, 'hex', 'base64'); + const secret2 = ecdh2.computeSecret(key1, 'latin1', 'buffer'); + + assert.strictEqual(secret1, secret2.toString('base64')); + + // Point formats + assert.strictEqual(ecdh1.getPublicKey('buffer', 'uncompressed')[0], 4); + let firstByte = ecdh1.getPublicKey('buffer', 'compressed')[0]; + assert(firstByte === 2 || firstByte === 3); + firstByte = ecdh1.getPublicKey('buffer', 'hybrid')[0]; + assert(firstByte === 6 || firstByte === 7); + // Format value should be string + + assert.throws( + () => ecdh1.getPublicKey('buffer', 10), + { + code: 'ERR_CRYPTO_ECDH_INVALID_FORMAT', + name: 'TypeError', + message: 'Invalid ECDH format: 10' + }); + + // ECDH should check that point is on curve + const ecdh3 = crypto.createECDH('secp256k1'); + const key3 = ecdh3.generateKeys(); + + assert.throws( + () => ecdh2.computeSecret(key3, 'latin1', 'buffer'), + { + code: 'ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY', + name: 'Error', + message: 'Public key is not valid for specified curve' + }); + + // ECDH should allow .setPrivateKey()/.setPublicKey() + const ecdh4 = crypto.createECDH('prime256v1'); + + ecdh4.setPrivateKey(ecdh1.getPrivateKey()); + ecdh4.setPublicKey(ecdh1.getPublicKey()); + + assert.throws(() => { + ecdh4.setPublicKey(ecdh3.getPublicKey()); + }, { message: 'Failed to convert Buffer to EC_POINT' }); + + // Verify that we can use ECDH without having to use newly generated keys. + const ecdh5 = crypto.createECDH('secp256k1'); + + // Verify errors are thrown when retrieving keys from an uninitialized object. + assert.throws(() => { + ecdh5.getPublicKey(); + }, /^Error: Failed to get ECDH public key$/); + + assert.throws(() => { + ecdh5.getPrivateKey(); + }, /^Error: Failed to get ECDH private key$/); + + // A valid private key for the secp256k1 curve. + const cafebabeKey = 'cafebabe'.repeat(8); + // Associated compressed and uncompressed public keys (points). + const cafebabePubPtComp = + '03672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3'; + const cafebabePubPtUnComp = + '04672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3' + + '2e02c7f93d13dc2732b760ca377a5897b9dd41a1c1b29dc0442fdce6d0a04d1d'; + ecdh5.setPrivateKey(cafebabeKey, 'hex'); + assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); + // Show that the public point (key) is generated while setting the + // private key. + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + + // Compressed and uncompressed public points/keys for other party's + // private key. + // 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF + const peerPubPtComp = + '02c6b754b20826eb925e052ee2c25285b162b51fdca732bcf67e39d647fb6830ae'; + const peerPubPtUnComp = + '04c6b754b20826eb925e052ee2c25285b162b51fdca732bcf67e39d647fb6830ae' + + 'b651944a574a362082a77e3f2b5d9223eb54d7f2f76846522bf75f3bedb8178e'; + + const sharedSecret = + '1da220b5329bbe8bfd19ceef5a5898593f411a6f12ea40f2a8eead9a5cf59970'; + + assert.strictEqual(ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'), + sharedSecret); + assert.strictEqual(ecdh5.computeSecret(peerPubPtUnComp, 'hex', 'hex'), + sharedSecret); + + // Verify that we still have the same key pair as before the computation. + assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + + // Verify setting and getting compressed and non-compressed serializations. + ecdh5.setPublicKey(cafebabePubPtComp, 'hex'); + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + assert.strictEqual( + ecdh5.getPublicKey('hex', 'compressed'), + cafebabePubPtComp + ); + ecdh5.setPublicKey(cafebabePubPtUnComp, 'hex'); + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + assert.strictEqual( + ecdh5.getPublicKey('hex', 'compressed'), + cafebabePubPtComp + ); + + // Show why allowing the public key to be set on this type + // does not make sense. + ecdh5.setPublicKey(peerPubPtComp, 'hex'); + assert.strictEqual(ecdh5.getPublicKey('hex'), peerPubPtUnComp); + assert.throws(() => { + // Error because the public key does not match the private key anymore. + ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'); + }, /Invalid key pair/); + + // Set to a valid key to show that later attempts to set an invalid key are + // rejected. + ecdh5.setPrivateKey(cafebabeKey, 'hex'); + + // Some invalid private keys for the secp256k1 curve. + const errMessage = /Private key is not valid for specified curve/; + ['0000000000000000000000000000000000000000000000000000000000000000', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', + ].forEach((element) => { + assert.throws(() => { + ecdh5.setPrivateKey(element, 'hex'); + }, errMessage); + // Verify object state did not change. + assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); + }); +} + +// Use of invalid keys was not cleaning up ERR stack, and was causing +// unexpected failure in subsequent signing operations. +if (availableCurves.has('prime256v1') && availableHashes.has('sha256')) { + const curve = crypto.createECDH('prime256v1'); + const invalidKey = Buffer.alloc(65); + invalidKey.fill('\0'); + curve.generateKeys(); + assert.throws( + () => curve.computeSecret(invalidKey), + { + code: 'ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY', + name: 'Error', + message: 'Public key is not valid for specified curve' + }); + // Check that signing operations are not impacted by the above error. + const ecPrivateKey = + '-----BEGIN EC PRIVATE KEY-----\n' + + 'MHcCAQEEIF+jnWY1D5kbVYDNvxxo/Y+ku2uJPDwS0r/VuPZQrjjVoAoGCCqGSM49\n' + + 'AwEHoUQDQgAEurOxfSxmqIRYzJVagdZfMMSjRNNhB8i3mXyIMq704m2m52FdfKZ2\n' + + 'pQhByd5eyj3lgZ7m7jbchtdgyOF8Io/1ng==\n' + + '-----END EC PRIVATE KEY-----'; + crypto.createSign('SHA256').sign(ecPrivateKey); +} diff --git a/test/js/node/test/parallel/test-crypto-dh-group-setters.js b/test/js/node/test/parallel/test-crypto-dh-group-setters.js new file mode 100644 index 0000000000..55086e293e --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-dh-group-setters.js @@ -0,0 +1,19 @@ +/* +Skipped test +https://github.com/electron/electron/blob/e57b69f106ae9c53a527038db4e8222692fa0ce7/script/node-disabled-tests.json#L14 + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +// Unlike DiffieHellman, DiffieHellmanGroup does not have any setters. +const dhg = crypto.getDiffieHellman('modp1'); +assert.strictEqual(dhg.constructor, crypto.DiffieHellmanGroup); +assert.strictEqual(dhg.setPrivateKey, undefined); +assert.strictEqual(dhg.setPublicKey, undefined); + +// */ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-dh-modp2-views.js b/test/js/node/test/parallel/test-crypto-dh-modp2-views.js new file mode 100644 index 0000000000..382d575a8a --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-dh-modp2-views.js @@ -0,0 +1,30 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L16 + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { modp2buf } = require('../common/crypto'); + +const modp2 = crypto.createDiffieHellmanGroup('modp2'); + +const views = common.getArrayBufferViews(modp2buf); +for (const buf of [modp2buf, ...views]) { + // Ensure specific generator (string with encoding) works as expected with + // any ArrayBufferViews as the first argument to createDiffieHellman(). + const exmodp2 = crypto.createDiffieHellman(buf, '02', 'hex'); + modp2.generateKeys(); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + .toString('hex'); + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + .toString('hex'); + assert.strictEqual(modp2Secret, exmodp2Secret); +} + +*/ diff --git a/test/js/node/test/parallel/test-crypto-dh-modp2.js b/test/js/node/test/parallel/test-crypto-dh-modp2.js new file mode 100644 index 0000000000..c60f6aa456 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-dh-modp2.js @@ -0,0 +1,49 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L15 + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { modp2buf } = require('../common/crypto'); +const modp2 = crypto.createDiffieHellmanGroup('modp2'); + +{ + // Ensure specific generator (buffer) works as expected. + const exmodp2 = crypto.createDiffieHellman(modp2buf, Buffer.from([2])); + modp2.generateKeys(); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + .toString('hex'); + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + .toString('hex'); + assert.strictEqual(modp2Secret, exmodp2Secret); +} + +{ + // Ensure specific generator (string without encoding) works as expected. + const exmodp2 = crypto.createDiffieHellman(modp2buf, '\x02'); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + .toString('hex'); + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + .toString('hex'); + assert.strictEqual(modp2Secret, exmodp2Secret); +} + +{ + // Ensure specific generator (numeric) works as expected. + const exmodp2 = crypto.createDiffieHellman(modp2buf, 2); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + .toString('hex'); + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + .toString('hex'); + assert.strictEqual(modp2Secret, exmodp2Secret); +} + +*/ diff --git a/test/js/node/test/parallel/test-crypto-ecb.js b/test/js/node/test/parallel/test-crypto-ecb.js new file mode 100644 index 0000000000..aeb569cbfc --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-ecb.js @@ -0,0 +1,60 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L17 + +// 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'); + +if (common.hasFipsCrypto) + common.skip('BF-ECB is not FIPS 140-2 compatible'); + +if (common.hasOpenSSL3) + common.skip('Blowfish is only available with the legacy provider in ' + + 'OpenSSl 3.x'); + +const assert = require('assert'); +const crypto = require('crypto'); + +// Testing whether EVP_CipherInit_ex is functioning correctly. +// Reference: bug#1997 + +{ + const encrypt = + crypto.createCipheriv('BF-ECB', 'SomeRandomBlahz0c5GZVnR', ''); + let hex = encrypt.update('Hello World!', 'ascii', 'hex'); + hex += encrypt.final('hex'); + assert.strictEqual(hex.toUpperCase(), '6D385F424AAB0CFBF0BB86E07FFB7D71'); +} + +{ + const decrypt = + crypto.createDecipheriv('BF-ECB', 'SomeRandomBlahz0c5GZVnR', ''); + let msg = decrypt.update('6D385F424AAB0CFBF0BB86E07FFB7D71', 'hex', 'ascii'); + msg += decrypt.final('ascii'); + assert.strictEqual(msg, 'Hello World!'); +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-ecdh-convert-key.js b/test/js/node/test/parallel/test-crypto-ecdh-convert-key.js new file mode 100644 index 0000000000..c0046099df --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-ecdh-convert-key.js @@ -0,0 +1,125 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); + +const { ECDH, createSign, getCurves } = require('crypto'); + +// A valid private key for the secp256k1 curve. +const cafebabeKey = 'cafebabe'.repeat(8); +// Associated compressed and uncompressed public keys (points). +const cafebabePubPtComp = + '03672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3'; +const cafebabePubPtUnComp = + '04672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3' + + '2e02c7f93d13dc2732b760ca377a5897b9dd41a1c1b29dc0442fdce6d0a04d1d'; + +// Invalid test: key argument is undefined. +assert.throws( + () => ECDH.convertKey(), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + +// Invalid test: curve argument is undefined. +assert.throws( + () => ECDH.convertKey(cafebabePubPtComp), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + +// Invalid test: curve argument is invalid. +assert.throws( + () => ECDH.convertKey(cafebabePubPtComp, 'badcurve'), + { + name: 'TypeError', + message: 'Invalid EC curve name' + }); + +if (getCurves().includes('secp256k1')) { + // Invalid test: format argument is undefined. + assert.throws( + () => ECDH.convertKey(cafebabePubPtComp, 'secp256k1', 'hex', 'hex', 10), + { + code: 'ERR_CRYPTO_ECDH_INVALID_FORMAT', + name: 'TypeError', + message: 'Invalid ECDH format: 10' + }); + + // Point formats. + let uncompressed = ECDH.convertKey(cafebabePubPtComp, + 'secp256k1', + 'hex', + 'buffer', + 'uncompressed'); + let compressed = ECDH.convertKey(cafebabePubPtComp, + 'secp256k1', + 'hex', + 'buffer', + 'compressed'); + let hybrid = ECDH.convertKey(cafebabePubPtComp, + 'secp256k1', + 'hex', + 'buffer', + 'hybrid'); + assert.strictEqual(uncompressed[0], 4); + let firstByte = compressed[0]; + assert(firstByte === 2 || firstByte === 3); + firstByte = hybrid[0]; + assert(firstByte === 6 || firstByte === 7); + + // Format conversion from hex to hex + uncompressed = ECDH.convertKey(cafebabePubPtComp, + 'secp256k1', + 'hex', + 'hex', + 'uncompressed'); + compressed = ECDH.convertKey(cafebabePubPtComp, + 'secp256k1', + 'hex', + 'hex', + 'compressed'); + hybrid = ECDH.convertKey(cafebabePubPtComp, + 'secp256k1', + 'hex', + 'hex', + 'hybrid'); + assert.strictEqual(uncompressed, cafebabePubPtUnComp); + assert.strictEqual(compressed, cafebabePubPtComp); + + // Compare to getPublicKey. + const ecdh1 = ECDH('secp256k1'); + ecdh1.generateKeys(); + ecdh1.setPrivateKey(cafebabeKey, 'hex'); + assert.strictEqual(ecdh1.getPublicKey('hex', 'uncompressed'), uncompressed); + assert.strictEqual(ecdh1.getPublicKey('hex', 'compressed'), compressed); + assert.strictEqual(ecdh1.getPublicKey('hex', 'hybrid'), hybrid); +} + +// See https://github.com/nodejs/node/issues/26133, failed ConvertKey +// operations should not leave errors on OpenSSL's error stack because +// that's observable by subsequent operations. +{ + const privateKey = + '-----BEGIN EC PRIVATE KEY-----\n' + + 'MHcCAQEEIF+jnWY1D5kbVYDNvxxo/Y+ku2uJPDwS0r/VuPZQrjjVoAoGCCqGSM49\n' + + 'AwEHoUQDQgAEurOxfSxmqIRYzJVagdZfMMSjRNNhB8i3mXyIMq704m2m52FdfKZ2\n' + + 'pQhByd5eyj3lgZ7m7jbchtdgyOF8Io/1ng==\n' + + '-----END EC PRIVATE KEY-----'; + + const sign = createSign('sha256').update('plaintext'); + + // TODO(bnoordhuis) This should really bubble up the specific OpenSSL error + // rather than Node's generic error message. + const badKey = 'f'.repeat(128); + assert.throws( + () => ECDH.convertKey(badKey, 'secp521r1', 'hex', 'hex', 'compressed'), + /Failed to convert Buffer to EC_POINT/); + + // Next statement should not throw an exception. + sign.sign(privateKey); +} diff --git a/test/js/node/test/parallel/test-crypto-fips.js b/test/js/node/test/parallel/test-crypto-fips.js new file mode 100644 index 0000000000..c862b617b2 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-fips.js @@ -0,0 +1,285 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L19 + +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const spawnSync = require('child_process').spawnSync; +const path = require('path'); +const fixtures = require('../common/fixtures'); +const { internalBinding } = require('internal/test/binding'); +const { testFipsCrypto } = internalBinding('crypto'); + +const FIPS_ENABLED = 1; +const FIPS_DISABLED = 0; +const FIPS_ERROR_STRING2 = + 'Error [ERR_CRYPTO_FIPS_FORCED]: Cannot set FIPS mode, it was forced with ' + + '--force-fips at startup.'; +const FIPS_UNSUPPORTED_ERROR_STRING = 'fips mode not supported'; +const FIPS_ENABLE_ERROR_STRING = 'OpenSSL error when trying to enable FIPS:'; + +const CNF_FIPS_ON = fixtures.path('openssl_fips_enabled.cnf'); +const CNF_FIPS_OFF = fixtures.path('openssl_fips_disabled.cnf'); + +let num_children_ok = 0; + +function sharedOpenSSL() { + return process.config.variables.node_shared_openssl; +} + +function testHelper(stream, args, expectedOutput, cmd, env) { + const fullArgs = args.concat(['-e', `console.log(${cmd})`]); + const child = spawnSync(process.execPath, fullArgs, { + cwd: path.dirname(process.execPath), + env: env + }); + + console.error( + `Spawned child [pid:${child.pid}] with cmd '${cmd}' expect %j with args '${ + args}' OPENSSL_CONF=%j`, expectedOutput, env.OPENSSL_CONF); + + function childOk(child) { + console.error(`Child #${++num_children_ok} [pid:${child.pid}] OK.`); + } + + function responseHandler(buffer, expectedOutput) { + const response = buffer.toString(); + assert.notStrictEqual(response.length, 0); + if (FIPS_ENABLED !== expectedOutput && FIPS_DISABLED !== expectedOutput) { + // In the case of expected errors just look for a substring. + assert.ok(response.includes(expectedOutput)); + } else { + const getFipsValue = Number(response); + if (!Number.isNaN(getFipsValue)) + // Normal path where we expect either FIPS enabled or disabled. + assert.strictEqual(getFipsValue, expectedOutput); + } + childOk(child); + } + + responseHandler(child[stream], expectedOutput); +} + +// --enable-fips should raise an error if OpenSSL is not FIPS enabled. +testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--enable-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_ENABLE_ERROR_STRING, + 'process.versions', + process.env); + +// --force-fips should raise an error if OpenSSL is not FIPS enabled. +testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--force-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_ENABLE_ERROR_STRING, + 'process.versions', + process.env); + +// By default FIPS should be off in both FIPS and non-FIPS builds +// unless Node.js was configured using --shared-openssl in +// which case it may be enabled by the system. +if (!sharedOpenSSL()) { + testHelper( + 'stdout', + [], + FIPS_DISABLED, + 'require("crypto").getFips()', + { ...process.env, 'OPENSSL_CONF': ' ' }); +} + +// Toggling fips with setFips should not be allowed from a worker thread +testHelper( + 'stderr', + [], + 'Calling crypto.setFips() is not supported in workers', + 'new worker_threads.Worker(\'require("crypto").setFips(true);\', { eval: true })', + process.env); + +// This should succeed for both FIPS and non-FIPS builds in combination with +// OpenSSL 1.1.1 or OpenSSL 3.0 +const test_result = testFipsCrypto(); +assert.ok(test_result === 1 || test_result === 0); + +// If Node was configured using --shared-openssl fips support might be +// available depending on how OpenSSL was built. If fips support is +// available the tests that toggle the fips_mode on/off using the config +// file option will succeed and return 1 instead of 0. +// +// Note that this case is different from when calling the fips setter as the +// configuration file is handled by OpenSSL, so it is not possible for us +// to try to call the fips setter, to try to detect this situation, as +// that would throw an error: +// ("Error: Cannot set FIPS mode in a non-FIPS build."). +// Due to this uncertainty the following tests are skipped when configured +// with --shared-openssl. +if (!sharedOpenSSL() && !common.hasOpenSSL3) { + // OpenSSL config file should be able to turn on FIPS mode + testHelper( + 'stdout', + [`--openssl-config=${CNF_FIPS_ON}`], + testFipsCrypto() ? FIPS_ENABLED : FIPS_DISABLED, + 'require("crypto").getFips()', + process.env); + + // OPENSSL_CONF should be able to turn on FIPS mode + testHelper( + 'stdout', + [], + testFipsCrypto() ? FIPS_ENABLED : FIPS_DISABLED, + 'require("crypto").getFips()', + Object.assign({}, process.env, { 'OPENSSL_CONF': CNF_FIPS_ON })); + + // --openssl-config option should override OPENSSL_CONF + testHelper( + 'stdout', + [`--openssl-config=${CNF_FIPS_ON}`], + testFipsCrypto() ? FIPS_ENABLED : FIPS_DISABLED, + 'require("crypto").getFips()', + Object.assign({}, process.env, { 'OPENSSL_CONF': CNF_FIPS_OFF })); +} + +// OpenSSL 3.x has changed the configuration files so the following tests +// will not work as expected with that version. +// TODO(danbev) Revisit these test once FIPS support is available in +// OpenSSL 3.x. +if (!common.hasOpenSSL3) { + testHelper( + 'stdout', + [`--openssl-config=${CNF_FIPS_OFF}`], + FIPS_DISABLED, + 'require("crypto").getFips()', + Object.assign({}, process.env, { 'OPENSSL_CONF': CNF_FIPS_ON })); + + // --enable-fips should take precedence over OpenSSL config file + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--enable-fips', `--openssl-config=${CNF_FIPS_OFF}`], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").getFips()', + process.env); + // --force-fips should take precedence over OpenSSL config file + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--force-fips', `--openssl-config=${CNF_FIPS_OFF}`], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").getFips()', + process.env); + // --enable-fips should turn FIPS mode on + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--enable-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").getFips()', + process.env); + + // --force-fips should turn FIPS mode on + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--force-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").getFips()', + process.env); + + // OPENSSL_CONF should _not_ make a difference to --enable-fips + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--enable-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").getFips()', + Object.assign({}, process.env, { 'OPENSSL_CONF': CNF_FIPS_OFF })); + + // Using OPENSSL_CONF should not make a difference to --force-fips + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--force-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").getFips()', + Object.assign({}, process.env, { 'OPENSSL_CONF': CNF_FIPS_OFF })); + + // setFipsCrypto should be able to turn FIPS mode on + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + [], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + '(require("crypto").setFips(true),' + + 'require("crypto").getFips())', + process.env); + + // setFipsCrypto should be able to turn FIPS mode on and off + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + [], + testFipsCrypto() ? FIPS_DISABLED : FIPS_UNSUPPORTED_ERROR_STRING, + '(require("crypto").setFips(true),' + + 'require("crypto").setFips(false),' + + 'require("crypto").getFips())', + process.env); + + // setFipsCrypto takes precedence over OpenSSL config file, FIPS on + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + [`--openssl-config=${CNF_FIPS_OFF}`], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + '(require("crypto").setFips(true),' + + 'require("crypto").getFips())', + process.env); + + // setFipsCrypto takes precedence over OpenSSL config file, FIPS off + testHelper( + 'stdout', + [`--openssl-config=${CNF_FIPS_ON}`], + FIPS_DISABLED, + '(require("crypto").setFips(false),' + + 'require("crypto").getFips())', + process.env); + + // --enable-fips does not prevent use of setFipsCrypto API + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--enable-fips'], + testFipsCrypto() ? FIPS_DISABLED : FIPS_UNSUPPORTED_ERROR_STRING, + '(require("crypto").setFips(false),' + + 'require("crypto").getFips())', + process.env); + + // --force-fips prevents use of setFipsCrypto API + testHelper( + 'stderr', + ['--force-fips'], + testFipsCrypto() ? FIPS_ERROR_STRING2 : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").setFips(false)', + process.env); + + // --force-fips makes setFipsCrypto enable a no-op (FIPS stays on) + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--force-fips'], + testFipsCrypto() ? FIPS_ENABLED : FIPS_UNSUPPORTED_ERROR_STRING, + '(require("crypto").setFips(true),' + + 'require("crypto").getFips())', + process.env); + + // --force-fips and --enable-fips order does not matter + testHelper( + 'stderr', + ['--force-fips', '--enable-fips'], + testFipsCrypto() ? FIPS_ERROR_STRING2 : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").setFips(false)', + process.env); + + // --enable-fips and --force-fips order does not matter + testHelper( + 'stderr', + ['--enable-fips', '--force-fips'], + testFipsCrypto() ? FIPS_ERROR_STRING2 : FIPS_UNSUPPORTED_ERROR_STRING, + 'require("crypto").setFips(false)', + process.env); +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-getcipherinfo.js b/test/js/node/test/parallel/test-crypto-getcipherinfo.js new file mode 100644 index 0000000000..fd41073b66 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-getcipherinfo.js @@ -0,0 +1,74 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { + getCiphers, + getCipherInfo +} = require('crypto'); + +const assert = require('assert'); + +const ciphers = getCiphers(); + +assert.strictEqual(getCipherInfo(-1), undefined); +assert.strictEqual(getCipherInfo('cipher that does not exist'), undefined); + +for (const cipher of ciphers) { + const info = getCipherInfo(cipher); + assert(info); + const info2 = getCipherInfo(info.nid); + assert.deepStrictEqual(info, info2); +} + +const info = getCipherInfo('aes-128-cbc'); +assert.strictEqual(info.name, 'AES-128-CBC'); +assert.strictEqual(info.nid, 419); +assert.strictEqual(info.blockSize, 16); +assert.strictEqual(info.ivLength, 16); +assert.strictEqual(info.keyLength, 16); +assert.strictEqual(info.mode, 'cbc'); + +[null, undefined, [], {}].forEach((arg) => { + assert.throws(() => getCipherInfo(arg), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +[null, '', 1, true].forEach((options) => { + assert.throws( + () => getCipherInfo('aes-192-cbc', options), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +[null, '', {}, [], true].forEach((len) => { + assert.throws( + () => getCipherInfo('aes-192-cbc', { keyLength: len }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws( + () => getCipherInfo('aes-192-cbc', { ivLength: len }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +assert(!getCipherInfo('aes-128-cbc', { keyLength: 12 })); +assert(getCipherInfo('aes-128-cbc', { keyLength: 16 })); +assert(!getCipherInfo('aes-128-cbc', { ivLength: 12 })); +assert(getCipherInfo('aes-128-cbc', { ivLength: 16 })); + +assert(!getCipherInfo('aes-128-ccm', { ivLength: 1 })); +assert(!getCipherInfo('aes-128-ccm', { ivLength: 14 })); +if (!common.openSSLIsBoringSSL) { + for (let n = 7; n <= 13; n++) + assert(getCipherInfo('aes-128-ccm', { ivLength: n })); +} + +assert(!getCipherInfo('aes-128-ocb', { ivLength: 16 })); +if (!common.openSSLIsBoringSSL) { +for (let n = 1; n < 16; n++) + assert(getCipherInfo('aes-128-ocb', { ivLength: n })); +} diff --git a/test/js/node/test/parallel/test-crypto-key-objects.js b/test/js/node/test/parallel/test-crypto-key-objects.js new file mode 100644 index 0000000000..b63b1c9054 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-key-objects.js @@ -0,0 +1,894 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L20 + +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createCipheriv, + createDecipheriv, + createSign, + createVerify, + createSecretKey, + createPublicKey, + createPrivateKey, + KeyObject, + randomBytes, + publicDecrypt, + publicEncrypt, + privateDecrypt, + privateEncrypt, + getCurves, + generateKeySync, + generateKeyPairSync, +} = require('crypto'); + +const fixtures = require('../common/fixtures'); + +const publicPem = fixtures.readKey('rsa_public.pem', 'ascii'); +const privatePem = fixtures.readKey('rsa_private.pem', 'ascii'); + +const publicDsa = fixtures.readKey('dsa_public_1025.pem', 'ascii'); +const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', + 'ascii'); + +{ + // Attempting to create a key of a wrong type should throw + const TYPE = 'wrong_type'; + + assert.throws(() => new KeyObject(TYPE), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: `The argument 'type' is invalid. Received '${TYPE}'` + }); +} + +{ + // Attempting to create a key with non-object handle should throw + assert.throws(() => new KeyObject('secret', ''), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "handle" argument must be of type object. Received type ' + + "string ('')" + }); +} + +{ + assert.throws(() => KeyObject.from('invalid_key'), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "key" argument must be an instance of CryptoKey. Received type ' + + "string ('invalid_key')" + }); +} + +{ + const keybuf = randomBytes(32); + const key = createSecretKey(keybuf); + assert.strictEqual(key.type, 'secret'); + assert.strictEqual(key.toString(), '[object KeyObject]'); + assert.strictEqual(key.symmetricKeySize, 32); + assert.strictEqual(key.asymmetricKeyType, undefined); + assert.strictEqual(key.asymmetricKeyDetails, undefined); + + const exportedKey = key.export(); + assert(keybuf.equals(exportedKey)); + + const plaintext = Buffer.from('Hello world', 'utf8'); + + const cipher = createCipheriv('aes-256-ecb', key, null); + const ciphertext = Buffer.concat([ + cipher.update(plaintext), cipher.final(), + ]); + + const decipher = createDecipheriv('aes-256-ecb', key, null); + const deciphered = Buffer.concat([ + decipher.update(ciphertext), decipher.final(), + ]); + + assert(plaintext.equals(deciphered)); +} + +{ + // Passing an existing public key object to createPublicKey should throw. + const publicKey = createPublicKey(publicPem); + assert.throws(() => createPublicKey(publicKey), { + name: 'TypeError', + code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', + message: 'Invalid key object type public, expected private.' + }); + + // Constructing a private key from a public key should be impossible, even + // if the public key was derived from a private key. + assert.throws(() => createPrivateKey(createPublicKey(privatePem)), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); + + // Similarly, passing an existing private key object to createPrivateKey + // should throw. + const privateKey = createPrivateKey(privatePem); + assert.throws(() => createPrivateKey(privateKey), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +{ + const jwk = { + e: 'AQAB', + n: 't9xYiIonscC3vz_A2ceR7KhZZlDu_5bye53nCVTcKnWd2seY6UAdKersX6njr83Dd5OVe' + + '1BW_wJvp5EjWTAGYbFswlNmeD44edEGM939B6Lq-_8iBkrTi8mGN4YCytivE24YI0D4XZ' + + 'MPfkLSpab2y_Hy4DjQKBq1ThZ0UBnK-9IhX37Ju_ZoGYSlTIGIhzyaiYBh7wrZBoPczIE' + + 'u6et_kN2VnnbRUtkYTF97ggcv5h-hDpUQjQW0ZgOMcTc8n-RkGpIt0_iM_bTjI3Tz_gsF' + + 'di6hHcpZgbopPL630296iByyigQCPJVzdusFrQN5DeC-zT_nGypQkZanLb4ZspSx9Q', + d: 'ktnq2LvIMqBj4txP82IEOorIRQGVsw1khbm8A-cEpuEkgM71Yi_0WzupKktucUeevQ5i0' + + 'Yh8w9e1SJiTLDRAlJz66kdky9uejiWWl6zR4dyNZVMFYRM43ijLC-P8rPne9Fz16IqHFW' + + '5VbJqA1xCBhKmuPMsD71RNxZ4Hrsa7Kt_xglQTYsLbdGIwDmcZihId9VGXRzvmCPsDRf2' + + 'fCkAj7HDeRxpUdEiEDpajADc-PWikra3r3b40tVHKWm8wxJLivOIN7GiYXKQIW6RhZgH-' + + 'Rk45JIRNKxNagxdeXUqqyhnwhbTo1Hite0iBDexN9tgoZk0XmdYWBn6ElXHRZ7VCDQ', + p: '8UovlB4nrBm7xH-u7XXBMbqxADQm5vaEZxw9eluc-tP7cIAI4sglMIvL_FMpbd2pEeP_B' + + 'kR76NTDzzDuPAZvUGRavgEjy0O9j2NAs_WPK4tZF-vFdunhnSh4EHAF4Ij9kbsUi90NOp' + + 'bGfVqPdOaHqzgHKoR23Cuusk9wFQ2XTV8', + q: 'wxHdEYT9xrpfrHPqSBQPpO0dWGKJEkrWOb-76rSfuL8wGR4OBNmQdhLuU9zTIh22pog-X' + + 'PnLPAecC-4yu_wtJ2SPCKiKDbJBre0CKPyRfGqzvA3njXwMxXazU4kGs-2Fg-xu_iKbaI' + + 'jxXrclBLhkxhBtySrwAFhxxOk6fFcPLSs', + dp: 'qS_Mdr5CMRGGMH0bKhPUWEtAixUGZhJaunX5wY71Xoc_Gh4cnO-b7BNJ_-5L8WZog0vr' + + '6PgiLhrqBaCYm2wjpyoG2o2wDHm-NAlzN_wp3G2EFhrSxdOux-S1c0kpRcyoiAO2n29rN' + + 'Da-jOzwBBcU8ACEPdLOCQl0IEFFJO33tl8', + dq: 'WAziKpxLKL7LnL4dzDcx8JIPIuwnTxh0plCDdCffyLaT8WJ9lXbXHFTjOvt8WfPrlDP_' + + 'Ylxmfkw5BbGZOP1VLGjZn2DkH9aMiwNmbDXFPdG0G3hzQovx_9fajiRV4DWghLHeT9wzJ' + + 'fZabRRiI0VQR472300AVEeX4vgbrDBn600', + qi: 'k7czBCT9rHn_PNwCa17hlTy88C4vXkwbz83Oa-aX5L4e5gw5lhcR2ZuZHLb2r6oMt9rl' + + 'D7EIDItSs-u21LOXWPTAlazdnpYUyw_CzogM_PN-qNwMRXn5uXFFhmlP2mVg2EdELTahX' + + 'ch8kWqHaCSX53yvqCtRKu_j76V31TfQZGM', + kty: 'RSA', + }; + const publicJwk = { kty: jwk.kty, e: jwk.e, n: jwk.n }; + + const publicKey = createPublicKey(publicPem); + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.toString(), '[object KeyObject]'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); + assert.strictEqual(publicKey.symmetricKeySize, undefined); + + const privateKey = createPrivateKey(privatePem); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.toString(), '[object KeyObject]'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); + assert.strictEqual(privateKey.symmetricKeySize, undefined); + + // It should be possible to derive a public key from a private key. + const derivedPublicKey = createPublicKey(privateKey); + assert.strictEqual(derivedPublicKey.type, 'public'); + assert.strictEqual(derivedPublicKey.toString(), '[object KeyObject]'); + assert.strictEqual(derivedPublicKey.asymmetricKeyType, 'rsa'); + assert.strictEqual(derivedPublicKey.symmetricKeySize, undefined); + + const publicKeyFromJwk = createPublicKey({ key: publicJwk, format: 'jwk' }); + assert.strictEqual(publicKeyFromJwk.type, 'public'); + assert.strictEqual(publicKeyFromJwk.toString(), '[object KeyObject]'); + assert.strictEqual(publicKeyFromJwk.asymmetricKeyType, 'rsa'); + assert.strictEqual(publicKeyFromJwk.symmetricKeySize, undefined); + + const privateKeyFromJwk = createPrivateKey({ key: jwk, format: 'jwk' }); + assert.strictEqual(privateKeyFromJwk.type, 'private'); + assert.strictEqual(privateKeyFromJwk.toString(), '[object KeyObject]'); + assert.strictEqual(privateKeyFromJwk.asymmetricKeyType, 'rsa'); + assert.strictEqual(privateKeyFromJwk.symmetricKeySize, undefined); + + // It should also be possible to import an encrypted private key as a public + // key. + const decryptedKey = createPublicKey({ + key: privateKey.export({ + type: 'pkcs8', + format: 'pem', + passphrase: '123', + cipher: 'aes-128-cbc' + }), + format: 'pem', + passphrase: '123' + }); + assert.strictEqual(decryptedKey.type, 'public'); + assert.strictEqual(decryptedKey.asymmetricKeyType, 'rsa'); + + // Test exporting with an invalid options object, this should throw. + for (const opt of [undefined, null, 'foo', 0, NaN]) { + assert.throws(() => publicKey.export(opt), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "options" argument must be of type object/ + }); + } + + for (const keyObject of [publicKey, derivedPublicKey, publicKeyFromJwk]) { + assert.deepStrictEqual( + keyObject.export({ format: 'jwk' }), + { kty: 'RSA', n: jwk.n, e: jwk.e } + ); + } + + for (const keyObject of [privateKey, privateKeyFromJwk]) { + assert.deepStrictEqual( + keyObject.export({ format: 'jwk' }), + jwk + ); + } + + // Exporting the key using JWK should not work since this format does not + // support key encryption + assert.throws(() => { + privateKey.export({ format: 'jwk', passphrase: 'secret' }); + }, { + message: 'The selected key encoding jwk does not support encryption.', + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' + }); + + const publicDER = publicKey.export({ + format: 'der', + type: 'pkcs1' + }); + + const privateDER = privateKey.export({ + format: 'der', + type: 'pkcs1' + }); + + assert(Buffer.isBuffer(publicDER)); + assert(Buffer.isBuffer(privateDER)); + + const plaintext = Buffer.from('Hello world', 'utf8'); + const testDecryption = (fn, ciphertexts, decryptionKeys) => { + for (const ciphertext of ciphertexts) { + for (const key of decryptionKeys) { + const deciphered = fn(key, ciphertext); + assert.deepStrictEqual(deciphered, plaintext); + } + } + }; + + testDecryption(privateDecrypt, [ + // Encrypt using the public key. + publicEncrypt(publicKey, plaintext), + publicEncrypt({ key: publicKey }, plaintext), + publicEncrypt({ key: publicJwk, format: 'jwk' }, plaintext), + + // Encrypt using the private key. + publicEncrypt(privateKey, plaintext), + publicEncrypt({ key: privateKey }, plaintext), + publicEncrypt({ key: jwk, format: 'jwk' }, plaintext), + + // Encrypt using a public key derived from the private key. + publicEncrypt(derivedPublicKey, plaintext), + publicEncrypt({ key: derivedPublicKey }, plaintext), + + // Test distinguishing PKCS#1 public and private keys based on the + // DER-encoded data only. + publicEncrypt({ format: 'der', type: 'pkcs1', key: publicDER }, plaintext), + publicEncrypt({ format: 'der', type: 'pkcs1', key: privateDER }, plaintext), + ], [ + privateKey, + { format: 'pem', key: privatePem }, + { format: 'der', type: 'pkcs1', key: privateDER }, + { key: jwk, format: 'jwk' }, + ]); + + testDecryption(publicDecrypt, [ + privateEncrypt(privateKey, plaintext), + ], [ + // Decrypt using the public key. + publicKey, + { format: 'pem', key: publicPem }, + { format: 'der', type: 'pkcs1', key: publicDER }, + { key: publicJwk, format: 'jwk' }, + + // Decrypt using the private key. + privateKey, + { format: 'pem', key: privatePem }, + { format: 'der', type: 'pkcs1', key: privateDER }, + { key: jwk, format: 'jwk' }, + ]); +} + +{ + // This should not cause a crash: https://github.com/nodejs/node/issues/25247 + assert.throws(() => { + createPrivateKey({ key: '' }); + }, common.hasOpenSSL3 ? { + message: 'error:1E08010C:DECODER routines::unsupported', + } : { + message: 'error:0909006C:PEM routines:get_name:no start line', + code: 'ERR_OSSL_PEM_NO_START_LINE', + reason: 'no start line', + library: 'PEM routines', + function: 'get_name', + }); + + // This should not abort either: https://github.com/nodejs/node/issues/29904 + assert.throws(() => { + createPrivateKey({ key: Buffer.alloc(0), format: 'der', type: 'spki' }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.type' is invalid. Received 'spki'" + }); + + // Unlike SPKI, PKCS#1 is a valid encoding for private keys (and public keys), + // so it should be accepted by createPrivateKey, but OpenSSL won't parse it. + assert.throws(() => { + const key = createPublicKey(publicPem).export({ + format: 'der', + type: 'pkcs1' + }); + createPrivateKey({ key, format: 'der', type: 'pkcs1' }); + }, common.hasOpenSSL3 ? { + message: /error:1E08010C:DECODER routines::unsupported/, + library: 'DECODER routines' + } : { + message: /asn1 encoding/, + library: 'asn1 encoding routines' + }); +} + +[ + { private: fixtures.readKey('ed25519_private.pem', 'ascii'), + public: fixtures.readKey('ed25519_public.pem', 'ascii'), + keyType: 'ed25519', + jwk: { + crv: 'Ed25519', + x: 'K1wIouqnuiA04b3WrMa-xKIKIpfHetNZRv3h9fBf768', + d: 'wVK6M3SMhQh3NK-7GRrSV-BVWQx1FO5pW8hhQeu_NdA', + kty: 'OKP' + } }, + { private: fixtures.readKey('ed448_private.pem', 'ascii'), + public: fixtures.readKey('ed448_public.pem', 'ascii'), + keyType: 'ed448', + jwk: { + crv: 'Ed448', + x: 'oX_ee5-jlcU53-BbGRsGIzly0V-SZtJ_oGXY0udf84q2hTW2RdstLktvwpkVJOoNb7o' + + 'Dgc2V5ZUA', + d: '060Ke71sN0GpIc01nnGgMDkp0sFNQ09woVo4AM1ffax1-mjnakK0-p-S7-Xf859QewX' + + 'jcR9mxppY', + kty: 'OKP' + } }, + { private: fixtures.readKey('x25519_private.pem', 'ascii'), + public: fixtures.readKey('x25519_public.pem', 'ascii'), + keyType: 'x25519', + jwk: { + crv: 'X25519', + x: 'aSb8Q-RndwfNnPeOYGYPDUN3uhAPnMLzXyfi-mqfhig', + d: 'mL_IWm55RrALUGRfJYzw40gEYWMvtRkesP9mj8o8Omc', + kty: 'OKP' + } }, + { private: fixtures.readKey('x448_private.pem', 'ascii'), + public: fixtures.readKey('x448_public.pem', 'ascii'), + keyType: 'x448', + jwk: { + crv: 'X448', + x: 'ioHSHVpTs6hMvghosEJDIR7ceFiE3-Xccxati64oOVJ7NWjfozE7ae31PXIUFq6cVYg' + + 'vSKsDFPA', + d: 'tMNtrO_q8dlY6Y4NDeSTxNQ5CACkHiPvmukidPnNIuX_EkcryLEXt_7i6j6YZMKsrWy' + + 'S0jlSYJk', + kty: 'OKP' + } }, +].forEach((info) => { + const keyType = info.keyType; + + { + const key = createPrivateKey(info.private); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } + + { + const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } + + { + for (const input of [ + info.private, info.public, { key: info.jwk, format: 'jwk' }]) { + const key = createPublicKey(input); + assert.strictEqual(key.type, 'public'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'spki', format: 'pem' }), info.public); + const jwk = { ...info.jwk }; + delete jwk.d; + assert.deepStrictEqual( + key.export({ format: 'jwk' }), jwk); + } + } +}); + +[ + { private: fixtures.readKey('ec_p256_private.pem', 'ascii'), + public: fixtures.readKey('ec_p256_public.pem', 'ascii'), + keyType: 'ec', + namedCurve: 'prime256v1', + jwk: { + crv: 'P-256', + d: 'DxBsPQPIgMuMyQbxzbb9toew6Ev6e9O6ZhpxLNgmAEo', + kty: 'EC', + x: 'X0mMYR_uleZSIPjNztIkAS3_ud5LhNpbiIFp6fNf2Gs', + y: 'UbJuPy2Xi0lW7UYTBxPK3yGgDu9EAKYIecjkHX5s2lI' + } }, + { private: fixtures.readKey('ec_secp256k1_private.pem', 'ascii'), + public: fixtures.readKey('ec_secp256k1_public.pem', 'ascii'), + keyType: 'ec', + namedCurve: 'secp256k1', + jwk: { + crv: 'secp256k1', + d: 'c34ocwTwpFa9NZZh3l88qXyrkoYSxvC0FEsU5v1v4IM', + kty: 'EC', + x: 'cOzhFSpWxhalCbWNdP2H_yUkdC81C9T2deDpfxK7owA', + y: '-A3DAZTk9IPppN-f03JydgHaFvL1fAHaoXf4SX4NXyo' + } }, + { private: fixtures.readKey('ec_p384_private.pem', 'ascii'), + public: fixtures.readKey('ec_p384_public.pem', 'ascii'), + keyType: 'ec', + namedCurve: 'secp384r1', + jwk: { + crv: 'P-384', + d: 'dwfuHuAtTlMRn7ZBCBm_0grpc1D_4hPeNAgevgelljuC0--k_LDFosDgBlLLmZsi', + kty: 'EC', + x: 'hON3nzGJgv-08fdHpQxgRJFZzlK-GZDGa5f3KnvM31cvvjJmsj4UeOgIdy3rDAjV', + y: 'fidHhtecNCGCfLqmrLjDena1NSzWzWH1u_oUdMKGo5XSabxzD7-8JZxjpc8sR9cl' + } }, + { private: fixtures.readKey('ec_p521_private.pem', 'ascii'), + public: fixtures.readKey('ec_p521_public.pem', 'ascii'), + keyType: 'ec', + namedCurve: 'secp521r1', + jwk: { + crv: 'P-521', + d: 'ABIIbmn3Gm_Y11uIDkC3g2ijpRxIrJEBY4i_JJYo5OougzTl3BX2ifRluPJMaaHcNer' + + 'bQH_WdVkLLX86ShlHrRyJ', + kty: 'EC', + x: 'AaLFgjwZtznM3N7qsfb86awVXe6c6djUYOob1FN-kllekv0KEXV0bwcDjPGQz5f6MxL' + + 'CbhMeHRavUS6P10rsTtBn', + y: 'Ad3flexBeAfXceNzRBH128kFbOWD6W41NjwKRqqIF26vmgW_8COldGKZjFkOSEASxPB' + + 'cvA2iFJRUyQ3whC00j0Np' + } }, +].forEach((info) => { + const { keyType, namedCurve } = info; + + { + const key = createPrivateKey(info.private); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.deepStrictEqual(key.asymmetricKeyDetails, { namedCurve }); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } + + { + const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.deepStrictEqual(key.asymmetricKeyDetails, { namedCurve }); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } + + { + for (const input of [ + info.private, info.public, { key: info.jwk, format: 'jwk' }]) { + const key = createPublicKey(input); + assert.strictEqual(key.type, 'public'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.deepStrictEqual(key.asymmetricKeyDetails, { namedCurve }); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'spki', format: 'pem' }), info.public); + const jwk = { ...info.jwk }; + delete jwk.d; + assert.deepStrictEqual( + key.export({ format: 'jwk' }), jwk); + } + } +}); + +{ + // Reading an encrypted key without a passphrase should fail. + assert.throws(() => createPrivateKey(privateDsa), common.hasOpenSSL3 ? { + name: 'Error', + message: 'error:07880109:common libcrypto routines::interrupted or ' + + 'cancelled', + } : { + name: 'TypeError', + code: 'ERR_MISSING_PASSPHRASE', + message: 'Passphrase required for encrypted key' + }); + + // Reading an encrypted key with a passphrase that exceeds OpenSSL's buffer + // size limit should fail with an appropriate error code. + assert.throws(() => createPrivateKey({ + key: privateDsa, + format: 'pem', + passphrase: Buffer.alloc(1025, 'a') + }), common.hasOpenSSL3 ? { name: 'Error' } : { + code: 'ERR_OSSL_PEM_BAD_PASSWORD_READ', + name: 'Error' + }); + + // The buffer has a size of 1024 bytes, so this passphrase should be permitted + // (but will fail decryption). + assert.throws(() => createPrivateKey({ + key: privateDsa, + format: 'pem', + passphrase: Buffer.alloc(1024, 'a') + }), { + message: /bad decrypt/ + }); + + const publicKey = createPublicKey(publicDsa); + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'dsa'); + assert.strictEqual(publicKey.symmetricKeySize, undefined); + assert.throws( + () => publicKey.export({ format: 'jwk' }), + { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); + + const privateKey = createPrivateKey({ + key: privateDsa, + format: 'pem', + passphrase: 'secret' + }); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'dsa'); + assert.strictEqual(privateKey.symmetricKeySize, undefined); + assert.throws( + () => privateKey.export({ format: 'jwk' }), + { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); +} + +{ + // Test RSA-PSS. + { + // This key pair does not restrict the message digest algorithm or salt + // length. + const publicPem = fixtures.readKey('rsa_pss_public_2048.pem'); + const privatePem = fixtures.readKey('rsa_pss_private_2048.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + // Because no RSASSA-PSS-params appears in the PEM, no defaults should be + // added for the PSS parameters. This is different from an empty + // RSASSA-PSS-params sequence (see test below). + const expectedKeyDetails = { + modulusLength: 2048, + publicExponent: 65537n + }; + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, expectedKeyDetails); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, expectedKeyDetails); + + assert.throws( + () => publicKey.export({ format: 'jwk' }), + { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); + assert.throws( + () => privateKey.export({ format: 'jwk' }), + { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); + + for (const key of [privatePem, privateKey]) { + // Any algorithm should work. + for (const algo of ['sha1', 'sha256']) { + // Any salt length should work. + for (const saltLength of [undefined, 8, 10, 12, 16, 18, 20]) { + const signature = createSign(algo) + .update('foo') + .sign({ key, saltLength }); + + for (const pkey of [key, publicKey, publicPem]) { + const okay = createVerify(algo) + .update('foo') + .verify({ key: pkey, saltLength }, signature); + + assert.ok(okay); + } + } + } + } + + // Exporting the key using PKCS#1 should not work since this would discard + // any algorithm restrictions. + assert.throws(() => { + publicKey.export({ format: 'pem', type: 'pkcs1' }); + }, { + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' + }); + } + + { + // This key pair enforces sha1 as the message digest and the MGF1 + // message digest and a salt length of 20 bytes. + + const publicPem = fixtures.readKey('rsa_pss_public_2048_sha1_sha1_20.pem'); + const privatePem = + fixtures.readKey('rsa_pss_private_2048_sha1_sha1_20.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + // Unlike the previous key pair, this key pair contains an RSASSA-PSS-params + // sequence. However, because all values in the RSASSA-PSS-params are set to + // their defaults (see RFC 3447), the ASN.1 structure contains an empty + // sequence. Node.js should add the default values to the key details. + const expectedKeyDetails = { + modulusLength: 2048, + publicExponent: 65537n, + hashAlgorithm: 'sha1', + mgf1HashAlgorithm: 'sha1', + saltLength: 20 + }; + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, expectedKeyDetails); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, expectedKeyDetails); + } + + { + // This key pair enforces sha256 as the message digest and the MGF1 + // message digest and a salt length of at least 16 bytes. + const publicPem = + fixtures.readKey('rsa_pss_public_2048_sha256_sha256_16.pem'); + const privatePem = + fixtures.readKey('rsa_pss_private_2048_sha256_sha256_16.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); + + for (const key of [privatePem, privateKey]) { + // Signing with anything other than sha256 should fail. + assert.throws(() => { + createSign('sha1').sign(key); + }, /digest not allowed/); + + // Signing with salt lengths less than 16 bytes should fail. + for (const saltLength of [8, 10, 12]) { + assert.throws(() => { + createSign('sha1').sign({ key, saltLength }); + }, /pss saltlen too small/); + } + + // Signing with sha256 and appropriate salt lengths should work. + for (const saltLength of [undefined, 16, 18, 20]) { + const signature = createSign('sha256') + .update('foo') + .sign({ key, saltLength }); + + for (const pkey of [key, publicKey, publicPem]) { + const okay = createVerify('sha256') + .update('foo') + .verify({ key: pkey, saltLength }, signature); + + assert.ok(okay); + } + } + } + } + + { + // This key enforces sha512 as the message digest and sha256 as the MGF1 + // message digest. + const publicPem = + fixtures.readKey('rsa_pss_public_2048_sha512_sha256_20.pem'); + const privatePem = + fixtures.readKey('rsa_pss_private_2048_sha512_sha256_20.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + const expectedKeyDetails = { + modulusLength: 2048, + publicExponent: 65537n, + hashAlgorithm: 'sha512', + mgf1HashAlgorithm: 'sha256', + saltLength: 20 + }; + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, expectedKeyDetails); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, expectedKeyDetails); + + // Node.js usually uses the same hash function for the message and for MGF1. + // However, when a different MGF1 message digest algorithm has been + // specified as part of the key, it should automatically switch to that. + // This behavior is required by sections 3.1 and 3.3 of RFC4055. + for (const key of [privatePem, privateKey]) { + // sha256 matches the MGF1 hash function and should be used internally, + // but it should not be permitted as the main message digest algorithm. + for (const algo of ['sha1', 'sha256']) { + assert.throws(() => { + createSign(algo).sign(key); + }, /digest not allowed/); + } + + // sha512 should produce a valid signature. + const signature = createSign('sha512') + .update('foo') + .sign(key); + + for (const pkey of [key, publicKey, publicPem]) { + const okay = createVerify('sha512') + .update('foo') + .verify(pkey, signature); + + assert.ok(okay); + } + } + } +} + +{ + // Exporting an encrypted private key requires a cipher + const privateKey = createPrivateKey(privatePem); + assert.throws(() => { + privateKey.export({ + format: 'pem', type: 'pkcs8', passphrase: 'super-secret' + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.cipher' is invalid. Received undefined" + }); +} + +{ + // SecretKeyObject export buffer format (default) + const buffer = Buffer.from('Hello World'); + const keyObject = createSecretKey(buffer); + assert.deepStrictEqual(keyObject.export(), buffer); + assert.deepStrictEqual(keyObject.export({}), buffer); + assert.deepStrictEqual(keyObject.export({ format: 'buffer' }), buffer); + assert.deepStrictEqual(keyObject.export({ format: undefined }), buffer); +} + +{ + // Exporting an "oct" JWK from a SecretKeyObject + const buffer = Buffer.from('Hello World'); + const keyObject = createSecretKey(buffer); + assert.deepStrictEqual( + keyObject.export({ format: 'jwk' }), + { kty: 'oct', k: 'SGVsbG8gV29ybGQ' } + ); +} + +{ + // Exporting a JWK unsupported curve EC key + const supported = ['prime256v1', 'secp256k1', 'secp384r1', 'secp521r1']; + // Find an unsupported curve regardless of whether a FIPS compliant crypto + // provider is currently in use. + const namedCurve = getCurves().find((curve) => !supported.includes(curve)); + assert(namedCurve); + const keyPair = generateKeyPairSync('ec', { namedCurve }); + const { publicKey, privateKey } = keyPair; + assert.throws( + () => publicKey.export({ format: 'jwk' }), + { + code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', + message: `Unsupported JWK EC curve: ${namedCurve}.` + }); + assert.throws( + () => privateKey.export({ format: 'jwk' }), + { + code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', + message: `Unsupported JWK EC curve: ${namedCurve}.` + }); +} + +{ + const first = Buffer.from('Hello'); + const second = Buffer.from('World'); + const keyObject = createSecretKey(first); + assert(createSecretKey(first).equals(createSecretKey(first))); + assert(!createSecretKey(first).equals(createSecretKey(second))); + + assert.throws(() => keyObject.equals(0), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "otherKeyObject" argument must be an instance of KeyObject. Received type number (0)' + }); + + assert(keyObject.equals(keyObject)); + assert(!keyObject.equals(createPublicKey(publicPem))); + assert(!keyObject.equals(createPrivateKey(privatePem))); +} + +{ + const first = generateKeyPairSync('ed25519'); + const second = generateKeyPairSync('ed25519'); + const secret = generateKeySync('aes', { length: 128 }); + + assert(first.publicKey.equals(first.publicKey)); + assert(first.publicKey.equals(createPublicKey( + first.publicKey.export({ format: 'pem', type: 'spki' })))); + assert(!first.publicKey.equals(second.publicKey)); + assert(!first.publicKey.equals(second.privateKey)); + assert(!first.publicKey.equals(secret)); + + assert(first.privateKey.equals(first.privateKey)); + assert(first.privateKey.equals(createPrivateKey( + first.privateKey.export({ format: 'pem', type: 'pkcs8' })))); + assert(!first.privateKey.equals(second.privateKey)); + assert(!first.privateKey.equals(second.publicKey)); + assert(!first.privateKey.equals(secret)); +} + +{ + const first = generateKeyPairSync('ed25519'); + const second = generateKeyPairSync('ed448'); + + assert(!first.publicKey.equals(second.publicKey)); + assert(!first.publicKey.equals(second.privateKey)); + assert(!first.privateKey.equals(second.privateKey)); + assert(!first.privateKey.equals(second.publicKey)); +} + +{ + const first = createSecretKey(Buffer.alloc(0)); + const second = createSecretKey(new ArrayBuffer(0)); + const third = createSecretKey(Buffer.alloc(1)); + assert(first.equals(first)); + assert(first.equals(second)); + assert(!first.equals(third)); + assert(!third.equals(first)); +} + +{ + // This should not cause a crash: https://github.com/nodejs/node/issues/44471 + for (const key of ['', 'foo', null, undefined, true, Boolean]) { + assert.throws(() => { + createPublicKey({ key, format: 'jwk' }); + }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.key" property must be of type object/ }); + assert.throws(() => { + createPrivateKey({ key, format: 'jwk' }); + }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.key" property must be of type object/ }); + } +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-keygen-deprecation.js b/test/js/node/test/parallel/test-crypto-keygen-deprecation.js new file mode 100644 index 0000000000..6aaf148d0e --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-deprecation.js @@ -0,0 +1,55 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L22 + +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const DeprecationWarning = []; +DeprecationWarning.push([ + '"options.hash" is deprecated, use "options.hashAlgorithm" instead.', + 'DEP0154']); +DeprecationWarning.push([ + '"options.mgf1Hash" is deprecated, use "options.mgf1HashAlgorithm" instead.', + 'DEP0154']); + +common.expectWarning({ DeprecationWarning }); + +const assert = require('assert'); +const { generateKeyPair } = require('crypto'); + +{ + // This test makes sure deprecated options still work as intended + + generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: 16, + hash: 'sha256', + mgf1Hash: 'sha256' + }, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { + modulusLength: 512, + publicExponent: 65537n, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha256', + saltLength: 16 + }); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { + modulusLength: 512, + publicExponent: 65537n, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha256', + saltLength: 16 + }); + })); +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-keygen.js b/test/js/node/test/parallel/test-crypto-keygen.js new file mode 100644 index 0000000000..1c32e0738a --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen.js @@ -0,0 +1,826 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L21 + +'use strict'; + +// This tests early errors for invalid encodings. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); + +const { + generateKeyPair, + generateKeyPairSync, +} = require('crypto'); +const { inspect } = require('util'); + + +// Test invalid parameter encoding. +{ + assert.throws(() => generateKeyPairSync('ec', { + namedCurve: 'P-256', + paramEncoding: 'otherEncoding', + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: 'aes-128-cbc', + passphrase: 'top secret' + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.paramEncoding' is invalid. " + + "Received 'otherEncoding'" + }); +} + +{ + // Test invalid key types. + for (const type of [undefined, null, 0]) { + assert.throws(() => generateKeyPairSync(type, {}), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "type" argument must be of type string.' + + common.invalidArgTypeHelper(type) + }); + } + + assert.throws(() => generateKeyPairSync('rsa2', {}), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The argument 'type' must be a supported key type. Received 'rsa2'" + }); +} + +{ + // Test keygen without options object. + assert.throws(() => generateKeyPair('rsa', common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options" argument must be of type object. ' + + 'Received undefined' + }); + + // Even if no options are required, it should be impossible to pass anything + // but an object (or undefined). + assert.throws(() => generateKeyPair('ed448', 0, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options" argument must be of type object. ' + + 'Received type number (0)' + }); +} + +{ + // Invalid publicKeyEncoding. + for (const enc of [0, 'a', true]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: enc, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.publicKeyEncoding' is invalid. " + + `Received ${inspect(enc)}` + }); + } + + // Missing publicKeyEncoding.type. + for (const type of [undefined, null, 0, true, {}]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type, + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.publicKeyEncoding.type' is invalid. " + + `Received ${inspect(type)}` + }); + } + + // Missing / invalid publicKeyEncoding.format. + for (const format of [undefined, null, 0, false, 'a', {}]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.publicKeyEncoding.format' is invalid. " + + `Received ${inspect(format)}` + }); + } + + // Invalid privateKeyEncoding. + for (const enc of [0, 'a', true]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: enc + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.privateKeyEncoding' is invalid. " + + `Received ${inspect(enc)}` + }); + } + + // Missing / invalid privateKeyEncoding.type. + for (const type of [undefined, null, 0, true, {}]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type, + format: 'pem' + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.privateKeyEncoding.type' is invalid. " + + `Received ${inspect(type)}` + }); + } + + // Missing / invalid privateKeyEncoding.format. + for (const format of [undefined, null, 0, false, 'a', {}]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs1', + format + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.privateKeyEncoding.format' is invalid. " + + `Received ${inspect(format)}` + }); + } + + // Cipher of invalid type. + for (const cipher of [0, true, {}]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem', + cipher + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.privateKeyEncoding.cipher' is invalid. " + + `Received ${inspect(cipher)}` + }); + } + + // Invalid cipher. + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: 'foo', + passphrase: 'secret' + } + }), { + name: 'Error', + code: 'ERR_CRYPTO_UNKNOWN_CIPHER', + message: 'Unknown cipher' + }); + + // Cipher, but no valid passphrase. + for (const passphrase of [undefined, null, 5, false, true]) { + assert.throws(() => generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: 'aes-128-cbc', + passphrase + } + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.privateKeyEncoding.passphrase' " + + `is invalid. Received ${inspect(passphrase)}` + }); + } + + // Test invalid callbacks. + for (const cb of [undefined, null, 0, {}]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength: 512, + publicKeyEncoding: { type: 'pkcs1', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs1', format: 'pem' } + }, cb), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE' + }); + } +} + +// Test RSA parameters. +{ + // Test invalid modulus lengths. (non-number) + for (const modulusLength of [undefined, null, 'a', true, {}, []]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength + }, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.modulusLength" property must be of type number.' + + common.invalidArgTypeHelper(modulusLength) + }); + } + + // Test invalid modulus lengths. (non-integer) + for (const modulusLength of [512.1, 1.3, 1.1, 5000.9, 100.5]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: + 'The value of "options.modulusLength" is out of range. ' + + 'It must be an integer. ' + + `Received ${inspect(modulusLength)}` + }); + } + + // Test invalid modulus lengths. (out of range) + for (const modulusLength of [-1, -9, 4294967297]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + }); + } + + // Test invalid exponents. (non-number) + for (const publicExponent of ['a', true, {}, []]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength: 4096, + publicExponent + }, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.publicExponent" property must be of type number.' + + common.invalidArgTypeHelper(publicExponent) + }); + } + + // Test invalid exponents. (non-integer) + for (const publicExponent of [3.5, 1.1, 50.5, 510.5]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength: 4096, + publicExponent + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: + 'The value of "options.publicExponent" is out of range. ' + + 'It must be an integer. ' + + `Received ${inspect(publicExponent)}` + }); + } + + // Test invalid exponents. (out of range) + for (const publicExponent of [-5, -3, 4294967297]) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength: 4096, + publicExponent + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + }); + } + + // Test invalid exponents. (caught by OpenSSL) + for (const publicExponent of [1, 1 + 0x10001]) { + generateKeyPair('rsa', { + modulusLength: 4096, + publicExponent + }, common.mustCall((err) => { + assert.strictEqual(err.name, 'Error'); + assert.match(err.message, common.hasOpenSSL3 ? /exponent/ : /bad e value/); + })); + } +} + +// Test DSA parameters. +{ + // Test invalid modulus lengths. (non-number) + for (const modulusLength of [undefined, null, 'a', true, {}, []]) { + assert.throws(() => generateKeyPair('dsa', { + modulusLength + }, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.modulusLength" property must be of type number.' + + common.invalidArgTypeHelper(modulusLength) + }); + } + + // Test invalid modulus lengths. (non-integer) + for (const modulusLength of [512.1, 1.3, 1.1, 5000.9, 100.5]) { + assert.throws(() => generateKeyPair('dsa', { + modulusLength + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + }); + } + + // Test invalid modulus lengths. (out of range) + for (const modulusLength of [-1, -9, 4294967297]) { + assert.throws(() => generateKeyPair('dsa', { + modulusLength + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + }); + } + + // Test invalid divisor lengths. (non-number) + for (const divisorLength of ['a', true, {}, []]) { + assert.throws(() => generateKeyPair('dsa', { + modulusLength: 2048, + divisorLength + }, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.divisorLength" property must be of type number.' + + common.invalidArgTypeHelper(divisorLength) + }); + } + + // Test invalid divisor lengths. (non-integer) + for (const divisorLength of [4096.1, 5.1, 6.9, 9.5]) { + assert.throws(() => generateKeyPair('dsa', { + modulusLength: 2048, + divisorLength + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: + 'The value of "options.divisorLength" is out of range. ' + + 'It must be an integer. ' + + `Received ${inspect(divisorLength)}` + }); + } + + // Test invalid divisor lengths. (out of range) + for (const divisorLength of [-1, -6, -9, 2147483648]) { + assert.throws(() => generateKeyPair('dsa', { + modulusLength: 2048, + divisorLength + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: + 'The value of "options.divisorLength" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + `Received ${inspect(divisorLength)}` + }); + } +} + +// Test EC parameters. +{ + // Test invalid curves. + assert.throws(() => { + generateKeyPairSync('ec', { + namedCurve: 'abcdef', + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'sec1', format: 'pem' } + }); + }, { + name: 'TypeError', + message: 'Invalid EC curve name' + }); + + // Test error type when curve is not a string + for (const namedCurve of [true, {}, [], 123]) { + assert.throws(() => { + generateKeyPairSync('ec', { + namedCurve, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'sec1', format: 'pem' } + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.namedCurve" property must be of type string.' + + common.invalidArgTypeHelper(namedCurve) + }); + } + + // It should recognize both NIST and standard curve names. + generateKeyPair('ec', { + namedCurve: 'P-256', + }, common.mustSucceed((publicKey, privateKey) => { + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { + namedCurve: 'prime256v1' + }); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { + namedCurve: 'prime256v1' + }); + })); + + generateKeyPair('ec', { + namedCurve: 'secp256k1', + }, common.mustSucceed((publicKey, privateKey) => { + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { + namedCurve: 'secp256k1' + }); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { + namedCurve: 'secp256k1' + }); + })); +} + +{ + assert.throws(() => { + generateKeyPair('dh', common.mustNotCall()); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options" argument must be of type object. Received undefined' + }); + + assert.throws(() => { + generateKeyPair('dh', {}, common.mustNotCall()); + }, { + name: 'TypeError', + code: 'ERR_MISSING_OPTION', + message: 'At least one of the group, prime, or primeLength options is ' + + 'required' + }); + + assert.throws(() => { + generateKeyPair('dh', { + group: 'modp0' + }, common.mustNotCall()); + }, { + name: 'Error', + code: 'ERR_CRYPTO_UNKNOWN_DH_GROUP', + message: 'Unknown DH group' + }); + + assert.throws(() => { + generateKeyPair('dh', { + primeLength: 2147483648 + }, common.mustNotCall()); + }, { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "options.primeLength" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + 'Received 2147483648', + }); + + assert.throws(() => { + generateKeyPair('dh', { + primeLength: -1 + }, common.mustNotCall()); + }, { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "options.primeLength" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + 'Received -1', + }); + + assert.throws(() => { + generateKeyPair('dh', { + primeLength: 2, + generator: 2147483648, + }, common.mustNotCall()); + }, { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "options.generator" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + 'Received 2147483648', + }); + + assert.throws(() => { + generateKeyPair('dh', { + primeLength: 2, + generator: -1, + }, common.mustNotCall()); + }, { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "options.generator" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + 'Received -1', + }); + + // Test incompatible options. + const allOpts = { + group: 'modp5', + prime: Buffer.alloc(0), + primeLength: 1024, + generator: 2 + }; + const incompatible = [ + ['group', 'prime'], + ['group', 'primeLength'], + ['group', 'generator'], + ['prime', 'primeLength'], + ]; + for (const [opt1, opt2] of incompatible) { + assert.throws(() => { + generateKeyPairSync('dh', { + [opt1]: allOpts[opt1], + [opt2]: allOpts[opt2] + }); + }, { + name: 'TypeError', + code: 'ERR_INCOMPATIBLE_OPTION_PAIR', + message: `Option "${opt1}" cannot be used in combination with option ` + + `"${opt2}"` + }); + } +} + +// Test invalid key encoding types. +{ + // Invalid public key type. + for (const type of ['foo', 'pkcs8', 'sec1']) { + assert.throws(() => { + generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { type, format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.publicKeyEncoding.type' is invalid. " + + `Received ${inspect(type)}` + }); + } + + // Invalid hash value. + for (const hashValue of [123, true, {}, []]) { + assert.throws(() => { + generateKeyPairSync('rsa-pss', { + modulusLength: 4096, + hashAlgorithm: hashValue + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.hashAlgorithm" property must be of type string.' + + common.invalidArgTypeHelper(hashValue) + }); + } + + // too long salt length + assert.throws(() => { + generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: 2147483648, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha256' + }, common.mustNotCall()); + }, { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "options.saltLength" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + 'Received 2147483648' + }); + + assert.throws(() => { + generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: -1, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha256' + }, common.mustNotCall()); + }, { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "options.saltLength" is out of range. ' + + 'It must be >= 0 && <= 2147483647. ' + + 'Received -1' + }); + + // Invalid private key type. + for (const type of ['foo', 'spki']) { + assert.throws(() => { + generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type, format: 'pem' } + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The property 'options.privateKeyEncoding.type' is invalid. " + + `Received ${inspect(type)}` + }); + } + + // Key encoding doesn't match key type. + for (const type of ['dsa', 'ec']) { + assert.throws(() => { + generateKeyPairSync(type, { + modulusLength: 4096, + namedCurve: 'P-256', + publicKeyEncoding: { type: 'pkcs1', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + }); + }, { + name: 'Error', + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', + message: 'The selected key encoding pkcs1 can only be used for RSA keys.' + }); + + assert.throws(() => { + generateKeyPairSync(type, { + modulusLength: 4096, + namedCurve: 'P-256', + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs1', format: 'pem' } + }); + }, { + name: 'Error', + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', + message: 'The selected key encoding pkcs1 can only be used for RSA keys.' + }); + } + + for (const type of ['rsa', 'dsa']) { + assert.throws(() => { + generateKeyPairSync(type, { + modulusLength: 4096, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'sec1', format: 'pem' } + }); + }, { + name: 'Error', + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', + message: 'The selected key encoding sec1 can only be used for EC keys.' + }); + } + + // Attempting to encrypt a DER-encoded, non-PKCS#8 key. + for (const type of ['pkcs1', 'sec1']) { + assert.throws(() => { + generateKeyPairSync(type === 'pkcs1' ? 'rsa' : 'ec', { + modulusLength: 4096, + namedCurve: 'P-256', + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { + type, + format: 'der', + cipher: 'aes-128-cbc', + passphrase: 'hello' + } + }); + }, { + name: 'Error', + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', + message: `The selected key encoding ${type} does not support encryption.` + }); + } +} + +{ + // Test RSA-PSS. + assert.throws( + () => { + generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: 16, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: undefined + }); + }, + { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + } + ); + + for (const mgf1HashAlgorithm of [null, 0, false, {}, []]) { + assert.throws( + () => { + generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: 16, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm + }, common.mustNotCall()); + }, + { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options.mgf1HashAlgorithm" property must be of type string.' + + common.invalidArgTypeHelper(mgf1HashAlgorithm) + + } + ); + } + + assert.throws(() => generateKeyPair('rsa-pss', { + modulusLength: 512, + hashAlgorithm: 'sha2', + }, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_CRYPTO_INVALID_DIGEST', + message: 'Invalid digest: sha2' + }); + + assert.throws(() => generateKeyPair('rsa-pss', { + modulusLength: 512, + mgf1HashAlgorithm: 'sha2', + }, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_CRYPTO_INVALID_DIGEST', + message: 'Invalid MGF1 digest: sha2' + }); +} + +{ + // This test makes sure deprecated and new options must + // be the same value. + + assert.throws(() => generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: 16, + mgf1Hash: 'sha256', + mgf1HashAlgorithm: 'sha1' + }, common.mustNotCall()), { code: 'ERR_INVALID_ARG_VALUE' }); + + assert.throws(() => generateKeyPair('rsa-pss', { + modulusLength: 512, + saltLength: 16, + hash: 'sha256', + hashAlgorithm: 'sha1' + }, common.mustNotCall()), { code: 'ERR_INVALID_ARG_VALUE' }); +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-pbkdf2.js b/test/js/node/test/parallel/test-crypto-pbkdf2.js new file mode 100644 index 0000000000..dac8aed95d --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-pbkdf2.js @@ -0,0 +1,241 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +function runPBKDF2(password, salt, iterations, keylen, hash) { + const syncResult = + crypto.pbkdf2Sync(password, salt, iterations, keylen, hash); + + crypto.pbkdf2(password, salt, iterations, keylen, hash, + common.mustSucceed((asyncResult) => { + assert.deepStrictEqual(asyncResult, syncResult); + })); + + return syncResult; +} + +function testPBKDF2(password, salt, iterations, keylen, expected, encoding) { + const actual = runPBKDF2(password, salt, iterations, keylen, 'sha256'); + assert.strictEqual(actual.toString(encoding || 'latin1'), expected); +} + +// +// Test PBKDF2 with RFC 6070 test vectors (except #4) +// + +testPBKDF2('password', 'salt', 1, 20, + '\x12\x0f\xb6\xcf\xfc\xf8\xb3\x2c\x43\xe7\x22\x52' + + '\x56\xc4\xf8\x37\xa8\x65\x48\xc9'); + +testPBKDF2('password', 'salt', 2, 20, + '\xae\x4d\x0c\x95\xaf\x6b\x46\xd3\x2d\x0a\xdf\xf9' + + '\x28\xf0\x6d\xd0\x2a\x30\x3f\x8e'); + +testPBKDF2('password', 'salt', 4096, 20, + '\xc5\xe4\x78\xd5\x92\x88\xc8\x41\xaa\x53\x0d\xb6' + + '\x84\x5c\x4c\x8d\x96\x28\x93\xa0'); + +testPBKDF2('passwordPASSWORDpassword', + 'saltSALTsaltSALTsaltSALTsaltSALTsalt', + 4096, + 25, + '\x34\x8c\x89\xdb\xcb\xd3\x2b\x2f\x32\xd8\x14\xb8\x11' + + '\x6e\x84\xcf\x2b\x17\x34\x7e\xbc\x18\x00\x18\x1c'); + +testPBKDF2('pass\0word', 'sa\0lt', 4096, 16, + '\x89\xb6\x9d\x05\x16\xf8\x29\x89\x3c\x69\x62\x26\x65' + + '\x0a\x86\x87'); + +testPBKDF2('password', 'salt', 32, 32, + '64c486c55d30d4c5a079b8823b7d7cb37ff0556f537da8410233bcec330ed956', + 'hex'); + +// Error path should not leak memory (check with valgrind). +assert.throws( + () => crypto.pbkdf2('password', 'salt', 1, 20, 'sha1'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } +); + +for (const iterations of [-1, 0, 2147483648]) { + assert.throws( + () => crypto.pbkdf2Sync('password', 'salt', iterations, 20, 'sha1'), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + } + ); +} + +['str', null, undefined, [], {}].forEach((notNumber) => { + assert.throws( + () => { + crypto.pbkdf2Sync('password', 'salt', 1, notNumber, 'sha256'); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "keylen" argument must be of type number.' + + `${common.invalidArgTypeHelper(notNumber)}` + }); +}); + +[Infinity, -Infinity, NaN].forEach((input) => { + assert.throws( + () => { + crypto.pbkdf2('password', 'salt', 1, input, 'sha256', + common.mustNotCall()); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "keylen" is out of range. It ' + + `must be an integer. Received ${input}` + }); +}); + +[-1, 2147483648, 4294967296].forEach((input) => { + assert.throws( + () => { + crypto.pbkdf2('password', 'salt', 1, input, 'sha256', + common.mustNotCall()); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + }); +}); + +// Should not get FATAL ERROR with empty password and salt +// https://github.com/nodejs/node/issues/8571 +crypto.pbkdf2('', '', 1, 32, 'sha256', common.mustSucceed()); + +assert.throws( + () => crypto.pbkdf2('password', 'salt', 8, 8, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "digest" argument must be of type string. ' + + 'Received undefined' + }); + +assert.throws( + () => crypto.pbkdf2Sync('password', 'salt', 8, 8), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "digest" argument must be of type string. ' + + 'Received undefined' + }); + +assert.throws( + () => crypto.pbkdf2Sync('password', 'salt', 8, 8, null), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "digest" argument must be of type string. ' + + 'Received null' + }); +[1, {}, [], true, undefined, null].forEach((input) => { + assert.throws( + () => crypto.pbkdf2(input, 'salt', 8, 8, 'sha256', common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } + ); + + assert.throws( + () => crypto.pbkdf2('pass', input, 8, 8, 'sha256', common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } + ); + + assert.throws( + () => crypto.pbkdf2Sync(input, 'salt', 8, 8, 'sha256'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } + ); + + assert.throws( + () => crypto.pbkdf2Sync('pass', input, 8, 8, 'sha256'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } + ); +}); + +['test', {}, [], true, undefined, null].forEach((i) => { + const received = common.invalidArgTypeHelper(i); + assert.throws( + () => crypto.pbkdf2('pass', 'salt', i, 8, 'sha256', common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "iterations" argument must be of type number.${received}` + } + ); + + assert.throws( + () => crypto.pbkdf2Sync('pass', 'salt', i, 8, 'sha256'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "iterations" argument must be of type number.${received}` + } + ); +}); + +// Any TypedArray should work for password and salt. +for (const SomeArray of [Uint8Array, Uint16Array, Uint32Array, Float32Array, + Float64Array, ArrayBuffer, SharedArrayBuffer]) { + runPBKDF2(new SomeArray(10), 'salt', 8, 8, 'sha256'); + runPBKDF2('pass', new SomeArray(10), 8, 8, 'sha256'); +} + +assert.throws( + () => crypto.pbkdf2('pass', 'salt', 8, 8, 'md55', common.mustNotCall()), + { + code: 'ERR_CRYPTO_INVALID_DIGEST', + name: 'TypeError', + message: 'Invalid digest: md55' + } +); + +assert.throws( + () => crypto.pbkdf2Sync('pass', 'salt', 8, 8, 'md55'), + { + code: 'ERR_CRYPTO_INVALID_DIGEST', + name: 'TypeError', + message: 'Invalid digest: md55' + } +); + +if (!common.openSSLIsBoringSSL) { + const kNotPBKDF2Supported = ['shake128', 'shake256']; + crypto.getHashes() + .filter((hash) => !kNotPBKDF2Supported.includes(hash)) + .forEach((hash) => { + runPBKDF2(new Uint8Array(10), 'salt', 8, 8, hash); + }); +} + +{ + // This should not crash. + assert.throws( + () => crypto.pbkdf2Sync('1', '2', 1, 1, '%'), + { + code: 'ERR_CRYPTO_INVALID_DIGEST', + name: 'TypeError', + message: 'Invalid digest: %' + } + ); +} diff --git a/test/js/node/test/parallel/test-crypto-psychic-signatures.js b/test/js/node/test/parallel/test-crypto-psychic-signatures.js new file mode 100644 index 0000000000..e8228b26be --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-psychic-signatures.js @@ -0,0 +1,100 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); + +const crypto = require('crypto'); + +// Tests for CVE-2022-21449 +// https://neilmadden.blog/2022/04/19/psychic-signatures-in-java/ +// Dubbed "Psychic Signatures", these signatures bypassed the ECDSA signature +// verification implementation in Java in 15, 16, 17, and 18. OpenSSL is not +// (and was not) vulnerable so these are a precaution. + +const vectors = { + 'ieee-p1363': [ + Buffer.from('0000000000000000000000000000000000000000000000000000000000000000' + + '0000000000000000000000000000000000000000000000000000000000000000', 'hex'), + Buffer.from('ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551' + + 'ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551', 'hex'), + ], + 'der': [ + Buffer.from('3046022100' + + '0000000000000000000000000000000000000000000000000000000000000000' + + '022100' + + '0000000000000000000000000000000000000000000000000000000000000000', 'hex'), + Buffer.from('3046022100' + + 'ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551' + + '022100' + + 'ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551', 'hex'), + ], +}; + +const keyPair = crypto.generateKeyPairSync('ec', { + namedCurve: 'P-256', + publicKeyEncoding: { + format: 'der', + type: 'spki' + }, +}); + +const data = Buffer.from('Hello!'); + +for (const [encoding, signatures] of Object.entries(vectors)) { + for (const signature of signatures) { + const key = { + key: keyPair.publicKey, + format: 'der', + type: 'spki', + dsaEncoding: encoding, + }; + + // one-shot sync + assert.strictEqual( + crypto.verify( + 'sha256', + data, + key, + signature, + ), + false, + ); + + // one-shot async + crypto.verify( + 'sha256', + data, + key, + signature, + common.mustSucceed((verified) => assert.strictEqual(verified, false)), + ); + + // stream + assert.strictEqual( + crypto.createVerify('sha256') + .update(data) + .verify(key, signature), + false, + ); + + // webcrypto + globalThis.crypto.subtle.importKey( + 'spki', + keyPair.publicKey, + { name: 'ECDSA', namedCurve: 'P-256' }, + false, + ['verify'], + ).then((publicKey) => { + return globalThis.crypto.subtle.verify( + { name: 'ECDSA', hash: 'SHA-256' }, + publicKey, + signature, + data, + ); + }).then(common.mustCall((verified) => { + assert.strictEqual(verified, false); + })); + } +} diff --git a/test/js/node/test/parallel/test-crypto-rsa-dsa.js b/test/js/node/test/parallel/test-crypto-rsa-dsa.js new file mode 100644 index 0000000000..84df67ce5b --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-rsa-dsa.js @@ -0,0 +1,549 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L24 + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +const constants = crypto.constants; + +const fixtures = require('../common/fixtures'); + +// Test certificates +const certPem = fixtures.readKey('rsa_cert.crt'); +const keyPem = fixtures.readKey('rsa_private.pem'); +const rsaKeySize = 2048; +const rsaPubPem = fixtures.readKey('rsa_public.pem', 'ascii'); +const rsaKeyPem = fixtures.readKey('rsa_private.pem', 'ascii'); +const rsaKeyPemEncrypted = fixtures.readKey('rsa_private_encrypted.pem', + 'ascii'); +const dsaPubPem = fixtures.readKey('dsa_public.pem', 'ascii'); +const dsaKeyPem = fixtures.readKey('dsa_private.pem', 'ascii'); +const dsaKeyPemEncrypted = fixtures.readKey('dsa_private_encrypted.pem', + 'ascii'); +const rsaPkcs8KeyPem = fixtures.readKey('rsa_private_pkcs8.pem'); +const dsaPkcs8KeyPem = fixtures.readKey('dsa_private_pkcs8.pem'); + +const ec = new TextEncoder(); + +const openssl1DecryptError = { + message: 'error:06065064:digital envelope routines:EVP_DecryptFinal_ex:' + + 'bad decrypt', + code: 'ERR_OSSL_EVP_BAD_DECRYPT', + reason: 'bad decrypt', + function: 'EVP_DecryptFinal_ex', + library: 'digital envelope routines', +}; + +const decryptError = common.hasOpenSSL3 ? + { message: 'error:1C800064:Provider routines::bad decrypt' } : + openssl1DecryptError; + +const decryptPrivateKeyError = common.hasOpenSSL3 ? { + message: 'error:1C800064:Provider routines::bad decrypt', +} : openssl1DecryptError; + +function getBufferCopy(buf) { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +} + +// Test RSA encryption/decryption +{ + const input = 'I AM THE WALRUS'; + const bufferToEncrypt = Buffer.from(input); + const bufferPassword = Buffer.from('password'); + + let encryptedBuffer = crypto.publicEncrypt(rsaPubPem, bufferToEncrypt); + + // Test other input types + let otherEncrypted; + { + const ab = getBufferCopy(ec.encode(rsaPubPem)); + const ab2enc = getBufferCopy(bufferToEncrypt); + + crypto.publicEncrypt(ab, ab2enc); + crypto.publicEncrypt(new Uint8Array(ab), new Uint8Array(ab2enc)); + crypto.publicEncrypt(new DataView(ab), new DataView(ab2enc)); + otherEncrypted = crypto.publicEncrypt({ + key: Buffer.from(ab).toString('hex'), + encoding: 'hex' + }, Buffer.from(ab2enc).toString('hex')); + } + + let decryptedBuffer = crypto.privateDecrypt(rsaKeyPem, encryptedBuffer); + const otherDecrypted = crypto.privateDecrypt(rsaKeyPem, otherEncrypted); + assert.strictEqual(decryptedBuffer.toString(), input); + assert.strictEqual(otherDecrypted.toString(), input); + + decryptedBuffer = crypto.privateDecrypt(rsaPkcs8KeyPem, encryptedBuffer); + assert.strictEqual(decryptedBuffer.toString(), input); + + let decryptedBufferWithPassword = crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'password' + }, encryptedBuffer); + + const otherDecryptedBufferWithPassword = crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: ec.encode('password') + }, encryptedBuffer); + + assert.strictEqual( + otherDecryptedBufferWithPassword.toString(), + decryptedBufferWithPassword.toString()); + + decryptedBufferWithPassword = crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'password' + }, encryptedBuffer); + + assert.strictEqual(decryptedBufferWithPassword.toString(), input); + + encryptedBuffer = crypto.publicEncrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'password' + }, bufferToEncrypt); + + decryptedBufferWithPassword = crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'password' + }, encryptedBuffer); + assert.strictEqual(decryptedBufferWithPassword.toString(), input); + + encryptedBuffer = crypto.privateEncrypt({ + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, bufferToEncrypt); + + decryptedBufferWithPassword = crypto.publicDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, encryptedBuffer); + assert.strictEqual(decryptedBufferWithPassword.toString(), input); + + // Now with explicit RSA_PKCS1_PADDING. + encryptedBuffer = crypto.privateEncrypt({ + padding: crypto.constants.RSA_PKCS1_PADDING, + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, bufferToEncrypt); + + decryptedBufferWithPassword = crypto.publicDecrypt({ + padding: crypto.constants.RSA_PKCS1_PADDING, + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, encryptedBuffer); + assert.strictEqual(decryptedBufferWithPassword.toString(), input); + + // Omitting padding should be okay because RSA_PKCS1_PADDING is the default. + decryptedBufferWithPassword = crypto.publicDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, encryptedBuffer); + assert.strictEqual(decryptedBufferWithPassword.toString(), input); + + // Now with RSA_NO_PADDING. Plaintext needs to match key size. + // OpenSSL 3.x has a rsa_check_padding that will cause an error if + // RSA_NO_PADDING is used. + if (!common.hasOpenSSL3) { + { + const plaintext = 'x'.repeat(rsaKeySize / 8); + encryptedBuffer = crypto.privateEncrypt({ + padding: crypto.constants.RSA_NO_PADDING, + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, Buffer.from(plaintext)); + + decryptedBufferWithPassword = crypto.publicDecrypt({ + padding: crypto.constants.RSA_NO_PADDING, + key: rsaKeyPemEncrypted, + passphrase: bufferPassword + }, encryptedBuffer); + assert.strictEqual(decryptedBufferWithPassword.toString(), plaintext); + } + } + + encryptedBuffer = crypto.publicEncrypt(certPem, bufferToEncrypt); + + decryptedBuffer = crypto.privateDecrypt(keyPem, encryptedBuffer); + assert.strictEqual(decryptedBuffer.toString(), input); + + encryptedBuffer = crypto.publicEncrypt(keyPem, bufferToEncrypt); + + decryptedBuffer = crypto.privateDecrypt(keyPem, encryptedBuffer); + assert.strictEqual(decryptedBuffer.toString(), input); + + encryptedBuffer = crypto.privateEncrypt(keyPem, bufferToEncrypt); + + decryptedBuffer = crypto.publicDecrypt(keyPem, encryptedBuffer); + assert.strictEqual(decryptedBuffer.toString(), input); + + assert.throws(() => { + crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'wrong' + }, bufferToEncrypt); + }, decryptError); + + assert.throws(() => { + crypto.publicEncrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'wrong' + }, encryptedBuffer); + }, decryptError); + + encryptedBuffer = crypto.privateEncrypt({ + key: rsaKeyPemEncrypted, + passphrase: Buffer.from('password') + }, bufferToEncrypt); + + assert.throws(() => { + crypto.publicDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: Buffer.from('wrong') + }, encryptedBuffer); + }, decryptError); +} + +function test_rsa(padding, encryptOaepHash, decryptOaepHash) { + const size = (padding === 'RSA_NO_PADDING') ? rsaKeySize / 8 : 32; + const input = Buffer.allocUnsafe(size); + for (let i = 0; i < input.length; i++) + input[i] = (i * 7 + 11) & 0xff; + const bufferToEncrypt = Buffer.from(input); + + padding = constants[padding]; + + const encryptedBuffer = crypto.publicEncrypt({ + key: rsaPubPem, + padding: padding, + oaepHash: encryptOaepHash + }, bufferToEncrypt); + + + if (padding === constants.RSA_PKCS1_PADDING) { + if (!process.config.variables.node_shared_openssl) { + assert.throws(() => { + crypto.privateDecrypt({ + key: rsaKeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + }, { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws(() => { + crypto.privateDecrypt({ + key: rsaPkcs8KeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + }, { code: 'ERR_INVALID_ARG_VALUE' }); + } else { + // The version of a linked against OpenSSL. May + // or may not support implicit rejection. Figuring + // this out in the test is not feasible but we + // require that it pass based on one of the two + // cases of supporting it or not. + try { + // The expected exceptions should be thrown if implicit rejection + // is not supported + assert.throws(() => { + crypto.privateDecrypt({ + key: rsaKeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + }, { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws(() => { + crypto.privateDecrypt({ + key: rsaPkcs8KeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + }, { code: 'ERR_INVALID_ARG_VALUE' }); + } catch (e) { + if (e.toString() === + 'AssertionError [ERR_ASSERTION]: Missing expected exception.') { + // Implicit rejection must be supported since + // we did not get the exceptions that are thrown + // when it is not, we should be able to decrypt + let decryptedBuffer = crypto.privateDecrypt({ + key: rsaKeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + assert.deepStrictEqual(decryptedBuffer, input); + + decryptedBuffer = crypto.privateDecrypt({ + key: rsaPkcs8KeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + assert.deepStrictEqual(decryptedBuffer, input); + } else { + // There was an exception but it is not the one we expect if implicit + // rejection is not supported so there was some other failure, + // re-throw it so the test fails + throw e; + } + } + } + } else { + let decryptedBuffer = crypto.privateDecrypt({ + key: rsaKeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + assert.deepStrictEqual(decryptedBuffer, input); + + decryptedBuffer = crypto.privateDecrypt({ + key: rsaPkcs8KeyPem, + padding: padding, + oaepHash: decryptOaepHash + }, encryptedBuffer); + assert.deepStrictEqual(decryptedBuffer, input); + } +} + +test_rsa('RSA_NO_PADDING'); +test_rsa('RSA_PKCS1_PADDING'); +test_rsa('RSA_PKCS1_OAEP_PADDING'); + +// Test OAEP with different hash functions. +test_rsa('RSA_PKCS1_OAEP_PADDING', undefined, 'sha1'); +test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha1', undefined); +test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha256', 'sha256'); +test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha512', 'sha512'); +assert.throws(() => { + test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha256', 'sha512'); +}, { + code: 'ERR_OSSL_RSA_OAEP_DECODING_ERROR' +}); + +// The following RSA-OAEP test cases were created using the WebCrypto API to +// ensure compatibility when using non-SHA1 hash functions. +{ + const { decryptionTests } = + JSON.parse(fixtures.readSync('rsa-oaep-test-vectors.js', 'utf8')); + + for (const { ct, oaepHash, oaepLabel } of decryptionTests) { + const label = oaepLabel ? Buffer.from(oaepLabel, 'hex') : undefined; + const copiedLabel = oaepLabel ? getBufferCopy(label) : undefined; + + const decrypted = crypto.privateDecrypt({ + key: rsaPkcs8KeyPem, + oaepHash, + oaepLabel: oaepLabel ? label : undefined + }, Buffer.from(ct, 'hex')); + + assert.strictEqual(decrypted.toString('utf8'), 'Hello Node.js'); + + const otherDecrypted = crypto.privateDecrypt({ + key: rsaPkcs8KeyPem, + oaepHash, + oaepLabel: copiedLabel + }, Buffer.from(ct, 'hex')); + + assert.strictEqual(otherDecrypted.toString('utf8'), 'Hello Node.js'); + } +} + +// Test invalid oaepHash and oaepLabel options. +for (const fn of [crypto.publicEncrypt, crypto.privateDecrypt]) { + assert.throws(() => { + fn({ + key: rsaPubPem, + oaepHash: 'Hello world' + }, Buffer.alloc(10)); + }, { + code: 'ERR_OSSL_EVP_INVALID_DIGEST' + }); + + for (const oaepHash of [0, false, null, Symbol(), () => {}]) { + assert.throws(() => { + fn({ + key: rsaPubPem, + oaepHash + }, Buffer.alloc(10)); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + } + + for (const oaepLabel of [0, false, null, Symbol(), () => {}, {}]) { + assert.throws(() => { + fn({ + key: rsaPubPem, + oaepLabel + }, Buffer.alloc(10)); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + } +} + +// Test RSA key signing/verification +let rsaSign = crypto.createSign('SHA1'); +let rsaVerify = crypto.createVerify('SHA1'); +assert.ok(rsaSign); +assert.ok(rsaVerify); + +const expectedSignature = fixtures.readKey( + 'rsa_public_sha1_signature_signedby_rsa_private_pkcs8.sha1', + 'hex' +); + +rsaSign.update(rsaPubPem); +let rsaSignature = rsaSign.sign(rsaKeyPem, 'hex'); +assert.strictEqual(rsaSignature, expectedSignature); + +rsaVerify.update(rsaPubPem); +assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); + +// Test RSA PKCS#8 key signing/verification +rsaSign = crypto.createSign('SHA1'); +rsaSign.update(rsaPubPem); +rsaSignature = rsaSign.sign(rsaPkcs8KeyPem, 'hex'); +assert.strictEqual(rsaSignature, expectedSignature); + +rsaVerify = crypto.createVerify('SHA1'); +rsaVerify.update(rsaPubPem); +assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); + +// Test RSA key signing/verification with encrypted key +rsaSign = crypto.createSign('SHA1'); +rsaSign.update(rsaPubPem); +const signOptions = { key: rsaKeyPemEncrypted, passphrase: 'password' }; +rsaSignature = rsaSign.sign(signOptions, 'hex'); +assert.strictEqual(rsaSignature, expectedSignature); + +rsaVerify = crypto.createVerify('SHA1'); +rsaVerify.update(rsaPubPem); +assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); + +rsaSign = crypto.createSign('SHA1'); +rsaSign.update(rsaPubPem); +assert.throws(() => { + const signOptions = { key: rsaKeyPemEncrypted, passphrase: 'wrong' }; + rsaSign.sign(signOptions, 'hex'); +}, decryptPrivateKeyError); + +// +// Test RSA signing and verification +// +{ + const privateKey = fixtures.readKey('rsa_private_b.pem'); + const publicKey = fixtures.readKey('rsa_public_b.pem'); + + const input = 'I AM THE WALRUS'; + + const signature = fixtures.readKey( + 'I_AM_THE_WALRUS_sha256_signature_signedby_rsa_private_b.sha256', + 'hex' + ); + + const sign = crypto.createSign('SHA256'); + sign.update(input); + + const output = sign.sign(privateKey, 'hex'); + assert.strictEqual(output, signature); + + const verify = crypto.createVerify('SHA256'); + verify.update(input); + + assert.strictEqual(verify.verify(publicKey, signature, 'hex'), true); + + // Test the legacy signature algorithm name. + const sign2 = crypto.createSign('RSA-SHA256'); + sign2.update(input); + + const output2 = sign2.sign(privateKey, 'hex'); + assert.strictEqual(output2, signature); + + const verify2 = crypto.createVerify('SHA256'); + verify2.update(input); + + assert.strictEqual(verify2.verify(publicKey, signature, 'hex'), true); +} + + +// +// Test DSA signing and verification +// +{ + const input = 'I AM THE WALRUS'; + + // DSA signatures vary across runs so there is no static string to verify + // against. + const sign = crypto.createSign('SHA1'); + sign.update(input); + const signature = sign.sign(dsaKeyPem, 'hex'); + + const verify = crypto.createVerify('SHA1'); + verify.update(input); + + assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); + + // Test the legacy 'DSS1' name. + const sign2 = crypto.createSign('DSS1'); + sign2.update(input); + const signature2 = sign2.sign(dsaKeyPem, 'hex'); + + const verify2 = crypto.createVerify('DSS1'); + verify2.update(input); + + assert.strictEqual(verify2.verify(dsaPubPem, signature2, 'hex'), true); +} + + +// +// Test DSA signing and verification with PKCS#8 private key +// +{ + const input = 'I AM THE WALRUS'; + + // DSA signatures vary across runs so there is no static string to verify + // against. + const sign = crypto.createSign('SHA1'); + sign.update(input); + const signature = sign.sign(dsaPkcs8KeyPem, 'hex'); + + const verify = crypto.createVerify('SHA1'); + verify.update(input); + + assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); +} + + +// +// Test DSA signing and verification with encrypted key +// +const input = 'I AM THE WALRUS'; + +{ + const sign = crypto.createSign('SHA1'); + sign.update(input); + assert.throws(() => { + sign.sign({ key: dsaKeyPemEncrypted, passphrase: 'wrong' }, 'hex'); + }, decryptPrivateKeyError); +} + +{ + // DSA signatures vary across runs so there is no static string to verify + // against. + const sign = crypto.createSign('SHA1'); + sign.update(input); + const signOptions = { key: dsaKeyPemEncrypted, passphrase: 'password' }; + const signature = sign.sign(signOptions, 'hex'); + + const verify = crypto.createVerify('SHA1'); + verify.update(input); + + assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-secure-heap.js b/test/js/node/test/parallel/test-crypto-secure-heap.js new file mode 100644 index 0000000000..9e19181e40 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-secure-heap.js @@ -0,0 +1,82 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L26 + +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (common.isWindows) + common.skip('Not supported on Windows'); + +if (common.isASan) + common.skip('ASan does not play well with secure heap allocations'); + +const assert = require('assert'); +const { fork } = require('child_process'); +const fixtures = require('../common/fixtures'); +const { + secureHeapUsed, + createDiffieHellman, +} = require('crypto'); + +if (process.argv[2] === 'child') { + + const a = secureHeapUsed(); + + assert(a); + assert.strictEqual(typeof a, 'object'); + assert.strictEqual(a.total, 65536); + assert.strictEqual(a.min, 4); + assert.strictEqual(a.used, 0); + + { + const size = common.hasFipsCrypto || common.hasOpenSSL3 ? 1024 : 256; + const dh1 = createDiffieHellman(size); + const p1 = dh1.getPrime('buffer'); + const dh2 = createDiffieHellman(p1, 'buffer'); + const key1 = dh1.generateKeys(); + const key2 = dh2.generateKeys('hex'); + dh1.computeSecret(key2, 'hex', 'base64'); + dh2.computeSecret(key1, 'latin1', 'buffer'); + + const b = secureHeapUsed(); + assert(b); + assert.strictEqual(typeof b, 'object'); + assert.strictEqual(b.total, 65536); + assert.strictEqual(b.min, 4); + // The amount used can vary on a number of factors + assert(b.used > 0); + assert(b.utilization > 0.0); + } + + return; +} + +const child = fork( + process.argv[1], + ['child'], + { execArgv: ['--secure-heap=65536', '--secure-heap-min=4'] }); + +child.on('exit', common.mustCall((code) => { + assert.strictEqual(code, 0); +})); + +{ + const child = fork(fixtures.path('a.js'), { + execArgv: ['--secure-heap=3', '--secure-heap-min=3'], + stdio: 'pipe' + }); + let res = ''; + child.on('exit', common.mustCall((code) => { + assert.notStrictEqual(code, 0); + assert.match(res, /--secure-heap must be a power of 2/); + assert.match(res, /--secure-heap-min must be a power of 2/); + })); + child.stderr.setEncoding('utf8'); + child.stderr.on('data', (chunk) => res += chunk); +} + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-crypto-verify-failure.js b/test/js/node/test/parallel/test-crypto-verify-failure.js new file mode 100644 index 0000000000..ad7d5d4f86 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-verify-failure.js @@ -0,0 +1,67 @@ +// 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 crypto = require('crypto'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const certPem = fixtures.readKey('rsa_cert.crt'); + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + +const server = tls.Server(options, (socket) => { + setImmediate(() => { + verify(); + setImmediate(() => { + socket.destroy(); + }); + }); +}); + +function verify() { + crypto.createVerify('SHA1') + .update('Test') + .verify(certPem, 'asdfasdfas', 'base64'); +} + +server.listen(0, common.mustCall(() => { + tls.connect({ + port: server.address().port, + rejectUnauthorized: false + }, common.mustCall(() => { + verify(); + })) + .on('error', common.mustNotCall()) + .on('close', common.mustCall(() => { + server.close(); + })).resume(); +})); + +server.unref(); diff --git a/test/js/node/test/parallel/test-crypto-webcrypto-aes-decrypt-tag-too-small.js b/test/js/node/test/parallel/test-crypto-webcrypto-aes-decrypt-tag-too-small.js new file mode 100644 index 0000000000..589a2f91a1 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-webcrypto-aes-decrypt-tag-too-small.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +subtle.importKey( + 'raw', + new Uint8Array(32), + { + name: 'AES-GCM' + }, + false, + [ 'encrypt', 'decrypt' ]) + .then((k) => + assert.rejects(() => { + return subtle.decrypt({ + name: 'AES-GCM', + iv: new Uint8Array(12), + }, k, new Uint8Array(0)); + }, { + name: 'OperationError', + message: /The provided data is too small/, + }) + ).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-crypto-x509.js b/test/js/node/test/parallel/test-crypto-x509.js new file mode 100644 index 0000000000..2530569bfb --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-x509.js @@ -0,0 +1,446 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { + X509Certificate, + createPrivateKey, + generateKeyPairSync, + createSign, +} = require('crypto'); + +// const { +// isX509Certificate +// } = require('internal/crypto/x509'); + +const { isX509Certificate } = process.binding("crypto/x509"); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const { readFileSync } = require('fs'); + +const cert = readFileSync(fixtures.path('keys', 'agent1-cert.pem')); +const key = readFileSync(fixtures.path('keys', 'agent1-key.pem')); +const ca = readFileSync(fixtures.path('keys', 'ca1-cert.pem')); + +const privateKey = createPrivateKey(key); + +[1, {}, false, null].forEach((i) => { + assert.throws(() => new X509Certificate(i), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +const subjectCheck = `C=US +ST=CA +L=SF +O=Joyent +OU=Node.js +CN=agent1 +emailAddress=ry@tinyclouds.org`; + +const issuerCheck = `C=US +ST=CA +L=SF +O=Joyent +OU=Node.js +CN=ca1 +emailAddress=ry@tinyclouds.org`; + +let infoAccessCheck = `OCSP - URI:http://ocsp.nodejs.org/ +CA Issuers - URI:http://ca.nodejs.org/ca.cert`; +if (!common.hasOpenSSL3) + infoAccessCheck += '\n'; + +const der = Buffer.from( + '308203e8308202d0a0030201020214147d36c1c2f74206de9fab5f2226d78adb00a42630' + + '0d06092a864886f70d01010b0500307a310b3009060355040613025553310b3009060355' + + '04080c024341310b300906035504070c025346310f300d060355040a0c064a6f79656e74' + + '3110300e060355040b0c074e6f64652e6a73310c300a06035504030c036361313120301e' + + '06092a864886f70d010901161172794074696e79636c6f7564732e6f72673020170d3232' + + '303930333231343033375a180f32323936303631373231343033375a307d310b30090603' + + '55040613025553310b300906035504080c024341310b300906035504070c025346310f30' + + '0d060355040a0c064a6f79656e743110300e060355040b0c074e6f64652e6a73310f300d' + + '06035504030c066167656e74313120301e06092a864886f70d010901161172794074696e' + + '79636c6f7564732e6f726730820122300d06092a864886f70d01010105000382010f0030' + + '82010a0282010100d456320afb20d3827093dc2c4284ed04dfbabd56e1ddae529e28b790' + + 'cd4256db273349f3735ffd337c7a6363ecca5a27b7f73dc7089a96c6d886db0c62388f1c' + + 'dd6a963afcd599d5800e587a11f908960f84ed50ba25a28303ecda6e684fbe7baedc9ce8' + + '801327b1697af25097cee3f175e400984c0db6a8eb87be03b4cf94774ba56fffc8c63c68' + + 'd6adeb60abbe69a7b14ab6a6b9e7baa89b5adab8eb07897c07f6d4fa3d660dff574107d2' + + '8e8f63467a788624c574197693e959cea1362ffae1bba10c8c0d88840abfef103631b2e8' + + 'f5c39b5548a7ea57e8a39f89291813f45a76c448033a2b7ed8403f4baa147cf35e2d2554' + + 'aa65ce49695797095bf4dc6b0203010001a361305f305d06082b06010505070101045130' + + '4f302306082b060105050730018617687474703a2f2f6f6373702e6e6f64656a732e6f72' + + '672f302806082b06010505073002861c687474703a2f2f63612e6e6f64656a732e6f7267' + + '2f63612e63657274300d06092a864886f70d01010b05000382010100c3349810632ccb7d' + + 'a585de3ed51e34ed154f0f7215608cf2701c00eda444dc2427072c8aca4da6472c1d9e68' + + 'f177f99a90a8b5dbf3884586d61cb1c14ea7016c8d38b70d1b46b42947db30edc1e9961e' + + 'd46c0f0e35da427bfbe52900771817e733b371adf19e12137235141a34347db0dfc05579' + + '8b1f269f3bdf5e30ce35d1339d56bb3c570de9096215433047f87ca42447b44e7e6b5d0e' + + '48f7894ab186f85b6b1a74561b520952fea888617f32f582afce1111581cd63efcc68986' + + '00d248bb684dedb9c3d6710c38de9e9bc21f9c3394b729d5f707d64ea890603e5989f8fa' + + '59c19ad1a00732e7adc851b89487cc00799dde068aa64b3b8fd976e8bc113ef2', + 'hex'); + +{ + const x509 = new X509Certificate(cert); + + assert(isX509Certificate(x509)); + + assert(!x509.ca); + assert.strictEqual(x509.subject, subjectCheck); + assert.strictEqual(x509.subjectAltName, undefined); + assert.strictEqual(x509.issuer, issuerCheck); + assert.strictEqual(x509.infoAccess, infoAccessCheck); + assert.strictEqual(x509.validFrom, 'Sep 3 21:40:37 2022 GMT'); + assert.strictEqual(x509.validTo, 'Jun 17 21:40:37 2296 GMT'); + assert.deepStrictEqual(x509.validFromDate, new Date('2022-09-03T21:40:37Z')); + assert.deepStrictEqual(x509.validToDate, new Date('2296-06-17T21:40:37Z')); + assert.strictEqual( + x509.fingerprint, + '8B:89:16:C4:99:87:D2:13:1A:64:94:36:38:A5:32:01:F0:95:3B:53'); + assert.strictEqual( + x509.fingerprint256, + '2C:62:59:16:91:89:AB:90:6A:3E:98:88:A6:D3:C5:58:58:6C:AE:FF:9C:33:' + + '22:7C:B6:77:D3:34:E7:53:4B:05' + ); + assert.strictEqual( + x509.fingerprint512, + '0B:6F:D0:4D:6B:22:53:99:66:62:51:2D:2C:96:F2:58:3F:95:1C:CC:4C:44:' + + '9D:B5:59:AA:AD:A8:F6:2A:24:8A:BB:06:A5:26:42:52:30:A3:37:61:30:A9:' + + '5A:42:63:E0:21:2F:D6:70:63:07:96:6F:27:A7:78:12:08:02:7A:8B' + ); + assert.strictEqual(x509.keyUsage, undefined); + assert.strictEqual(x509.serialNumber.toUpperCase(), '147D36C1C2F74206DE9FAB5F2226D78ADB00A426'); + + assert.deepStrictEqual(x509.raw, der); + + assert(x509.publicKey); + assert.strictEqual(x509.publicKey.type, 'public'); + + assert.strictEqual(x509.toString().replaceAll('\r\n', '\n'), + cert.toString().replaceAll('\r\n', '\n')); + assert.strictEqual(x509.toJSON(), x509.toString()); + + assert(x509.checkPrivateKey(privateKey)); + assert.throws(() => x509.checkPrivateKey(x509.publicKey), { + code: 'ERR_INVALID_ARG_VALUE' + }); + + assert.strictEqual(x509.checkIP('127.0.0.1'), undefined); + assert.strictEqual(x509.checkIP('::'), undefined); + assert.strictEqual(x509.checkHost('agent1'), 'agent1'); + assert.strictEqual(x509.checkHost('agent2'), undefined); + assert.strictEqual(x509.checkEmail('ry@tinyclouds.org'), 'ry@tinyclouds.org'); + assert.strictEqual(x509.checkEmail('sally@example.com'), undefined); + assert.throws(() => x509.checkHost('agent\x001'), { + code: 'ERR_INVALID_ARG_VALUE' + }); + assert.throws(() => x509.checkIP('[::]'), { + code: 'ERR_INVALID_ARG_VALUE' + }); + assert.throws(() => x509.checkEmail('not\x00hing'), { + code: 'ERR_INVALID_ARG_VALUE' + }); + + [1, false, null].forEach((i) => { + assert.throws(() => x509.checkHost('agent1', i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => x509.checkHost('agent1', { subject: i }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [ + 'wildcards', + 'partialWildcards', + 'multiLabelWildcards', + 'singleLabelSubdomains', + ].forEach((key) => { + [1, '', null, {}].forEach((i) => { + assert.throws(() => x509.checkHost('agent1', { [key]: i }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + }); + + const ca_cert = new X509Certificate(ca); + + assert(x509.checkIssued(ca_cert)); + assert(!x509.checkIssued(x509)); + assert(x509.verify(ca_cert.publicKey)); + assert(!x509.verify(x509.publicKey)); + + assert.throws(() => x509.checkIssued({}), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => x509.checkIssued(''), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => x509.verify({}), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => x509.verify(''), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => x509.verify(privateKey), { + code: 'ERR_INVALID_ARG_VALUE' + }); + + { + // https://github.com/nodejs/node/issues/45377 + // https://github.com/nodejs/node/issues/45485 + // Confirm failures of + // X509Certificate:verify() + // X509Certificate:CheckPrivateKey() + // X509Certificate:CheckCA() + // X509Certificate:CheckIssued() + // X509Certificate:ToLegacy() + // do not affect other functions that use OpenSSL. + // Subsequent calls to e.g. createPrivateKey should not throw. + const keyPair = generateKeyPairSync('ed25519'); + assert(!x509.verify(keyPair.publicKey)); + createPrivateKey(key); + assert(!x509.checkPrivateKey(keyPair.privateKey)); + createPrivateKey(key); + const certPem = ` +-----BEGIN CERTIFICATE----- +MIID6zCCAtOgAwIBAgIUTUREAaNcNL0zPkxAlMX0GJtJ/FcwDQYJKoZIhvcNAQEN +BQAwgYkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMREwDwYDVQQH +DAhDYXJsc2JhZDEPMA0GA1UECgwGVmlhc2F0MR0wGwYDVQQLDBRWaWFzYXQgU2Vj +dXJlIE1vYmlsZTEiMCAGA1UEAwwZSGFja2VyT25lIHJlcG9ydCAjMTgwODU5NjAi +GA8yMDIyMTIxNjAwMDAwMFoYDzIwMjMxMjE1MjM1OTU5WjCBiTELMAkGA1UEBhMC +VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExETAPBgNVBAcMCENhcmxzYmFkMQ8wDQYD +VQQKDAZWaWFzYXQxHTAbBgNVBAsMFFZpYXNhdCBTZWN1cmUgTW9iaWxlMSIwIAYD +VQQDDBlIYWNrZXJPbmUgcmVwb3J0ICMxODA4NTk2MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA6I7RBPm4E/9rIrCHV5lfsHI/yYzXtACJmoyP8OMkjbeB +h21oSJJF9FEnbivk6bYaHZIPasa+lSAydRM2rbbmfhF+jQoWYCIbV2ztrbFR70S1 +wAuJrlYYm+8u+1HUru5UBZWUr/p1gFtv3QjpA8+43iwE4pXytTBKPXFo1f5iZwGI +D5Bz6DohT7Tyb8cpQ1uMCMCT0EJJ4n8wUrvfBgwBO94O4qlhs9vYgnDKepJDjptc +uSuEpvHALO8+EYkQ7nkM4Xzl/WK1yFtxxE93Jvd1OvViDGVrRVfsq+xYTKknGLX0 +QIeoDDnIr0OjlYPd/cqyEgMcFyFxwDSzSc1esxdCpQIDAQABo0UwQzAdBgNVHQ4E +FgQUurygsEKdtQk0T+sjM0gEURdveRUwEgYDVR0TAQH/BAgwBgEB/wIB/zAOBgNV +HQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQENBQADggEBAH7mIIXiQsQ4/QGNNFOQzTgP +/bUbMSZJsY5TPAvS9rF9yQVzs4dJZnQk5kEb/qrDQSe27oP0L0hfFm1wTGy+aKfa +BVGHdRmmvHtDUPLA9URCFShqKuS+GXp+6zt7dyZPRrPmiZaciiCMPHOnx59xSdPm +AZG8cD3fmK2ThC4FAMyvRb0qeobka3s22xTQ2kjwJO5gykTkZ+BR6SzRHQTjYMuT +iry9Bu8Kvbzu3r5n+/bmNz+xRNmEeehgT2qsHjA5b2YBVTr9MdN9Ro3H3saA3upr +oans248kpal88CGqsN2so/wZKxVnpiXlPHMdiNL7hRSUqlHkUi07FrP2Htg8kjI= +-----END CERTIFICATE-----`.trim(); + const c = new X509Certificate(certPem); + assert(!c.ca); + const signer = createSign('SHA256'); + assert(signer.sign(key, 'hex')); + + const c1 = new X509Certificate(certPem); + assert(!c1.checkIssued(c1)); + const signer1 = createSign('SHA256'); + assert(signer1.sign(key, 'hex')); + + const c2 = new X509Certificate(certPem); + assert(c2.toLegacyObject()); + const signer2 = createSign('SHA256'); + assert(signer2.sign(key, 'hex')); + } + + // X509Certificate can be cloned via MessageChannel/MessagePort + const mc = new MessageChannel(); + mc.port1.onmessage = common.mustCall(({ data }) => { + assert(isX509Certificate(data)); + assert.deepStrictEqual(data.raw, x509.raw); + mc.port1.close(); + }); + mc.port2.postMessage(x509); + + const modulusOSSL = 'D456320AFB20D3827093DC2C4284ED04DFBABD56E1DDAE529E28B790CD42' + + '56DB273349F3735FFD337C7A6363ECCA5A27B7F73DC7089A96C6D886DB0C' + + '62388F1CDD6A963AFCD599D5800E587A11F908960F84ED50BA25A28303EC' + + 'DA6E684FBE7BAEDC9CE8801327B1697AF25097CEE3F175E400984C0DB6A8' + + 'EB87BE03B4CF94774BA56FFFC8C63C68D6ADEB60ABBE69A7B14AB6A6B9E7' + + 'BAA89B5ADAB8EB07897C07F6D4FA3D660DFF574107D28E8F63467A788624' + + 'C574197693E959CEA1362FFAE1BBA10C8C0D88840ABFEF103631B2E8F5C3' + + '9B5548A7EA57E8A39F89291813F45A76C448033A2B7ED8403F4BAA147CF3' + + '5E2D2554AA65CE49695797095BF4DC6B'; + + // Verify that legacy encoding works + const legacyObjectCheck = { + subject: Object.assign({ __proto__: null }, { + C: 'US', + ST: 'CA', + L: 'SF', + O: 'Joyent', + OU: 'Node.js', + CN: 'agent1', + emailAddress: 'ry@tinyclouds.org', + }), + issuer: Object.assign({ __proto__: null }, { + C: 'US', + ST: 'CA', + L: 'SF', + O: 'Joyent', + OU: 'Node.js', + CN: 'ca1', + emailAddress: 'ry@tinyclouds.org', + }), + infoAccess: Object.assign({ __proto__: null }, { + 'OCSP - URI': ['http://ocsp.nodejs.org/'], + 'CA Issuers - URI': ['http://ca.nodejs.org/ca.cert'] + }), + modulusPattern: new RegExp(`^${modulusOSSL}$`, 'i'), + bits: 2048, + exponent: '0x10001', + valid_from: 'Sep 3 21:40:37 2022 GMT', + valid_to: 'Jun 17 21:40:37 2296 GMT', + fingerprint: '8B:89:16:C4:99:87:D2:13:1A:64:94:36:38:A5:32:01:F0:95:3B:53', + fingerprint256: + '2C:62:59:16:91:89:AB:90:6A:3E:98:88:A6:D3:C5:58:58:6C:AE:FF:9C:33:' + + '22:7C:B6:77:D3:34:E7:53:4B:05', + fingerprint512: + '51:62:18:39:E2:E2:77:F5:86:11:E8:C0:CA:54:43:7C:76:83:19:05:D0:03:' + + '24:21:B8:EB:14:61:FB:24:16:EB:BD:51:1A:17:91:04:30:03:EB:68:5F:DC:' + + '86:E1:D1:7C:FB:AF:78:ED:63:5F:29:9C:32:AF:A1:8E:22:96:D1:02', + serialNumberPattern: /^147D36C1C2F74206DE9FAB5F2226D78ADB00A426$/i + }; + + const legacyObject = x509.toLegacyObject(); + + assert.deepStrictEqual(legacyObject.raw, x509.raw); + assert.deepStrictEqual(legacyObject.subject, legacyObjectCheck.subject); + assert.deepStrictEqual(legacyObject.issuer, legacyObjectCheck.issuer); + assert.deepStrictEqual(legacyObject.infoAccess, legacyObjectCheck.infoAccess); + assert.match(legacyObject.modulus, legacyObjectCheck.modulusPattern); + assert.strictEqual(legacyObject.bits, legacyObjectCheck.bits); + assert.strictEqual(legacyObject.exponent, legacyObjectCheck.exponent); + assert.strictEqual(legacyObject.valid_from, legacyObjectCheck.valid_from); + assert.strictEqual(legacyObject.valid_to, legacyObjectCheck.valid_to); + assert.strictEqual(legacyObject.fingerprint, legacyObjectCheck.fingerprint); + assert.strictEqual( + legacyObject.fingerprint256, + legacyObjectCheck.fingerprint256); + assert.match( + legacyObject.serialNumber, + legacyObjectCheck.serialNumberPattern); +} + + +/* +https://github.com/electron/electron/blob/e57b69f106ae9c53a527038db4e8222692fa0ce7/patches/node/fix_crypto_tests_to_run_with_bssl.patch#L549 +{ + // This X.509 Certificate can be parsed by OpenSSL because it contains a + // structurally sound TBSCertificate structure. However, the SPKI field of the + // TBSCertificate contains the subjectPublicKey as a BIT STRING, and this bit + // sequence is not a valid public key. Ensure that X509Certificate.publicKey + // does not abort in this case. + + const certPem = `-----BEGIN CERTIFICATE----- +MIIDpDCCAw0CFEc1OZ8g17q+PZnna3iQ/gfoZ7f3MA0GCSqGSIb3DQEBBQUAMIHX +MRMwEQYLKwYBBAGCNzwCAQMTAkdJMR0wGwYDVQQPExRQcml2YXRlIE9yZ2FuaXph +dGlvbjEOMAwGA1UEBRMFOTkxOTExCzAJBgNVBAYTAkdJMRIwEAYDVQQIFAlHaWJy +YWx0YXIxEjAQBgNVBAcUCUdpYnJhbHRhcjEgMB4GA1UEChQXV0hHIChJbnRlcm5h +dGlvbmFsKSBMdGQxHDAaBgNVBAsUE0ludGVyYWN0aXZlIEJldHRpbmcxHDAaBgNV +BAMUE3d3dy53aWxsaWFtaGlsbC5jb20wIhgPMjAxNDAyMDcwMDAwMDBaGA8yMDE1 +MDIyMTIzNTk1OVowgbAxCzAJBgNVBAYTAklUMQ0wCwYDVQQIEwRSb21lMRAwDgYD +VQQHEwdQb21lemlhMRYwFAYDVQQKEw1UZWxlY29taXRhbGlhMRIwEAYDVQQrEwlB +RE0uQVAuUE0xHTAbBgNVBAMTFHd3dy50ZWxlY29taXRhbGlhLml0MTUwMwYJKoZI +hvcNAQkBFiZ2YXNlc2VyY2l6aW9wb3J0YWxpY29AdGVsZWNvbWl0YWxpYS5pdDCB +nzANBgkqhkiG9w0BAQEFAAOBjQA4gYkCgYEA5m/Vf7PevH+inMfUJOc8GeR7WVhM +CQwcMM5k46MSZo7kCk7VZuaq5G2JHGAGnLPaPUkeXlrf5qLpTxXXxHNtz+WrDlFt +boAdnTcqpX3+72uBGOaT6Wi/9YRKuCs5D5/cAxAc3XjHfpRXMoXObj9Vy7mLndfV +/wsnTfU9QVeBkgsCAwEAAaOBkjCBjzAdBgNVHQ4EFgQUfLjAjEiC83A+NupGrx5+ +Qe6nhRMwbgYIKwYBBQUHAQwEYjBgoV6gXDBaMFgwVhYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUS2u5KJYGDLvQUjibKaxLB4shBRgwJhYkaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nbzEuZ2lmMA0GCSqGSIb3DQEBBQUAA4GBALLiAMX0cIMp ++V/JgMRhMEUKbrt5lYKfv9dil/f22ezZaFafb070jGMMPVy9O3/PavDOkHtTv3vd +tAt3hIKFD1bJt6c6WtMH2Su3syosWxmdmGk5ihslB00lvLpfj/wed8i3bkcB1doq +UcXd/5qu2GhokrKU2cPttU+XAN2Om6a0 +-----END CERTIFICATE-----`; + + const cert = new X509Certificate(certPem); + assert.throws(() => cert.publicKey, { + message: common.hasOpenSSL3 ? /decode error/ : /wrong tag/, + name: 'Error' + }); + + assert.strictEqual(cert.checkIssued(cert), false); +} +*/ + +{ + // Test date parsing of `validFromDate` and `validToDate` fields, according to RFC 5280. + + // Validity dates up until the year 2049 are encoded as UTCTime. + // The formatting of UTCTime changes from the year ~1949 to 1950~. + const certPemUTCTime = `-----BEGIN CERTIFICATE----- +MIIE/TCCAuWgAwIBAgIUHbXPaFnjeBehMvdHkXZ+E3a78QswDQYJKoZIhvcNAQEL +BQAwDTELMAkGA1UEBhMCS1IwIBgPMTk0OTEyMjUyMzU5NThaFw01MDAxMDEyMzU5 +NThaMA0xCzAJBgNVBAYTAktSMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEAtFfV2DB2dZFFaR1PPZMmyo0mSDAxGReoixxlhQTFZZymU71emWV/6gR8MxAE +L5+uzpgBvOZWgEbELWeV/gzZGU/x1Cki0dSJ0B8Qwr5HvKX6oOZrJ8t+wn4SRceq +r6MRPskDpTjnvelt+VURGmawtKKHll5fSqfjRWkQC8WQHdogXylRjd3oIh9p1D5P +hphK/jKddxsRkLhJKQWqTjAy2v8hsJAxvpCPnlqMCXxjbQV41UTY8+kY3RPG3d6c +yHBGM7dzM7XWVc79V9z/rjdRcxE2eBqrJT/yR3Cok8wWVVfQEgBfpolHUZxA8K4N +tubTez9zsJy7xUG7udf91wXWVHMBHXg6m/u5nIW0fAXGMtnG/H6FMyyBDbJoUlqm +VRTG71DzvBXpd/qx2P5LkU1JjWY3U8HSn6Q1DJzMIrbOmWpdlFYXxzLlXU2vG8Q3 +PmdAHDDYW3M2YBVCdKqOtsuL2dMDuqRWdi3iCCPSR2UCm4HzAVYSe2FP8SPcY3xs +1NX+oDSpTxXruJYHGUp10/pXoqMrGT1IBgv2Dhsm3jcfRLSXkaBDJIKLO6dXmLBt +rlxM0DphiKnP6lDjpv7EDMdwsakz0zib3JrTmSLSbwZXR4abITmtbYbTpY3XAq7c +adO8YCMTCtb50ZbYEpGDAjOcWFHUlQQMsgZM2zc8ZHPY4EkCAwEAAaNTMFEwHQYD +VR0OBBYEFExDmZyzdo8ccjX7iFIwU7JYMV+qMB8GA1UdIwQYMBaAFExDmZyzdo8c +cjX7iFIwU7JYMV+qMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIB +ADEF/JIH+Ku9NqrO47Q/CEn9qpIgmqX10d1joDjchPY3OHIIyt8Xpo845mPBTM7L +dnMJSlkzJEk0ep9qAGGdKpBnLq8B/1mgCWQ81jwrwdYSsY+4xark+7+y0fij6qAt +L4T6aA37nbV5q5/DMOwZucFwRTf9ZI1IjC+MaQmnV01vGCogqqfLQ9v26bVBRE1K +UIixH0r3f/LWtuo0KaebZbb+oq6Zb8ljKJaUlt5OB8Zy5NrcP69r29QJUR57ukT6 +rt7fk5mOj2NBLMCErLHa7E6+GAUG94QEgdKzZ4yr2aduhMAfnOnK/HfuXO8TVa8/ ++oYENr47M8x139+yu92C8Be1MRk0VHteBaScUL+IaY3HgGbYR1lT0azvIyBN/DCN +bYczI7JQGYVitLuaUYFw/RtK7Qg1957/ZmGeGa+86aTLXbqsGjI951D81EIzdqod +1QW/Jn3yMNeVIzF9eYVEy2DIJjGgM2A8NWbqfWGUAUMRgyTxH1j42tnWG3eRnMsX +UnQfpY8i3v6gYoNNgEZktrqgpmukTWgl08TlDtBCjXTBkcBt4dxDApeoy7XWKq+/ +qBY/+uIsG30BRgJhAwApjdnCs7l5xpwtqluXFwOxyTWNV5IfChO7QFqWPlSVIHML +UidvpWWipVLZgK+oDks+bKTobcoXGW9oXobiIYqslXPy +-----END CERTIFICATE-----`.trim(); + const c1 = new X509Certificate(certPemUTCTime); + + assert.deepStrictEqual(c1.validFromDate, new Date('1949-12-25T23:59:58Z')); + assert.deepStrictEqual(c1.validToDate, new Date('1950-01-01T23:59:58Z')); + + // The GeneralizedTime format is used for dates in 2050 or later. + const certPemGeneralizedTime = `-----BEGIN CERTIFICATE----- +MIIE/TCCAuWgAwIBAgIUYHPUNd6S5xlNMjrWSaekgCBrbDQwDQYJKoZIhvcNAQEL +BQAwDTELMAkGA1UEBhMCS1IwIBcNNDkxMjI2MDAwMDAxWhgPMjA1MDAxMDIwMDAw +MDFaMA0xCzAJBgNVBAYTAktSMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEAlBPjQXHTzQWflvq6Lc01E0gVSSUQd5XnfK9K8TEN8ic/6iJVBWK8OwTmwh6u +KdSO+DrTpoTA3Wo4T7oSL89xsyJN5JHiIT2VdZvgcXkv+ZL+rZ2INzYSSXbPQ8V+ +Md5A7tNWGJOvneD1Pb+AKrVXn6N1+xiKuv08U+d6ZCcv8P2cGUJCQr5BSg6eXPm2 +ZIoFhNLDaqleci0P/Bs7uMwKjVr2IP99bCMwTS2STxexEmYf4J3wgNXBOHxspLcS +p7Yt3JgezvzRn5kijQi7ceS24q/fsGCCwB706mOKdYLCfEL1DhhEr27+XICw7zOF +Q8tSe33IfSdxejEVV+lf/jGW5zFH5m+lDTJC0VAUCBG5E7q57yFaoQ44CQWtbMHZ ++dtodKx4B0lzWXJs8xkGo0rl9/1CuY2iPX3lB6xxlX50ruj8stccMwarRzUvfkjw +AhnbUs9X1ooFyVXmVYXWzR0gP1/q05Zob03khX1NipGbMf0RBI4WlItkiRsrEl9x +08YPbrUyd7JnFkgG0O5TcmTzHr9cTJHg5BzclQA9/V0HuslSVOkKMMlKHky2zcqY +dDBmWtfTrvowaB7hTGD6YK4R9JCDUy7oeeK4ZUxRNCnJY698HodE9lQu+F0cJpbY +uZExFapE/AWA8ftlw2/fXoK0L3DhYsOVQkHd2YbrvzZEHVMCAwEAAaNTMFEwHQYD +VR0OBBYEFNptaIzozylFlD0+JKULue+5gvfZMB8GA1UdIwQYMBaAFNptaIzozylF +lD0+JKULue+5gvfZMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIB +AFXP4SCP6VFMINaKE/rLJOaFiVVuS4qDfpGRjuBryNxey4ErBFsXcPi/D0LIsNkb +c3qsex1cJadZdg3sqdkyliyYgzjJn1fSsiPT8GMiMnckw9awIwqtucGf+6SPdL6o +9jp0xg6KsRHWjN0GetCz89hy9HwSiSwiLpTxVYOMLjQ+ey8KXPk0LNaXve/++hrr +gN+cvcPKkspAE5SMTSKqHwVUD4MRJgdQqYDqB6demCq9Yl+kyQg9gVnuzkpKeNBT +qNVeeA6gczCpYV4rUMqT0UVVPbPOcygwZP2o7tUyNk6fmYzyLpi5R+FYD/PoowFp +LOrIaG426QaXhLr4U0i+HD/LhHZ4AWWt0OYAvbkk/xrhmagUcyeOxUrcYl6tA3NQ +sjPV2FNGitX+zOyxfMxcjf0RpaBbyMsO6DSfQidDchFvPR9VFX4THs/0mP02IK27 +MpsZj8AG2/jjPz6ytnWBJGuLeIt2sWnluZyldX+V9QEEhEmrEweUolacKF5ESODG +SHyZZVSUCK0bJfDfk5rXCQokWCIe+jHbW3CSWWmBRz6blZDeO/wI8nN4TWHDMCu6 +lawls1QdAwfP4CWIq4T7gsn/YqxMs74zDCXIF0tfuPmw5FMeCYVgnXQ7et8HBfeE +CWwQO8JZjJqFtqtuzy2n+gLCvqePgG/gmSqHOPm2ZbLW +-----END CERTIFICATE-----`.trim(); + const c2 = new X509Certificate(certPemGeneralizedTime); + + assert.deepStrictEqual(c2.validFromDate, new Date('2049-12-26T00:00:01Z')); + assert.deepStrictEqual(c2.validToDate, new Date('2050-01-02T00:00:01Z')); +} diff --git a/test/js/node/test/parallel/test-fs-fchmod.js b/test/js/node/test/parallel/test-fs-fchmod.js index b986183fa5..6896a38ffb 100644 --- a/test/js/node/test/parallel/test-fs-fchmod.js +++ b/test/js/node/test/parallel/test-fs-fchmod.js @@ -36,7 +36,7 @@ assert.throws(() => fs.fchmod(1, '123x', common.mustNotCall()), { const errObj = { code: 'ERR_OUT_OF_RANGE', name: 'RangeError', - message: 'The value of "fd" is out of range. It must be >= 0 && <= ' + + message: 'The value of "fd" is out of range. It must be >= 0 and <= ' + `2147483647. Received ${input}` }; assert.throws(() => fs.fchmod(input, 0o666, () => {}), errObj); @@ -47,7 +47,7 @@ assert.throws(() => fs.fchmod(1, '123x', common.mustNotCall()), { const errObj = { code: 'ERR_OUT_OF_RANGE', name: 'RangeError', - message: 'The value of "mode" is out of range. It must be >= 0 && <= ' + + message: 'The value of "mode" is out of range. It must be >= 0 and <= ' + `4294967295. Received ${input}` }; diff --git a/test/js/node/test/parallel/test-fs-fchown.js b/test/js/node/test/parallel/test-fs-fchown.js index 10e6a977ca..cbdd038951 100644 --- a/test/js/node/test/parallel/test-fs-fchown.js +++ b/test/js/node/test/parallel/test-fs-fchown.js @@ -50,10 +50,10 @@ function testGid(input, errObj) { code: 'ERR_OUT_OF_RANGE', name: 'RangeError', message: 'The value of "fd" is out of range. It must be ' + - `>= 0 && <= 2147483647. Received ${input}` + `>= 0 and <= 2147483647. Received ${input}` }; testFd(input, errObj); - errObj.message = 'The value of "uid" is out of range. It must be >= -1 && ' + + errObj.message = 'The value of "uid" is out of range. It must be >= -1 and ' + `<= 4294967295. Received ${input}`; testUid(input, errObj); errObj.message = errObj.message.replace('uid', 'gid'); diff --git a/test/js/node/test/parallel/test-fs-lchmod.js b/test/js/node/test/parallel/test-fs-lchmod.js index d439710291..c5f1f30bb9 100644 --- a/test/js/node/test/parallel/test-fs-lchmod.js +++ b/test/js/node/test/parallel/test-fs-lchmod.js @@ -57,7 +57,7 @@ assert.throws(() => fs.lchmodSync(f, '123x'), { const errObj = { code: 'ERR_OUT_OF_RANGE', name: 'RangeError', - message: 'The value of "mode" is out of range. It must be >= 0 && <= ' + + message: 'The value of "mode" is out of range. It must be >= 0 and <= ' + `4294967295. Received ${input}` }; diff --git a/test/js/node/test/parallel/test-webcrypto-derivebits-cfrg.js b/test/js/node/test/parallel/test-webcrypto-derivebits-cfrg.js new file mode 100644 index 0000000000..4ab325efa4 --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-derivebits-cfrg.js @@ -0,0 +1,230 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L142 + +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +const kTests = [ + { + name: 'X25519', + size: 32, + pkcs8: '302e020100300506032b656e04220420c8838e76d057dfb7d8c95a69e138160ad' + + 'd6373fd71a4d276bb56e3a81b64ff61', + spki: '302a300506032b656e0321001cf2b1e6022ec537371ed7f53e54fa1154d83e98eb' + + '64ea51fae5b3307cfe9706', + result: '2768409dfab99ec23b8c89b93ff5880295f76176088f89e43dfebe7ea1950008' + }, + { + name: 'X448', + size: 56, + pkcs8: '3046020100300506032b656f043a043858c7d29a3eb519b29d00cfb191bb64fc6' + + 'd8a42d8f17176272b89f2272d1819295c6525c0829671b052ef0727530f188e31' + + 'd0cc53bf26929e', + spki: '3042300506032b656f033900b604a1d1a5cd1d9426d561ef630a9eb16cbe69d5b9' + + 'ca615edc53633efb52ea31e6e6a0a1dbacc6e76cbce6482d7e4ba3d55d9e802765' + + 'ce6f', + result: 'f0f6c5f17f94f4291eab7178866d37ec8906dd6c514143dc85be7cf28deff39b' + + '726e0f6dcf810eb594dca97b4882bd44c43ea7dc67f49a4e', + }, +]; + +async function prepareKeys() { + const keys = {}; + await Promise.all( + kTests.map(async ({ name, size, pkcs8, spki, result }) => { + const [ + privateKey, + publicKey, + ] = await Promise.all([ + subtle.importKey( + 'pkcs8', + Buffer.from(pkcs8, 'hex'), + { name }, + true, + ['deriveKey', 'deriveBits']), + subtle.importKey( + 'spki', + Buffer.from(spki, 'hex'), + { name }, + true, + []), + ]); + keys[name] = { + privateKey, + publicKey, + size, + result, + }; + })); + return keys; +} + +(async function() { + const keys = await prepareKeys(); + + await Promise.all( + Object.keys(keys).map(async (name) => { + const { size, result, privateKey, publicKey } = keys[name]; + + { + // Good parameters + const bits = await subtle.deriveBits({ + name, + public: publicKey + }, privateKey, 8 * size); + + assert(bits instanceof ArrayBuffer); + assert.strictEqual(Buffer.from(bits).toString('hex'), result); + } + + { + // Case insensitivity + const bits = await subtle.deriveBits({ + name: name.toLowerCase(), + public: publicKey + }, privateKey, 8 * size); + + assert.strictEqual(Buffer.from(bits).toString('hex'), result); + } + + { + // Null length + const bits = await subtle.deriveBits({ + name, + public: publicKey + }, privateKey, null); + + assert.strictEqual(Buffer.from(bits).toString('hex'), result); + } + + { + // Default length + const bits = await subtle.deriveBits({ + name, + public: publicKey + }, privateKey); + + assert.strictEqual(Buffer.from(bits).toString('hex'), result); + } + + { + // Short Result + const bits = await subtle.deriveBits({ + name, + public: publicKey + }, privateKey, 8 * size - 32); + + assert.strictEqual( + Buffer.from(bits).toString('hex'), + result.slice(0, -8)); + } + + { + // Too long result + await assert.rejects(subtle.deriveBits({ + name, + public: publicKey + }, privateKey, 8 * size + 8), { + message: /derived bit length is too small/ + }); + } + + { + // Non-multiple of 8 + const bits = await subtle.deriveBits({ + name, + public: publicKey + }, privateKey, 8 * size - 11); + + const expected = Buffer.from(result.slice(0, -2), 'hex'); + expected[size - 2] = expected[size - 2] & 0b11111000; + assert.deepStrictEqual( + Buffer.from(bits), + expected); + } + })); + + // Error tests + { + // Missing public property + await assert.rejects( + subtle.deriveBits( + { name: 'X448' }, + keys.X448.privateKey, + 8 * keys.X448.size), + { code: 'ERR_MISSING_OPTION' }); + } + + { + // The public property is not a CryptoKey + await assert.rejects( + subtle.deriveBits( + { + name: 'X448', + public: { message: 'Not a CryptoKey' } + }, + keys.X448.privateKey, + 8 * keys.X448.size), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + + { + // Mismatched types + await assert.rejects( + subtle.deriveBits( + { + name: 'X448', + public: keys.X25519.publicKey + }, + keys.X448.privateKey, + 8 * keys.X448.size), + { message: 'The public and private keys must be of the same type' }); + } + + { + // Base key is not a private key + await assert.rejects(subtle.deriveBits({ + name: 'X448', + public: keys.X448.publicKey + }, keys.X448.publicKey, null), { + name: 'InvalidAccessError' + }); + } + + { + // Base key is not a private key + await assert.rejects(subtle.deriveBits({ + name: 'X448', + public: keys.X448.privateKey + }, keys.X448.publicKey, null), { + name: 'InvalidAccessError' + }); + } + + { + // Public is a secret key + const keyData = globalThis.crypto.getRandomValues(new Uint8Array(32)); + const key = await subtle.importKey( + 'raw', + keyData, + { name: 'AES-CBC', length: 256 }, + false, ['encrypt']); + + await assert.rejects(subtle.deriveBits({ + name: 'X448', + public: key + }, keys.X448.publicKey, null), { + name: 'InvalidAccessError' + }); + } +})().then(common.mustCall()); + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-webcrypto-derivekey-cfrg.js b/test/js/node/test/parallel/test-webcrypto-derivekey-cfrg.js new file mode 100644 index 0000000000..7c4a315a37 --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-derivekey-cfrg.js @@ -0,0 +1,193 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L143 + +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +const kTests = [ + { + name: 'X25519', + size: 32, + pkcs8: '302e020100300506032b656e04220420c8838e76d057dfb7d8c95a69e138160ad' + + 'd6373fd71a4d276bb56e3a81b64ff61', + spki: '302a300506032b656e0321001cf2b1e6022ec537371ed7f53e54fa1154d83e98eb' + + '64ea51fae5b3307cfe9706', + result: '2768409dfab99ec23b8c89b93ff5880295f76176088f89e43dfebe7ea1950008' + }, + { + name: 'X448', + size: 56, + pkcs8: '3046020100300506032b656f043a043858c7d29a3eb519b29d00cfb191bb64fc6' + + 'd8a42d8f17176272b89f2272d1819295c6525c0829671b052ef0727530f188e31' + + 'd0cc53bf26929e', + spki: '3042300506032b656f033900b604a1d1a5cd1d9426d561ef630a9eb16cbe69d5b9' + + 'ca615edc53633efb52ea31e6e6a0a1dbacc6e76cbce6482d7e4ba3d55d9e802765' + + 'ce6f', + result: 'f0f6c5f17f94f4291eab7178866d37ec8906dd6c514143dc85be7cf28deff39b' + }, +]; + +async function prepareKeys() { + const keys = {}; + await Promise.all( + kTests.map(async ({ name, size, pkcs8, spki, result }) => { + const [ + privateKey, + publicKey, + ] = await Promise.all([ + subtle.importKey( + 'pkcs8', + Buffer.from(pkcs8, 'hex'), + { name }, + true, + ['deriveKey', 'deriveBits']), + subtle.importKey( + 'spki', + Buffer.from(spki, 'hex'), + { name }, + true, + []), + ]); + keys[name] = { + privateKey, + publicKey, + size, + result, + }; + })); + return keys; +} + +(async function() { + const keys = await prepareKeys(); + const otherArgs = [ + { name: 'HMAC', hash: 'SHA-256', length: 256 }, + true, + ['sign', 'verify']]; + + await Promise.all( + Object.keys(keys).map(async (name) => { + const { result, privateKey, publicKey } = keys[name]; + + { + // Good parameters + const key = await subtle.deriveKey({ + name, + public: publicKey + }, privateKey, ...otherArgs); + + const raw = await subtle.exportKey('raw', key); + + assert.strictEqual(Buffer.from(raw).toString('hex'), result); + } + + { + // Case insensitivity + const key = await subtle.deriveKey({ + name: name.toLowerCase(), + public: publicKey + }, privateKey, { + name: 'HmAc', + hash: 'SHA-256', + length: 256 + }, true, ['sign', 'verify']); + + const raw = await subtle.exportKey('raw', key); + + assert.strictEqual(Buffer.from(raw).toString('hex'), result); + } + })); + + // Error tests + { + // Missing public property + await assert.rejects( + subtle.deriveKey( + { name: 'X448' }, + keys.X448.privateKey, + ...otherArgs), + { code: 'ERR_MISSING_OPTION' }); + } + + { + // The public property is not a CryptoKey + await assert.rejects( + subtle.deriveKey( + { + name: 'X448', + public: { message: 'Not a CryptoKey' } + }, + keys.X448.privateKey, + ...otherArgs), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + + { + // Mismatched named curves + await assert.rejects( + subtle.deriveKey( + { + name: 'X448', + public: keys.X25519.publicKey + }, + keys.X448.privateKey, + ...otherArgs), + { message: 'The public and private keys must be of the same type' }); + } + + { + // Base key is not a private key + await assert.rejects( + subtle.deriveKey( + { + name: 'X448', + public: keys.X448.publicKey + }, + keys.X448.publicKey, + ...otherArgs), + { name: 'InvalidAccessError' }); + } + + { + // Public is not a public key + await assert.rejects( + subtle.deriveKey( + { + name: 'X448', + public: keys.X448.privateKey + }, + keys.X448.privateKey, + ...otherArgs), + { name: 'InvalidAccessError' }); + } + + { + // Public is a secret key + const keyData = globalThis.crypto.getRandomValues(new Uint8Array(32)); + const key = await subtle.importKey( + 'raw', + keyData, + { name: 'AES-CBC', length: 256 }, + false, ['encrypt']); + + await assert.rejects( + subtle.deriveKey( + { + name: 'X448', + public: key + }, + keys.X448.publicKey, + ...otherArgs), + { name: 'InvalidAccessError' }); + } +})().then(common.mustCall()); + +*/ \ No newline at end of file diff --git a/test/js/node/test/parallel/test-webcrypto-derivekey.js b/test/js/node/test/parallel/test-webcrypto-derivekey.js new file mode 100644 index 0000000000..f42bf8f4be --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-derivekey.js @@ -0,0 +1,211 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; +const { KeyObject } = require('crypto'); + +// This is only a partial test. The WebCrypto Web Platform Tests +// will provide much greater coverage. + +// Test ECDH key derivation +{ + async function test(namedCurve) { + const [alice, bob] = await Promise.all([ + subtle.generateKey({ name: 'ECDH', namedCurve }, true, ['deriveKey']), + subtle.generateKey({ name: 'ECDH', namedCurve }, true, ['deriveKey']), + ]); + + const [secret1, secret2] = await Promise.all([ + subtle.deriveKey({ + name: 'ECDH', namedCurve, public: alice.publicKey + }, bob.privateKey, { + name: 'AES-CBC', + length: 256 + }, true, ['encrypt']), + subtle.deriveKey({ + name: 'ECDH', namedCurve, public: bob.publicKey + }, alice.privateKey, { + name: 'AES-CBC', + length: 256 + }, true, ['encrypt']), + ]); + + const [raw1, raw2] = await Promise.all([ + subtle.exportKey('raw', secret1), + subtle.exportKey('raw', secret2), + ]); + + assert.deepStrictEqual(raw1, raw2); + } + + test('P-521').then(common.mustCall()); +} + +// Test HKDF key derivation +{ + async function test(pass, info, salt, hash, expected) { + const ec = new TextEncoder(); + const key = await subtle.importKey( + 'raw', + ec.encode(pass), + { name: 'HKDF', hash }, + false, ['deriveKey']); + + const secret = await subtle.deriveKey({ + name: 'HKDF', + hash, + salt: ec.encode(salt), + info: ec.encode(info) + }, key, { + name: 'AES-CTR', + length: 256 + }, true, ['encrypt']); + + const raw = await subtle.exportKey('raw', secret); + + assert.strictEqual(Buffer.from(raw).toString('hex'), expected); + } + + const kTests = [ + ['hello', 'there', 'my friend', 'SHA-256', + '14d93b0ccd99d4f2cbd9fbfe9c830b5b8a43e3e45e32941ef21bdeb0fa87b6b6'], + ['hello', 'there', 'my friend', 'SHA-384', + 'e36cf2cf943d8f3a88adb80f478745c336ac811b1a86d03a7d10eb0b6b52295c'], + ]; + + const tests = Promise.all(kTests.map((args) => test(...args))); + + tests.then(common.mustCall()); +} + +// Test PBKDF2 key derivation +{ + async function test(pass, salt, iterations, hash, expected) { + const ec = new TextEncoder(); + const key = await subtle.importKey( + 'raw', + ec.encode(pass), + { name: 'PBKDF2', hash }, + false, ['deriveKey']); + const secret = await subtle.deriveKey({ + name: 'PBKDF2', + hash, + salt: ec.encode(salt), + iterations, + }, key, { + name: 'AES-CTR', + length: 256 + }, true, ['encrypt']); + + const raw = await subtle.exportKey('raw', secret); + + assert.strictEqual(Buffer.from(raw).toString('hex'), expected); + } + + const kTests = [ + ['hello', 'there', 10, 'SHA-256', + 'f72d1cf4853fffbd16a42751765d11f8dc7939498ee7b7ce7678b4cb16fad880'], + ['hello', 'there', 5, 'SHA-384', + '201509b012c9cd2fbe7ea938f0c509b36ecb140f38bf9130e96923f55f46756d'], + ]; + + const tests = Promise.all(kTests.map((args) => test(...args))); + + tests.then(common.mustCall()); +} + +// Test default key lengths +{ + const vectors = [ + ['PBKDF2', 'deriveKey', 528], + ['HKDF', 'deriveKey', 528], + [{ name: 'HMAC', hash: 'SHA-1' }, 'sign', 512], + [{ name: 'HMAC', hash: 'SHA-256' }, 'sign', 512], + // Not long enough secret generated by ECDH + // [{ name: 'HMAC', hash: 'SHA-384' }, 'sign', 1024], + // [{ name: 'HMAC', hash: 'SHA-512' }, 'sign', 1024], + ]; + + (async () => { + const keyPair = await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-521' }, false, ['deriveKey']); + for (const [derivedKeyAlgorithm, usage, expected] of vectors) { + const derived = await subtle.deriveKey( + { name: 'ECDH', public: keyPair.publicKey }, + keyPair.privateKey, + derivedKeyAlgorithm, + false, + [usage]); + + if (derived.algorithm.name === 'HMAC') { + assert.strictEqual(derived.algorithm.length, expected); + } else { + // KDFs cannot be exportable and do not indicate their length + const secretKey = KeyObject.from(derived); + assert.strictEqual(secretKey.symmetricKeySize, expected / 8); + } + } + })().then(common.mustCall()); +} + +{ + const vectors = [ + [{ name: 'HMAC', hash: 'SHA-1' }, 'sign', 512], + [{ name: 'HMAC', hash: 'SHA-256' }, 'sign', 512], + [{ name: 'HMAC', hash: 'SHA-384' }, 'sign', 1024], + [{ name: 'HMAC', hash: 'SHA-512' }, 'sign', 1024], + ]; + + (async () => { + for (const [derivedKeyAlgorithm, usage, expected] of vectors) { + const derived = await subtle.deriveKey( + { name: 'PBKDF2', salt: new Uint8Array([]), hash: 'SHA-256', iterations: 20 }, + await subtle.importKey('raw', new Uint8Array([]), { name: 'PBKDF2' }, false, ['deriveKey']), + derivedKeyAlgorithm, + false, + [usage]); + + assert.strictEqual(derived.algorithm.length, expected); + } + })().then(common.mustCall()); +} + +// Test X25519 and X448 key derivation +if (!common.openSSLIsBoringSSL) { + async function test(name) { + const [alice, bob] = await Promise.all([ + subtle.generateKey({ name }, true, ['deriveKey']), + subtle.generateKey({ name }, true, ['deriveKey']), + ]); + + const [secret1, secret2] = await Promise.all([ + subtle.deriveKey({ + name, public: alice.publicKey + }, bob.privateKey, { + name: 'AES-CBC', + length: 256 + }, true, ['encrypt']), + subtle.deriveKey({ + name, public: bob.publicKey + }, alice.privateKey, { + name: 'AES-CBC', + length: 256 + }, true, ['encrypt']), + ]); + + const [raw1, raw2] = await Promise.all([ + subtle.exportKey('raw', secret1), + subtle.exportKey('raw', secret2), + ]); + + assert.deepStrictEqual(raw1, raw2); + } + + test('X25519').then(common.mustCall()); + test('X448').then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-webcrypto-digest.js b/test/js/node/test/parallel/test-webcrypto-digest.js new file mode 100644 index 0000000000..bfd01ecc12 --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-digest.js @@ -0,0 +1,174 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { Buffer } = require('buffer'); +const { subtle } = globalThis.crypto; +const { createHash } = require('crypto'); + +const kTests = [ + ['SHA-1', 'sha1', 160], + ['SHA-256', 'sha256', 256], + ['SHA-384', 'sha384', 384], + ['SHA-512', 'sha512', 512], +]; + +// Empty hash just works, not checking result +subtle.digest('SHA-512', Buffer.alloc(0)) + .then(common.mustCall()); + +// TODO(@jasnell): Need to move this particular test to pummel +// // Careful, this is an expensive operation because of both the memory +// // allocation and the cost of performing the hash on such a large +// // input. +// subtle.digest('SHA-512', new ArrayBuffer(2 ** 31 - 1)) +// .then(common.mustCall()); + +// TODO(@jasnell): Change max to 2 ** 31 - 1 +// assert.rejects(subtle.digest('SHA-512', new ArrayBuffer(kMaxLength + 1)), { +// code: 'ERR_OUT_OF_RANGE' +// }); + +const kData = (new TextEncoder()).encode('hello'); +(async function() { + await Promise.all(kTests.map(async (test) => { + // Get the digest using the legacy crypto API + const checkValue = + createHash(test[1]).update(kData).digest().toString('hex'); + + // Get the digest using the SubtleCrypto API + const values = Promise.all([ + subtle.digest({ name: test[0] }, kData), + subtle.digest({ name: test[0], length: test[2] }, kData), + subtle.digest(test[0], kData), + subtle.digest(test[0], kData.buffer), + subtle.digest(test[0], new DataView(kData.buffer)), + subtle.digest(test[0], Buffer.from(kData)), + ]); + + // subtle.digest copies the input data, so changing it + // while we're waiting on the Promises should never + // cause the test to fail. + kData[0] = 0x1; + kData[2] = 0x2; + kData[4] = 0x3; + + // Compare that the legacy crypto API and SubtleCrypto API + // produce the same results + (await values).forEach((v) => { + assert(v instanceof ArrayBuffer); + assert.strictEqual(checkValue, Buffer.from(v).toString('hex')); + }); + })); +})().then(common.mustCall()); + +Promise.all([1, null, undefined].map((i) => + assert.rejects(subtle.digest(i, Buffer.alloc(0)), { + message: /Unrecognized algorithm name/, + name: 'NotSupportedError', + }) +)).then(common.mustCall()); + +assert.rejects(subtle.digest('', Buffer.alloc(0)), { + message: /Unrecognized algorithm name/, + name: 'NotSupportedError', +}).then(common.mustCall()); + +Promise.all([1, [], {}, null, undefined].map((i) => + assert.rejects(subtle.digest('SHA-256', i), { + code: 'ERR_INVALID_ARG_TYPE' + }) +)).then(common.mustCall()); + +const kSourceData = { + empty: '', + short: '156eea7cc14c56cb94db030a4a9d95ff', + medium: 'b6c8f9df648cd088b70f38e74197b18cb81e1e435' + + '0d50bccb8fb5a7379c87bb2e3d6ed5461ed1e9f36' + + 'f340a3962a446b815b794b4bd43a4403502077b22' + + '56cc807837f3aacd118eb4b9c2baeb897068625ab' + + 'aca193', + long: null +}; + +kSourceData.long = kSourceData.medium.repeat(1024); + +const kDigestedData = { + 'sha-1': { + empty: 'da39a3ee5e6b4b0d3255bfef95601890afd80709', + short: 'c91318cdf2396a015e3f4e6a86a0ba65b8635944', + medium: 'e541060870eb16bf33b68e51f513526893986729', + long: '3098b50037ecd02ebd657653b2bfa01eee27a2ea' + }, + 'sha-256': { + empty: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + short: 'a2831186984792c7d32d59c89740687f19addc1b959e71a1cc538a3b7ed843f2', + medium: '535367877ef014d7fc717e5cb7843e59b61aee62c7029cec7ec6c12fd924e0e4', + long: '14cdea9dc75f5a6274d9fc1e64009912f1dcd306b48fe8e9cf122de671571781' + }, + 'sha-384': { + empty: '38b060a751ac96384cd9327eb1b1e36a21fdb71114b' + + 'e07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2' + + 'f14898b95b', + short: '6bf5ea6524d1cddc43f7cf3b56ee059227404a2f538' + + 'f022a3db7447a782c06c1ed05e8ab4f5edc17f37114' + + '40dfe97731', + medium: 'cbc2c588fe5b25f916da28b4e47a484ae6fc1fe490' + + '2dd5c9939a6bfd034ab3b48b39087436011f6a9987' + + '9d279540e977', + long: '49f4fdb3981968f97d57370f85345067cd5296a97dd1' + + 'a18e06911e756e9608492529870e1ad130998d57cbfb' + + 'b7c1d09e' + }, + 'sha-512': { + empty: 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5' + + '715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318' + + 'd2877eec2f63b931bd47417a81a538327af927da3e', + short: '375248be5ff34be74cab4ff1c3bc8dc68bd5f8dff40' + + '23e98f87b865cff2c724292df189443a64ff4134a65' + + 'cd4635b9d4f5dc0d3fb67528002a63acf26c9da575', + medium: 'b9109f839e8ea43c890f293ce11dc6e2798d1e2431' + + 'f1e4b919e3b20c4f36303ba39c916db306c45a3b65' + + '761ff5be85328eeaf42c3830f1d95e7a41165b7d2d36', + long: '4b02caf650276030ea5617e597c5d53fd9daa68b78bfe' + + '60b22aab8d36a4c2a3affdb71234f49276737c575ddf7' + + '4d14054cbd6fdb98fd0ddcbcb46f91ad76b6ee' + } +}; + +async function testDigest(size, name) { + const digest = await subtle.digest( + name, + Buffer.from(kSourceData[size], 'hex')); + + assert.strictEqual( + Buffer.from(digest).toString('hex'), + kDigestedData[name.toLowerCase()][size]); +} + +(async function() { + const variations = []; + Object.keys(kSourceData).forEach((size) => { + Object.keys(kDigestedData).forEach((alg) => { + const upCase = alg.toUpperCase(); + const downCase = alg.toLowerCase(); + const mixedCase = upCase.slice(0, 1) + downCase.slice(1); + + variations.push(testDigest(size, upCase)); + variations.push(testDigest(size, downCase)); + variations.push(testDigest(size, mixedCase)); + }); + }); + + await Promise.all(variations); +})().then(common.mustCall()); + +(async () => { + await assert.rejects(subtle.digest('RSA-OAEP', Buffer.alloc(1)), { + name: 'NotSupportedError', + }); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-webcrypto-encrypt-decrypt-aes.js b/test/js/node/test/parallel/test-webcrypto-encrypt-decrypt-aes.js new file mode 100644 index 0000000000..6a6f45bab0 --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-encrypt-decrypt-aes.js @@ -0,0 +1,241 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +async function testEncrypt({ keyBuffer, algorithm, plaintext, result }) { + // Using a copy of plaintext to prevent tampering of the original + plaintext = Buffer.from(plaintext); + + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: algorithm.name }, + false, + ['encrypt', 'decrypt']); + + const output = await subtle.encrypt(algorithm, key, plaintext); + plaintext[0] = 255 - plaintext[0]; + + assert.strictEqual( + Buffer.from(output).toString('hex'), + Buffer.from(result).toString('hex')); + + // Converting the returned ArrayBuffer into a Buffer right away, + // so that the next line works + const check = Buffer.from(await subtle.decrypt(algorithm, key, output)); + check[0] = 255 - check[0]; + + assert.strictEqual( + Buffer.from(check).toString('hex'), + Buffer.from(plaintext).toString('hex')); +} + +async function testEncryptNoEncrypt({ keyBuffer, algorithm, plaintext }) { + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: algorithm.name }, + false, + ['decrypt']); + + return assert.rejects(subtle.encrypt(algorithm, key, plaintext), { + message: /CryptoKey doesn't support encryption/ + }); +} + +async function testEncryptNoDecrypt({ keyBuffer, algorithm, plaintext }) { + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: algorithm.name }, + false, + ['encrypt']); + + const output = await subtle.encrypt(algorithm, key, plaintext); + + return assert.rejects(subtle.decrypt(algorithm, key, output), { + message: /CryptoKey doesn't support decryption/ + }); +} + +async function testEncryptWrongAlg({ keyBuffer, algorithm, plaintext }, alg) { + assert.notStrictEqual(algorithm.name, alg); + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: alg }, + false, + ['encrypt']); + + return assert.rejects(subtle.encrypt(algorithm, key, plaintext), { + message: /CryptoKey doesn't match AlgorithmIdentifier/ + }); +} + +async function testDecrypt({ keyBuffer, algorithm, result }) { + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: algorithm.name }, + false, + ['encrypt', 'decrypt']); + + await subtle.decrypt(algorithm, key, result); +} + +// Test aes-cbc vectors +{ + const { + passing, + failing, + decryptionFailing + } = require('../fixtures/crypto/aes_cbc')(); + + (async function() { + const variations = []; + + passing.forEach((vector) => { + variations.push(testEncrypt(vector)); + variations.push(testEncryptNoEncrypt(vector)); + variations.push(testEncryptNoDecrypt(vector)); + variations.push(testEncryptWrongAlg(vector, 'AES-CTR')); + }); + + failing.forEach((vector) => { + variations.push(assert.rejects(testEncrypt(vector), { + message: /algorithm\.iv must contain exactly 16 bytes/ + })); + variations.push(assert.rejects(testDecrypt(vector), { + message: /algorithm\.iv must contain exactly 16 bytes/ + })); + }); + + decryptionFailing.forEach((vector) => { + variations.push(assert.rejects(testDecrypt(vector), { + name: 'OperationError' + })); + }); + + await Promise.all(variations); + })().then(common.mustCall()); +} + +// Test aes-ctr vectors +{ + const { + passing, + failing, + decryptionFailing + } = require('../fixtures/crypto/aes_ctr')(); + + (async function() { + const variations = []; + + passing.forEach((vector) => { + variations.push(testEncrypt(vector)); + variations.push(testEncryptNoEncrypt(vector)); + variations.push(testEncryptNoDecrypt(vector)); + variations.push(testEncryptWrongAlg(vector, 'AES-CBC')); + }); + + // TODO(@jasnell): These fail for different reasons. Need to + // make them consistent + failing.forEach((vector) => { + variations.push(assert.rejects(testEncrypt(vector), { + message: /.*/ + })); + variations.push(assert.rejects(testDecrypt(vector), { + message: /.*/ + })); + }); + + decryptionFailing.forEach((vector) => { + variations.push(assert.rejects(testDecrypt(vector), { + name: 'OperationError' + })); + }); + + await Promise.all(variations); + })().then(common.mustCall()); +} + +// Test aes-gcm vectors +{ + const { + passing, + failing, + decryptionFailing + } = require('../fixtures/crypto/aes_gcm')(); + + (async function() { + const variations = []; + + passing.forEach((vector) => { + variations.push(testEncrypt(vector)); + variations.push(testEncryptNoEncrypt(vector)); + variations.push(testEncryptNoDecrypt(vector)); + variations.push(testEncryptWrongAlg(vector, 'AES-CBC')); + }); + + failing.forEach((vector) => { + variations.push(assert.rejects(testEncrypt(vector), { + message: /is not a valid AES-GCM tag length/ + })); + variations.push(assert.rejects(testDecrypt(vector), { + message: /is not a valid AES-GCM tag length/ + })); + }); + + decryptionFailing.forEach((vector) => { + variations.push(assert.rejects(testDecrypt(vector), { + name: 'OperationError' + })); + }); + + await Promise.all(variations); + })().then(common.mustCall()); +} + +{ + (async function() { + const secretKey = await subtle.generateKey( + { + name: 'AES-GCM', + length: 256, + }, + false, + ['encrypt', 'decrypt'], + ); + + const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)); + const aad = globalThis.crypto.getRandomValues(new Uint8Array(32)); + + const encrypted = await subtle.encrypt( + { + name: 'AES-GCM', + iv, + additionalData: aad, + tagLength: 128 + }, + secretKey, + globalThis.crypto.getRandomValues(new Uint8Array(32)) + ); + + await subtle.decrypt( + { + name: 'AES-GCM', + iv, + additionalData: aad, + tagLength: 128, + }, + secretKey, + new Uint8Array(encrypted), + ); + })().then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-webcrypto-sign-verify.js b/test/js/node/test/parallel/test-webcrypto-sign-verify.js new file mode 100644 index 0000000000..e8899a7a0b --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-sign-verify.js @@ -0,0 +1,146 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +// This is only a partial test. The WebCrypto Web Platform Tests +// will provide much greater coverage. + +// Test Sign/Verify RSASSA-PKCS1-v1_5 +{ + async function test(data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'RSASSA-PKCS1-v1_5' + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name: 'RSASSA-PKCS1-v1_5' + }, publicKey, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} + +// Test Sign/Verify RSA-PSS +{ + async function test(data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'RSA-PSS', + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'RSA-PSS', + saltLength: 256, + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name: 'RSA-PSS', + saltLength: 256, + }, publicKey, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} + +// Test Sign/Verify ECDSA +{ + async function test(data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'ECDSA', + namedCurve: 'P-384', + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'ECDSA', + hash: 'SHA-384', + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name: 'ECDSA', + hash: 'SHA-384', + }, publicKey, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} + +// Test Sign/Verify HMAC +{ + async function test(data) { + const ec = new TextEncoder(); + + const key = await subtle.generateKey({ + name: 'HMAC', + length: 256, + hash: 'SHA-256' + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'HMAC', + }, key, ec.encode(data)); + + assert(await subtle.verify({ + name: 'HMAC', + }, key, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} + +// Test Sign/Verify Ed25519 +{ + async function test(data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'Ed25519', + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'Ed25519', + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name: 'Ed25519', + }, publicKey, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} + +// Test Sign/Verify Ed448 +if (!common.openSSLIsBoringSSL){ + async function test(data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'Ed448', + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'Ed448', + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name: 'Ed448', + }, publicKey, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-webcrypto-wrap-unwrap.js b/test/js/node/test/parallel/test-webcrypto-wrap-unwrap.js new file mode 100644 index 0000000000..dae7fe0068 --- /dev/null +++ b/test/js/node/test/parallel/test-webcrypto-wrap-unwrap.js @@ -0,0 +1,310 @@ +/* +Skipped test +https://github.com/electron/electron/blob/5680c628b6718385bbd975b51ec2640aa7df226b/script/node-disabled-tests.json#L150 + +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +const kWrappingData = { + 'RSA-OAEP': { + generate: { + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + wrap: { label: new Uint8Array(8) }, + pair: true + }, + 'AES-CTR': { + generate: { length: 128 }, + wrap: { counter: new Uint8Array(16), length: 64 }, + pair: false + }, + 'AES-CBC': { + generate: { length: 128 }, + wrap: { iv: new Uint8Array(16) }, + pair: false + }, + 'AES-GCM': { + generate: { length: 128 }, + wrap: { + iv: new Uint8Array(16), + additionalData: new Uint8Array(16), + tagLength: 64 + }, + pair: false + }, + 'AES-KW': { + generate: { length: 128 }, + wrap: { }, + pair: false + } +}; + +function generateWrappingKeys() { + return Promise.all(Object.keys(kWrappingData).map(async (name) => { + const keys = await subtle.generateKey( + { name, ...kWrappingData[name].generate }, + true, + ['wrapKey', 'unwrapKey']); + if (kWrappingData[name].pair) { + kWrappingData[name].wrappingKey = keys.publicKey; + kWrappingData[name].unwrappingKey = keys.privateKey; + } else { + kWrappingData[name].wrappingKey = keys; + kWrappingData[name].unwrappingKey = keys; + } + })); +} + +async function generateKeysToWrap() { + const parameters = [ + { + algorithm: { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'RSA-PSS', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'RSA-OAEP', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, + privateUsages: ['decrypt'], + publicUsages: ['encrypt'], + pair: true, + }, + { + algorithm: { + name: 'ECDSA', + namedCurve: 'P-384' + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'ECDH', + namedCurve: 'P-384' + }, + privateUsages: ['deriveBits'], + publicUsages: [], + pair: true, + }, + { + algorithm: { + name: 'Ed25519', + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'Ed448', + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'X25519', + }, + privateUsages: ['deriveBits'], + publicUsages: [], + pair: true, + }, + { + algorithm: { + name: 'X448', + }, + privateUsages: ['deriveBits'], + publicUsages: [], + pair: true, + }, + { + algorithm: { + name: 'AES-CTR', + length: 128 + }, + usages: ['encrypt', 'decrypt'], + pair: false, + }, + { + algorithm: { + name: 'AES-CBC', + length: 128 + }, + usages: ['encrypt', 'decrypt'], + pair: false, + }, + { + algorithm: { + name: 'AES-GCM', length: 128 + }, + usages: ['encrypt', 'decrypt'], + pair: false, + }, + { + algorithm: { + name: 'AES-KW', + length: 128 + }, + usages: ['wrapKey', 'unwrapKey'], + pair: false, + }, + { + algorithm: { + name: 'HMAC', + length: 128, + hash: 'SHA-256' + }, + usages: ['sign', 'verify'], + pair: false, + }, + ]; + + const allkeys = await Promise.all(parameters.map(async (params) => { + const usages = 'usages' in params ? + params.usages : + params.publicUsages.concat(params.privateUsages); + + const keys = await subtle.generateKey(params.algorithm, true, usages); + + if (params.pair) { + return [ + { + algorithm: params.algorithm, + usages: params.publicUsages, + key: keys.publicKey, + }, + { + algorithm: params.algorithm, + usages: params.privateUsages, + key: keys.privateKey, + }, + ]; + } + + return [{ + algorithm: params.algorithm, + usages: params.usages, + key: keys, + }]; + })); + + return allkeys.flat(); +} + +function getFormats(key) { + switch (key.key.type) { + case 'secret': return ['raw', 'jwk']; + case 'public': return ['spki', 'jwk']; + case 'private': return ['pkcs8', 'jwk']; + } +} + +// If the wrapping algorithm is AES-KW, the exported key +// material length must be a multiple of 8. +// If the wrapping algorithm is RSA-OAEP, the exported key +// material maximum length is a factor of the modulusLength +// +// As per the NOTE in step 13 https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey +// we're padding AES-KW wrapped JWK to make sure it is always a multiple of 8 bytes +// in length +async function wrappingIsPossible(name, exported) { + if ('byteLength' in exported) { + switch (name) { + case 'AES-KW': + return exported.byteLength % 8 === 0; + case 'RSA-OAEP': + return exported.byteLength <= 446; + } + } else if ('kty' in exported && name === 'RSA-OAEP') { + return JSON.stringify(exported).length <= 478; + } + return true; +} + +async function testWrap(wrappingKey, unwrappingKey, key, wrap, format) { + const exported = await subtle.exportKey(format, key.key); + if (!(await wrappingIsPossible(wrappingKey.algorithm.name, exported))) + return; + + const wrapped = + await subtle.wrapKey( + format, + key.key, + wrappingKey, + { name: wrappingKey.algorithm.name, ...wrap }); + const unwrapped = + await subtle.unwrapKey( + format, + wrapped, + unwrappingKey, + { name: wrappingKey.algorithm.name, ...wrap }, + key.algorithm, + true, + key.usages); + assert(unwrapped.extractable); + + const exportedAgain = await subtle.exportKey(format, unwrapped); + assert.deepStrictEqual(exported, exportedAgain); +} + +function testWrapping(name, keys) { + const variations = []; + + const { + wrappingKey, + unwrappingKey, + wrap + } = kWrappingData[name]; + + keys.forEach((key) => { + getFormats(key).forEach((format) => { + variations.push(testWrap(wrappingKey, unwrappingKey, key, wrap, format)); + }); + }); + + return variations; +} + +(async function() { + await generateWrappingKeys(); + const keys = await generateKeysToWrap(); + const variations = []; + Object.keys(kWrappingData).forEach((name) => { + variations.push(...testWrapping(name, keys)); + }); + await Promise.all(variations); +})().then(common.mustCall()); + +*/ \ No newline at end of file diff --git a/test/js/node/tls/node-tls-connect.test.ts b/test/js/node/tls/node-tls-connect.test.ts index 0852df396e..a4ce788966 100644 --- a/test/js/node/tls/node-tls-connect.test.ts +++ b/test/js/node/tls/node-tls-connect.test.ts @@ -241,7 +241,7 @@ for (const { name, connect } of tests) { expect(cert.ca).toBeFalse(); expect(cert.bits).toBe(2048); expect(cert.modulus).toBe( - "BEEE8773AF7C8861EC11351188B9B1798734FB0729B674369BE3285A29FE5DACBFAB700D09D7904CF1027D89298BD68BE0EF1DF94363012B0DEB97F632CB76894BCC216535337B9DB6125EF68996DD35B4BEA07E86C41DA071907A86651E84F8C72141F889CC0F770554791E9F07BBE47C375D2D77B44DBE2AB0ED442BC1F49ABE4F8904977E3DFD61CD501D8EFF819FF1792AEDFFACA7D281FD1DB8C5D972D22F68FA7103CA11AC9AAED1CDD12C33C0B8B47964B37338953D2415EDCE8B83D52E2076CA960385CC3A5CA75A75951AAFDB2AD3DB98A6FDD4BAA32F575FEA7B11F671A9EAA95D7D9FAF958AC609F3C48DEC5BDDCF1BC1542031ED9D4B281D7DD1", + "beee8773af7c8861ec11351188b9b1798734fb0729b674369be3285a29fe5dacbfab700d09d7904cf1027d89298bd68be0ef1df94363012b0deb97f632cb76894bcc216535337b9db6125ef68996dd35b4bea07e86c41da071907a86651e84f8c72141f889cc0f770554791e9f07bbe47c375d2d77b44dbe2ab0ed442bc1f49abe4f8904977e3dfd61cd501d8eff819ff1792aedffaca7d281fd1db8c5d972d22f68fa7103ca11ac9aaed1cdd12c33c0b8b47964b37338953d2415edce8b83d52e2076ca960385cc3a5ca75a75951aafdb2ad3db98a6fdd4baa32f575fea7b11f671a9eaa95d7d9faf958ac609f3c48dec5bddcf1bc1542031ed9d4b281d7dd1", ); expect(cert.exponent).toBe("0x10001"); expect(cert.pubkey).toBeInstanceOf(Buffer); @@ -254,7 +254,7 @@ for (const { name, connect } of tests) { expect(cert.fingerprint512).toBe( "2D:31:CB:D2:A0:CA:E5:D4:B5:59:11:48:4B:BC:65:11:4F:AB:02:24:59:D8:73:43:2F:9A:31:92:BC:AF:26:66:CD:DB:8B:03:74:0C:C1:84:AF:54:2D:7C:FD:EF:07:6E:85:66:98:6B:82:4F:A5:72:97:A2:19:8C:7B:57:D6:15", ); - expect(cert.serialNumber).toBe("1DA7A7B8D71402ED2D8C3646A5CEDF2B8117EFC8"); + expect(cert.serialNumber).toBe("1da7a7b8d71402ed2d8c3646a5cedf2b8117efc8"); expect(cert.raw).toBeInstanceOf(Buffer); } finally { socket.end(); diff --git a/test/js/node/tls/node-tls-server.test.ts b/test/js/node/tls/node-tls-server.test.ts index 2cefec9c40..acf35ce584 100644 --- a/test/js/node/tls/node-tls-server.test.ts +++ b/test/js/node/tls/node-tls-server.test.ts @@ -319,7 +319,7 @@ describe("tls.createServer", () => { expect(cert.ca).toBeFalse(); expect(cert.bits).toBe(2048); expect(cert.modulus).toBe( - "BEEE8773AF7C8861EC11351188B9B1798734FB0729B674369BE3285A29FE5DACBFAB700D09D7904CF1027D89298BD68BE0EF1DF94363012B0DEB97F632CB76894BCC216535337B9DB6125EF68996DD35B4BEA07E86C41DA071907A86651E84F8C72141F889CC0F770554791E9F07BBE47C375D2D77B44DBE2AB0ED442BC1F49ABE4F8904977E3DFD61CD501D8EFF819FF1792AEDFFACA7D281FD1DB8C5D972D22F68FA7103CA11AC9AAED1CDD12C33C0B8B47964B37338953D2415EDCE8B83D52E2076CA960385CC3A5CA75A75951AAFDB2AD3DB98A6FDD4BAA32F575FEA7B11F671A9EAA95D7D9FAF958AC609F3C48DEC5BDDCF1BC1542031ED9D4B281D7DD1", + "beee8773af7c8861ec11351188b9b1798734fb0729b674369be3285a29fe5dacbfab700d09d7904cf1027d89298bd68be0ef1df94363012b0deb97f632cb76894bcc216535337b9db6125ef68996dd35b4bea07e86c41da071907a86651e84f8c72141f889cc0f770554791e9f07bbe47c375d2d77b44dbe2ab0ed442bc1f49abe4f8904977e3dfd61cd501d8eff819ff1792aedffaca7d281fd1db8c5d972d22f68fa7103ca11ac9aaed1cdd12c33c0b8b47964b37338953d2415edce8b83d52e2076ca960385cc3a5ca75a75951aafdb2ad3db98a6fdd4baa32f575fea7b11f671a9eaa95d7d9faf958ac609f3c48dec5bddcf1bc1542031ed9d4b281d7dd1", ); expect(cert.exponent).toBe("0x10001"); expect(cert.pubkey).toBeInstanceOf(Buffer); @@ -333,7 +333,7 @@ describe("tls.createServer", () => { expect(cert.fingerprint512).toBe( "2D:31:CB:D2:A0:CA:E5:D4:B5:59:11:48:4B:BC:65:11:4F:AB:02:24:59:D8:73:43:2F:9A:31:92:BC:AF:26:66:CD:DB:8B:03:74:0C:C1:84:AF:54:2D:7C:FD:EF:07:6E:85:66:98:6B:82:4F:A5:72:97:A2:19:8C:7B:57:D6:15", ); - expect(cert.serialNumber).toBe("1DA7A7B8D71402ED2D8C3646A5CEDF2B8117EFC8"); + expect(cert.serialNumber).toBe("1da7a7b8d71402ed2d8c3646a5cedf2b8117efc8"); expect(cert.raw).toBeInstanceOf(Buffer); client?.end();