diff options
author | Simon Wülker <simon.wuelker@arcor.de> | 2024-11-11 20:32:51 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-11-11 19:32:51 +0000 |
commit | 8d3d7b74035a0cb501d9b3192a79636ede8bfce2 (patch) | |
tree | 67149fde1aeefaafd991f9691460b9888a2bd8f7 /components/script/dom | |
parent | deddcf2c7a7ad182720aed3da50d028c1e5ecb7d (diff) | |
download | servo-8d3d7b74035a0cb501d9b3192a79636ede8bfce2.tar.gz servo-8d3d7b74035a0cb501d9b3192a79636ede8bfce2.zip |
Implement `crypto.subtle.sign/verify` with HMAC (#34223)
* Allow importing HMAC keys
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
* Implement crypto.subtle.sign with HMAC
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
* Implement crypto.subtle.verify with HMAC
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
* Update WPT expectations
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
---------
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
Diffstat (limited to 'components/script/dom')
-rw-r--r-- | components/script/dom/bindings/codegen/Bindings.conf | 4 | ||||
-rw-r--r-- | components/script/dom/cryptokey.rs | 2 | ||||
-rw-r--r-- | components/script/dom/subtlecrypto.rs | 441 | ||||
-rw-r--r-- | components/script/dom/webidls/SubtleCrypto.webidl | 26 |
4 files changed, 460 insertions, 13 deletions
diff --git a/components/script/dom/bindings/codegen/Bindings.conf b/components/script/dom/bindings/codegen/Bindings.conf index 9042edde542..c777c6c6163 100644 --- a/components/script/dom/bindings/codegen/Bindings.conf +++ b/components/script/dom/bindings/codegen/Bindings.conf @@ -428,8 +428,8 @@ DOMInterfaces = { }, 'SubtleCrypto': { - 'inRealms': ['Encrypt', 'Decrypt', 'GenerateKey', 'DeriveKey', 'DeriveBits', 'Digest', 'ImportKey', 'ExportKey'], - 'canGc': ['Encrypt', 'Decrypt', 'GenerateKey', 'DeriveKey', 'DeriveBits', 'Digest', 'ImportKey', 'ExportKey'], + 'inRealms': ['Encrypt', 'Decrypt', 'Sign', 'Verify', 'GenerateKey', 'DeriveKey', 'DeriveBits', 'Digest', 'ImportKey', 'ExportKey'], + 'canGc': ['Encrypt', 'Decrypt', 'Sign', 'Verify', 'GenerateKey', 'DeriveKey', 'DeriveBits', 'Digest', 'ImportKey', 'ExportKey'], }, 'SVGElement': { diff --git a/components/script/dom/cryptokey.rs b/components/script/dom/cryptokey.rs index 6e0ade288bf..15918d66908 100644 --- a/components/script/dom/cryptokey.rs +++ b/components/script/dom/cryptokey.rs @@ -28,6 +28,7 @@ pub enum Handle { Aes256(Vec<u8>), Pbkdf2(Vec<u8>), Hkdf(Vec<u8>), + Hmac(Vec<u8>), } /// <https://w3c.github.io/webcrypto/#cryptokey-interface> @@ -150,6 +151,7 @@ impl Handle { Self::Aes256(bytes) => bytes, Self::Pbkdf2(bytes) => bytes, Self::Hkdf(bytes) => bytes, + Self::Hmac(bytes) => bytes, } } } diff --git a/components/script/dom/subtlecrypto.rs b/components/script/dom/subtlecrypto.rs index edf52a5f966..7ab6b9c9318 100644 --- a/components/script/dom/subtlecrypto.rs +++ b/components/script/dom/subtlecrypto.rs @@ -17,7 +17,7 @@ use js::jsapi::{JSObject, JS_NewObject}; use js::jsval::ObjectValue; use js::rust::MutableHandleObject; use js::typedarray::ArrayBufferU8; -use ring::{digest, hkdf, pbkdf2}; +use ring::{digest, hkdf, hmac, pbkdf2}; use servo_rand::{RngCore, ServoRng}; use crate::dom::bindings::buffer_source::create_buffer_source; @@ -27,8 +27,8 @@ use crate::dom::bindings::codegen::Bindings::CryptoKeyBinding::{ }; use crate::dom::bindings::codegen::Bindings::SubtleCryptoBinding::{ AesCbcParams, AesCtrParams, AesDerivedKeyParams, AesKeyAlgorithm, AesKeyGenParams, Algorithm, - AlgorithmIdentifier, HkdfParams, JsonWebKey, KeyAlgorithm, KeyFormat, Pbkdf2Params, - SubtleCryptoMethods, + AlgorithmIdentifier, HkdfParams, HmacImportParams, HmacKeyAlgorithm, JsonWebKey, KeyAlgorithm, + KeyFormat, Pbkdf2Params, SubtleCryptoMethods, }; use crate::dom::bindings::codegen::UnionTypes::{ ArrayBufferViewOrArrayBuffer, ArrayBufferViewOrArrayBufferOrJsonWebKey, @@ -253,6 +253,179 @@ impl SubtleCryptoMethods for SubtleCrypto { promise } + /// <https://w3c.github.io/webcrypto/#SubtleCrypto-method-sign> + fn Sign( + &self, + cx: SafeJSContext, + algorithm: AlgorithmIdentifier, + key: &CryptoKey, + data: ArrayBufferViewOrArrayBuffer, + comp: InRealm, + can_gc: CanGc, + ) -> Rc<Promise> { + // Step 1. Let algorithm and key be the algorithm and key parameters passed to the sign() method, respectively. + + // Step 2. Let data be the result of getting a copy of the bytes held by the data parameter passed to + // the sign() method. + let data = match &data { + ArrayBufferViewOrArrayBuffer::ArrayBufferView(view) => view.to_vec(), + ArrayBufferViewOrArrayBuffer::ArrayBuffer(buffer) => buffer.to_vec(), + }; + + // Step 3. Let normalizedAlgorithm be the result of normalizing an algorithm, with alg set to algorithm and + // op set to "sign". + let promise = Promise::new_in_current_realm(comp, can_gc); + let normalized_algorithm = match normalize_algorithm_for_sign_or_verify(cx, &algorithm) { + Ok(algorithm) => algorithm, + Err(e) => { + // Step 4. If an error occurred, return a Promise rejected with normalizedAlgorithm. + promise.reject_error(e); + return promise; + }, + }; + + // Step 5. Let promise be a new Promise. + // NOTE: We did that in preparation of Step 4. + + // Step 6. Return promise and perform the remaining steps in parallel. + let (task_source, canceller) = self.task_source_with_canceller(); + let trusted_promise = TrustedPromise::new(promise.clone()); + let trusted_key = Trusted::new(key); + + let _ = task_source.queue_with_canceller( + task!(sign: move || { + // Step 7. If the following steps or referenced procedures say to throw an error, reject promise + // with the returned error and then terminate the algorithm. + let promise = trusted_promise.root(); + let key = trusted_key.root(); + + // Step 8. If the name member of normalizedAlgorithm is not equal to the name attribute of the + // [[algorithm]] internal slot of key then throw an InvalidAccessError. + if normalized_algorithm.name() != key.algorithm() { + promise.reject_error(Error::InvalidAccess); + return; + } + + // Step 9. If the [[usages]] internal slot of key does not contain an entry that is "sign", + // then throw an InvalidAccessError. + if !key.usages().contains(&KeyUsage::Sign) { + promise.reject_error(Error::InvalidAccess); + return; + } + + // Step 10. Let result be the result of performing the sign operation specified by normalizedAlgorithm + // using key and algorithm and with data as message. + let cx = GlobalScope::get_cx(); + let result = match normalized_algorithm.sign(cx, &key, &data) { + Ok(signature) => signature, + Err(e) => { + promise.reject_error(e); + return; + } + }; + + rooted!(in(*cx) let mut array_buffer_ptr = ptr::null_mut::<JSObject>()); + create_buffer_source::<ArrayBufferU8>(cx, &result, array_buffer_ptr.handle_mut()) + .expect("failed to create buffer source for exported key."); + + // Step 9. Resolve promise with result. + promise.resolve_native(&*array_buffer_ptr); + }), + &canceller, + ); + + promise + } + + /// <https://w3c.github.io/webcrypto/#SubtleCrypto-method-verify> + fn Verify( + &self, + cx: SafeJSContext, + algorithm: AlgorithmIdentifier, + key: &CryptoKey, + signature: ArrayBufferViewOrArrayBuffer, + data: ArrayBufferViewOrArrayBuffer, + comp: InRealm, + can_gc: CanGc, + ) -> Rc<Promise> { + // Step 1. Let algorithm and key be the algorithm and key parameters passed to the verify() method, + // respectively. + + // Step 2. Let signature be the result of getting a copy of the bytes held by the signature parameter passed + // to the verify() method. + let signature = match &signature { + ArrayBufferViewOrArrayBuffer::ArrayBufferView(view) => view.to_vec(), + ArrayBufferViewOrArrayBuffer::ArrayBuffer(buffer) => buffer.to_vec(), + }; + + // Step 3. Let data be the result of getting a copy of the bytes held by the data parameter passed to the + // verify() method. + let data = match &data { + ArrayBufferViewOrArrayBuffer::ArrayBufferView(view) => view.to_vec(), + ArrayBufferViewOrArrayBuffer::ArrayBuffer(buffer) => buffer.to_vec(), + }; + + // Step 4. Let normalizedAlgorithm be the result of normalizing an algorithm, with alg set to + // algorithm and op set to "verify". + let promise = Promise::new_in_current_realm(comp, can_gc); + let normalized_algorithm = match normalize_algorithm_for_sign_or_verify(cx, &algorithm) { + Ok(algorithm) => algorithm, + Err(e) => { + // Step 5. If an error occurred, return a Promise rejected with normalizedAlgorithm. + promise.reject_error(e); + return promise; + }, + }; + + // Step 6. Let promise be a new Promise. + // NOTE: We did that in preparation of Step 6. + + // Step 7. Return promise and perform the remaining steps in parallel. + let (task_source, canceller) = self.task_source_with_canceller(); + let trusted_promise = TrustedPromise::new(promise.clone()); + let trusted_key = Trusted::new(key); + + let _ = task_source.queue_with_canceller( + task!(sign: move || { + // Step 8. If the following steps or referenced procedures say to throw an error, reject promise + // with the returned error and then terminate the algorithm. + let promise = trusted_promise.root(); + let key = trusted_key.root(); + + // Step 9. If the name member of normalizedAlgorithm is not equal to the name attribute of the + // [[algorithm]] internal slot of key then throw an InvalidAccessError. + if normalized_algorithm.name() != key.algorithm() { + promise.reject_error(Error::InvalidAccess); + return; + } + + // Step 10. If the [[usages]] internal slot of key does not contain an entry that is "verify", + // then throw an InvalidAccessError. + if !key.usages().contains(&KeyUsage::Verify) { + promise.reject_error(Error::InvalidAccess); + return; + } + + // Step 1. Let result be the result of performing the verify operation specified by normalizedAlgorithm + // using key, algorithm and signature and with data as message. + let cx = GlobalScope::get_cx(); + let result = match normalized_algorithm.verify(cx, &key, &data, &signature) { + Ok(result) => result, + Err(e) => { + promise.reject_error(e); + return; + } + }; + + // Step 9. Resolve promise with result. + promise.resolve_native(&result); + }), + &canceller, + ); + + promise + } + /// <https://w3c.github.io/webcrypto/#SubtleCrypto-method-digest> fn Digest( &self, @@ -762,6 +935,26 @@ impl From<AesKeyGenParams> for SubtleAesKeyGenParams { } } +/// <https://w3c.github.io/webcrypto/#dfn-HmacImportParams> +struct SubtleHmacImportParams { + /// <https://w3c.github.io/webcrypto/#dfn-HmacKeyAlgorithm-hash> + hash: DigestAlgorithm, + + /// <https://w3c.github.io/webcrypto/#dfn-HmacKeyGenParams-length> + length: Option<u32>, +} + +impl SubtleHmacImportParams { + fn new(cx: JSContext, params: RootedTraceableBox<HmacImportParams>) -> Fallible<Self> { + let hash = normalize_algorithm_for_digest(cx, ¶ms.hash)?; + let params = Self { + hash, + length: params.length, + }; + Ok(params) + } +} + /// <https://w3c.github.io/webcrypto/#hkdf-params> #[derive(Clone, Debug)] pub struct SubtleHkdfParams { @@ -848,6 +1041,7 @@ enum DigestAlgorithm { enum ImportKeyAlgorithm { AesCbc, AesCtr, + Hmac(SubtleHmacImportParams), Pbkdf2, Hkdf, } @@ -868,6 +1062,13 @@ enum EncryptionAlgorithm { AesCtr(SubtleAesCtrParams), } +/// A normalized algorithm returned by [`normalize_algorithm`] with operation `"sign"` or `"verify"` +/// +/// [`normalize_algorithm`]: https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm +enum SignatureAlgorithm { + Hmac, +} + /// A normalized algorithm returned by [`normalize_algorithm`] with operation `"generateKey"` /// /// [`normalize_algorithm`]: https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm @@ -959,7 +1160,14 @@ fn normalize_algorithm_for_import_key( return Err(Error::Syntax); }; - algorithm.name.str().to_uppercase() + let name = algorithm.name.str().to_uppercase(); + if name == ALG_HMAC { + let params = value_from_js_object!(HmacImportParams, cx, value); + let subtle_params = SubtleHmacImportParams::new(cx, params)?; + return Ok(ImportKeyAlgorithm::Hmac(subtle_params)); + } + + name }, AlgorithmIdentifier::String(name) => name.str().to_uppercase(), }; @@ -1034,6 +1242,33 @@ fn normalize_algorithm_for_encrypt_or_decrypt( Ok(normalized_algorithm) } +/// <https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm> with operation `"sign"` +/// or `"verify"` +fn normalize_algorithm_for_sign_or_verify( + cx: JSContext, + algorithm: &AlgorithmIdentifier, +) -> Result<SignatureAlgorithm, Error> { + let name = match algorithm { + AlgorithmIdentifier::Object(obj) => { + rooted!(in(*cx) let value = ObjectValue(obj.get())); + let Ok(ConversionResult::Success(algorithm)) = Algorithm::new(cx, value.handle()) + else { + return Err(Error::Syntax); + }; + + algorithm.name.str().to_uppercase() + }, + AlgorithmIdentifier::String(name) => name.str().to_uppercase(), + }; + + let normalized_algorithm = match name.as_str() { + ALG_HMAC => SignatureAlgorithm::Hmac, + _ => return Err(Error::NotSupported), + }; + + Ok(normalized_algorithm) +} + /// <https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm> with operation `"generateKey"` fn normalize_algorithm_for_generate_key( cx: JSContext, @@ -1398,6 +1633,101 @@ impl SubtleCrypto { } } + /// <https://w3c.github.io/webcrypto/#hmac-operations> + #[allow(unsafe_code)] + fn import_key_hmac( + &self, + normalized_algorithm: &SubtleHmacImportParams, + format: KeyFormat, + key_data: &[u8], + extractable: bool, + usages: Vec<KeyUsage>, + ) -> Result<DomRoot<CryptoKey>, Error> { + // Step 1. Let keyData be the key data to be imported. + // Step 2. If usages contains an entry which is not "sign" or "verify", then throw a SyntaxError. + if usages + .iter() + .any(|usage| !matches!(usage, KeyUsage::Sign | KeyUsage::Verify)) + { + return Err(Error::Syntax); + } + + // Step 3. Let hash be a new KeyAlgorithm. + let hash; + + // Step 4. + let data; + match format { + // If format is "raw": + KeyFormat::Raw => { + // Step 4.1 Let data be the octet string contained in keyData. + data = key_data; + + // Step 4.2 Set hash to equal the hash member of normalizedAlgorithm. + hash = normalized_algorithm.hash; + }, + // If format is "jwk": + KeyFormat::Jwk => { + // TODO: This seems to require having key_data be more than just &[u8] + return Err(Error::NotSupported); + }, + // Otherwise: + _ => { + // throw a NotSupportedError. + return Err(Error::NotSupported); + }, + } + + // Step 5. Let length be equivalent to the length, in octets, of data, multiplied by 8. + let mut length = data.len() as u32 * 8; + + // Step 6. If length is zero then throw a DataError. + if length == 0 { + return Err(Error::Data); + } + + // Step 7. If the length member of normalizedAlgorithm is present: + if let Some(given_length) = normalized_algorithm.length { + // If the length member of normalizedAlgorithm is greater than length: + if given_length > length { + // throw a DataError. + return Err(Error::Data); + } + // Otherwise: + else { + // Set length equal to the length member of normalizedAlgorithm. + length = given_length; + } + } + + // Step 8. Let key be a new CryptoKey object representing an HMAC key with the first length bits of data. + // Step 9. Set the [[type]] internal slot of key to "secret". + // Step 10. Let algorithm be a new HmacKeyAlgorithm. + // Step 11. Set the name attribute of algorithm to "HMAC". + // Step 12. Set the length attribute of algorithm to length. + // Step 13. Set the hash attribute of algorithm to hash. + // Step 14. Set the [[algorithm]] internal slot of key to algorithm. + let truncated_data = data[..length as usize / 8].to_vec(); + let name = DOMString::from(ALG_HMAC); + let cx = GlobalScope::get_cx(); + rooted!(in(*cx) let mut algorithm_object = unsafe {JS_NewObject(*cx, ptr::null()) }); + assert!(!algorithm_object.is_null()); + HmacKeyAlgorithm::from_length_and_hash(length, hash, algorithm_object.handle_mut(), cx); + + let key = CryptoKey::new( + &self.global(), + KeyType::Secret, + extractable, + name, + algorithm_object.handle(), + usages, + Handle::Hmac(truncated_data), + ); + + // Step 15. Return key. + Ok(key) + } + /// <https://w3c.github.io/webcrypto/#pbkdf2-operations> #[allow(unsafe_code)] fn import_key_pbkdf2( @@ -1480,6 +1810,28 @@ impl KeyAlgorithm { } } +impl HmacKeyAlgorithm { + #[allow(unsafe_code)] + fn from_length_and_hash( + length: u32, + hash: DigestAlgorithm, + out: MutableHandleObject, + cx: JSContext, + ) { + let hmac_key_algorithm = Self { + parent: KeyAlgorithm { + name: ALG_HMAC.into(), + }, + length, + hash: KeyAlgorithm { name: hash.name() }, + }; + + unsafe { + hmac_key_algorithm.to_jsobject(*cx, out); + } + } +} + impl AesKeyAlgorithm { /// Fill the object referenced by `out` with an [AesKeyAlgorithm] /// of the specified name and size. @@ -1614,6 +1966,17 @@ impl GetKeyLengthAlgorithm { } impl DigestAlgorithm { + /// <https://w3c.github.io/webcrypto/#dom-algorithm-name> + fn name(&self) -> DOMString { + match self { + Self::Sha1 => ALG_SHA1, + Self::Sha256 => ALG_SHA256, + Self::Sha384 => ALG_SHA384, + Self::Sha512 => ALG_SHA512, + } + .into() + } + fn digest(&self, data: &[u8]) -> Result<impl AsRef<[u8]>, Error> { let algorithm = match self { Self::Sha1 => &digest::SHA1_FOR_LEGACY_USE_ONLY, @@ -1641,6 +2004,9 @@ impl ImportKeyAlgorithm { Self::AesCtr => { subtle.import_key_aes(format, secret, extractable, key_usages, ALG_AES_CTR) }, + Self::Hmac(params) => { + subtle.import_key_hmac(params, format, secret, extractable, key_usages) + }, Self::Pbkdf2 => subtle.import_key_pbkdf2(format, secret, extractable, key_usages), Self::Hkdf => subtle.import_key_hkdf(format, secret, extractable, key_usages), } @@ -1704,6 +2070,32 @@ impl EncryptionAlgorithm { } } +impl SignatureAlgorithm { + fn name(&self) -> &str { + match self { + Self::Hmac => ALG_HMAC, + } + } + + fn sign(&self, cx: JSContext, key: &CryptoKey, data: &[u8]) -> Result<Vec<u8>, Error> { + match self { + Self::Hmac => sign_hmac(cx, key, data).map(|s| s.as_ref().to_vec()), + } + } + + fn verify( + &self, + cx: JSContext, + key: &CryptoKey, + data: &[u8], + signature: &[u8], + ) -> Result<bool, Error> { + match self { + Self::Hmac => verify_hmac(cx, key, data, signature), + } + } +} + impl KeyGenerationAlgorithm { // FIXME: This doesn't really need the "SubtleCrypto" argument fn generate_key( @@ -1717,3 +2109,44 @@ impl KeyGenerationAlgorithm { } } } + +/// <https://w3c.github.io/webcrypto/#hmac-operations> +fn sign_hmac(cx: JSContext, key: &CryptoKey, data: &[u8]) -> Result<impl AsRef<[u8]>, Error> { + // Step 1. Let mac be the result of performing the MAC Generation operation described in Section 4 of [FIPS-198-1] + // using the key represented by [[handle]] internal slot of key, the hash function identified by the hash attribute + // of the [[algorithm]] internal slot of key and message as the input data text. + rooted!(in(*cx) let mut algorithm_slot = ObjectValue(key.Algorithm(cx).as_ptr())); + let params = value_from_js_object!(HmacKeyAlgorithm, cx, algorithm_slot); + + let hash_algorithm = match params.hash.name.str() { + ALG_SHA1 => hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, + ALG_SHA256 => hmac::HMAC_SHA256, + ALG_SHA384 => hmac::HMAC_SHA384, + ALG_SHA512 => hmac::HMAC_SHA512, + _ => return Err(Error::NotSupported), + }; + + let sign_key = hmac::Key::new(hash_algorithm, key.handle().as_bytes()); + let mac = hmac::sign(&sign_key, data); + + // Step 2. Return the result of creating an ArrayBuffer containing mac. + // NOTE: This is done by the caller + Ok(mac) +} + +/// <https://w3c.github.io/webcrypto/#hmac-operations> +fn verify_hmac( + cx: JSContext, + key: &CryptoKey, + data: &[u8], + signature: &[u8], +) -> Result<bool, Error> { + // Step 1. Let mac be the result of performing the MAC Generation operation described in Section 4 of [FIPS-198-1] + // using the key represented by [[handle]] internal slot of key, the hash function identified by the hash attribute + // of the [[algorithm]] internal slot of key and message as the input data text. + let mac = sign_hmac(cx, key, data)?; + + // Step 2. Return true if mac is equal to signature and false otherwise. + let is_valid = mac.as_ref() == signature; + Ok(is_valid) +} diff --git a/components/script/dom/webidls/SubtleCrypto.webidl b/components/script/dom/webidls/SubtleCrypto.webidl index 118b54c1428..e9a0546ec66 100644 --- a/components/script/dom/webidls/SubtleCrypto.webidl +++ b/components/script/dom/webidls/SubtleCrypto.webidl @@ -26,13 +26,13 @@ interface SubtleCrypto { Promise<any> decrypt(AlgorithmIdentifier algorithm, CryptoKey key, BufferSource data); - // Promise<any> sign(AlgorithmIdentifier algorithm, - // CryptoKey key, - // BufferSource data); - // Promise<any> verify(AlgorithmIdentifier algorithm, - // CryptoKey key, - // BufferSource signature, - // BufferSource data); + Promise<any> sign(AlgorithmIdentifier algorithm, + CryptoKey key, + BufferSource data); + Promise<any> verify(AlgorithmIdentifier algorithm, + CryptoKey key, + BufferSource signature, + BufferSource data); Promise<any> digest(AlgorithmIdentifier algorithm, BufferSource data); @@ -92,6 +92,18 @@ dictionary AesCtrParams : Algorithm { required [EnforceRange] octet length; }; +// https://w3c.github.io/webcrypto/#dfn-HmacImportParams +dictionary HmacImportParams : Algorithm { + required HashAlgorithmIdentifier hash; + [EnforceRange] unsigned long length; +}; + +// https://w3c.github.io/webcrypto/#dfn-HmacKeyAlgorithm +dictionary HmacKeyAlgorithm : KeyAlgorithm { + required KeyAlgorithm hash; + required unsigned long length; +}; + // https://w3c.github.io/webcrypto/#hkdf-params dictionary HkdfParams : Algorithm { required HashAlgorithmIdentifier hash; |