Skip to content

Commit 2aeca4a

Browse files
authored
fix(ext/crypto): add SHA3 support to crypto.subtle.digest (#32342)
The CryptoHash enum was missing SHA3-256, SHA3-384, and SHA3-512 variants, causing serde deserialization to fail when calling crypto.subtle.digest() with SHA3 algorithms despite them being registered in the JS layer. Closes #32330 Draft spec: https://wicg.github.io/webcrypto-modern-algos/ This was added in Node.js in nodejs/node#59365
1 parent 876b9a4 commit 2aeca4a

4 files changed

Lines changed: 127 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,34 @@ cargo test specs
153153
cargo test spec::test_name
154154
```
155155

156+
### Unit Tests (`tests/unit/`)
157+
158+
JavaScript/TypeScript unit tests live in `tests/unit/` as `*_test.ts` files. Run
159+
them via `cargo test`:
160+
161+
```bash
162+
# Run all unit tests in a specific file
163+
cargo test unit::webcrypto_test
164+
165+
# Run all unit tests
166+
cargo test unit::
167+
168+
# Run Node.js compatibility unit tests (tests/unit_node/)
169+
cargo test unit_node::crypto_test
170+
171+
# Run all Node.js compat unit tests
172+
cargo test unit_node::
173+
```
174+
175+
Do NOT run these directly with `./target/debug/deno test` — they depend on the
176+
cargo test harness for correct setup.
177+
156178
### Test Organization
157179

158180
- **Spec tests** (`tests/specs/`) - Main integration tests, CLI command
159181
execution and output validation
160-
- **Unit tests** - Inline with source code in each module
182+
- **Unit tests** (`tests/unit/`) - JavaScript/TypeScript unit tests for runtime
183+
APIs
161184
- **Integration tests** (`cli/tests/`) - Additional integration tests
162185
- **WPT** (`tests/wpt/`) - Web Platform Tests for web standards compliance
163186

ext/crypto/key.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ pub enum CryptoHash {
1919
Sha384,
2020
#[serde(rename = "SHA-512")]
2121
Sha512,
22+
#[serde(rename = "SHA3-256")]
23+
Sha3_256,
24+
#[serde(rename = "SHA3-384")]
25+
Sha3_384,
26+
#[serde(rename = "SHA3-512")]
27+
Sha3_512,
2228
}
2329

2430
#[derive(Serialize, Deserialize, Copy, Clone)]
@@ -74,6 +80,11 @@ impl From<CryptoHash> for HmacAlgorithm {
7480
CryptoHash::Sha256 => aws_lc_rs::hmac::HMAC_SHA256,
7581
CryptoHash::Sha384 => aws_lc_rs::hmac::HMAC_SHA384,
7682
CryptoHash::Sha512 => aws_lc_rs::hmac::HMAC_SHA512,
83+
// SHA3 is only supported for digest, not HMAC.
84+
// The JS layer prevents SHA3 from reaching here.
85+
CryptoHash::Sha3_256 | CryptoHash::Sha3_384 | CryptoHash::Sha3_512 => {
86+
unreachable!("SHA3 is not supported for HMAC")
87+
}
7788
}
7889
}
7990
}
@@ -85,6 +96,9 @@ impl From<CryptoHash> for &'static digest::Algorithm {
8596
CryptoHash::Sha256 => &digest::SHA256,
8697
CryptoHash::Sha384 => &digest::SHA384,
8798
CryptoHash::Sha512 => &digest::SHA512,
99+
CryptoHash::Sha3_256 => &digest::SHA3_256,
100+
CryptoHash::Sha3_384 => &digest::SHA3_384,
101+
CryptoHash::Sha3_512 => &digest::SHA3_512,
88102
}
89103
}
90104
}

ext/crypto/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ pub async fn op_crypto_sign_key(
326326
let signing_key = SigningKey::<Sha512>::new(private_key);
327327
signing_key.sign(data)
328328
}
329+
_ => return Err(CryptoError::UnsupportedAlgorithm),
329330
}
330331
.to_vec()
331332
}
@@ -359,6 +360,7 @@ pub async fn op_crypto_sign_key(
359360
let hashed = Sha512::digest(data);
360361
signing_key.sign(Some(&mut rng), &private_key, &hashed)?
361362
}
363+
_ => return Err(CryptoError::UnsupportedAlgorithm),
362364
}
363365
.to_vec()
364366
}
@@ -377,6 +379,7 @@ pub async fn op_crypto_sign_key(
377379
CryptoHash::Sha256 => sha2::Sha256::digest(data).to_vec(),
378380
CryptoHash::Sha384 => sha2::Sha384::digest(data).to_vec(),
379381
CryptoHash::Sha512 => sha2::Sha512::digest(data).to_vec(),
382+
_ => return Err(CryptoError::UnsupportedAlgorithm),
380383
};
381384
// Sign the prehashed message, producing a raw r||s signature.
382385
let signature: P256Signature =
@@ -392,6 +395,7 @@ pub async fn op_crypto_sign_key(
392395
CryptoHash::Sha256 => sha2::Sha256::digest(data).to_vec(),
393396
CryptoHash::Sha384 => sha2::Sha384::digest(data).to_vec(),
394397
CryptoHash::Sha512 => sha2::Sha512::digest(data).to_vec(),
398+
_ => return Err(CryptoError::UnsupportedAlgorithm),
395399
};
396400
let signature: P384Signature =
397401
signing_key.sign_prehash(&prehash)?;
@@ -408,6 +412,7 @@ pub async fn op_crypto_sign_key(
408412
CryptoHash::Sha256 => sha2::Sha256::digest(data).to_vec(),
409413
CryptoHash::Sha384 => sha2::Sha384::digest(data).to_vec(),
410414
CryptoHash::Sha512 => sha2::Sha512::digest(data).to_vec(),
415+
_ => return Err(CryptoError::UnsupportedAlgorithm),
411416
};
412417
// P-521 field size is 66 bytes; bits2field requires at least
413418
// half that (33 bytes). Left-pad shorter hashes to meet the
@@ -488,6 +493,7 @@ pub async fn op_crypto_verify_key(
488493
let verifying_key = VerifyingKey::<Sha512>::new(public_key);
489494
verifying_key.verify(data, &signature).is_ok()
490495
}
496+
_ => return Err(CryptoError::UnsupportedAlgorithm),
491497
}
492498
}
493499
Algorithm::RsaPss => {
@@ -520,6 +526,7 @@ pub async fn op_crypto_verify_key(
520526
let hashed = Sha512::digest(data);
521527
pss.verify(&public_key, &hashed, signature).is_ok()
522528
}
529+
_ => return Err(CryptoError::UnsupportedAlgorithm),
523530
}
524531
}
525532
Algorithm::Hmac => {
@@ -555,6 +562,7 @@ pub async fn op_crypto_verify_key(
555562
CryptoHash::Sha256 => sha2::Sha256::digest(data).to_vec(),
556563
CryptoHash::Sha384 => sha2::Sha384::digest(data).to_vec(),
557564
CryptoHash::Sha512 => sha2::Sha512::digest(data).to_vec(),
565+
_ => return Err(CryptoError::UnsupportedAlgorithm),
558566
};
559567
verifying_key.verify_prehash(&prehash, &signature).is_ok()
560568
}
@@ -583,6 +591,7 @@ pub async fn op_crypto_verify_key(
583591
CryptoHash::Sha256 => sha2::Sha256::digest(data).to_vec(),
584592
CryptoHash::Sha384 => sha2::Sha384::digest(data).to_vec(),
585593
CryptoHash::Sha512 => sha2::Sha512::digest(data).to_vec(),
594+
_ => return Err(CryptoError::UnsupportedAlgorithm),
586595
};
587596
verifying_key.verify_prehash(&prehash, &signature).is_ok()
588597
}
@@ -613,6 +622,7 @@ pub async fn op_crypto_verify_key(
613622
CryptoHash::Sha256 => sha2::Sha256::digest(data).to_vec(),
614623
CryptoHash::Sha384 => sha2::Sha384::digest(data).to_vec(),
615624
CryptoHash::Sha512 => sha2::Sha512::digest(data).to_vec(),
625+
_ => return Err(CryptoError::UnsupportedAlgorithm),
616626
};
617627
// P-521 field size is 66 bytes; bits2field requires at least
618628
// half that (33 bytes). Left-pad shorter hashes to meet the
@@ -679,6 +689,7 @@ pub async fn op_crypto_derive_bits(
679689
CryptoHash::Sha256 => pbkdf2::PBKDF2_HMAC_SHA256,
680690
CryptoHash::Sha384 => pbkdf2::PBKDF2_HMAC_SHA384,
681691
CryptoHash::Sha512 => pbkdf2::PBKDF2_HMAC_SHA512,
692+
_ => return Err(CryptoError::UnsupportedAlgorithm),
682693
};
683694

684695
// This will never panic. We have already checked length earlier.
@@ -810,6 +821,7 @@ pub async fn op_crypto_derive_bits(
810821
CryptoHash::Sha256 => hkdf::HKDF_SHA256,
811822
CryptoHash::Sha384 => hkdf::HKDF_SHA384,
812823
CryptoHash::Sha512 => hkdf::HKDF_SHA512,
824+
_ => return Err(CryptoError::UnsupportedAlgorithm),
813825
};
814826

815827
let info = args.info.ok_or(CryptoError::MissingArgumentInfo)?;

tests/unit/webcrypto_test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2194,6 +2194,83 @@ Deno.test(async function x25519ExportJwk() {
21942194
assert(jwk.x);
21952195
});
21962196

2197+
// https://github.com/denoland/deno/issues/32330
2198+
Deno.test(async function testSha3DigestAlgorithms() {
2199+
function toHex(buf: ArrayBuffer): string {
2200+
return [...new Uint8Array(buf)]
2201+
.map((b) => b.toString(16).padStart(2, "0"))
2202+
.join("");
2203+
}
2204+
2205+
const data = new TextEncoder().encode("Hello, Deno!");
2206+
2207+
// SHA3-256 produces 32 bytes
2208+
// deno-lint-ignore camelcase
2209+
const sha3_256 = await crypto.subtle.digest("SHA3-256", data);
2210+
assertEquals(sha3_256.byteLength, 32);
2211+
2212+
// SHA3-384 produces 48 bytes
2213+
// deno-lint-ignore camelcase
2214+
const sha3_384 = await crypto.subtle.digest("SHA3-384", data);
2215+
assertEquals(sha3_384.byteLength, 48);
2216+
2217+
// SHA3-512 produces 64 bytes
2218+
// deno-lint-ignore camelcase
2219+
const sha3_512 = await crypto.subtle.digest("SHA3-512", data);
2220+
assertEquals(sha3_512.byteLength, 64);
2221+
2222+
// Verify deterministic: same input always gives same output
2223+
// deno-lint-ignore camelcase
2224+
const sha3_256_again = await crypto.subtle.digest("SHA3-256", data);
2225+
assertEquals(toHex(sha3_256), toHex(sha3_256_again));
2226+
});
2227+
2228+
Deno.test(async function testSha3DigestKnownVectors() {
2229+
function toHex(buf: ArrayBuffer): string {
2230+
return [...new Uint8Array(buf)]
2231+
.map((b) => b.toString(16).padStart(2, "0"))
2232+
.join("");
2233+
}
2234+
2235+
// Empty input test vectors (from NIST)
2236+
const empty = new Uint8Array(0);
2237+
2238+
assertEquals(
2239+
toHex(await crypto.subtle.digest("SHA3-256", empty)),
2240+
"a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a",
2241+
);
2242+
assertEquals(
2243+
toHex(await crypto.subtle.digest("SHA3-384", empty)),
2244+
"0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004",
2245+
);
2246+
assertEquals(
2247+
toHex(await crypto.subtle.digest("SHA3-512", empty)),
2248+
"a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26",
2249+
);
2250+
});
2251+
2252+
Deno.test(async function testSha3DigestVariousInputTypes() {
2253+
const text = new TextEncoder().encode("test");
2254+
const expected = await crypto.subtle.digest("SHA3-256", text);
2255+
2256+
// ArrayBuffer input
2257+
const fromBuffer = await crypto.subtle.digest("SHA3-256", text.buffer);
2258+
assertEquals(
2259+
new Uint8Array(fromBuffer),
2260+
new Uint8Array(expected),
2261+
);
2262+
2263+
// DataView input
2264+
const fromDataView = await crypto.subtle.digest(
2265+
"SHA3-256",
2266+
new DataView(text.buffer),
2267+
);
2268+
assertEquals(
2269+
new Uint8Array(fromDataView),
2270+
new Uint8Array(expected),
2271+
);
2272+
});
2273+
21972274
// Regression test for https://github.com/denoland/deno/issues/30243
21982275
// Importing a PKCS#8 RSA key with the wrong algorithm (ECDSA) should throw, not panic.
21992276
Deno.test("crypto.subtle.importKey PKCS#8 with wrong algorithm does not panic", async () => {

0 commit comments

Comments
 (0)