Skip to content
Merged
2 changes: 1 addition & 1 deletion scripts/build/deps/boringssl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import type { Dependency } from "../source.ts";

const BORINGSSL_COMMIT = "4f4f5ef8ebc6e23cbf393428f0ab1b526773f7ac";
const BORINGSSL_COMMIT = "0c5fce43b7ed5eb6001487ee48ac65766f5ddcd1";

export const boringssl: Dependency = {
name: "boringssl",
Expand Down
4 changes: 4 additions & 0 deletions src/bun.js/api/crypto/EVP.zig
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ pub const Algorithm = enum {
.sha512 => BoringSSL.EVP_sha512(),
.@"sha512-224" => BoringSSL.EVP_sha512_224(),
.@"sha512-256" => BoringSSL.EVP_sha512_256(),
.@"sha3-224" => BoringSSL.EVP_sha3_224(),
.@"sha3-256" => BoringSSL.EVP_sha3_256(),
.@"sha3-384" => BoringSSL.EVP_sha3_384(),
.@"sha3-512" => BoringSSL.EVP_sha3_512(),
else => null,
};
}
Expand Down
3 changes: 3 additions & 0 deletions src/bun.js/bindings/AsymmetricKeyValue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
23 changes: 22 additions & 1 deletion src/bun.js/bindings/webcore/SerializedScriptValue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -520,9 +520,12 @@ enum class CryptoAlgorithmIdentifierTag {
PBKDF2 = 21,
ED25519 = 22,
X25519 = 23,
SHA3_256 = 24,
SHA3_384 = 25,
SHA3_512 = 26,
};

const uint8_t cryptoAlgorithmIdentifierTagMaximumValue = 22;
const uint8_t cryptoAlgorithmIdentifierTagMaximumValue = 26;

static unsigned countUsages(CryptoKeyUsageBitmap usages)
{
Expand Down Expand Up @@ -2394,6 +2397,15 @@ class CloneSerializer : public CloneBase {
case CryptoAlgorithmIdentifier::SHA_512:
write(CryptoAlgorithmIdentifierTag::SHA_512);
break;
case CryptoAlgorithmIdentifier::SHA3_256:
write(CryptoAlgorithmIdentifierTag::SHA3_256);
break;
case CryptoAlgorithmIdentifier::SHA3_384:
write(CryptoAlgorithmIdentifierTag::SHA3_384);
break;
case CryptoAlgorithmIdentifier::SHA3_512:
write(CryptoAlgorithmIdentifierTag::SHA3_512);
break;
case CryptoAlgorithmIdentifier::HKDF:
write(CryptoAlgorithmIdentifierTag::HKDF);
break;
Expand Down Expand Up @@ -3880,6 +3892,15 @@ class CloneDeserializer : public CloneBase {
case CryptoAlgorithmIdentifierTag::SHA_512:
result = CryptoAlgorithmIdentifier::SHA_512;
break;
case CryptoAlgorithmIdentifierTag::SHA3_256:
result = CryptoAlgorithmIdentifier::SHA3_256;
break;
case CryptoAlgorithmIdentifierTag::SHA3_384:
result = CryptoAlgorithmIdentifier::SHA3_384;
break;
case CryptoAlgorithmIdentifierTag::SHA3_512:
result = CryptoAlgorithmIdentifier::SHA3_512;
break;
case CryptoAlgorithmIdentifierTag::HKDF:
result = CryptoAlgorithmIdentifier::HKDF;
break;
Expand Down
8 changes: 8 additions & 0 deletions src/bun.js/bindings/webcrypto/CryptoAlgorithmHMAC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ void CryptoAlgorithmHMAC::importKey(CryptoKeyFormat format, KeyData&& data, cons
return alg.isNull() || alg == ALG384;
case CryptoAlgorithmIdentifier::SHA_512:
return alg.isNull() || alg == ALG512;
case CryptoAlgorithmIdentifier::SHA3_256:
case CryptoAlgorithmIdentifier::SHA3_384:
case CryptoAlgorithmIdentifier::SHA3_512:
return alg.isNull();
default:
return false;
}
Comment on lines 125 to 134
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 When exporting an HMAC-SHA3-256/384/512 key as JWK, no alg field is emitted (the switch in exportKey falls through with a bare break at lines 182-188 of CryptoAlgorithmHMAC.cpp), and the import checkAlgCallback at lines 125-131 accepts any SHA3 JWK whenever alg is null — so a SHA3-256 key exported to JWK can be silently re-imported as SHA3-512. The JWK roundtrip is lossy with respect to the hash variant; consider throwing NotSupportedError for SHA3 HMAC JWK export/import, or documenting that JWK does not preserve the SHA3 hash variant.

Extended reasoning...

What the bug is and how it manifests

When exportKey('jwk', hmacSha3Key) is called, CryptoAlgorithmHMAC.cpp lines 182-188 match all three SHA3 variants and execute a bare break without setting jwk.alg. This is intentional — no IANA-registered JWK alg identifiers exist for HMAC-SHA3. The exported JWK therefore looks like {kty:'oct', k:'...'} with no alg field for any SHA3 variant.

The specific code path that triggers it

During importKey('jwk', jwkData, {name:'HMAC', hash:'SHA3-512'}), the code reaches CryptoKeyHMAC::importJwk which calls the checkAlgCallback lambda at lines 125-131. For all three SHA3 identifiers the callback returns alg.isNull(). Since the exported JWK never sets alg regardless of which SHA3 variant was used, alg.isNull() is always true — the check passes silently. The import then proceeds with SHA3_512 as the hash but with the key material of the original SHA3-256 key.

Why existing code does not prevent it

For SHA-2 HMAC, exportKey always sets jwk.alg to a specific string (HS256, HS384, etc.), and the import callback verifies alg == 'HS512' (for example). Cross-variant confusion is rejected at import time. For SHA3, no analogous IANA-registered alg string exists, so the export produces no alg field and the import check is reduced to the vacuous alg.isNull() condition — which is true for all SHA3 export JWKs and therefore cannot distinguish between variants.

What the impact would be

The JWK roundtrip does not preserve the SHA3 hash variant. A developer who exports a SHA3-256 HMAC key, transmits the JWK, and re-imports it with hash:'SHA3-512' will get an HMAC-SHA3-512 key backed by 32-byte key material without any error. HMAC values computed with the original key will not match those from the re-imported key, silently breaking correctness guarantees.

Step-by-step proof

  1. const key256 = await crypto.subtle.generateKey({name:'HMAC', hash:'SHA3-256'}, true, ['sign','verify']); — creates a SHA3-256 HMAC key (default 1088 bits per CryptoKeyHMAC.cpp line 46, i.e. 136 bytes).
  2. const jwk = await crypto.subtle.exportKey('jwk', key256); — the switch in exportKey hits SHA3_256 case at line 182 and breaks; jwk.alg is never set (null/absent).
  3. const key512 = await crypto.subtle.importKey('jwk', jwk, {name:'HMAC', hash:'SHA3-512'}, true, ['sign','verify']); — importJwk calls checkAlgCallback(SHA3_512, null_string); the case at line 128 returns alg.isNull() == true; import succeeds.
  4. key512 is now an HMAC-SHA3-512 key backed by the 136-byte key material of the original SHA3-256 key — wrong variant, no error.

How to fix it

Option A (clean): Return NotSupportedError from exportKey and importKey for SHA3 HMAC in JWK format, making the limitation explicit and consistent with the existing rejection of SHA3 as a standalone importKey algorithm in SubtleCrypto.cpp lines 381-387.

Option B (best-effort): Emit a non-standard alg string (e.g. HS3-256) and verify it in the import callback, preventing cross-variant confusion at the cost of interoperability with other implementations that follow the JWK spec strictly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This matches Node.js's webcrypto behavior:

> const k = await crypto.subtle.generateKey({name:"HMAC",hash:"SHA3-256",length:256},true,["sign"])
> (await crypto.subtle.exportKey("jwk", k)).alg
undefined

There's no IETF-registered JWK alg value for HMAC-SHA3 or RSA-SHA3, so omitting it is the only correct behavior — emitting a made-up identifier would break import in other implementations. The hash variant must be supplied at import time, same as it would be for an alg-less JWK from any source. Throwing on export would make the feature useless for key persistence with no real safety benefit (re-importing with wrong hash → wrong-size signatures → fails immediately on first verify, not silent corruption).

Expand Down Expand Up @@ -178,6 +182,10 @@ void CryptoAlgorithmHMAC::exportKey(CryptoKeyFormat format, Ref<CryptoKey>&& key
case CryptoAlgorithmIdentifier::SHA_512:
jwk.alg = String(ALG512);
break;
case CryptoAlgorithmIdentifier::SHA3_256:
case CryptoAlgorithmIdentifier::SHA3_384:
case CryptoAlgorithmIdentifier::SHA3_512:
break;
default:
ASSERT_NOT_REACHED();
}
Expand Down
3 changes: 3 additions & 0 deletions src/bun.js/bindings/webcrypto/CryptoAlgorithmIdentifier.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ enum class CryptoAlgorithmIdentifier : uint8_t {
SHA_256,
SHA_384,
SHA_512,
SHA3_256,
SHA3_384,
SHA3_512,
HKDF,
PBKDF2,
Ed25519,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
#include "CryptoAlgorithmSHA256.h"
#include "CryptoAlgorithmSHA384.h"
#include "CryptoAlgorithmSHA512.h"
#include "CryptoAlgorithmSHA3.h"
#include "CryptoAlgorithmX25519.h"

namespace WebCore {
Expand All @@ -73,6 +74,9 @@ void CryptoAlgorithmRegistry::platformRegisterAlgorithms()
registerAlgorithmWithAlternativeName<CryptoAlgorithmSHA256>();
registerAlgorithmWithAlternativeName<CryptoAlgorithmSHA384>();
registerAlgorithmWithAlternativeName<CryptoAlgorithmSHA512>();
registerAlgorithm<CryptoAlgorithmSHA3_256>();
registerAlgorithm<CryptoAlgorithmSHA3_384>();
registerAlgorithm<CryptoAlgorithmSHA3_512>();
registerAlgorithm<CryptoAlgorithmEd25519>();
registerAlgorithm<CryptoAlgorithmX25519>();
}
Expand Down
90 changes: 90 additions & 0 deletions src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2024 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.h"

#if ENABLE(WEB_CRYPTO)

#include "CryptoDigest.h"
#include "ScriptExecutionContext.h"

namespace WebCore {

static void dispatchDigest(PAL::CryptoDigest::Algorithm algorithm,
Vector<uint8_t>&& message, CryptoAlgorithm::VectorCallback&& callback,
CryptoAlgorithm::ExceptionCallback&& exceptionCallback,
ScriptExecutionContext& context, WorkQueue& workQueue)
{
auto digest = PAL::CryptoDigest::create(algorithm);
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);
});
});
}

#define DEFINE_SHA3(ClassName, DigestAlgo) \
Ref<CryptoAlgorithm> ClassName::create() { return adoptRef(*new ClassName); } \
CryptoAlgorithmIdentifier ClassName::identifier() const { return s_identifier; } \
void ClassName::digest(Vector<uint8_t>&& message, VectorCallback&& callback, \
ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, \
WorkQueue& workQueue) \
{ \
dispatchDigest(DigestAlgo, WTF::move(message), WTF::move(callback), \
WTF::move(exceptionCallback), context, workQueue); \
}

DEFINE_SHA3(CryptoAlgorithmSHA3_256, PAL::CryptoDigest::Algorithm::SHA3_256)
DEFINE_SHA3(CryptoAlgorithmSHA3_384, PAL::CryptoDigest::Algorithm::SHA3_384)
DEFINE_SHA3(CryptoAlgorithmSHA3_512, PAL::CryptoDigest::Algorithm::SHA3_512)

#undef DEFINE_SHA3

} // namespace WebCore

#endif // ENABLE(WEB_CRYPTO)
72 changes: 72 additions & 0 deletions src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (C) 2024 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

#if ENABLE(WEB_CRYPTO)

#include "CryptoAlgorithm.h"

namespace WebCore {

class CryptoAlgorithmSHA3_256 final : public CryptoAlgorithm {
public:
static constexpr ASCIILiteral s_name = "SHA3-256"_s;
static constexpr CryptoAlgorithmIdentifier s_identifier = CryptoAlgorithmIdentifier::SHA3_256;
static Ref<CryptoAlgorithm> create();

private:
CryptoAlgorithmSHA3_256() = default;
CryptoAlgorithmIdentifier identifier() const final;
void digest(Vector<uint8_t>&&, VectorCallback&&, ExceptionCallback&&, ScriptExecutionContext&, WorkQueue&) final;
};

class CryptoAlgorithmSHA3_384 final : public CryptoAlgorithm {
public:
static constexpr ASCIILiteral s_name = "SHA3-384"_s;
static constexpr CryptoAlgorithmIdentifier s_identifier = CryptoAlgorithmIdentifier::SHA3_384;
static Ref<CryptoAlgorithm> create();

private:
CryptoAlgorithmSHA3_384() = default;
CryptoAlgorithmIdentifier identifier() const final;
void digest(Vector<uint8_t>&&, VectorCallback&&, ExceptionCallback&&, ScriptExecutionContext&, WorkQueue&) final;
};

class CryptoAlgorithmSHA3_512 final : public CryptoAlgorithm {
public:
static constexpr ASCIILiteral s_name = "SHA3-512"_s;
static constexpr CryptoAlgorithmIdentifier s_identifier = CryptoAlgorithmIdentifier::SHA3_512;
static Ref<CryptoAlgorithm> create();

private:
CryptoAlgorithmSHA3_512() = default;
CryptoAlgorithmIdentifier identifier() const final;
void digest(Vector<uint8_t>&&, VectorCallback&&, ExceptionCallback&&, ScriptExecutionContext&, WorkQueue&) final;
};

} // namespace WebCore

#endif // ENABLE(WEB_CRYPTO)
42 changes: 42 additions & 0 deletions src/bun.js/bindings/webcrypto/CryptoDigest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "config.h"
#include "CryptoDigest.h"

#include <openssl/digest.h>
#include <openssl/sha.h>

namespace {
Expand Down Expand Up @@ -103,6 +104,38 @@ struct CryptoDigestContextImpl : public CryptoDigestContext {
SHAContext m_context;
};

struct CryptoDigestContextEVP : public CryptoDigestContext {
WTF_DEPRECATED_MAKE_STRUCT_FAST_ALLOCATED(CryptoDigestContextEVP);

static std::unique_ptr<CryptoDigestContext> create(const EVP_MD* md)
{
return makeUnique<CryptoDigestContextEVP>(md);
}

explicit CryptoDigestContextEVP(const EVP_MD* md)
{
EVP_MD_CTX_init(&m_context);
EVP_DigestInit_ex(&m_context, md, nullptr);
}
Comment thread
claude[bot] marked this conversation as resolved.

~CryptoDigestContextEVP() override { EVP_MD_CTX_cleanup(&m_context); }

void addBytes(const void* input, size_t length) override
{
EVP_DigestUpdate(&m_context, input, length);
}

Vector<uint8_t> computeHash() override
{
Vector<uint8_t> result(EVP_MD_CTX_size(&m_context));
EVP_DigestFinal_ex(&m_context, result.begin(), nullptr);
return result;
}
Comment thread
claude[bot] marked this conversation as resolved.

private:
EVP_MD_CTX m_context;
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

CryptoDigest::CryptoDigest()
{
}
Expand Down Expand Up @@ -131,6 +164,15 @@ std::unique_ptr<CryptoDigest> CryptoDigest::create(CryptoDigest::Algorithm algor
case CryptoDigest::Algorithm::SHA_512:
digest->m_context = CryptoDigestContextImpl<SHA512_CTX, SHA512Functions>::create();
return digest;
case CryptoDigest::Algorithm::SHA3_256:
digest->m_context = CryptoDigestContextEVP::create(EVP_sha3_256());
return digest;
case CryptoDigest::Algorithm::SHA3_384:
digest->m_context = CryptoDigestContextEVP::create(EVP_sha3_384());
return digest;
case CryptoDigest::Algorithm::SHA3_512:
digest->m_context = CryptoDigestContextEVP::create(EVP_sha3_512());
return digest;
}

return nullptr;
Expand Down
3 changes: 3 additions & 0 deletions src/bun.js/bindings/webcrypto/CryptoDigest.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class CryptoDigest {
SHA_256,
SHA_384,
SHA_512,
SHA3_256,
SHA3_384,
SHA3_512,
};
PAL_EXPORT static std::unique_ptr<CryptoDigest> create(Algorithm);
PAL_EXPORT ~CryptoDigest();
Expand Down
6 changes: 6 additions & 0 deletions src/bun.js/bindings/webcrypto/CryptoKeyHMAC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ static size_t getKeyLengthFromHash(CryptoAlgorithmIdentifier hash)
case CryptoAlgorithmIdentifier::SHA_384:
case CryptoAlgorithmIdentifier::SHA_512:
return 1024;
case CryptoAlgorithmIdentifier::SHA3_256:
return 1088;
case CryptoAlgorithmIdentifier::SHA3_384:
return 832;
case CryptoAlgorithmIdentifier::SHA3_512:
return 576;
default:
ASSERT_NOT_REACHED();
return 0;
Expand Down
6 changes: 6 additions & 0 deletions src/bun.js/bindings/webcrypto/OpenSSLUtilities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ const EVP_MD* digestAlgorithm(CryptoAlgorithmIdentifier hashFunction)
return EVP_sha384();
case CryptoAlgorithmIdentifier::SHA_512:
return EVP_sha512();
case CryptoAlgorithmIdentifier::SHA3_256:
return EVP_sha3_256();
case CryptoAlgorithmIdentifier::SHA3_384:
return EVP_sha3_384();
case CryptoAlgorithmIdentifier::SHA3_512:
return EVP_sha3_512();
default:
return nullptr;
}
Expand Down
Loading
Loading