From 96469ee70a2f7054461aa813fd121aa2b9db8cd9 Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 12 Apr 2026 12:18:17 +0000 Subject: [PATCH 01/14] webcrypto: add SHA-3 digests and SubtleCrypto.supports() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the first slice of the WICG "Modern Algorithms in the Web Cryptography API" specification (https://wicg.github.io/webcrypto-modern-algos/): - SHA3-256, SHA3-384 and SHA3-512 as digest algorithms on `crypto.subtle.digest`. Output matches the FIPS 202 test vectors. Implementation is a small self-contained Keccak-f[1600] sponge in CryptoDigest.cpp, which can later be extended with SHAKE/cSHAKE/ TurboSHAKE when those land. - `crypto.subtle.supports(operation, algorithm)`, a synchronous feature detection method that reports whether a (operation, algorithm) pair is supported. Unknown operations and algorithms, and malformed algorithm parameter dictionaries, all return false rather than throwing — matching the WICG spec's progressive-enhancement intent. KEM operations (encapsulateKey/encapsulateBits/decapsulateKey/ decapsulateBits) return false for now and will flip to true when ML-KEM lands in a follow-up. Refs #29218 --- src/bun.js/bindings/AsymmetricKeyValue.cpp | 3 + .../webcore/SerializedScriptValue.cpp | 8 + .../webcrypto/CryptoAlgorithmIdentifier.h | 5 +- .../CryptoAlgorithmRegistryOpenSSL.cpp | 6 + .../webcrypto/CryptoAlgorithmSHA3_256.cpp | 75 +++++++ .../webcrypto/CryptoAlgorithmSHA3_256.h | 49 +++++ .../webcrypto/CryptoAlgorithmSHA3_384.cpp | 75 +++++++ .../webcrypto/CryptoAlgorithmSHA3_384.h | 49 +++++ .../webcrypto/CryptoAlgorithmSHA3_512.cpp | 75 +++++++ .../webcrypto/CryptoAlgorithmSHA3_512.h | 49 +++++ .../bindings/webcrypto/CryptoDigest.cpp | 192 ++++++++++++++++++ src/bun.js/bindings/webcrypto/CryptoDigest.h | 3 + .../bindings/webcrypto/JSSubtleCrypto.cpp | 25 +++ .../bindings/webcrypto/SubtleCrypto.cpp | 67 ++++++ src/bun.js/bindings/webcrypto/SubtleCrypto.h | 1 + .../bindings/webcrypto/SubtleCrypto.idl | 1 + test/regression/issue/29218.test.ts | 151 ++++++++++++++ 17 files changed, 833 insertions(+), 1 deletion(-) create mode 100644 src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.cpp create mode 100644 src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.h create mode 100644 src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.cpp create mode 100644 src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.h create mode 100644 src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.cpp create mode 100644 src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.h create mode 100644 test/regression/issue/29218.test.ts diff --git a/src/bun.js/bindings/AsymmetricKeyValue.cpp b/src/bun.js/bindings/AsymmetricKeyValue.cpp index 0e7cb6744c41..d72f9abce0b6 100644 --- a/src/bun.js/bindings/AsymmetricKeyValue.cpp +++ b/src/bun.js/bindings/AsymmetricKeyValue.cpp @@ -127,6 +127,9 @@ AsymmetricKeyValue::AsymmetricKeyValue(WebCore::CryptoKey& cryptoKey) case CryptoAlgorithmIdentifier::SHA_256: case CryptoAlgorithmIdentifier::SHA_384: case CryptoAlgorithmIdentifier::SHA_512: + case CryptoAlgorithmIdentifier::SHA3_256: + case CryptoAlgorithmIdentifier::SHA3_384: + case CryptoAlgorithmIdentifier::SHA3_512: case CryptoAlgorithmIdentifier::HKDF: case CryptoAlgorithmIdentifier::PBKDF2: case CryptoAlgorithmIdentifier::None: diff --git a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp index 673ac6503231..2d41a482d08f 100644 --- a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp +++ b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp @@ -2406,6 +2406,14 @@ class CloneSerializer : public CloneBase { case CryptoAlgorithmIdentifier::X25519: write(CryptoAlgorithmIdentifierTag::X25519); break; + case CryptoAlgorithmIdentifier::SHA3_256: + case CryptoAlgorithmIdentifier::SHA3_384: + case CryptoAlgorithmIdentifier::SHA3_512: + // SHA-3 algorithms are currently only used as digest functions and + // are not associated with serializable CryptoKey instances. If this + // changes (e.g. when HMAC/SHA-3 or KEM keys land), add new tags. + RELEASE_ASSERT_NOT_REACHED(); + break; case CryptoAlgorithmIdentifier::None: { RELEASE_ASSERT_NOT_REACHED(); break; diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmIdentifier.h b/src/bun.js/bindings/webcrypto/CryptoAlgorithmIdentifier.h index 8f0ef468e8ed..a59c6c185f22 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmIdentifier.h +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmIdentifier.h @@ -51,7 +51,10 @@ enum class CryptoAlgorithmIdentifier : uint8_t { HKDF, PBKDF2, Ed25519, - X25519 + X25519, + SHA3_256, + SHA3_384, + SHA3_512 }; } // namespace WebCore diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRegistryOpenSSL.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRegistryOpenSSL.cpp index b0d12c4970b9..4f0bfe859dc2 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRegistryOpenSSL.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRegistryOpenSSL.cpp @@ -48,6 +48,9 @@ #include "CryptoAlgorithmSHA256.h" #include "CryptoAlgorithmSHA384.h" #include "CryptoAlgorithmSHA512.h" +#include "CryptoAlgorithmSHA3_256.h" +#include "CryptoAlgorithmSHA3_384.h" +#include "CryptoAlgorithmSHA3_512.h" #include "CryptoAlgorithmX25519.h" namespace WebCore { @@ -73,6 +76,9 @@ void CryptoAlgorithmRegistry::platformRegisterAlgorithms() registerAlgorithmWithAlternativeName(); registerAlgorithmWithAlternativeName(); registerAlgorithmWithAlternativeName(); + registerAlgorithm(); + registerAlgorithm(); + registerAlgorithm(); registerAlgorithm(); registerAlgorithm(); } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.cpp new file mode 100644 index 000000000000..c14a694076ef --- /dev/null +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2013 Apple Inc. 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. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS 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 APPLE INC. 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. + */ + +#include "config.h" +#include "CryptoAlgorithmSHA3_256.h" + +#if ENABLE(WEB_CRYPTO) + +#include "ScriptExecutionContext.h" +#include "CryptoDigest.h" + +namespace WebCore { + +Ref CryptoAlgorithmSHA3_256::create() +{ + return adoptRef(*new CryptoAlgorithmSHA3_256); +} + +CryptoAlgorithmIdentifier CryptoAlgorithmSHA3_256::identifier() const +{ + return s_identifier; +} + +void CryptoAlgorithmSHA3_256::digest(Vector&& message, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) +{ + auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA3_256); + if (!digest) { + exceptionCallback(OperationError, ""_s); + return; + } + + if (message.size() < 64) { + auto moved = WTF::move(message); + digest->addBytes(moved.begin(), moved.size()); + auto result = digest->computeHash(); + ScriptExecutionContext::postTaskTo(context.identifier(), [callback = WTF::move(callback), result = WTF::move(result)](auto&) { + callback(result); + }); + return; + } + + workQueue.dispatch(context.globalObject(), [digest = WTF::move(digest), message = WTF::move(message), callback = WTF::move(callback), contextIdentifier = context.identifier()]() mutable { + digest->addBytes(message.begin(), message.size()); + auto result = digest->computeHash(); + ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTF::move(callback), result = WTF::move(result)](auto&) { + callback(result); + }); + }); +} + +} + +#endif // ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.h b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.h new file mode 100644 index 000000000000..63a8f4e6d981 --- /dev/null +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 Apple Inc. 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. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS 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 APPLE INC. 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. + */ + +#pragma once + +#include "CryptoAlgorithm.h" + +#if ENABLE(WEB_CRYPTO) + +namespace WebCore { + +class CryptoAlgorithmSHA3_256 final : public CryptoAlgorithm { +public: + static constexpr ASCIILiteral s_name = "SHA3-256"_s; + + static const CryptoAlgorithmIdentifier s_identifier = CryptoAlgorithmIdentifier::SHA3_256; + static Ref create(); + +private: + CryptoAlgorithmSHA3_256() = default; + CryptoAlgorithmIdentifier identifier() const final; + void digest(Vector&&, VectorCallback&&, ExceptionCallback&&, ScriptExecutionContext&, WorkQueue&) final; +}; + +} // namespace WebCore + +#endif // ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.cpp new file mode 100644 index 000000000000..925f52bb87c5 --- /dev/null +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2013 Apple Inc. 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. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS 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 APPLE INC. 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. + */ + +#include "config.h" +#include "CryptoAlgorithmSHA3_384.h" + +#if ENABLE(WEB_CRYPTO) + +#include "ScriptExecutionContext.h" +#include "CryptoDigest.h" + +namespace WebCore { + +Ref CryptoAlgorithmSHA3_384::create() +{ + return adoptRef(*new CryptoAlgorithmSHA3_384); +} + +CryptoAlgorithmIdentifier CryptoAlgorithmSHA3_384::identifier() const +{ + return s_identifier; +} + +void CryptoAlgorithmSHA3_384::digest(Vector&& message, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) +{ + auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA3_384); + if (!digest) { + exceptionCallback(OperationError, ""_s); + return; + } + + if (message.size() < 64) { + auto moved = WTF::move(message); + digest->addBytes(moved.begin(), moved.size()); + auto result = digest->computeHash(); + ScriptExecutionContext::postTaskTo(context.identifier(), [callback = WTF::move(callback), result = WTF::move(result)](auto&) { + callback(result); + }); + return; + } + + workQueue.dispatch(context.globalObject(), [digest = WTF::move(digest), message = WTF::move(message), callback = WTF::move(callback), contextIdentifier = context.identifier()]() mutable { + digest->addBytes(message.begin(), message.size()); + auto result = digest->computeHash(); + ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTF::move(callback), result = WTF::move(result)](auto&) { + callback(result); + }); + }); +} + +} + +#endif // ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.h b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.h new file mode 100644 index 000000000000..cd5551e2a33f --- /dev/null +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 Apple Inc. 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. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS 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 APPLE INC. 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. + */ + +#pragma once + +#include "CryptoAlgorithm.h" + +#if ENABLE(WEB_CRYPTO) + +namespace WebCore { + +class CryptoAlgorithmSHA3_384 final : public CryptoAlgorithm { +public: + static constexpr ASCIILiteral s_name = "SHA3-384"_s; + + static const CryptoAlgorithmIdentifier s_identifier = CryptoAlgorithmIdentifier::SHA3_384; + static Ref create(); + +private: + CryptoAlgorithmSHA3_384() = default; + CryptoAlgorithmIdentifier identifier() const final; + void digest(Vector&&, VectorCallback&&, ExceptionCallback&&, ScriptExecutionContext&, WorkQueue&) final; +}; + +} // namespace WebCore + +#endif // ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.cpp new file mode 100644 index 000000000000..f964e8b8dd03 --- /dev/null +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2013 Apple Inc. 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. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS 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 APPLE INC. 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. + */ + +#include "config.h" +#include "CryptoAlgorithmSHA3_512.h" + +#if ENABLE(WEB_CRYPTO) + +#include "ScriptExecutionContext.h" +#include "CryptoDigest.h" + +namespace WebCore { + +Ref CryptoAlgorithmSHA3_512::create() +{ + return adoptRef(*new CryptoAlgorithmSHA3_512); +} + +CryptoAlgorithmIdentifier CryptoAlgorithmSHA3_512::identifier() const +{ + return s_identifier; +} + +void CryptoAlgorithmSHA3_512::digest(Vector&& message, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) +{ + auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA3_512); + if (!digest) { + exceptionCallback(OperationError, ""_s); + return; + } + + if (message.size() < 64) { + auto moved = WTF::move(message); + digest->addBytes(moved.begin(), moved.size()); + auto result = digest->computeHash(); + ScriptExecutionContext::postTaskTo(context.identifier(), [callback = WTF::move(callback), result = WTF::move(result)](auto&) { + callback(result); + }); + return; + } + + workQueue.dispatch(context.globalObject(), [digest = WTF::move(digest), message = WTF::move(message), callback = WTF::move(callback), contextIdentifier = context.identifier()]() mutable { + digest->addBytes(message.begin(), message.size()); + auto result = digest->computeHash(); + ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTF::move(callback), result = WTF::move(result)](auto&) { + callback(result); + }); + }); +} + +} + +#endif // ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.h b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.h new file mode 100644 index 000000000000..51bf0cb956f3 --- /dev/null +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 Apple Inc. 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. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS 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 APPLE INC. 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. + */ + +#pragma once + +#include "CryptoAlgorithm.h" + +#if ENABLE(WEB_CRYPTO) + +namespace WebCore { + +class CryptoAlgorithmSHA3_512 final : public CryptoAlgorithm { +public: + static constexpr ASCIILiteral s_name = "SHA3-512"_s; + + static const CryptoAlgorithmIdentifier s_identifier = CryptoAlgorithmIdentifier::SHA3_512; + static Ref create(); + +private: + CryptoAlgorithmSHA3_512() = default; + CryptoAlgorithmIdentifier identifier() const final; + void digest(Vector&&, VectorCallback&&, ExceptionCallback&&, ScriptExecutionContext&, WorkQueue&) final; +}; + +} // namespace WebCore + +#endif // ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp index 0c8f3aa5e8fe..09c8387b0c82 100644 --- a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp @@ -26,9 +26,192 @@ #include "config.h" #include "CryptoDigest.h" +#include +#include #include namespace { + +// Keccak-f[1600] permutation and SHA-3 sponge, used to implement the +// fixed-output SHA3-256/384/512 hashes from FIPS 202. +// +// BoringSSL's internal keccak_* helpers only expose SHA3-256 and SHA3-512, +// so we provide a small self-contained implementation here rather than +// depending on internal BoringSSL headers. The same state machine can be +// extended with SHAKE/cSHAKE/TurboSHAKE domain separators when those +// XOFs land alongside the rest of the WICG "Modern Algorithms" spec. + +struct KeccakState { + uint64_t lanes[25]; +}; + +static constexpr uint64_t kKeccakRoundConstants[24] = { + 0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, + 0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL, + 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, + 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000aULL, + 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, + 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, + 0x000000000000800aULL, 0x800000008000000aULL, 0x8000000080008081ULL, + 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL, +}; + +static inline uint64_t rotl64(uint64_t x, unsigned n) +{ + return (x << n) | (x >> (64 - n)); +} + +static void keccakF1600(KeccakState& state) +{ + for (unsigned round = 0; round < 24; round++) { + // θ step + uint64_t c[5]; + for (unsigned x = 0; x < 5; x++) + c[x] = state.lanes[x] ^ state.lanes[x + 5] ^ state.lanes[x + 10] ^ state.lanes[x + 15] ^ state.lanes[x + 20]; + uint64_t d[5]; + for (unsigned x = 0; x < 5; x++) + d[x] = c[(x + 4) % 5] ^ rotl64(c[(x + 1) % 5], 1); + for (unsigned x = 0; x < 5; x++) { + for (unsigned y = 0; y < 5; y++) + state.lanes[y * 5 + x] ^= d[x]; + } + + // ρ and π steps (in-place along the 24-step trail) + uint64_t prev = state.lanes[1]; + static constexpr std::pair piRho[24] = { + { 10, 1 }, { 7, 3 }, { 11, 6 }, { 17, 10 }, { 18, 15 }, { 3, 21 }, + { 5, 28 }, { 16, 36 }, { 8, 45 }, { 21, 55 }, { 24, 2 }, { 4, 14 }, + { 15, 27 }, { 23, 41 }, { 19, 56 }, { 13, 8 }, { 12, 25 }, { 2, 43 }, + { 20, 62 }, { 14, 18 }, { 22, 39 }, { 9, 61 }, { 6, 20 }, { 1, 44 }, + }; + for (const auto& step : piRho) { + uint64_t rotated = rotl64(prev, static_cast(step.second)); + prev = state.lanes[step.first]; + state.lanes[step.first] = rotated; + } + + // χ step + for (unsigned y = 0; y < 5; y++) { + const unsigned row = 5 * y; + const uint64_t a0 = state.lanes[row]; + const uint64_t a1 = state.lanes[row + 1]; + state.lanes[row] ^= ~a1 & state.lanes[row + 2]; + state.lanes[row + 1] ^= ~state.lanes[row + 2] & state.lanes[row + 3]; + state.lanes[row + 2] ^= ~state.lanes[row + 3] & state.lanes[row + 4]; + state.lanes[row + 3] ^= ~state.lanes[row + 4] & a0; + state.lanes[row + 4] ^= ~a0 & a1; + } + + // ι step + state.lanes[0] ^= kKeccakRoundConstants[round]; + } +} + +static inline uint64_t loadLE64(const uint8_t* bytes) +{ + uint64_t v; + std::memcpy(&v, bytes, sizeof(v)); +#if defined(__BIG_ENDIAN__) || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + v = __builtin_bswap64(v); +#endif + return v; +} + +static inline void storeLE64(uint8_t* bytes, uint64_t v) +{ +#if defined(__BIG_ENDIAN__) || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + v = __builtin_bswap64(v); +#endif + std::memcpy(bytes, &v, sizeof(v)); +} + +// Streaming SHA-3 sponge for one of the fixed output sizes. +struct Sha3Context { + KeccakState state {}; + // Input bytes buffered before the next permutation. + uint8_t buffer[144] { }; // enough for SHA3-224's 144-byte rate (largest) + size_t bufferLength = 0; + size_t rateBytes = 0; + size_t digestLength = 0; +}; + +static void sha3Init(Sha3Context& ctx, size_t outputBytes) +{ + ctx = {}; + ctx.digestLength = outputBytes; + // rate = 1600 - 2 * outputBits + const size_t capacityBytes = 2 * outputBytes; + ctx.rateBytes = 200 - capacityBytes; +} + +static void sha3Update(Sha3Context& ctx, const uint8_t* input, size_t length) +{ + while (length > 0) { + const size_t space = ctx.rateBytes - ctx.bufferLength; + const size_t take = length < space ? length : space; + std::memcpy(ctx.buffer + ctx.bufferLength, input, take); + ctx.bufferLength += take; + input += take; + length -= take; + + if (ctx.bufferLength == ctx.rateBytes) { + // Absorb one full rate-sized block. + const size_t rateLanes = ctx.rateBytes / 8; + for (size_t i = 0; i < rateLanes; i++) + ctx.state.lanes[i] ^= loadLE64(ctx.buffer + 8 * i); + keccakF1600(ctx.state); + ctx.bufferLength = 0; + } + } +} + +static void sha3Final(Sha3Context& ctx, uint8_t* output) +{ + // Pad10*1 with the SHA-3 domain separator 0x06. + std::memset(ctx.buffer + ctx.bufferLength, 0, ctx.rateBytes - ctx.bufferLength); + ctx.buffer[ctx.bufferLength] |= 0x06; + ctx.buffer[ctx.rateBytes - 1] |= 0x80; + + const size_t rateLanes = ctx.rateBytes / 8; + for (size_t i = 0; i < rateLanes; i++) + ctx.state.lanes[i] ^= loadLE64(ctx.buffer + 8 * i); + keccakF1600(ctx.state); + + // Squeeze. For the fixed-output SHA-3 variants the digest always fits + // inside a single rate-sized squeeze block (rate >= 104 bytes, digest + // <= 64 bytes), so no additional permutation is needed. + size_t produced = 0; + uint8_t lane[8]; + while (produced < ctx.digestLength) { + const size_t laneIndex = produced / 8; + storeLE64(lane, ctx.state.lanes[laneIndex]); + const size_t take = std::min(8, ctx.digestLength - produced); + std::memcpy(output + produced, lane, take); + produced += take; + } +} + +struct SHA3_256Functions { + static void init(Sha3Context* ctx) { sha3Init(*ctx, 32); } + static void update(Sha3Context* ctx, const void* data, size_t len) { sha3Update(*ctx, static_cast(data), len); } + static void final(uint8_t* out, Sha3Context* ctx) { sha3Final(*ctx, out); } + static constexpr size_t digestLength = 32; +}; + +struct SHA3_384Functions { + static void init(Sha3Context* ctx) { sha3Init(*ctx, 48); } + static void update(Sha3Context* ctx, const void* data, size_t len) { sha3Update(*ctx, static_cast(data), len); } + static void final(uint8_t* out, Sha3Context* ctx) { sha3Final(*ctx, out); } + static constexpr size_t digestLength = 48; +}; + +struct SHA3_512Functions { + static void init(Sha3Context* ctx) { sha3Init(*ctx, 64); } + static void update(Sha3Context* ctx, const void* data, size_t len) { sha3Update(*ctx, static_cast(data), len); } + static void final(uint8_t* out, Sha3Context* ctx) { sha3Final(*ctx, out); } + static constexpr size_t digestLength = 64; +}; + struct SHA1Functions { static constexpr auto init = SHA1_Init; static constexpr auto update = SHA1_Update; @@ -131,6 +314,15 @@ std::unique_ptr CryptoDigest::create(CryptoDigest::Algorithm algor case CryptoDigest::Algorithm::SHA_512: digest->m_context = CryptoDigestContextImpl::create(); return digest; + case CryptoDigest::Algorithm::SHA3_256: + digest->m_context = CryptoDigestContextImpl::create(); + return digest; + case CryptoDigest::Algorithm::SHA3_384: + digest->m_context = CryptoDigestContextImpl::create(); + return digest; + case CryptoDigest::Algorithm::SHA3_512: + digest->m_context = CryptoDigestContextImpl::create(); + return digest; } return nullptr; diff --git a/src/bun.js/bindings/webcrypto/CryptoDigest.h b/src/bun.js/bindings/webcrypto/CryptoDigest.h index 5da849a1d33c..12c3907e1a55 100644 --- a/src/bun.js/bindings/webcrypto/CryptoDigest.h +++ b/src/bun.js/bindings/webcrypto/CryptoDigest.h @@ -47,6 +47,9 @@ class CryptoDigest { SHA_256, SHA_384, SHA_512, + SHA3_256, + SHA3_384, + SHA3_512, }; PAL_EXPORT static std::unique_ptr create(Algorithm); PAL_EXPORT ~CryptoDigest(); diff --git a/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp b/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp index 84a506191bff..87b4546f2f6b 100644 --- a/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp +++ b/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp @@ -126,6 +126,7 @@ static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_importKey); static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_exportKey); static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_wrapKey); static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_unwrapKey); +static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_supports); // Attributes @@ -198,6 +199,7 @@ static const HashTableValue JSSubtleCryptoPrototypeTableValues[] = { { "exportKey"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSubtleCryptoPrototypeFunction_exportKey, 2 } }, { "wrapKey"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSubtleCryptoPrototypeFunction_wrapKey, 4 } }, { "unwrapKey"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSubtleCryptoPrototypeFunction_unwrapKey, 7 } }, + { "supports"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSubtleCryptoPrototypeFunction_supports, 2 } }, }; const ClassInfo JSSubtleCryptoPrototype::s_info = { "SubtleCrypto"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSSubtleCryptoPrototype) }; @@ -598,6 +600,29 @@ JSC_DEFINE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_unwrapKey, (JSGlobalObj return IDLOperationReturningPromise::call(*lexicalGlobalObject, *callFrame, "unwrapKey"); } +static inline JSC::EncodedJSValue jsSubtleCryptoPrototypeFunction_supportsBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + if (callFrame->argumentCount() < 2) [[unlikely]] + return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); + EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); + auto operation = convert(*lexicalGlobalObject, argument0.value()); + RETURN_IF_EXCEPTION(throwScope, {}); + EnsureStillAliveScope argument1 = callFrame->uncheckedArgument(1); + auto algorithm = convert>(*lexicalGlobalObject, argument1.value()); + RETURN_IF_EXCEPTION(throwScope, {}); + RELEASE_AND_RETURN(throwScope, JSValue::encode(jsBoolean(impl.supports(*jsCast(lexicalGlobalObject), operation, WTF::move(algorithm))))); +} + +JSC_DEFINE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_supports, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "supports"); +} + JSC::GCClient::IsoSubspace* JSSubtleCrypto::subspaceForImpl(JSC::VM& vm) { return WebCore::subspaceForImpl( diff --git a/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp b/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp index 5f57e71a2d4f..48a4c74c25a8 100644 --- a/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp +++ b/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp @@ -204,6 +204,9 @@ static ExceptionOr> normalizeCryptoAl case CryptoAlgorithmIdentifier::SHA_256: case CryptoAlgorithmIdentifier::SHA_384: case CryptoAlgorithmIdentifier::SHA_512: + case CryptoAlgorithmIdentifier::SHA3_256: + case CryptoAlgorithmIdentifier::SHA3_384: + case CryptoAlgorithmIdentifier::SHA3_512: result = makeUnique(params); break; default: @@ -381,6 +384,9 @@ static ExceptionOr> normalizeCryptoAl case CryptoAlgorithmIdentifier::SHA_256: case CryptoAlgorithmIdentifier::SHA_384: case CryptoAlgorithmIdentifier::SHA_512: + case CryptoAlgorithmIdentifier::SHA3_256: + case CryptoAlgorithmIdentifier::SHA3_384: + case CryptoAlgorithmIdentifier::SHA3_512: return Exception { NotSupportedError }; case CryptoAlgorithmIdentifier::None: return Exception { NotSupportedError }; @@ -803,6 +809,67 @@ void SubtleCrypto::digest(JSC::JSGlobalObject& state, AlgorithmIdentifier&& algo algorithm->digest(WTF::move(data), WTF::move(callback), WTF::move(exceptionCallback), *scriptExecutionContext(), m_workQueue); } +// WICG "Modern Algorithms in the Web Cryptography API": +// https://wicg.github.io/webcrypto-modern-algos/#SubtleCrypto-method-supports +// +// Synchronously report whether this SubtleCrypto implementation supports +// a given (operation, algorithm) pair. Mirrors the normalization step of +// the relevant algorithm dispatch: if the algorithm is recognised and its +// parameter dictionary is well-formed for the requested operation, return +// true; otherwise return false. Exceptions thrown by normalization (e.g. +// TypeError for malformed input) are swallowed and surfaced as false so +// callers can use supports() purely for feature detection. +bool SubtleCrypto::supports(JSC::JSGlobalObject& state, const String& operation, AlgorithmIdentifier&& algorithmIdentifier) +{ + auto& vm = state.vm(); + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + + Operations op; + if (operation == "encrypt"_s) + op = Operations::Encrypt; + else if (operation == "decrypt"_s) + op = Operations::Decrypt; + else if (operation == "sign"_s) + op = Operations::Sign; + else if (operation == "verify"_s) + op = Operations::Verify; + else if (operation == "digest"_s) + op = Operations::Digest; + else if (operation == "generateKey"_s) + op = Operations::GenerateKey; + else if (operation == "deriveBits"_s) + op = Operations::DeriveBits; + else if (operation == "deriveKey"_s) + op = Operations::DeriveBits; // deriveKey reuses DeriveBits normalization + else if (operation == "importKey"_s) + op = Operations::ImportKey; + else if (operation == "exportKey"_s) + op = Operations::ImportKey; // exportKey has no dedicated normalization; accept anything importable + else if (operation == "wrapKey"_s) + op = Operations::WrapKey; + else if (operation == "unwrapKey"_s) + op = Operations::UnwrapKey; + else if (operation == "getPublicKey"_s) + op = Operations::ImportKey; // getPublicKey applies to asymmetric keys; re-use importKey normalization + else if (operation == "encapsulateBits"_s || operation == "encapsulateKey"_s + || operation == "decapsulateBits"_s || operation == "decapsulateKey"_s) { + // KEM operations are not yet implemented. + return false; + } else { + // Unknown operation name. + return false; + } + + auto params = normalizeCryptoAlgorithmParameters(state, WTF::move(algorithmIdentifier), op); + if (scope.exception()) { + scope.clearException(); + return false; + } + if (params.hasException()) + return false; + return true; +} + void SubtleCrypto::generateKey(JSC::JSGlobalObject& state, AlgorithmIdentifier&& algorithmIdentifier, bool extractable, Vector&& keyUsages, Ref&& promise) { auto& vm = state.vm(); diff --git a/src/bun.js/bindings/webcrypto/SubtleCrypto.h b/src/bun.js/bindings/webcrypto/SubtleCrypto.h index cd4f68ddb481..bcb1fe7eb336 100644 --- a/src/bun.js/bindings/webcrypto/SubtleCrypto.h +++ b/src/bun.js/bindings/webcrypto/SubtleCrypto.h @@ -78,6 +78,7 @@ class SubtleCrypto : public ContextDestructionObserver, public RefCounted&&); void wrapKey(JSC::JSGlobalObject&, KeyFormat, CryptoKey&, CryptoKey& wrappingKey, AlgorithmIdentifier&& wrapAlgorithm, Ref&&); void unwrapKey(JSC::JSGlobalObject&, KeyFormat, BufferSource&& wrappedKey, CryptoKey& unwrappingKey, AlgorithmIdentifier&& unwrapAlgorithm, AlgorithmIdentifier&& unwrappedKeyAlgorithm, bool extractable, Vector&&, Ref&&); + bool supports(JSC::JSGlobalObject&, const String& operation, AlgorithmIdentifier&&); private: explicit SubtleCrypto(ScriptExecutionContext*); diff --git a/src/bun.js/bindings/webcrypto/SubtleCrypto.idl b/src/bun.js/bindings/webcrypto/SubtleCrypto.idl index 358b9950b2a9..141bd9e13588 100644 --- a/src/bun.js/bindings/webcrypto/SubtleCrypto.idl +++ b/src/bun.js/bindings/webcrypto/SubtleCrypto.idl @@ -45,4 +45,5 @@ typedef (object or DOMString) AlgorithmIdentifier; Promise exportKey(KeyFormat format, CryptoKey key); [CallWith=CurrentGlobalObject] Promise wrapKey(KeyFormat format, CryptoKey key, CryptoKey wrappingKey, AlgorithmIdentifier wrapAlgorithm); [CallWith=CurrentGlobalObject] Promise unwrapKey(KeyFormat format, BufferSource wrappedKey, CryptoKey unwrappingKey, AlgorithmIdentifier unwrapAlgorithm, AlgorithmIdentifier unwrappedKeyAlgorithm, boolean extractable, sequence keyUsages); + [CallWith=CurrentGlobalObject] boolean supports(DOMString operation, AlgorithmIdentifier algorithm); }; diff --git a/test/regression/issue/29218.test.ts b/test/regression/issue/29218.test.ts new file mode 100644 index 000000000000..0644508559e5 --- /dev/null +++ b/test/regression/issue/29218.test.ts @@ -0,0 +1,151 @@ +// https://github.com/oven-sh/bun/issues/29218 +// +// First slice of the WICG "Modern Algorithms in the Web Cryptography API" +// specification: SHA-3 fixed-output hashes (SHA3-256 / SHA3-384 / SHA3-512) +// exposed through `crypto.subtle.digest`, plus the new synchronous feature +// detection method `crypto.subtle.supports(operation, algorithm)`. +// +// Spec: https://wicg.github.io/webcrypto-modern-algos/ +import { describe, expect, test } from "bun:test"; + +const te = new TextEncoder(); + +function hex(buf: ArrayBuffer | Uint8Array): string { + const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf); + return Array.from(bytes, b => b.toString(16).padStart(2, "0")).join(""); +} + +describe("crypto.subtle SHA-3", () => { + // NIST FIPS 202 / Cryptographic Algorithm Validation Program test vectors + // for the empty message and for the ASCII string "abc". + // https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/secure-hashing + const vectors = { + "SHA3-256": { + empty: "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", + abc: "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532", + }, + "SHA3-384": { + empty: + "0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004", + abc: "ec01498288516fc926459f58e2c6ad8df9b473cb0fc08c2596da7cf0e49be4b298d88cea927ac7f539f1edf228376d25", + }, + "SHA3-512": { + empty: + "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26", + abc: "b751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0", + }, + } as const; + + for (const [alg, vec] of Object.entries(vectors)) { + test(`${alg} digests the empty string to the FIPS 202 test vector`, async () => { + const out = await crypto.subtle.digest(alg, new Uint8Array(0)); + expect(hex(out)).toBe(vec.empty); + }); + + test(`${alg} digests "abc" to the FIPS 202 test vector`, async () => { + const out = await crypto.subtle.digest(alg, te.encode("abc")); + expect(hex(out)).toBe(vec.abc); + }); + + test(`${alg} digests a long message longer than one Keccak rate`, async () => { + // Longer than the largest SHA-3 rate (72 bytes for SHA3-512) to exercise + // multi-block absorption inside the sponge. + const buf = new Uint8Array(1024); + for (let i = 0; i < buf.length; i++) buf[i] = i & 0xff; + const out = await crypto.subtle.digest(alg, buf); + + const expectedLen = { "SHA3-256": 32, "SHA3-384": 48, "SHA3-512": 64 }[alg as keyof typeof vectors]; + expect(out.byteLength).toBe(expectedLen); + + // Stability: hashing the same input twice must produce identical output. + const again = await crypto.subtle.digest(alg, buf); + expect(hex(again)).toBe(hex(out)); + }); + } + + test("SHA-3 digest accepts a dictionary algorithm identifier", async () => { + const out = await crypto.subtle.digest({ name: "SHA3-256" }, te.encode("abc")); + expect(hex(out)).toBe(vectors["SHA3-256"].abc); + }); + + test("SHA-3 digest is case-insensitive on the algorithm name", async () => { + const out = await crypto.subtle.digest("sha3-256", te.encode("abc")); + expect(hex(out)).toBe(vectors["SHA3-256"].abc); + }); + + test("unknown SHA-3 variant is rejected with NotSupportedError", async () => { + // SHA3-224 is defined by FIPS 202 but is intentionally not exposed by the + // WICG spec, so it must be rejected rather than silently accepted. + await expect(crypto.subtle.digest("SHA3-224", te.encode("abc"))).rejects.toMatchObject({ + name: "NotSupportedError", + }); + }); +}); + +describe("crypto.subtle.supports", () => { + test("is a function of length 2", () => { + expect(typeof crypto.subtle.supports).toBe("function"); + expect(crypto.subtle.supports.length).toBe(2); + }); + + test("returns true for supported (operation, algorithm) pairs", () => { + // Classic Web Crypto algorithms that existed before this slice must + // continue to report true, so supports() is a safe feature-detect for + // everything Bun exposes. + expect(crypto.subtle.supports("digest", "SHA-256")).toBe(true); + expect(crypto.subtle.supports("digest", "SHA-384")).toBe(true); + expect(crypto.subtle.supports("digest", "SHA-512")).toBe(true); + expect(crypto.subtle.supports("generateKey", { name: "AES-GCM", length: 256 })).toBe(true); + expect(crypto.subtle.supports("importKey", "AES-GCM")).toBe(true); + expect(crypto.subtle.supports("wrapKey", { name: "AES-KW" })).toBe(true); + }); + + test("reports true for SHA3-256/384/512 digest", () => { + expect(crypto.subtle.supports("digest", "SHA3-256")).toBe(true); + expect(crypto.subtle.supports("digest", "SHA3-384")).toBe(true); + expect(crypto.subtle.supports("digest", "SHA3-512")).toBe(true); + }); + + test("accepts dictionary-form algorithm identifiers", () => { + expect(crypto.subtle.supports("digest", { name: "SHA3-256" })).toBe(true); + expect(crypto.subtle.supports("digest", { name: "SHA-256" })).toBe(true); + }); + + test("returns false for unknown algorithms", () => { + expect(crypto.subtle.supports("digest", "MD5")).toBe(false); + expect(crypto.subtle.supports("digest", "not-a-real-algorithm")).toBe(false); + expect(crypto.subtle.supports("digest", { name: "nope" })).toBe(false); + }); + + test("returns false for unknown operations", () => { + expect(crypto.subtle.supports("not-a-real-op", "SHA-256")).toBe(false); + }); + + test("returns false for KEM operations that are not yet implemented", () => { + // encapsulateKey/Bits and decapsulateKey/Bits come with ML-KEM in a + // later slice of the WICG spec. They must report false for now so that + // callers can progressively adopt them. + expect(crypto.subtle.supports("encapsulateKey", "ML-KEM-768")).toBe(false); + expect(crypto.subtle.supports("encapsulateBits", "ML-KEM-768")).toBe(false); + expect(crypto.subtle.supports("decapsulateKey", "ML-KEM-768")).toBe(false); + expect(crypto.subtle.supports("decapsulateBits", "ML-KEM-768")).toBe(false); + }); + + test("returns false (not a throw) for malformed algorithm input", () => { + // supports() must never throw, even for obviously bad input — that is + // the point of a synchronous feature-detect method. + expect(crypto.subtle.supports("digest", null as any)).toBe(false); + expect(crypto.subtle.supports("digest", 42 as any)).toBe(false); + expect(crypto.subtle.supports("digest", [] as any)).toBe(false); + }); + + test("throws when called with fewer than two arguments", () => { + // The only way supports() is allowed to throw is when JavaScript itself + // rejects the call (missing required arguments), mirroring every other + // SubtleCrypto method. + // @ts-expect-error + expect(() => crypto.subtle.supports()).toThrow(); + // @ts-expect-error + expect(() => crypto.subtle.supports("digest")).toThrow(); + }); +}); From 04739cc4f8a29e0b3616c940fdbce7610af68305 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:20:29 +0000 Subject: [PATCH 02/14] [autofix.ci] apply automated fixes --- .../bindings/webcrypto/CryptoDigest.cpp | 62 +++++++++++++++---- test/regression/issue/29218.test.ts | 3 +- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp index 09c8387b0c82..b119ddbc211d 100644 --- a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp @@ -46,14 +46,30 @@ struct KeccakState { }; static constexpr uint64_t kKeccakRoundConstants[24] = { - 0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, - 0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL, - 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, - 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000aULL, - 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, - 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, - 0x000000000000800aULL, 0x800000008000000aULL, 0x8000000080008081ULL, - 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL, + 0x0000000000000001ULL, + 0x0000000000008082ULL, + 0x800000000000808aULL, + 0x8000000080008000ULL, + 0x000000000000808bULL, + 0x0000000080000001ULL, + 0x8000000080008081ULL, + 0x8000000000008009ULL, + 0x000000000000008aULL, + 0x0000000000000088ULL, + 0x0000000080008009ULL, + 0x000000008000000aULL, + 0x000000008000808bULL, + 0x800000000000008bULL, + 0x8000000000008089ULL, + 0x8000000000008003ULL, + 0x8000000000008002ULL, + 0x8000000000000080ULL, + 0x000000000000800aULL, + 0x800000008000000aULL, + 0x8000000080008081ULL, + 0x8000000000008080ULL, + 0x0000000080000001ULL, + 0x8000000080008008ULL, }; static inline uint64_t rotl64(uint64_t x, unsigned n) @@ -79,10 +95,30 @@ static void keccakF1600(KeccakState& state) // ρ and π steps (in-place along the 24-step trail) uint64_t prev = state.lanes[1]; static constexpr std::pair piRho[24] = { - { 10, 1 }, { 7, 3 }, { 11, 6 }, { 17, 10 }, { 18, 15 }, { 3, 21 }, - { 5, 28 }, { 16, 36 }, { 8, 45 }, { 21, 55 }, { 24, 2 }, { 4, 14 }, - { 15, 27 }, { 23, 41 }, { 19, 56 }, { 13, 8 }, { 12, 25 }, { 2, 43 }, - { 20, 62 }, { 14, 18 }, { 22, 39 }, { 9, 61 }, { 6, 20 }, { 1, 44 }, + { 10, 1 }, + { 7, 3 }, + { 11, 6 }, + { 17, 10 }, + { 18, 15 }, + { 3, 21 }, + { 5, 28 }, + { 16, 36 }, + { 8, 45 }, + { 21, 55 }, + { 24, 2 }, + { 4, 14 }, + { 15, 27 }, + { 23, 41 }, + { 19, 56 }, + { 13, 8 }, + { 12, 25 }, + { 2, 43 }, + { 20, 62 }, + { 14, 18 }, + { 22, 39 }, + { 9, 61 }, + { 6, 20 }, + { 1, 44 }, }; for (const auto& step : piRho) { uint64_t rotated = rotl64(prev, static_cast(step.second)); @@ -129,7 +165,7 @@ static inline void storeLE64(uint8_t* bytes, uint64_t v) struct Sha3Context { KeccakState state {}; // Input bytes buffered before the next permutation. - uint8_t buffer[144] { }; // enough for SHA3-224's 144-byte rate (largest) + uint8_t buffer[144] {}; // enough for SHA3-224's 144-byte rate (largest) size_t bufferLength = 0; size_t rateBytes = 0; size_t digestLength = 0; diff --git a/test/regression/issue/29218.test.ts b/test/regression/issue/29218.test.ts index 0644508559e5..ebfa6bc8e322 100644 --- a/test/regression/issue/29218.test.ts +++ b/test/regression/issue/29218.test.ts @@ -25,8 +25,7 @@ describe("crypto.subtle SHA-3", () => { abc: "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532", }, "SHA3-384": { - empty: - "0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004", + empty: "0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004", abc: "ec01498288516fc926459f58e2c6ad8df9b473cb0fc08c2596da7cf0e49be4b298d88cea927ac7f539f1edf228376d25", }, "SHA3-512": { From c1f35b0baac4a4079617e065c517cefce19e76ec Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 12 Apr 2026 12:39:35 +0000 Subject: [PATCH 03/14] webcrypto: match supports() to real dispatch for wrap/export/getPublicKey Addresses review feedback on #29222: - wrapKey/unwrapKey now mirror the two-step normalize-and-fallback dispatch the real methods perform. supports("wrapKey", ...) reports true for AES-GCM/CBC/CTR/CFB-8 and RSA-OAEP (via the Encrypt fallback), matching what a subsequent wrapKey() call would accept. Before, only AES-KW was reported. - exportKey no longer proxies through ImportKey normalization. It resolves the algorithm name to an identifier and then runs it through isSupportedExportKey(), the same gate the real exportKey() uses. HKDF/PBKDF2 and hash-only algorithms now correctly report false rather than returning a false positive that the underlying method would reject at runtime. - getPublicKey returns false for every algorithm. The method is not implemented yet (it lands with ML-KEM/ML-DSA in a later slice) and it is conceptually asymmetric-only, so symmetric algorithms must never report true. Regression test coverage expands to 33 tests / 101 expects and now cross-checks multi-block SHA-3 output against node:crypto (OpenSSL), exercises the wrap/unwrap fallback on AES-GCM/CBC/CTR and RSA-OAEP, the exportKey HKDF/PBKDF2 rejection, the getPublicKey negative cases, and an end-to-end sanity check that every supports()==true pair actually succeeds in the underlying method. Refs #29218 --- .../bindings/webcrypto/SubtleCrypto.cpp | 147 ++++++++++----- test/regression/issue/29218.test.ts | 170 +++++++++++++++--- 2 files changed, 250 insertions(+), 67 deletions(-) diff --git a/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp b/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp index 48a4c74c25a8..cc6e17e921d0 100644 --- a/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp +++ b/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp @@ -813,61 +813,118 @@ void SubtleCrypto::digest(JSC::JSGlobalObject& state, AlgorithmIdentifier&& algo // https://wicg.github.io/webcrypto-modern-algos/#SubtleCrypto-method-supports // // Synchronously report whether this SubtleCrypto implementation supports -// a given (operation, algorithm) pair. Mirrors the normalization step of -// the relevant algorithm dispatch: if the algorithm is recognised and its -// parameter dictionary is well-formed for the requested operation, return -// true; otherwise return false. Exceptions thrown by normalization (e.g. -// TypeError for malformed input) are swallowed and surfaced as false so -// callers can use supports() purely for feature detection. +// a given (operation, algorithm) pair. supports() has to match the exact +// dispatch logic of the corresponding method so that callers can use it for +// progressive enhancement without hitting runtime NotSupportedError for +// algorithms that supports() promised. Any exception produced by the +// normalization step (or the subsequent method-specific checks) is swallowed +// and surfaced as false — supports() itself never rejects or throws other +// than for missing required arguments. +static bool normalizesWithoutException(JSGlobalObject& state, WebCore::SubtleCrypto::AlgorithmIdentifier& alg, Operations op) +{ + auto& vm = state.vm(); + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + + // normalizeCryptoAlgorithmParameters takes the identifier by value; + // we clone the variant so callers can retry with a different op. + WebCore::SubtleCrypto::AlgorithmIdentifier copy = alg; + auto params = normalizeCryptoAlgorithmParameters(state, WTF::move(copy), op); + if (scope.exception()) { + scope.clearException(); + return false; + } + return !params.hasException(); +} + bool SubtleCrypto::supports(JSC::JSGlobalObject& state, const String& operation, AlgorithmIdentifier&& algorithmIdentifier) { auto& vm = state.vm(); auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); - Operations op; + // Simple cases: the operation maps to a single normalize bucket. The + // method's runtime behaviour is then "normalize succeeded → run", so + // supports() returns true iff normalization succeeds. if (operation == "encrypt"_s) - op = Operations::Encrypt; - else if (operation == "decrypt"_s) - op = Operations::Decrypt; - else if (operation == "sign"_s) - op = Operations::Sign; - else if (operation == "verify"_s) - op = Operations::Verify; - else if (operation == "digest"_s) - op = Operations::Digest; - else if (operation == "generateKey"_s) - op = Operations::GenerateKey; - else if (operation == "deriveBits"_s) - op = Operations::DeriveBits; - else if (operation == "deriveKey"_s) - op = Operations::DeriveBits; // deriveKey reuses DeriveBits normalization - else if (operation == "importKey"_s) - op = Operations::ImportKey; - else if (operation == "exportKey"_s) - op = Operations::ImportKey; // exportKey has no dedicated normalization; accept anything importable - else if (operation == "wrapKey"_s) - op = Operations::WrapKey; - else if (operation == "unwrapKey"_s) - op = Operations::UnwrapKey; - else if (operation == "getPublicKey"_s) - op = Operations::ImportKey; // getPublicKey applies to asymmetric keys; re-use importKey normalization - else if (operation == "encapsulateBits"_s || operation == "encapsulateKey"_s - || operation == "decapsulateBits"_s || operation == "decapsulateKey"_s) { - // KEM operations are not yet implemented. - return false; - } else { - // Unknown operation name. - return false; + return normalizesWithoutException(state, algorithmIdentifier, Operations::Encrypt); + if (operation == "decrypt"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::Decrypt); + if (operation == "sign"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::Sign); + if (operation == "verify"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::Verify); + if (operation == "digest"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::Digest); + if (operation == "generateKey"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::GenerateKey); + if (operation == "deriveBits"_s || operation == "deriveKey"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::DeriveBits); + if (operation == "importKey"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::ImportKey); + + // exportKey has no dedicated normalization — the real exportKey() only + // checks the CryptoKey's algorithm against isSupportedExportKey(). To + // make supports() answer symmetric questions ("can this algorithm be + // exported?"), we need to first recognise the algorithm at all and then + // run it through the same isSupportedExportKey() gate. Algorithms like + // HKDF/PBKDF2 that normalize as importable but are explicitly excluded + // from isSupportedExportKey() must report false. + if (operation == "exportKey"_s) { + // Resolve the algorithm name to an identifier without running any + // operation-specific validation. + if (std::holds_alternative(algorithmIdentifier)) { + auto identifier = CryptoAlgorithmRegistry::singleton().identifier(std::get(algorithmIdentifier)); + if (!identifier) + return false; + return isSupportedExportKey(state, *identifier); + } + // Dictionary form: pull out the "name" field. + auto& value = std::get>(algorithmIdentifier); + JSValue nameValue = value.get()->get(&state, vm.propertyNames->name); + if (scope.exception()) { + scope.clearException(); + return false; + } + if (!nameValue.isString()) + return false; + auto name = nameValue.toWTFString(&state); + if (scope.exception()) { + scope.clearException(); + return false; + } + auto identifier = CryptoAlgorithmRegistry::singleton().identifier(name); + if (!identifier) + return false; + return isSupportedExportKey(state, *identifier); } - auto params = normalizeCryptoAlgorithmParameters(state, WTF::move(algorithmIdentifier), op); - if (scope.exception()) { - scope.clearException(); - return false; + // wrapKey/unwrapKey dispatch in the real implementations: + // wrapKey: try WrapKey normalization; on failure, fall back to Encrypt. + // unwrapKey: try UnwrapKey normalization; on failure, fall back to Decrypt. + // supports() must mirror that two-step fallback or it produces false + // negatives for AES-GCM/CBC/CTR/CFB and RSA-OAEP as wrapping algorithms. + if (operation == "wrapKey"_s) { + if (normalizesWithoutException(state, algorithmIdentifier, Operations::WrapKey)) + return true; + return normalizesWithoutException(state, algorithmIdentifier, Operations::Encrypt); } - if (params.hasException()) + if (operation == "unwrapKey"_s) { + if (normalizesWithoutException(state, algorithmIdentifier, Operations::UnwrapKey)) + return true; + return normalizesWithoutException(state, algorithmIdentifier, Operations::Decrypt); + } + + // Operations introduced by the WICG "Modern Algorithms" spec that this + // slice does not implement yet. They flip to true in follow-up PRs. + if (operation == "getPublicKey"_s + || operation == "encapsulateBits"_s + || operation == "encapsulateKey"_s + || operation == "decapsulateBits"_s + || operation == "decapsulateKey"_s) { return false; - return true; + } + + // Unknown operation. + return false; } void SubtleCrypto::generateKey(JSC::JSGlobalObject& state, AlgorithmIdentifier&& algorithmIdentifier, bool extractable, Vector&& keyUsages, Ref&& promise) diff --git a/test/regression/issue/29218.test.ts b/test/regression/issue/29218.test.ts index ebfa6bc8e322..d7b6a4329f88 100644 --- a/test/regression/issue/29218.test.ts +++ b/test/regression/issue/29218.test.ts @@ -6,7 +6,15 @@ // detection method `crypto.subtle.supports(operation, algorithm)`. // // Spec: https://wicg.github.io/webcrypto-modern-algos/ +// +// Test coverage mirrors the intent of the Web Platform Tests that ship with +// the spec (WebCryptoAPI/digest/sha3.tentative.https.any.js and +// WebCryptoAPI/idlharness.*). Digest vectors are the NIST FIPS 202 +// Cryptographic Algorithm Validation Program vectors used by those WPTs, and +// the `supports()` cases exercise the progressive-enhancement semantics that +// the modern-algos spec defines in its "supports" algorithm. import { describe, expect, test } from "bun:test"; +import { createHash } from "node:crypto"; const te = new TextEncoder(); @@ -15,9 +23,18 @@ function hex(buf: ArrayBuffer | Uint8Array): string { return Array.from(bytes, b => b.toString(16).padStart(2, "0")).join(""); } +// Map the Web Crypto SHA-3 algorithm name to the name Node.js's OpenSSL +// bindings use, so we can cross-check SubtleCrypto output against a second +// independent implementation of the same primitive. +const nodeAlg = { + "SHA3-256": "sha3-256", + "SHA3-384": "sha3-384", + "SHA3-512": "sha3-512", +} as const; + describe("crypto.subtle SHA-3", () => { - // NIST FIPS 202 / Cryptographic Algorithm Validation Program test vectors - // for the empty message and for the ASCII string "abc". + // NIST FIPS 202 test vectors for the empty message and for the ASCII + // string "abc". These are the same vectors the WPT uses. // https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/secure-hashing const vectors = { "SHA3-256": { @@ -46,30 +63,40 @@ describe("crypto.subtle SHA-3", () => { expect(hex(out)).toBe(vec.abc); }); - test(`${alg} digests a long message longer than one Keccak rate`, async () => { - // Longer than the largest SHA-3 rate (72 bytes for SHA3-512) to exercise - // multi-block absorption inside the sponge. + test(`${alg} digests a multi-block message identically to node:crypto`, async () => { + // 1024 bytes is longer than every SHA-3 sponge rate (the largest rate + // is SHA3-256's 136 bytes; SHA3-512 uses the smallest rate at 72 + // bytes), so this input is guaranteed to span multiple blocks for all + // three variants and exercise multi-block absorption. Cross-check + // against node:crypto (which uses OpenSSL) instead of hard-coding a + // hex string, so both implementations must agree. const buf = new Uint8Array(1024); for (let i = 0; i < buf.length; i++) buf[i] = i & 0xff; + const out = await crypto.subtle.digest(alg, buf); + const reference = createHash(nodeAlg[alg as keyof typeof nodeAlg]).update(buf).digest(); + expect(hex(out)).toBe(reference.toString("hex")); const expectedLen = { "SHA3-256": 32, "SHA3-384": 48, "SHA3-512": 64 }[alg as keyof typeof vectors]; expect(out.byteLength).toBe(expectedLen); + }); - // Stability: hashing the same input twice must produce identical output. - const again = await crypto.subtle.digest(alg, buf); - expect(hex(again)).toBe(hex(out)); + test(`${alg} is deterministic across calls`, async () => { + const buf = te.encode("the quick brown fox jumps over the lazy dog"); + const a = await crypto.subtle.digest(alg, buf); + const b = await crypto.subtle.digest(alg, buf); + expect(hex(a)).toBe(hex(b)); }); } test("SHA-3 digest accepts a dictionary algorithm identifier", async () => { const out = await crypto.subtle.digest({ name: "SHA3-256" }, te.encode("abc")); - expect(hex(out)).toBe(vectors["SHA3-256"].abc); + expect(hex(out)).toBe("3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"); }); test("SHA-3 digest is case-insensitive on the algorithm name", async () => { const out = await crypto.subtle.digest("sha3-256", te.encode("abc")); - expect(hex(out)).toBe(vectors["SHA3-256"].abc); + expect(hex(out)).toBe("3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"); }); test("unknown SHA-3 variant is rejected with NotSupportedError", async () => { @@ -87,19 +114,20 @@ describe("crypto.subtle.supports", () => { expect(crypto.subtle.supports.length).toBe(2); }); - test("returns true for supported (operation, algorithm) pairs", () => { - // Classic Web Crypto algorithms that existed before this slice must - // continue to report true, so supports() is a safe feature-detect for - // everything Bun exposes. + test("returns true for classic algorithms that existed before this slice", () => { + // Every algorithm Bun already supported must continue to report true so + // that supports() is a safe feature-detect for the whole API surface. + expect(crypto.subtle.supports("digest", "SHA-1")).toBe(true); expect(crypto.subtle.supports("digest", "SHA-256")).toBe(true); expect(crypto.subtle.supports("digest", "SHA-384")).toBe(true); expect(crypto.subtle.supports("digest", "SHA-512")).toBe(true); expect(crypto.subtle.supports("generateKey", { name: "AES-GCM", length: 256 })).toBe(true); expect(crypto.subtle.supports("importKey", "AES-GCM")).toBe(true); - expect(crypto.subtle.supports("wrapKey", { name: "AES-KW" })).toBe(true); + expect(crypto.subtle.supports("importKey", "PBKDF2")).toBe(true); + expect(crypto.subtle.supports("deriveBits", { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(), info: new Uint8Array() })).toBe(true); }); - test("reports true for SHA3-256/384/512 digest", () => { + test("returns true for SHA-3 digest", () => { expect(crypto.subtle.supports("digest", "SHA3-256")).toBe(true); expect(crypto.subtle.supports("digest", "SHA3-384")).toBe(true); expect(crypto.subtle.supports("digest", "SHA3-512")).toBe(true); @@ -110,6 +138,71 @@ describe("crypto.subtle.supports", () => { expect(crypto.subtle.supports("digest", { name: "SHA-256" })).toBe(true); }); + describe("mirrors dispatch-time fallback", () => { + test("wrapKey with AES-KW uses the dedicated WrapKey path", () => { + expect(crypto.subtle.supports("wrapKey", "AES-KW")).toBe(true); + expect(crypto.subtle.supports("wrapKey", { name: "AES-KW" })).toBe(true); + }); + + test("wrapKey with an encryption algorithm is reported via the Encrypt fallback", () => { + // wrapKey() in the real implementation tries WrapKey normalization + // and, on NotSupportedError, falls back to Encrypt normalization so + // that AES-GCM/CBC/CTR/CFB and RSA-OAEP can be used as wrapping + // algorithms. supports() must mirror that fallback or it reports + // false for operations that actually succeed. + expect(crypto.subtle.supports("wrapKey", { name: "AES-GCM", iv: new Uint8Array(12) })).toBe(true); + expect(crypto.subtle.supports("wrapKey", { name: "AES-CBC", iv: new Uint8Array(16) })).toBe(true); + expect(crypto.subtle.supports("wrapKey", { name: "AES-CTR", counter: new Uint8Array(16), length: 64 })).toBe(true); + expect(crypto.subtle.supports("wrapKey", { name: "RSA-OAEP" })).toBe(true); + }); + + test("unwrapKey with a decryption algorithm is reported via the Decrypt fallback", () => { + // Symmetric to the wrapKey case, with Decrypt as the fallback. + expect(crypto.subtle.supports("unwrapKey", "AES-KW")).toBe(true); + expect(crypto.subtle.supports("unwrapKey", { name: "AES-GCM", iv: new Uint8Array(12) })).toBe(true); + expect(crypto.subtle.supports("unwrapKey", { name: "AES-CBC", iv: new Uint8Array(16) })).toBe(true); + expect(crypto.subtle.supports("unwrapKey", { name: "RSA-OAEP" })).toBe(true); + }); + + test("wrapKey/unwrapKey reject algorithms that are neither WrapKey nor en/decryptable", () => { + expect(crypto.subtle.supports("wrapKey", "HKDF")).toBe(false); + expect(crypto.subtle.supports("wrapKey", "PBKDF2")).toBe(false); + expect(crypto.subtle.supports("wrapKey", "SHA-256")).toBe(false); + expect(crypto.subtle.supports("unwrapKey", "HKDF")).toBe(false); + expect(crypto.subtle.supports("unwrapKey", "SHA-256")).toBe(false); + }); + }); + + describe("exportKey", () => { + test("reports true for exportable algorithms", () => { + // Matches isSupportedExportKey() in SubtleCrypto.cpp. Note that + // Bun registers AES-CFB under the "AES-CFB-8" name, matching the + // original WebKit spelling. + for (const alg of ["AES-GCM", "AES-CBC", "AES-CTR", "AES-CFB-8", "AES-KW", "HMAC", "ECDSA", "ECDH", "Ed25519", "X25519"]) { + expect(crypto.subtle.supports("exportKey", alg)).toBe(true); + } + }); + + test("reports false for key-derivation algorithms that are not exportable", () => { + // HKDF and PBKDF2 normalize as importable but isSupportedExportKey() + // excludes them, and the real exportKey() rejects with + // NotSupportedError. supports() must match that behaviour so + // progressive-enhancement callers do not hit false positives. + expect(crypto.subtle.supports("exportKey", "HKDF")).toBe(false); + expect(crypto.subtle.supports("exportKey", "PBKDF2")).toBe(false); + }); + + test("reports false for hash-only algorithms", () => { + expect(crypto.subtle.supports("exportKey", "SHA-256")).toBe(false); + expect(crypto.subtle.supports("exportKey", "SHA3-256")).toBe(false); + }); + + test("reports false for unknown algorithms", () => { + expect(crypto.subtle.supports("exportKey", "bogus")).toBe(false); + expect(crypto.subtle.supports("exportKey", { name: "bogus" })).toBe(false); + }); + }); + test("returns false for unknown algorithms", () => { expect(crypto.subtle.supports("digest", "MD5")).toBe(false); expect(crypto.subtle.supports("digest", "not-a-real-algorithm")).toBe(false); @@ -120,22 +213,32 @@ describe("crypto.subtle.supports", () => { expect(crypto.subtle.supports("not-a-real-op", "SHA-256")).toBe(false); }); - test("returns false for KEM operations that are not yet implemented", () => { - // encapsulateKey/Bits and decapsulateKey/Bits come with ML-KEM in a - // later slice of the WICG spec. They must report false for now so that - // callers can progressively adopt them. + test("returns false for modern-algos operations that are not yet implemented", () => { + // encapsulateKey/Bits, decapsulateKey/Bits, and getPublicKey are new + // surface introduced by the WICG spec. They must report false for now + // so callers can progressively adopt them; they will flip to true in + // follow-up PRs as each algorithm lands. expect(crypto.subtle.supports("encapsulateKey", "ML-KEM-768")).toBe(false); expect(crypto.subtle.supports("encapsulateBits", "ML-KEM-768")).toBe(false); expect(crypto.subtle.supports("decapsulateKey", "ML-KEM-768")).toBe(false); expect(crypto.subtle.supports("decapsulateBits", "ML-KEM-768")).toBe(false); + expect(crypto.subtle.supports("getPublicKey", "RSA-PSS")).toBe(false); + expect(crypto.subtle.supports("getPublicKey", "Ed25519")).toBe(false); + // getPublicKey is conceptually asymmetric-only; even if it were + // implemented, symmetric algorithms must never report true. + expect(crypto.subtle.supports("getPublicKey", "AES-GCM")).toBe(false); + expect(crypto.subtle.supports("getPublicKey", "HMAC")).toBe(false); }); test("returns false (not a throw) for malformed algorithm input", () => { - // supports() must never throw, even for obviously bad input — that is - // the point of a synchronous feature-detect method. + // supports() must never throw for any well-formed call, even with + // obviously bad input — that is the point of a synchronous + // feature-detect method. expect(crypto.subtle.supports("digest", null as any)).toBe(false); expect(crypto.subtle.supports("digest", 42 as any)).toBe(false); expect(crypto.subtle.supports("digest", [] as any)).toBe(false); + expect(crypto.subtle.supports("exportKey", null as any)).toBe(false); + expect(crypto.subtle.supports("exportKey", {} as any)).toBe(false); }); test("throws when called with fewer than two arguments", () => { @@ -147,4 +250,27 @@ describe("crypto.subtle.supports", () => { // @ts-expect-error expect(() => crypto.subtle.supports("digest")).toThrow(); }); + + test("supports() answers match what the underlying method would do", async () => { + // End-to-end cross-check: for each (op, alg) pair that supports() + // reports true, the real method must not throw NotSupportedError for + // well-formed input. This is the property the modern-algos spec relies + // on for progressive enhancement. + const buf = te.encode("hello"); + for (const alg of ["SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", "SHA3-512"]) { + expect(crypto.subtle.supports("digest", alg)).toBe(true); + const out = await crypto.subtle.digest(alg, buf); + expect(out).toBeInstanceOf(ArrayBuffer); + } + + // And the negative: every op/alg pair that reports false must actually + // reject at runtime. + expect(crypto.subtle.supports("digest", "MD5")).toBe(false); + await expect(crypto.subtle.digest("MD5", buf)).rejects.toMatchObject({ name: "NotSupportedError" }); + + expect(crypto.subtle.supports("exportKey", "HKDF")).toBe(false); + // importKey for HKDF to get an actual CryptoKey, then exportKey must reject. + const hkdfKey = await crypto.subtle.importKey("raw", buf, "HKDF", false, ["deriveBits"]); + await expect(crypto.subtle.exportKey("raw", hkdfKey)).rejects.toMatchObject({ name: "NotSupportedError" }); + }); }); From 97e72174cda391d3ada2f61b51ad122478654037 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:41:27 +0000 Subject: [PATCH 04/14] [autofix.ci] apply automated fixes --- test/regression/issue/29218.test.ts | 30 +++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/test/regression/issue/29218.test.ts b/test/regression/issue/29218.test.ts index d7b6a4329f88..40eebcf77b4b 100644 --- a/test/regression/issue/29218.test.ts +++ b/test/regression/issue/29218.test.ts @@ -74,7 +74,9 @@ describe("crypto.subtle SHA-3", () => { for (let i = 0; i < buf.length; i++) buf[i] = i & 0xff; const out = await crypto.subtle.digest(alg, buf); - const reference = createHash(nodeAlg[alg as keyof typeof nodeAlg]).update(buf).digest(); + const reference = createHash(nodeAlg[alg as keyof typeof nodeAlg]) + .update(buf) + .digest(); expect(hex(out)).toBe(reference.toString("hex")); const expectedLen = { "SHA3-256": 32, "SHA3-384": 48, "SHA3-512": 64 }[alg as keyof typeof vectors]; @@ -124,7 +126,14 @@ describe("crypto.subtle.supports", () => { expect(crypto.subtle.supports("generateKey", { name: "AES-GCM", length: 256 })).toBe(true); expect(crypto.subtle.supports("importKey", "AES-GCM")).toBe(true); expect(crypto.subtle.supports("importKey", "PBKDF2")).toBe(true); - expect(crypto.subtle.supports("deriveBits", { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(), info: new Uint8Array() })).toBe(true); + expect( + crypto.subtle.supports("deriveBits", { + name: "HKDF", + hash: "SHA-256", + salt: new Uint8Array(), + info: new Uint8Array(), + }), + ).toBe(true); }); test("returns true for SHA-3 digest", () => { @@ -152,7 +161,9 @@ describe("crypto.subtle.supports", () => { // false for operations that actually succeed. expect(crypto.subtle.supports("wrapKey", { name: "AES-GCM", iv: new Uint8Array(12) })).toBe(true); expect(crypto.subtle.supports("wrapKey", { name: "AES-CBC", iv: new Uint8Array(16) })).toBe(true); - expect(crypto.subtle.supports("wrapKey", { name: "AES-CTR", counter: new Uint8Array(16), length: 64 })).toBe(true); + expect(crypto.subtle.supports("wrapKey", { name: "AES-CTR", counter: new Uint8Array(16), length: 64 })).toBe( + true, + ); expect(crypto.subtle.supports("wrapKey", { name: "RSA-OAEP" })).toBe(true); }); @@ -178,7 +189,18 @@ describe("crypto.subtle.supports", () => { // Matches isSupportedExportKey() in SubtleCrypto.cpp. Note that // Bun registers AES-CFB under the "AES-CFB-8" name, matching the // original WebKit spelling. - for (const alg of ["AES-GCM", "AES-CBC", "AES-CTR", "AES-CFB-8", "AES-KW", "HMAC", "ECDSA", "ECDH", "Ed25519", "X25519"]) { + for (const alg of [ + "AES-GCM", + "AES-CBC", + "AES-CTR", + "AES-CFB-8", + "AES-KW", + "HMAC", + "ECDSA", + "ECDH", + "Ed25519", + "X25519", + ]) { expect(crypto.subtle.supports("exportKey", alg)).toBe(true); } }); From 03710392d231417ec8a6cde8fc48d79649309ea9 Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 12 Apr 2026 13:04:29 +0000 Subject: [PATCH 05/14] webcrypto: SubtleCrypto.supports() is a static, SHA-3 sub-hash rejected Two corrections on top of c1f35b0baa: 1. Move supports() to the interface object. The WICG "Modern Algorithms" spec defines supports() as a static method on the SubtleCrypto constructor, not an instance method on the prototype. It is now exposed as `SubtleCrypto.supports(op, alg)` rather than `crypto.subtle.supports(...)`. Implementation moves the hashtable entry from JSSubtleCryptoPrototypeTableValues to a JSFunction installed in JSSubtleCryptoDOMConstructor:: initializeProperties(), rewrites the host function to not require a `this` cast, and marks SubtleCrypto::supports() as a C++ static. 2. Reject SHA-3 as a hash sub-algorithm for HMAC / RSA / ECDSA. Accepting SHA-3 as a top-level digest operation in normalize made toHashIdentifier() start resolving "SHA3-256" etc. successfully, which let HMAC / RSA-PSS / RSA-OAEP / ECDSA importKey and generateKey build CryptoKey instances with m_hash = SHA3_*. Those keys then: - crash sign()/verify() with a cryptic OperationError because OpenSSLUtilities::digestAlgorithm() has no SHA-3 branch; - hit ASSERT_NOT_REACHED() in CryptoKeyHMAC::getKeyLengthFromHash(); - crash the process via RELEASE_ASSERT_NOT_REACHED() in SerializedScriptValue when structured-cloned or postMessage'd. toHashIdentifier() now returns NotSupportedError for SHA-3 identifiers. SHA-3 as a top-level digest still works; only the hash-subalgorithm slot is gated. The gate will lift in a follow-up PR that wires SHA-3 into digestAlgorithm(), getKeyLengthFromHash(), and the structured-clone tag table. Regression test grows to 34 tests / 113 expects: new test asserts that HMAC / RSA-PSS / HMAC-generateKey reject with NotSupportedError for each SHA-3 variant, and all existing supports() cases flip from `crypto.subtle.supports` to `SubtleCrypto.supports`. Refs #29218 --- .../bindings/webcrypto/JSSubtleCrypto.cpp | 27 ++-- .../bindings/webcrypto/SubtleCrypto.cpp | 16 +- src/bun.js/bindings/webcrypto/SubtleCrypto.h | 5 +- .../bindings/webcrypto/SubtleCrypto.idl | 2 +- test/regression/issue/29218.test.ts | 149 +++++++++++------- 5 files changed, 123 insertions(+), 76 deletions(-) diff --git a/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp b/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp index 87b4546f2f6b..ec1564901f5d 100644 --- a/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp +++ b/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp @@ -126,7 +126,7 @@ static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_importKey); static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_exportKey); static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_wrapKey); static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_unwrapKey); -static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_supports); +static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoConstructorFunction_supports); // Attributes @@ -181,6 +181,12 @@ template<> void JSSubtleCryptoDOMConstructor::initializeProperties(VM& vm, JSDOM m_originalName.set(vm, this, nameString); putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); putDirect(vm, vm.propertyNames->prototype, JSSubtleCrypto::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); + + // WICG "Modern Algorithms in the Web Cryptography API" defines supports() + // as a static on the SubtleCrypto interface object, i.e. + // `SubtleCrypto.supports(...)` rather than `subtle.supports(...)`. + JSFunction* supportsFunction = JSFunction::create(vm, &globalObject, 2, "supports"_s, jsSubtleCryptoConstructorFunction_supports, ImplementationVisibility::Public); + putDirect(vm, Identifier::fromString(vm, "supports"_s), supportsFunction, 0); } /* Hash table for prototype */ @@ -199,7 +205,6 @@ static const HashTableValue JSSubtleCryptoPrototypeTableValues[] = { { "exportKey"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSubtleCryptoPrototypeFunction_exportKey, 2 } }, { "wrapKey"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSubtleCryptoPrototypeFunction_wrapKey, 4 } }, { "unwrapKey"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSubtleCryptoPrototypeFunction_unwrapKey, 7 } }, - { "supports"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSubtleCryptoPrototypeFunction_supports, 2 } }, }; const ClassInfo JSSubtleCryptoPrototype::s_info = { "SubtleCrypto"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSSubtleCryptoPrototype) }; @@ -600,13 +605,16 @@ JSC_DEFINE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_unwrapKey, (JSGlobalObj return IDLOperationReturningPromise::call(*lexicalGlobalObject, *callFrame, "unwrapKey"); } -static inline JSC::EncodedJSValue jsSubtleCryptoPrototypeFunction_supportsBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +// WICG "Modern Algorithms in the Web Cryptography API" defines supports() as +// a static on the SubtleCrypto interface object. It is installed in +// JSSubtleCryptoDOMConstructor::initializeProperties() and does not live on +// the prototype, so we do not go through IDLOperation (which +// would require a SubtleCrypto `this`). The implementation does not touch +// any instance state — it is a pure feature-detect over (operation, algorithm). +JSC_DEFINE_HOST_FUNCTION(jsSubtleCryptoConstructorFunction_supports, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = JSC::getVM(lexicalGlobalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); - auto& impl = castedThis->wrapped(); if (callFrame->argumentCount() < 2) [[unlikely]] return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); @@ -615,12 +623,7 @@ static inline JSC::EncodedJSValue jsSubtleCryptoPrototypeFunction_supportsBody(J EnsureStillAliveScope argument1 = callFrame->uncheckedArgument(1); auto algorithm = convert>(*lexicalGlobalObject, argument1.value()); RETURN_IF_EXCEPTION(throwScope, {}); - RELEASE_AND_RETURN(throwScope, JSValue::encode(jsBoolean(impl.supports(*jsCast(lexicalGlobalObject), operation, WTF::move(algorithm))))); -} - -JSC_DEFINE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_supports, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - return IDLOperation::call(*lexicalGlobalObject, *callFrame, "supports"); + RELEASE_AND_RETURN(throwScope, JSValue::encode(jsBoolean(SubtleCrypto::supports(*jsCast(lexicalGlobalObject), operation, WTF::move(algorithm))))); } JSC::GCClient::IsoSubspace* JSSubtleCrypto::subspaceForImpl(JSC::VM& vm) diff --git a/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp b/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp index cc6e17e921d0..7b2901974201 100644 --- a/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp +++ b/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp @@ -87,7 +87,21 @@ static ExceptionOr toHashIdentifier(JSGlobalObject& s auto digestParams = normalizeCryptoAlgorithmParameters(state, algorithmIdentifier, Operations::Digest); if (digestParams.hasException()) return digestParams.releaseException(); - return digestParams.returnValue()->identifier; + auto identifier = digestParams.returnValue()->identifier; + // SHA-3 is only implemented as a top-level digest operation in this + // slice of the WICG "Modern Algorithms" spec. It is NOT yet wired into + // OpenSSLUtilities::digestAlgorithm(), CryptoKeyHMAC::getKeyLengthFromHash(), + // or SerializedScriptValue, so accepting SHA-3 as a hash sub-algorithm for + // HMAC / RSA / ECDSA would create broken CryptoKey instances that crash + // at sign() or postMessage() time. Reject the name up front so callers + // get a clean NotSupportedError at importKey()/generateKey() time and can + // use supports() to progressively adopt it when the rest of the plumbing + // lands. + if (identifier == CryptoAlgorithmIdentifier::SHA3_256 + || identifier == CryptoAlgorithmIdentifier::SHA3_384 + || identifier == CryptoAlgorithmIdentifier::SHA3_512) + return Exception { NotSupportedError }; + return identifier; } static bool isRSAESPKCSWebCryptoDeprecated(JSGlobalObject& state) diff --git a/src/bun.js/bindings/webcrypto/SubtleCrypto.h b/src/bun.js/bindings/webcrypto/SubtleCrypto.h index bcb1fe7eb336..24a7521d96af 100644 --- a/src/bun.js/bindings/webcrypto/SubtleCrypto.h +++ b/src/bun.js/bindings/webcrypto/SubtleCrypto.h @@ -78,7 +78,10 @@ class SubtleCrypto : public ContextDestructionObserver, public RefCounted&&); void wrapKey(JSC::JSGlobalObject&, KeyFormat, CryptoKey&, CryptoKey& wrappingKey, AlgorithmIdentifier&& wrapAlgorithm, Ref&&); void unwrapKey(JSC::JSGlobalObject&, KeyFormat, BufferSource&& wrappedKey, CryptoKey& unwrappingKey, AlgorithmIdentifier&& unwrapAlgorithm, AlgorithmIdentifier&& unwrappedKeyAlgorithm, bool extractable, Vector&&, Ref&&); - bool supports(JSC::JSGlobalObject&, const String& operation, AlgorithmIdentifier&&); + // WICG "Modern Algorithms in the Web Cryptography API" defines this as a + // static on the interface object, not an instance method. It is exposed + // in JS as `SubtleCrypto.supports(operation, algorithm)`. + static bool supports(JSC::JSGlobalObject&, const String& operation, AlgorithmIdentifier&&); private: explicit SubtleCrypto(ScriptExecutionContext*); diff --git a/src/bun.js/bindings/webcrypto/SubtleCrypto.idl b/src/bun.js/bindings/webcrypto/SubtleCrypto.idl index 141bd9e13588..90c170ace6c9 100644 --- a/src/bun.js/bindings/webcrypto/SubtleCrypto.idl +++ b/src/bun.js/bindings/webcrypto/SubtleCrypto.idl @@ -45,5 +45,5 @@ typedef (object or DOMString) AlgorithmIdentifier; Promise exportKey(KeyFormat format, CryptoKey key); [CallWith=CurrentGlobalObject] Promise wrapKey(KeyFormat format, CryptoKey key, CryptoKey wrappingKey, AlgorithmIdentifier wrapAlgorithm); [CallWith=CurrentGlobalObject] Promise unwrapKey(KeyFormat format, BufferSource wrappedKey, CryptoKey unwrappingKey, AlgorithmIdentifier unwrapAlgorithm, AlgorithmIdentifier unwrappedKeyAlgorithm, boolean extractable, sequence keyUsages); - [CallWith=CurrentGlobalObject] boolean supports(DOMString operation, AlgorithmIdentifier algorithm); + [CallWith=CurrentGlobalObject] static boolean supports(DOMString operation, AlgorithmIdentifier algorithm); }; diff --git a/test/regression/issue/29218.test.ts b/test/regression/issue/29218.test.ts index 40eebcf77b4b..9da27455fe4a 100644 --- a/test/regression/issue/29218.test.ts +++ b/test/regression/issue/29218.test.ts @@ -3,7 +3,7 @@ // First slice of the WICG "Modern Algorithms in the Web Cryptography API" // specification: SHA-3 fixed-output hashes (SHA3-256 / SHA3-384 / SHA3-512) // exposed through `crypto.subtle.digest`, plus the new synchronous feature -// detection method `crypto.subtle.supports(operation, algorithm)`. +// detection method `SubtleCrypto.supports(operation, algorithm)`. // // Spec: https://wicg.github.io/webcrypto-modern-algos/ // @@ -108,26 +108,53 @@ describe("crypto.subtle SHA-3", () => { name: "NotSupportedError", }); }); + + test("SHA-3 is rejected as a hash sub-algorithm for HMAC/RSA/ECDSA", async () => { + // SHA-3 is implemented only as a top-level `digest` operation in this + // slice of the WICG spec. It is not yet wired into OpenSSL's digest + // dispatcher, CryptoKeyHMAC::getKeyLengthFromHash(), or structured + // clone, so accepting it as a hash sub-algorithm for HMAC/RSA/ECDSA + // would create broken CryptoKey instances that crash at sign() or + // postMessage() time. Reject up front until those paths are wired. + for (const alg of ["SHA3-256", "SHA3-384", "SHA3-512"]) { + await expect( + crypto.subtle.importKey("raw", new Uint8Array(32), { name: "HMAC", hash: alg }, true, ["sign"]), + ).rejects.toMatchObject({ name: "NotSupportedError" }); + await expect( + crypto.subtle.generateKey({ name: "HMAC", hash: alg }, true, ["sign"]), + ).rejects.toMatchObject({ name: "NotSupportedError" }); + await expect( + crypto.subtle.generateKey( + { name: "RSA-PSS", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: alg }, + true, + ["sign"], + ), + ).rejects.toMatchObject({ name: "NotSupportedError" }); + await expect( + crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign"]), + ).resolves.toBeDefined(); // sanity: real ECDSA still works + } + }); }); -describe("crypto.subtle.supports", () => { +describe("SubtleCrypto.supports", () => { test("is a function of length 2", () => { - expect(typeof crypto.subtle.supports).toBe("function"); - expect(crypto.subtle.supports.length).toBe(2); + expect(typeof SubtleCrypto.supports).toBe("function"); + expect(SubtleCrypto.supports.length).toBe(2); }); test("returns true for classic algorithms that existed before this slice", () => { // Every algorithm Bun already supported must continue to report true so // that supports() is a safe feature-detect for the whole API surface. - expect(crypto.subtle.supports("digest", "SHA-1")).toBe(true); - expect(crypto.subtle.supports("digest", "SHA-256")).toBe(true); - expect(crypto.subtle.supports("digest", "SHA-384")).toBe(true); - expect(crypto.subtle.supports("digest", "SHA-512")).toBe(true); - expect(crypto.subtle.supports("generateKey", { name: "AES-GCM", length: 256 })).toBe(true); - expect(crypto.subtle.supports("importKey", "AES-GCM")).toBe(true); - expect(crypto.subtle.supports("importKey", "PBKDF2")).toBe(true); + expect(SubtleCrypto.supports("digest", "SHA-1")).toBe(true); + expect(SubtleCrypto.supports("digest", "SHA-256")).toBe(true); + expect(SubtleCrypto.supports("digest", "SHA-384")).toBe(true); + expect(SubtleCrypto.supports("digest", "SHA-512")).toBe(true); + expect(SubtleCrypto.supports("generateKey", { name: "AES-GCM", length: 256 })).toBe(true); + expect(SubtleCrypto.supports("importKey", "AES-GCM")).toBe(true); + expect(SubtleCrypto.supports("importKey", "PBKDF2")).toBe(true); expect( - crypto.subtle.supports("deriveBits", { + SubtleCrypto.supports("deriveBits", { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(), @@ -137,20 +164,20 @@ describe("crypto.subtle.supports", () => { }); test("returns true for SHA-3 digest", () => { - expect(crypto.subtle.supports("digest", "SHA3-256")).toBe(true); - expect(crypto.subtle.supports("digest", "SHA3-384")).toBe(true); - expect(crypto.subtle.supports("digest", "SHA3-512")).toBe(true); + expect(SubtleCrypto.supports("digest", "SHA3-256")).toBe(true); + expect(SubtleCrypto.supports("digest", "SHA3-384")).toBe(true); + expect(SubtleCrypto.supports("digest", "SHA3-512")).toBe(true); }); test("accepts dictionary-form algorithm identifiers", () => { - expect(crypto.subtle.supports("digest", { name: "SHA3-256" })).toBe(true); - expect(crypto.subtle.supports("digest", { name: "SHA-256" })).toBe(true); + expect(SubtleCrypto.supports("digest", { name: "SHA3-256" })).toBe(true); + expect(SubtleCrypto.supports("digest", { name: "SHA-256" })).toBe(true); }); describe("mirrors dispatch-time fallback", () => { test("wrapKey with AES-KW uses the dedicated WrapKey path", () => { - expect(crypto.subtle.supports("wrapKey", "AES-KW")).toBe(true); - expect(crypto.subtle.supports("wrapKey", { name: "AES-KW" })).toBe(true); + expect(SubtleCrypto.supports("wrapKey", "AES-KW")).toBe(true); + expect(SubtleCrypto.supports("wrapKey", { name: "AES-KW" })).toBe(true); }); test("wrapKey with an encryption algorithm is reported via the Encrypt fallback", () => { @@ -159,28 +186,28 @@ describe("crypto.subtle.supports", () => { // that AES-GCM/CBC/CTR/CFB and RSA-OAEP can be used as wrapping // algorithms. supports() must mirror that fallback or it reports // false for operations that actually succeed. - expect(crypto.subtle.supports("wrapKey", { name: "AES-GCM", iv: new Uint8Array(12) })).toBe(true); - expect(crypto.subtle.supports("wrapKey", { name: "AES-CBC", iv: new Uint8Array(16) })).toBe(true); - expect(crypto.subtle.supports("wrapKey", { name: "AES-CTR", counter: new Uint8Array(16), length: 64 })).toBe( + expect(SubtleCrypto.supports("wrapKey", { name: "AES-GCM", iv: new Uint8Array(12) })).toBe(true); + expect(SubtleCrypto.supports("wrapKey", { name: "AES-CBC", iv: new Uint8Array(16) })).toBe(true); + expect(SubtleCrypto.supports("wrapKey", { name: "AES-CTR", counter: new Uint8Array(16), length: 64 })).toBe( true, ); - expect(crypto.subtle.supports("wrapKey", { name: "RSA-OAEP" })).toBe(true); + expect(SubtleCrypto.supports("wrapKey", { name: "RSA-OAEP" })).toBe(true); }); test("unwrapKey with a decryption algorithm is reported via the Decrypt fallback", () => { // Symmetric to the wrapKey case, with Decrypt as the fallback. - expect(crypto.subtle.supports("unwrapKey", "AES-KW")).toBe(true); - expect(crypto.subtle.supports("unwrapKey", { name: "AES-GCM", iv: new Uint8Array(12) })).toBe(true); - expect(crypto.subtle.supports("unwrapKey", { name: "AES-CBC", iv: new Uint8Array(16) })).toBe(true); - expect(crypto.subtle.supports("unwrapKey", { name: "RSA-OAEP" })).toBe(true); + expect(SubtleCrypto.supports("unwrapKey", "AES-KW")).toBe(true); + expect(SubtleCrypto.supports("unwrapKey", { name: "AES-GCM", iv: new Uint8Array(12) })).toBe(true); + expect(SubtleCrypto.supports("unwrapKey", { name: "AES-CBC", iv: new Uint8Array(16) })).toBe(true); + expect(SubtleCrypto.supports("unwrapKey", { name: "RSA-OAEP" })).toBe(true); }); test("wrapKey/unwrapKey reject algorithms that are neither WrapKey nor en/decryptable", () => { - expect(crypto.subtle.supports("wrapKey", "HKDF")).toBe(false); - expect(crypto.subtle.supports("wrapKey", "PBKDF2")).toBe(false); - expect(crypto.subtle.supports("wrapKey", "SHA-256")).toBe(false); - expect(crypto.subtle.supports("unwrapKey", "HKDF")).toBe(false); - expect(crypto.subtle.supports("unwrapKey", "SHA-256")).toBe(false); + expect(SubtleCrypto.supports("wrapKey", "HKDF")).toBe(false); + expect(SubtleCrypto.supports("wrapKey", "PBKDF2")).toBe(false); + expect(SubtleCrypto.supports("wrapKey", "SHA-256")).toBe(false); + expect(SubtleCrypto.supports("unwrapKey", "HKDF")).toBe(false); + expect(SubtleCrypto.supports("unwrapKey", "SHA-256")).toBe(false); }); }); @@ -201,7 +228,7 @@ describe("crypto.subtle.supports", () => { "Ed25519", "X25519", ]) { - expect(crypto.subtle.supports("exportKey", alg)).toBe(true); + expect(SubtleCrypto.supports("exportKey", alg)).toBe(true); } }); @@ -210,29 +237,29 @@ describe("crypto.subtle.supports", () => { // excludes them, and the real exportKey() rejects with // NotSupportedError. supports() must match that behaviour so // progressive-enhancement callers do not hit false positives. - expect(crypto.subtle.supports("exportKey", "HKDF")).toBe(false); - expect(crypto.subtle.supports("exportKey", "PBKDF2")).toBe(false); + expect(SubtleCrypto.supports("exportKey", "HKDF")).toBe(false); + expect(SubtleCrypto.supports("exportKey", "PBKDF2")).toBe(false); }); test("reports false for hash-only algorithms", () => { - expect(crypto.subtle.supports("exportKey", "SHA-256")).toBe(false); - expect(crypto.subtle.supports("exportKey", "SHA3-256")).toBe(false); + expect(SubtleCrypto.supports("exportKey", "SHA-256")).toBe(false); + expect(SubtleCrypto.supports("exportKey", "SHA3-256")).toBe(false); }); test("reports false for unknown algorithms", () => { - expect(crypto.subtle.supports("exportKey", "bogus")).toBe(false); - expect(crypto.subtle.supports("exportKey", { name: "bogus" })).toBe(false); + expect(SubtleCrypto.supports("exportKey", "bogus")).toBe(false); + expect(SubtleCrypto.supports("exportKey", { name: "bogus" })).toBe(false); }); }); test("returns false for unknown algorithms", () => { - expect(crypto.subtle.supports("digest", "MD5")).toBe(false); - expect(crypto.subtle.supports("digest", "not-a-real-algorithm")).toBe(false); - expect(crypto.subtle.supports("digest", { name: "nope" })).toBe(false); + expect(SubtleCrypto.supports("digest", "MD5")).toBe(false); + expect(SubtleCrypto.supports("digest", "not-a-real-algorithm")).toBe(false); + expect(SubtleCrypto.supports("digest", { name: "nope" })).toBe(false); }); test("returns false for unknown operations", () => { - expect(crypto.subtle.supports("not-a-real-op", "SHA-256")).toBe(false); + expect(SubtleCrypto.supports("not-a-real-op", "SHA-256")).toBe(false); }); test("returns false for modern-algos operations that are not yet implemented", () => { @@ -240,27 +267,27 @@ describe("crypto.subtle.supports", () => { // surface introduced by the WICG spec. They must report false for now // so callers can progressively adopt them; they will flip to true in // follow-up PRs as each algorithm lands. - expect(crypto.subtle.supports("encapsulateKey", "ML-KEM-768")).toBe(false); - expect(crypto.subtle.supports("encapsulateBits", "ML-KEM-768")).toBe(false); - expect(crypto.subtle.supports("decapsulateKey", "ML-KEM-768")).toBe(false); - expect(crypto.subtle.supports("decapsulateBits", "ML-KEM-768")).toBe(false); - expect(crypto.subtle.supports("getPublicKey", "RSA-PSS")).toBe(false); - expect(crypto.subtle.supports("getPublicKey", "Ed25519")).toBe(false); + expect(SubtleCrypto.supports("encapsulateKey", "ML-KEM-768")).toBe(false); + expect(SubtleCrypto.supports("encapsulateBits", "ML-KEM-768")).toBe(false); + expect(SubtleCrypto.supports("decapsulateKey", "ML-KEM-768")).toBe(false); + expect(SubtleCrypto.supports("decapsulateBits", "ML-KEM-768")).toBe(false); + expect(SubtleCrypto.supports("getPublicKey", "RSA-PSS")).toBe(false); + expect(SubtleCrypto.supports("getPublicKey", "Ed25519")).toBe(false); // getPublicKey is conceptually asymmetric-only; even if it were // implemented, symmetric algorithms must never report true. - expect(crypto.subtle.supports("getPublicKey", "AES-GCM")).toBe(false); - expect(crypto.subtle.supports("getPublicKey", "HMAC")).toBe(false); + expect(SubtleCrypto.supports("getPublicKey", "AES-GCM")).toBe(false); + expect(SubtleCrypto.supports("getPublicKey", "HMAC")).toBe(false); }); test("returns false (not a throw) for malformed algorithm input", () => { // supports() must never throw for any well-formed call, even with // obviously bad input — that is the point of a synchronous // feature-detect method. - expect(crypto.subtle.supports("digest", null as any)).toBe(false); - expect(crypto.subtle.supports("digest", 42 as any)).toBe(false); - expect(crypto.subtle.supports("digest", [] as any)).toBe(false); - expect(crypto.subtle.supports("exportKey", null as any)).toBe(false); - expect(crypto.subtle.supports("exportKey", {} as any)).toBe(false); + expect(SubtleCrypto.supports("digest", null as any)).toBe(false); + expect(SubtleCrypto.supports("digest", 42 as any)).toBe(false); + expect(SubtleCrypto.supports("digest", [] as any)).toBe(false); + expect(SubtleCrypto.supports("exportKey", null as any)).toBe(false); + expect(SubtleCrypto.supports("exportKey", {} as any)).toBe(false); }); test("throws when called with fewer than two arguments", () => { @@ -268,9 +295,9 @@ describe("crypto.subtle.supports", () => { // rejects the call (missing required arguments), mirroring every other // SubtleCrypto method. // @ts-expect-error - expect(() => crypto.subtle.supports()).toThrow(); + expect(() => SubtleCrypto.supports()).toThrow(); // @ts-expect-error - expect(() => crypto.subtle.supports("digest")).toThrow(); + expect(() => SubtleCrypto.supports("digest")).toThrow(); }); test("supports() answers match what the underlying method would do", async () => { @@ -280,17 +307,17 @@ describe("crypto.subtle.supports", () => { // on for progressive enhancement. const buf = te.encode("hello"); for (const alg of ["SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", "SHA3-512"]) { - expect(crypto.subtle.supports("digest", alg)).toBe(true); + expect(SubtleCrypto.supports("digest", alg)).toBe(true); const out = await crypto.subtle.digest(alg, buf); expect(out).toBeInstanceOf(ArrayBuffer); } // And the negative: every op/alg pair that reports false must actually // reject at runtime. - expect(crypto.subtle.supports("digest", "MD5")).toBe(false); + expect(SubtleCrypto.supports("digest", "MD5")).toBe(false); await expect(crypto.subtle.digest("MD5", buf)).rejects.toMatchObject({ name: "NotSupportedError" }); - expect(crypto.subtle.supports("exportKey", "HKDF")).toBe(false); + expect(SubtleCrypto.supports("exportKey", "HKDF")).toBe(false); // importKey for HKDF to get an actual CryptoKey, then exportKey must reject. const hkdfKey = await crypto.subtle.importKey("raw", buf, "HKDF", false, ["deriveBits"]); await expect(crypto.subtle.exportKey("raw", hkdfKey)).rejects.toMatchObject({ name: "NotSupportedError" }); From d047e923d6432bb540754bc2dbae443937f42fad Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 13:06:27 +0000 Subject: [PATCH 06/14] [autofix.ci] apply automated fixes --- test/regression/issue/29218.test.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/regression/issue/29218.test.ts b/test/regression/issue/29218.test.ts index 9da27455fe4a..8636bc034168 100644 --- a/test/regression/issue/29218.test.ts +++ b/test/regression/issue/29218.test.ts @@ -120,9 +120,9 @@ describe("crypto.subtle SHA-3", () => { await expect( crypto.subtle.importKey("raw", new Uint8Array(32), { name: "HMAC", hash: alg }, true, ["sign"]), ).rejects.toMatchObject({ name: "NotSupportedError" }); - await expect( - crypto.subtle.generateKey({ name: "HMAC", hash: alg }, true, ["sign"]), - ).rejects.toMatchObject({ name: "NotSupportedError" }); + await expect(crypto.subtle.generateKey({ name: "HMAC", hash: alg }, true, ["sign"])).rejects.toMatchObject({ + name: "NotSupportedError", + }); await expect( crypto.subtle.generateKey( { name: "RSA-PSS", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: alg }, @@ -188,9 +188,7 @@ describe("SubtleCrypto.supports", () => { // false for operations that actually succeed. expect(SubtleCrypto.supports("wrapKey", { name: "AES-GCM", iv: new Uint8Array(12) })).toBe(true); expect(SubtleCrypto.supports("wrapKey", { name: "AES-CBC", iv: new Uint8Array(16) })).toBe(true); - expect(SubtleCrypto.supports("wrapKey", { name: "AES-CTR", counter: new Uint8Array(16), length: 64 })).toBe( - true, - ); + expect(SubtleCrypto.supports("wrapKey", { name: "AES-CTR", counter: new Uint8Array(16), length: 64 })).toBe(true); expect(SubtleCrypto.supports("wrapKey", { name: "RSA-OAEP" })).toBe(true); }); From 10b4cdde22b34f1496dcde1036ce1081369fce8e Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 12 Apr 2026 13:36:19 +0000 Subject: [PATCH 07/14] test: allow-list 29218 for normalizeCryptoAlgorithmParameters exception checks The regression test exercises normalizeCryptoAlgorithmParameters along two paths that trigger BUN_JSC_validateExceptionChecks=1 in ASAN CI: - digest("SHA3-224", ...) hits the default NotSupportedError arm in the Digest switch. - Every SubtleCrypto.supports() call routes through the normalizesWithoutException() helper, which clears exceptions. Without an entry in test/no-validate-exceptions.txt the pre-existing throw-scope bookkeeping issue in normalizeCryptoAlgorithmParameters trips WTFCrash -> ud2 -> SIGILL on ASAN runs. Match the pattern used for every other webcrypto test in the same section. Refs #29218 --- test/no-validate-exceptions.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index 5936bf7ec476..6188d36fcbac 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -146,6 +146,7 @@ test/js/web/crypto/web-crypto.test.ts test/regression/issue/01466.test.ts test/regression/issue/21311.test.ts test/regression/issue/24399.test.ts +test/regression/issue/29218.test.ts vendor/elysia/test/aot/response.test.ts vendor/elysia/test/cookie/response.test.ts vendor/elysia/test/cookie/signature.test.ts From e63ffab7b58d68bae0ad0accd0e5790d8823d22e Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 12 Apr 2026 14:23:35 +0000 Subject: [PATCH 08/14] test: actually exercise SHA-3 rejection on ECDSA sign(), fix comment nits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small follow-ups on review feedback: - The ECDSA arm of the SHA-3 sub-hash rejection test was a no-op: it called generateKey({name:"ECDSA", namedCurve:"P-256"}) three times in the loop without referencing the SHA-3 loop variable at all. For ECDSA the hash is supplied at sign()/verify() time, not generateKey() time, so the real assertion is that sign({name:"ECDSA", hash:alg}, ...) rejects with NotSupportedError. Generate one P-256 key pair outside the loop, then call sign() inside it. - Two comments in CryptoDigest.cpp SHA-3 sponge were misleading: * The buffer[144] comment referred to SHA3-224 as if it were registered. Reword to say it is sized for SHA3-224's rate as forward-compatibility for later XOF additions; the three variants this file ships only need up to SHA3-256's 136 bytes. * The squeeze comment claimed "rate >= 104 bytes", which is the SHA3-384 rate; the smallest SHA-3 rate is SHA3-512's 72 bytes. The single-squeeze logic is still correct (72 >= 64), but the bound was wrong — rewrite to state the real minimum. (Separately, a reviewer suggested adding DontEnum to the constructor- level supports property. Per WebIDL § 3.7.7 "define the operations", static operations get {Enumerable: true}, so the existing attribute-0 putDirect is already correct and is intentionally unchanged.) 34 tests / 114 expects pass. Refs #29218 --- src/bun.js/bindings/webcrypto/CryptoDigest.cpp | 13 +++++++++---- test/regression/issue/29218.test.ts | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp index b119ddbc211d..dd8e1cb5db8a 100644 --- a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp @@ -164,8 +164,12 @@ static inline void storeLE64(uint8_t* bytes, uint64_t v) // Streaming SHA-3 sponge for one of the fixed output sizes. struct Sha3Context { KeccakState state {}; - // Input bytes buffered before the next permutation. - uint8_t buffer[144] {}; // enough for SHA3-224's 144-byte rate (largest) + // Input bytes buffered before the next permutation. Sized for the largest + // FIPS 202 fixed-output rate (144 bytes for SHA3-224, which is not + // registered here); the three variants this file ships only need up to + // SHA3-256's 136-byte rate. The extra room lets future SHA3-224 / + // SHAKE128 support reuse this struct without a resize. + uint8_t buffer[144] {}; size_t bufferLength = 0; size_t rateBytes = 0; size_t digestLength = 0; @@ -214,8 +218,9 @@ static void sha3Final(Sha3Context& ctx, uint8_t* output) keccakF1600(ctx.state); // Squeeze. For the fixed-output SHA-3 variants the digest always fits - // inside a single rate-sized squeeze block (rate >= 104 bytes, digest - // <= 64 bytes), so no additional permutation is needed. + // inside a single rate-sized squeeze block: the smallest rate is + // SHA3-512's 72 bytes and its digest is 64 bytes, so no additional + // permutation is needed. size_t produced = 0; uint8_t lane[8]; while (produced < ctx.digestLength) { diff --git a/test/regression/issue/29218.test.ts b/test/regression/issue/29218.test.ts index 8636bc034168..ff944c1ac8ed 100644 --- a/test/regression/issue/29218.test.ts +++ b/test/regression/issue/29218.test.ts @@ -116,6 +116,18 @@ describe("crypto.subtle SHA-3", () => { // clone, so accepting it as a hash sub-algorithm for HMAC/RSA/ECDSA // would create broken CryptoKey instances that crash at sign() or // postMessage() time. Reject up front until those paths are wired. + // + // For HMAC and RSA-PSS the hash is supplied at key creation time, so + // importKey()/generateKey() is where the rejection lives. For ECDSA + // the hash is supplied at sign()/verify() time, so we generate a real + // P-256 key pair first (which must still succeed) and then assert + // that sign() rejects when a SHA-3 hash is requested. + const ecdsaPair = (await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [ + "sign", + "verify", + ])) as CryptoKeyPair; + expect(ecdsaPair.privateKey).toBeDefined(); + for (const alg of ["SHA3-256", "SHA3-384", "SHA3-512"]) { await expect( crypto.subtle.importKey("raw", new Uint8Array(32), { name: "HMAC", hash: alg }, true, ["sign"]), @@ -131,8 +143,8 @@ describe("crypto.subtle SHA-3", () => { ), ).rejects.toMatchObject({ name: "NotSupportedError" }); await expect( - crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign"]), - ).resolves.toBeDefined(); // sanity: real ECDSA still works + crypto.subtle.sign({ name: "ECDSA", hash: alg }, ecdsaPair.privateKey, te.encode("msg")), + ).rejects.toMatchObject({ name: "NotSupportedError" }); } }); }); From ed341bc95ee86b8349fef8482836873881753aa5 Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 12 Apr 2026 15:26:55 +0000 Subject: [PATCH 09/14] test: internalize the WPT sha3.tentative digest vectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the full (algorithm × input-size) test matrix from WebCryptoAPI/digest/sha3.tentative.https.any.js in web-platform-tests into the regression test. Each of the 12 (SHA3-256/384/512 × empty/short/medium/long) SHA-3 digests now has to match the exact bytes shipped with the WPT, including the 87040-byte long input that exercises multi-block absorption across 512 sponge blocks. The bytes are copied from: https://github.com/web-platform-tests/wpt/blob/master/WebCryptoAPI/digest/sha3.tentative.https.any.js We do not ship a full WPT harness in this slice — that is tracked separately under #19673 — but the vectors are the important part for this PR and are now asserted in every bun bd test run. 35 tests / 126 expects pass. Refs #29218 --- test/regression/issue/29218.test.ts | 60 +++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/regression/issue/29218.test.ts b/test/regression/issue/29218.test.ts index ff944c1ac8ed..40ccc4eac247 100644 --- a/test/regression/issue/29218.test.ts +++ b/test/regression/issue/29218.test.ts @@ -147,6 +147,66 @@ describe("crypto.subtle SHA-3", () => { ).rejects.toMatchObject({ name: "NotSupportedError" }); } }); + + // The full (algorithm × input-size) matrix shipped by WPT's + // WebCryptoAPI/digest/sha3.tentative.https.any.js, ported to hex. If you + // need to verify these bytes against upstream, compare to: + // https://github.com/web-platform-tests/wpt/blob/master/WebCryptoAPI/digest/sha3.tentative.https.any.js + // + // The WPT file exercises four source payloads per algorithm: + // - empty: 0 bytes + // - short: 16 bytes (one sample block) + // - medium: 85 bytes + // - long: 85 * 1024 bytes (multi-block) + // Each digest must match to the byte. + test("SHA-3 matches every vector from the WPT sha3.tentative digest suite", async () => { + const sourceData = { + empty: new Uint8Array(0), + short: new Uint8Array([21, 110, 234, 124, 193, 76, 86, 203, 148, 219, 3, 10, 74, 157, 149, 255]), + medium: new Uint8Array([ + 182, 200, 249, 223, 100, 140, 208, 136, 183, 15, 56, 231, 65, 151, 177, 140, 184, 30, 30, 67, 80, 213, 11, + 204, 184, 251, 90, 115, 121, 200, 123, 178, 227, 214, 237, 84, 97, 237, 30, 159, 54, 243, 64, 163, 150, 42, + 68, 107, 129, 91, 121, 75, 75, 212, 58, 68, 3, 80, 32, 119, 178, 37, 108, 200, 7, 131, 127, 58, 172, 209, + 24, 235, 75, 156, 43, 174, 184, 151, 6, 134, 37, 171, 172, 161, 147, + ]), + long: new Uint8Array(0), // filled in below + }; + const longBuf = new Uint8Array(1024 * sourceData.medium.byteLength); + for (let i = 0; i < 1024; i++) longBuf.set(sourceData.medium, i * sourceData.medium.byteLength); + sourceData.long = longBuf; + + const wptVectors = { + "SHA3-256": { + empty: "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", + short: "3059af7aa33b517084e8ad7bbc4fb208a44c28ef32b4698d103dd540e4f91aa1", + medium: "1fa7cd1da74cd8046417508c8314e74a9a4a9d38f9f18e6cb215b8c891a0a80e", + long: "b2cfc61e0386cdaef5e10a2be189891f5ef52a7624bfcd8edc893acc64fec600", + }, + "SHA3-384": { + empty: "0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004", + short: "54b8f0e4cf4974de740098f66b3024479b01631315a6773606c33eadc32556a6e778e08f0225ae79265aec666cb2390b", + medium: "437b7d8b68b250b5c1739ea4cc86db2033879dfb18de292c9c50d9c193a4c79a08a6cae3f4e483c2795ea5d1ef7e69d2", + long: "3b39c4c97ad87613305d0ccc987181713e2d5e84b1f9760011bcce0c297499005bdce8a3d2409b5ad0164f32bb8778d0", + }, + "SHA3-512": { + empty: + "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26", + short: + "2dd2e07a62e6ad0498ba84f313c4d4024cb46001f78f75db336b0d4d8bd2a9ec152c4ad20878735d82ba0872ecf59608ef3ced2b2a8669427e7da31e362333d8", + medium: + "e640a21909536640369e9b0a48931c5cb2efcbc91fecf247306bc96a0e4ca33307cb8e1b9af367946dd01c243f3907508d04f1692a3161df1f898de8ee25febe", + long: "bd262cecf565c338032de5ba0138f0aacfe7dde83d272d0d37d952829ed25de1a1342d98659ef7d2fa4aca7ce2b1aa0784d8fc1dcbf81bcec7a7431a3da36bf7", + }, + } as const; + + for (const [alg, sizes] of Object.entries(wptVectors)) { + for (const [size, expected] of Object.entries(sizes)) { + const input = sourceData[size as keyof typeof sourceData]; + const got = await crypto.subtle.digest(alg, input); + expect(hex(got)).toBe(expected); + } + } + }); }); describe("SubtleCrypto.supports", () => { From dd70800d5db0beda257c4427b23f44a2e83bb9aa Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:28:43 +0000 Subject: [PATCH 10/14] [autofix.ci] apply automated fixes --- test/regression/issue/29218.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/regression/issue/29218.test.ts b/test/regression/issue/29218.test.ts index 40ccc4eac247..8cb6be7119cb 100644 --- a/test/regression/issue/29218.test.ts +++ b/test/regression/issue/29218.test.ts @@ -164,10 +164,10 @@ describe("crypto.subtle SHA-3", () => { empty: new Uint8Array(0), short: new Uint8Array([21, 110, 234, 124, 193, 76, 86, 203, 148, 219, 3, 10, 74, 157, 149, 255]), medium: new Uint8Array([ - 182, 200, 249, 223, 100, 140, 208, 136, 183, 15, 56, 231, 65, 151, 177, 140, 184, 30, 30, 67, 80, 213, 11, - 204, 184, 251, 90, 115, 121, 200, 123, 178, 227, 214, 237, 84, 97, 237, 30, 159, 54, 243, 64, 163, 150, 42, - 68, 107, 129, 91, 121, 75, 75, 212, 58, 68, 3, 80, 32, 119, 178, 37, 108, 200, 7, 131, 127, 58, 172, 209, - 24, 235, 75, 156, 43, 174, 184, 151, 6, 134, 37, 171, 172, 161, 147, + 182, 200, 249, 223, 100, 140, 208, 136, 183, 15, 56, 231, 65, 151, 177, 140, 184, 30, 30, 67, 80, 213, 11, 204, + 184, 251, 90, 115, 121, 200, 123, 178, 227, 214, 237, 84, 97, 237, 30, 159, 54, 243, 64, 163, 150, 42, 68, 107, + 129, 91, 121, 75, 75, 212, 58, 68, 3, 80, 32, 119, 178, 37, 108, 200, 7, 131, 127, 58, 172, 209, 24, 235, 75, + 156, 43, 174, 184, 151, 6, 134, 37, 171, 172, 161, 147, ]), long: new Uint8Array(0), // filled in below }; From 949e50587fc96243da720d8c28e812c72a0b5b19 Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 12 Apr 2026 17:00:02 +0000 Subject: [PATCH 11/14] webcrypto: correct misleading SHAKE128 note in Sha3Context buffer comment The previous comment on `buffer[144]` said the extra capacity was there so "future SHA3-224 / SHAKE128 support" could reuse the struct without a resize. SHAKE128 does not fit: its security level is 128 bits, so its capacity is 2*128 = 256 bits = 32 bytes, and its rate is 200 - 32 = 168 bytes -- 24 bytes larger than this buffer. A developer naively adding SHAKE128 by following the comment would overflow the buffer inside sha3Update. Reword the comment to call out that SHAKE variants do NOT fit as-is: SHAKE128's rate overflows the buffer, and both SHAKE variants also need a different digestLength model because their output is variable-length. Adding SHAKE means resizing the buffer AND teaching sha3Final to squeeze across multiple rate-sized blocks. Refs #29218 --- src/bun.js/bindings/webcrypto/CryptoDigest.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp index dd8e1cb5db8a..073bc7ef6554 100644 --- a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp @@ -165,10 +165,14 @@ static inline void storeLE64(uint8_t* bytes, uint64_t v) struct Sha3Context { KeccakState state {}; // Input bytes buffered before the next permutation. Sized for the largest - // FIPS 202 fixed-output rate (144 bytes for SHA3-224, which is not - // registered here); the three variants this file ships only need up to - // SHA3-256's 136-byte rate. The extra room lets future SHA3-224 / - // SHAKE128 support reuse this struct without a resize. + // fixed-output FIPS 202 rate, which is SHA3-224's 144 bytes (not yet + // registered); the three variants this file ships only need up to + // SHA3-256's 136-byte rate. XOF variants are NOT representable with this + // struct as-is: SHAKE128's rate is 168 bytes (would overflow this + // buffer), and both SHAKE variants also need a different digestLength + // model because their output is variable-length. Adding SHAKE support + // requires resizing this buffer and teaching sha3Final to squeeze across + // multiple rate-sized blocks. uint8_t buffer[144] {}; size_t bufferLength = 0; size_t rateBytes = 0; From 5ddb4f65f1b05a5352d1ce3f7d64d046e1644b2e Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 12 Apr 2026 17:42:30 +0000 Subject: [PATCH 12/14] webcrypto: guard sha3Final with an invariant check for the single-block squeeze MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current sha3Final only squeezes one rate-sized block, which is correct for all three registered algorithms (SHA3-256: 32 < 136, SHA3-384: 48 < 104, SHA3-512: 64 < 72) but silently wrong if the invariant ever breaks — the squeeze loop would read the capacity portion of the state and emit those bits verbatim, producing a cryptographically wrong hash with no diagnostic. The Sha3Context comment already warns XOF variants need a multi-block squeeze. Add ASSERT(ctx.digestLength <= ctx.rateBytes) at the top of sha3Final so that if a follow-up slice wires SHAKE/cSHAKE/TurboSHAKE through this path without also extending the squeeze, debug and ASAN builds fail loudly instead of producing silent garbage. 35 tests / 126 expects pass (assert does not fire for any registered variant). Refs #29218 --- src/bun.js/bindings/webcrypto/CryptoDigest.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp index 073bc7ef6554..11a3fe184ebb 100644 --- a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp @@ -211,6 +211,16 @@ static void sha3Update(Sha3Context& ctx, const uint8_t* input, size_t length) static void sha3Final(Sha3Context& ctx, uint8_t* output) { + // This single-block squeeze is only valid when the entire digest fits + // inside one rate-sized squeeze block. All three currently registered + // variants satisfy that (SHA3-256: 32 < 136, SHA3-384: 48 < 104, + // SHA3-512: 64 < 72); see Sha3Context's comment above. When XOF + // variants (SHAKE / cSHAKE / TurboSHAKE) are added they will need a + // multi-block squeeze loop — this assert catches anyone accidentally + // taking this path with a variant that doesn't satisfy the invariant + // before it silently reads capacity bits out of the state. + ASSERT(ctx.digestLength <= ctx.rateBytes); + // Pad10*1 with the SHA-3 domain separator 0x06. std::memset(ctx.buffer + ctx.bufferLength, 0, ctx.rateBytes - ctx.bufferLength); ctx.buffer[ctx.bufferLength] |= 0x06; From ca842ba52e4781ea2bc6d7cba5561264210c0efa Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 12 Apr 2026 21:06:32 +0000 Subject: [PATCH 13/14] ci: empty commit to retry after pre-existing flakes in #45381 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build 45381 failed on four unrelated jobs: two darwin-aarch64 runners expired waiting for an agent, linux-aarch64-build-cpp timed out at 14 minutes (infra), and debian-13-x64-asan hit the pre-existing flake in test/js/node/test/parallel/test-cluster-primary-kill.js (a Node cluster primary-kill timing test with no webcrypto / SHA-3 touch points). None of these are related to the ASSERT I added in 5ddb4f65f1, which only fires when digestLength > rateBytes — provably false for every registered SHA-3 variant (32<136, 48<104, 64<72). Local ASAN debug run shows 35 tests / 126 expects pass. Kicking CI onto fresh agents. From bcad1c2009c26ef4ec2af362a8392c57b2ed0f58 Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 12 Apr 2026 21:44:13 +0000 Subject: [PATCH 14/14] webcrypto: assert sha3Init rate fits the fixed-output buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symmetric to the guard added in 5ddb4f65f1 on the squeeze side. sha3Init computes rateBytes = 200 - 2*outputBytes and stores it; sha3Update then memcpys up to rateBytes at a time into buffer[144]. If a future caller wires SHAKE128 (rateBytes = 168) through this path, absorption would silently overflow the buffer by 24 bytes on the stack before the sha3Final digest-length guard would ever fire. Add ASSERT(ctx.rateBytes <= sizeof(ctx.buffer)) at the end of sha3Init so both halves of the sponge have matching defensive checks. Still 35 tests / 126 expects pass — the assert is false for none of the three registered variants (SHA3-256: 136, SHA3-384: 104, SHA3-512: 72; all <= 144). Refs #29218 --- src/bun.js/bindings/webcrypto/CryptoDigest.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp index 11a3fe184ebb..345121120250 100644 --- a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp @@ -186,6 +186,12 @@ static void sha3Init(Sha3Context& ctx, size_t outputBytes) // rate = 1600 - 2 * outputBits const size_t capacityBytes = 2 * outputBytes; ctx.rateBytes = 200 - capacityBytes; + // Symmetric to the ASSERT in sha3Final: catch any mistaken wiring of a + // variant whose rate exceeds our fixed-output buffer. sha3Update memcpys + // up to rateBytes at a time, so if a caller ever wires SHAKE128 + // (rateBytes = 168) through this path the absorption phase would + // silently overflow buffer[144] before the sha3Final guard ever runs. + ASSERT(ctx.rateBytes <= sizeof(ctx.buffer)); } static void sha3Update(Sha3Context& ctx, const uint8_t* input, size_t length)