script: Refactoring of algorithm normalization in SubtleCrypto (#39431)

In our current implementation, we have multiple functions such as
`normalize_algoirthm_for_encrypt_or_decrypt` and
`normalize_algorithm_for_sign_or_verify` to normalize an algorithm, and
each of them works slightly differently. However, the spec defines a
single normalization procedure to handle all normalization.

This patch tries to consolidate our functions into a single
spec-compliant normalization function named `normalize_algorithm`.

The refactoring involves many existing code, so this patch only
introduces the new infrastructure without touching the existing. When
this patch gets approved and merged, we can then start migrating the
existing to the new infrastructure. (Note that SHA's digestion and
AES_CTR's encryption are also copied to the new infrastructure as
demonstration.)

More details about the refactoring can be found in the comment:
https://github.com/servo/servo/issues/39368#issuecomment-3316943206

Testing: The new code is not in used right now. No test is needed.
Fixes: Part of #39368

---------

Signed-off-by: Kingsley Yung <kingsley@kkoyung.dev>
This commit is contained in:
Kingsley Yung 2025-09-25 01:05:34 +08:00 committed by GitHub
parent 2ccaf86ff6
commit c15495b3e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 288 additions and 9 deletions

View file

@ -2,6 +2,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
mod aes_operation;
mod sha_operation;
use std::num::NonZero; use std::num::NonZero;
use std::ptr; use std::ptr;
use std::rc::Rc; use std::rc::Rc;
@ -18,7 +21,7 @@ use base64::prelude::*;
use cipher::consts::{U12, U16, U32}; use cipher::consts::{U12, U16, U32};
use dom_struct::dom_struct; use dom_struct::dom_struct;
use js::conversions::ConversionResult; use js::conversions::ConversionResult;
use js::jsapi::{JS_NewObject, JSObject}; use js::jsapi::{Heap, JS_NewObject, JSObject};
use js::jsval::{ObjectValue, UndefinedValue}; use js::jsval::{ObjectValue, UndefinedValue};
use js::rust::wrappers::JS_ParseJSON; use js::rust::wrappers::JS_ParseJSON;
use js::rust::{HandleValue, MutableHandleObject}; use js::rust::{HandleValue, MutableHandleObject};
@ -37,7 +40,7 @@ use crate::dom::bindings::codegen::Bindings::SubtleCryptoBinding::{
RsaOtherPrimesInfo, SubtleCryptoMethods, RsaOtherPrimesInfo, SubtleCryptoMethods,
}; };
use crate::dom::bindings::codegen::UnionTypes::{ use crate::dom::bindings::codegen::UnionTypes::{
ArrayBufferViewOrArrayBuffer, ArrayBufferViewOrArrayBufferOrJsonWebKey, ArrayBufferViewOrArrayBuffer, ArrayBufferViewOrArrayBufferOrJsonWebKey, ObjectOrString,
}; };
use crate::dom::bindings::conversions::SafeToJSValConvertible; use crate::dom::bindings::conversions::SafeToJSValConvertible;
use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::error::{Error, Fallible};
@ -96,6 +99,24 @@ const NAMED_CURVE_P521: &str = "P-521";
#[allow(dead_code)] #[allow(dead_code)]
static SUPPORTED_CURVES: &[&str] = &[NAMED_CURVE_P256, NAMED_CURVE_P384, NAMED_CURVE_P521]; static SUPPORTED_CURVES: &[&str] = &[NAMED_CURVE_P256, NAMED_CURVE_P384, NAMED_CURVE_P521];
/// <https://w3c.github.io/webcrypto/#supported-operation>
#[allow(unused)]
enum Operation {
Encrypt,
Decrypt,
Sign,
Verify,
Digest,
GenerateKey,
DeriveKey,
DeriveBits,
ImportKey,
ExportKey,
WrapKey,
UnwrapKey,
GetKeyLength,
}
type Aes128CbcEnc = cbc::Encryptor<Aes128>; type Aes128CbcEnc = cbc::Encryptor<Aes128>;
type Aes128CbcDec = cbc::Decryptor<Aes128>; type Aes128CbcDec = cbc::Decryptor<Aes128>;
type Aes192CbcEnc = cbc::Encryptor<Aes192>; type Aes192CbcEnc = cbc::Encryptor<Aes192>;
@ -1254,17 +1275,16 @@ impl SubtleCryptoMethods<crate::DomTypeHolder> for SubtleCrypto {
// These "subtle" structs are proxies for the codegen'd dicts which don't hold a DOMString // These "subtle" structs are proxies for the codegen'd dicts which don't hold a DOMString
// so they can be sent safely when running steps in parallel. // so they can be sent safely when running steps in parallel.
#[allow(dead_code)] /// <https://w3c.github.io/webcrypto/#dfn-Algorithm>
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct SubtleAlgorithm { struct SubtleAlgorithm {
#[allow(dead_code)] name: String,
pub(crate) name: String,
} }
impl From<DOMString> for SubtleAlgorithm { impl From<Algorithm> for SubtleAlgorithm {
fn from(name: DOMString) -> Self { fn from(params: Algorithm) -> Self {
SubtleAlgorithm { SubtleAlgorithm {
name: name.to_string(), name: params.name.to_string(),
} }
} }
} }
@ -3817,3 +3837,167 @@ impl JsonWebKeyExt for JsonWebKey {
Ok(()) Ok(())
} }
} }
/// The successful output of [`normalize_algorithm`], in form of an union type of (our "subtle"
/// binding of) IDL dictionary types.
///
/// <https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm>
#[allow(unused)]
enum NormalizedAlgorithm {
Algorithm(SubtleAlgorithm),
AesCtrParams(SubtleAesCtrParams),
}
/// <https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm>
#[allow(unused)]
fn normalize_algorithm(
cx: JSContext,
op: &Operation,
alg: &AlgorithmIdentifier,
can_gc: CanGc,
) -> Result<NormalizedAlgorithm, Error> {
match alg {
// If alg is an instance of a DOMString:
ObjectOrString::String(name) => {
// Return the result of running the normalize an algorithm algorithm, with the alg set
// to a new Algorithm dictionary whose name attribute is alg, and with the op set to
// op.
let alg = Algorithm {
name: name.to_owned(),
};
rooted!(in(*cx) let mut alg_value = UndefinedValue());
alg.safe_to_jsval(cx, alg_value.handle_mut());
let alg_obj = RootedTraceableBox::new(Heap::default());
alg_obj.set(alg_value.to_object());
normalize_algorithm(cx, op, &ObjectOrString::Object(alg_obj), can_gc)
},
// If alg is an object:
ObjectOrString::Object(obj) => {
// Step 1. Let registeredAlgorithms be the associative container stored at the op key
// of supportedAlgorithms.
// NOTE: The supportedAlgorithms and registeredAlgorithms are expressed as match arms
// in Step 5.2 - Step 10.
// Stpe 2. Let initialAlg be the result of converting the ECMAScript object represented
// by alg to the IDL dictionary type Algorithm, as defined by [WebIDL].
// Step 3. If an error occurred, return the error and terminate this algorithm.
rooted!(in(*cx) let value = ObjectValue(obj.get()));
let initial_alg = value_from_js_object::<Algorithm>(cx, value.handle(), can_gc)?;
// Step 4. Let algName be the value of the name attribute of initialAlg.
// Step 5.
// If registeredAlgorithms contains a key that is a case-insensitive string match
// for algName:
// Step 5.1. Set algName to the value of the matching key.
// Otherwise:
// Return a new NotSupportedError and terminate this algorithm.
let Some(&alg_name) = SUPPORTED_ALGORITHMS.iter().find(|supported_algorithm| {
supported_algorithm.eq_ignore_ascii_case(initial_alg.name.str())
}) else {
return Err(Error::NotSupported);
};
// Step 5.2. Let desiredType be the IDL dictionary type stored at algName in
// registeredAlgorithms.
// Step 6. Let normalizedAlgorithm be the result of converting the ECMAScript object
// represented by alg to the IDL dictionary type desiredType, as defined by [WebIDL].
// Step 7. Set the name attribute of normalizedAlgorithm to algName.
// Step 8. If an error occurred, return the error and terminate this algorithm.
// Step 9. Let dictionaries be a list consisting of the IDL dictionary type desiredType
// and all of desiredType's inherited dictionaries, in order from least to most
// derived.
// Step 10. For each dictionary dictionary in dictionaries:
// Step 10.1. For each dictionary member member declared on dictionary, in order:
// Step 10.1.1. Let key be the identifier of member.
// Step 10.1.2. Let idlValue be the value of the dictionary member with key
// name of key on normalizedAlgorithm.
// Step 10.1.3.
// If member is of the type BufferSource and is present:
// Set the dictionary member on normalizedAlgorithm with key name key
// to the result of getting a copy of the bytes held by idlValue,
// replacing the current value.
// If member is of the type HashAlgorithmIdentifier:
// Set the dictionary member on normalizedAlgorithm with key name key
// to the result of normalizing an algorithm, with the alg set to
// idlValue and the op set to "digest".
// If member is of the type AlgorithmIdentifier:
// Set the dictionary member on normalizedAlgorithm with key name key
// to the result of normalizing an algorithm, with the alg set to
// idlValue and the op set to the operation defined by the
// specification that defines the algorithm identified by algName.
//
// NOTE: Instead of calculating the desiredType in Step 5.2 and filling in the IDL
// dictionary in Step 7-10, we directly convert the JS object to our "subtle" binding
// structs to complete Step 6, and put it in the NormalizedAlgorithm enum.
let normalized_algorithm = match (alg_name, op) {
// <https://w3c.github.io/webcrypto/#aes-ctr-registration>
(ALG_AES_CTR, Operation::Encrypt) => {
let params =
boxed_value_from_js_object::<AesCtrParams>(cx, value.handle(), can_gc)?;
NormalizedAlgorithm::AesCtrParams(params.into())
},
// <https://w3c.github.io/webcrypto/#sha-registration>
(ALG_SHA1, Operation::Digest) => {
let params = value_from_js_object::<Algorithm>(cx, value.handle(), can_gc)?;
NormalizedAlgorithm::Algorithm(params.into())
},
(ALG_SHA256, Operation::Digest) => {
let params = value_from_js_object::<Algorithm>(cx, value.handle(), can_gc)?;
NormalizedAlgorithm::Algorithm(params.into())
},
(ALG_SHA384, Operation::Digest) => {
let params = value_from_js_object::<Algorithm>(cx, value.handle(), can_gc)?;
NormalizedAlgorithm::Algorithm(params.into())
},
(ALG_SHA512, Operation::Digest) => {
let params = value_from_js_object::<Algorithm>(cx, value.handle(), can_gc)?;
NormalizedAlgorithm::Algorithm(params.into())
},
_ => return Err(Error::NotSupported),
};
// Step 11. Return normalizedAlgorithm.
Ok(normalized_algorithm)
},
}
}
impl NormalizedAlgorithm {
#[allow(unused)]
fn encrypt(&self, key: &CryptoKey, plaintext: &[u8]) -> Result<Vec<u8>, Error> {
match self {
NormalizedAlgorithm::AesCtrParams(algo) => {
aes_operation::encrypt_aes_ctr(algo, key, plaintext)
},
_ => Err(Error::NotSupported),
}
}
// TODO:
// decrypt
// sign
// verify
#[allow(unused)]
fn digest(&self, message: &[u8]) -> Result<Vec<u8>, Error> {
match self {
NormalizedAlgorithm::Algorithm(algo) => match algo.name.as_str() {
ALG_SHA1 | ALG_SHA256 | ALG_SHA384 | ALG_SHA512 => {
sha_operation::digest(algo, message)
},
_ => Err(Error::NotSupported),
},
_ => Err(Error::NotSupported),
}
}
// TODO:
// derive_bits
// wrap_key
// unwrap_key
// generate_key
// import_key
// export_key
// get_key_length
}

View file

@ -0,0 +1,52 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use aes::cipher::generic_array::GenericArray;
use aes::cipher::{KeyIvInit, StreamCipher};
use aes::{Aes128, Aes192, Aes256};
use crate::dom::bindings::error::Error;
use crate::dom::cryptokey::{CryptoKey, Handle};
use crate::dom::subtlecrypto::SubtleAesCtrParams;
/// <https://w3c.github.io/webcrypto/#aes-ctr-operations-encrypt>
pub(crate) fn encrypt_aes_ctr(
params: &SubtleAesCtrParams,
key: &CryptoKey,
data: &[u8],
) -> Result<Vec<u8>, Error> {
// Step 1. If the counter member of normalizedAlgorithm does not have a length of 16 bytes,
// then throw an OperationError.
// Step 2. If the length member of normalizedAlgorithm is zero or is greater than 128, then
// throw an OperationError.
if params.counter.len() != 16 || params.length == 0 || params.length > 128 {
return Err(Error::Operation);
}
// Step 3. Let ciphertext be the result of performing the CTR Encryption operation described in
// Section 6.5 of [NIST-SP800-38A] using AES as the block cipher, the counter member of
// normalizedAlgorithm as the initial value of the counter block, the length member of
// normalizedAlgorithm as the input parameter m to the standard counter block incrementing
// function defined in Appendix B.1 of [NIST-SP800-38A] and plaintext as the input plaintext.
let mut ciphertext = Vec::from(data);
let counter = GenericArray::from_slice(&params.counter);
match key.handle() {
Handle::Aes128(data) => {
let key_data = GenericArray::from_slice(data);
ctr::Ctr64BE::<Aes128>::new(key_data, counter).apply_keystream(&mut ciphertext)
},
Handle::Aes192(data) => {
let key_data = GenericArray::from_slice(data);
ctr::Ctr64BE::<Aes192>::new(key_data, counter).apply_keystream(&mut ciphertext)
},
Handle::Aes256(data) => {
let key_data = GenericArray::from_slice(data);
ctr::Ctr64BE::<Aes256>::new(key_data, counter).apply_keystream(&mut ciphertext)
},
_ => return Err(Error::Data),
};
// Step 3. Return ciphertext.
Ok(ciphertext)
}

View file

@ -0,0 +1,43 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use aws_lc_rs::digest;
use crate::dom::bindings::error::Error;
use crate::dom::subtlecrypto::{ALG_SHA1, ALG_SHA256, ALG_SHA384, ALG_SHA512, SubtleAlgorithm};
/// <https://w3c.github.io/webcrypto/#sha-operations-digest>
pub(crate) fn digest(
nomrmalized_algorithm: &SubtleAlgorithm,
message: &[u8],
) -> Result<Vec<u8>, Error> {
// Step 1.
// If the name member of normalizedAlgorithm is a cases-sensitive string match for "SHA-1":
// Let result be the result of performing the SHA-1 hash function defined in Section 6.1 of
// [FIPS-180-4] using message as the input message, M.
// If the name member of normalizedAlgorithm is a cases-sensitive string match for "SHA-256":
// Let result be the result of performing the SHA-256 hash function defined in Section 6.2
// of [FIPS-180-4] using message as the input message, M.
// If the name member of normalizedAlgorithm is a cases-sensitive string match for "SHA-384":
// Let result be the result of performing the SHA-384 hash function defined in Section 6.5
// of [FIPS-180-4] using message as the input message, M.
// If the name member of normalizedAlgorithm is a cases-sensitive string match for "SHA-512":
// Let result be the result of performing the SHA-512 hash function defined in Section 6.4
// of [FIPS-180-4] using message as the input message, M.
// Step 2. If performing the operation results in an error, then throw an OperationError.
let result = match nomrmalized_algorithm.name.as_str() {
ALG_SHA1 => digest::digest(&digest::SHA1_FOR_LEGACY_USE_ONLY, message)
.as_ref()
.to_vec(),
ALG_SHA256 => digest::digest(&digest::SHA256, message).as_ref().to_vec(),
ALG_SHA384 => digest::digest(&digest::SHA384, message).as_ref().to_vec(),
ALG_SHA512 => digest::digest(&digest::SHA512, message).as_ref().to_vec(),
_ => {
return Err(Error::NotSupported);
},
};
// Step 3. Return result.
Ok(result)
}