mirror of
https://github.com/servo/servo.git
synced 2025-09-10 15:08:21 +01:00
webcrypto: Reduce usage of standalone helper functions for JWK format (#39084)
Reduce the reliance on standalone helper functions for handling JWK format. Instead, those functionalities are now integrated into the `JsonWebKey` type generated by script_binding, via the local trait `JsonWebKeyExt`, for internal use. The `parse_jwk` function remains for now. It will be removed when once we refactor `SubtleCrypto::ImportKey` to support a more generic approach across different cryptographic algorithms. Testing: Refactoring. Existing WPT tests should suffice. --------- Signed-off-by: Kingsley Yung <kingsley@kkoyung.dev>
This commit is contained in:
parent
ae5b40ebf9
commit
f722419861
1 changed files with 119 additions and 73 deletions
|
@ -5,6 +5,7 @@
|
||||||
use std::num::NonZero;
|
use std::num::NonZero;
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use aes::cipher::block_padding::Pkcs7;
|
use aes::cipher::block_padding::Pkcs7;
|
||||||
use aes::cipher::generic_array::GenericArray;
|
use aes::cipher::generic_array::GenericArray;
|
||||||
|
@ -18,8 +19,9 @@ 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::{JS_NewObject, JSObject};
|
||||||
use js::jsval::ObjectValue;
|
use js::jsval::{ObjectValue, UndefinedValue};
|
||||||
use js::rust::MutableHandleObject;
|
use js::rust::MutableHandleObject;
|
||||||
|
use js::rust::wrappers::JS_ParseJSON;
|
||||||
use js::typedarray::ArrayBufferU8;
|
use js::typedarray::ArrayBufferU8;
|
||||||
use servo_rand::{RngCore, ServoRng};
|
use servo_rand::{RngCore, ServoRng};
|
||||||
|
|
||||||
|
@ -32,7 +34,7 @@ use crate::dom::bindings::codegen::Bindings::SubtleCryptoBinding::{
|
||||||
AesCbcParams, AesCtrParams, AesDerivedKeyParams, AesGcmParams, AesKeyAlgorithm,
|
AesCbcParams, AesCtrParams, AesDerivedKeyParams, AesGcmParams, AesKeyAlgorithm,
|
||||||
AesKeyGenParams, Algorithm, AlgorithmIdentifier, HkdfParams, HmacImportParams,
|
AesKeyGenParams, Algorithm, AlgorithmIdentifier, HkdfParams, HmacImportParams,
|
||||||
HmacKeyAlgorithm, HmacKeyGenParams, JsonWebKey, KeyAlgorithm, KeyFormat, Pbkdf2Params,
|
HmacKeyAlgorithm, HmacKeyGenParams, JsonWebKey, KeyAlgorithm, KeyFormat, Pbkdf2Params,
|
||||||
SubtleCryptoMethods,
|
RsaOtherPrimesInfo, SubtleCryptoMethods,
|
||||||
};
|
};
|
||||||
use crate::dom::bindings::codegen::UnionTypes::{
|
use crate::dom::bindings::codegen::UnionTypes::{
|
||||||
ArrayBufferViewOrArrayBuffer, ArrayBufferViewOrArrayBufferOrJsonWebKey,
|
ArrayBufferViewOrArrayBuffer, ArrayBufferViewOrArrayBufferOrJsonWebKey,
|
||||||
|
@ -1069,7 +1071,7 @@ impl SubtleCryptoMethods<crate::DomTypeHolder> for SubtleCrypto {
|
||||||
let import_key_bytes = match format {
|
let import_key_bytes = match format {
|
||||||
KeyFormat::Raw | KeyFormat::Spki | KeyFormat::Pkcs8 => bytes,
|
KeyFormat::Raw | KeyFormat::Spki | KeyFormat::Pkcs8 => bytes,
|
||||||
KeyFormat::Jwk => {
|
KeyFormat::Jwk => {
|
||||||
match parse_jwk(&bytes, normalized_key_algorithm.clone(), extractable, &key_usages) {
|
match parse_jwk(cx, &bytes, normalized_key_algorithm.clone(), extractable, &key_usages) {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
promise.reject_error(e, CanGc::note());
|
promise.reject_error(e, CanGc::note());
|
||||||
|
@ -3049,41 +3051,30 @@ impl KeyWrapAlgorithm {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <https://w3c.github.io/webcrypto/#concept-parse-a-jwk>
|
|
||||||
fn parse_jwk(
|
fn parse_jwk(
|
||||||
|
cx: JSContext,
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
import_alg: ImportKeyAlgorithm,
|
import_alg: ImportKeyAlgorithm,
|
||||||
extractable: bool,
|
extractable: bool,
|
||||||
key_usages: &[KeyUsage],
|
key_usages: &[KeyUsage],
|
||||||
) -> Result<Vec<u8>, Error> {
|
) -> Result<Vec<u8>, Error> {
|
||||||
let value = serde_json::from_slice(bytes)
|
let jwk = JsonWebKey::parse(cx, bytes)?;
|
||||||
.map_err(|_| Error::Type("Failed to parse JWK string".into()))?;
|
|
||||||
let serde_json::Value::Object(obj) = value else {
|
|
||||||
return Err(Error::Data);
|
|
||||||
};
|
|
||||||
|
|
||||||
let kty = get_jwk_string(&obj, "kty")?;
|
let kty = jwk.kty.as_ref().ok_or(Error::Data)?;
|
||||||
let ext = get_jwk_bool(&obj, "ext")?;
|
let ext = jwk.ext.ok_or(Error::Data)?;
|
||||||
if !ext && extractable {
|
if !ext && extractable {
|
||||||
return Err(Error::Data);
|
return Err(Error::Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the key_ops field of jwk is present, and is invalid according to the requirements of JSON Web Key [JWK]
|
// If the key_ops field of jwk is present, and is invalid according to the requirements of JSON Web Key [JWK]
|
||||||
// or does not contain all of the specified usages values, then throw a DataError.
|
// or does not contain all of the specified usages values, then throw a DataError.
|
||||||
if let Some(serde_json::Value::Array(key_ops)) = obj.get("key_ops") {
|
if jwk.key_ops.is_some() {
|
||||||
if key_ops.iter().any(|op| {
|
let key_ops_usages = jwk.get_usages_from_key_ops()?;
|
||||||
let op_string = match op {
|
|
||||||
serde_json::Value::String(op_string) => op_string,
|
if !key_usages
|
||||||
_ => return true,
|
.iter()
|
||||||
};
|
.all(|usage| key_ops_usages.contains(usage))
|
||||||
let usage = match usage_from_str(op_string) {
|
{
|
||||||
Ok(usage) => usage,
|
|
||||||
Err(_) => {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
!key_usages.contains(&usage)
|
|
||||||
}) {
|
|
||||||
return Err(Error::Data);
|
return Err(Error::Data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3096,8 +3087,8 @@ fn parse_jwk(
|
||||||
if kty != "oct" {
|
if kty != "oct" {
|
||||||
return Err(Error::Data);
|
return Err(Error::Data);
|
||||||
}
|
}
|
||||||
let k = get_jwk_string(&obj, "k")?;
|
let k = jwk.k.as_ref().ok_or(Error::Data)?;
|
||||||
let alg = get_jwk_string(&obj, "alg")?;
|
let alg = jwk.alg.as_ref().ok_or(Error::Data)?;
|
||||||
|
|
||||||
let data = base64::engine::general_purpose::STANDARD_NO_PAD
|
let data = base64::engine::general_purpose::STANDARD_NO_PAD
|
||||||
.decode(k.as_bytes())
|
.decode(k.as_bytes())
|
||||||
|
@ -3123,10 +3114,8 @@ fn parse_jwk(
|
||||||
return Err(Error::Data);
|
return Err(Error::Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(serde_json::Value::String(use_)) = obj.get("use") {
|
if !key_usages.is_empty() && jwk.use_.as_ref().is_some_and(|use_| use_ != "enc") {
|
||||||
if use_ != "enc" {
|
return Err(Error::Data);
|
||||||
return Err(Error::Data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
|
@ -3135,8 +3124,8 @@ fn parse_jwk(
|
||||||
if kty != "oct" {
|
if kty != "oct" {
|
||||||
return Err(Error::Data);
|
return Err(Error::Data);
|
||||||
}
|
}
|
||||||
let k = get_jwk_string(&obj, "k")?;
|
let k = jwk.k.as_ref().ok_or(Error::Data)?;
|
||||||
let alg = get_jwk_string(&obj, "alg")?;
|
let alg = jwk.alg.as_ref().ok_or(Error::Data)?;
|
||||||
|
|
||||||
let expected_alg = match params.hash {
|
let expected_alg = match params.hash {
|
||||||
DigestAlgorithm::Sha1 => "HS1",
|
DigestAlgorithm::Sha1 => "HS1",
|
||||||
|
@ -3149,10 +3138,8 @@ fn parse_jwk(
|
||||||
return Err(Error::Data);
|
return Err(Error::Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(serde_json::Value::String(use_)) = obj.get("use") {
|
if !key_usages.is_empty() && jwk.use_.as_ref().is_some_and(|use_| use_ != "sign") {
|
||||||
if use_ != "sign" {
|
return Err(Error::Data);
|
||||||
return Err(Error::Data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
base64::engine::general_purpose::STANDARD_NO_PAD
|
base64::engine::general_purpose::STANDARD_NO_PAD
|
||||||
|
@ -3163,43 +3150,102 @@ fn parse_jwk(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_jwk_string(
|
#[expect(unused)]
|
||||||
value: &serde_json::Map<String, serde_json::Value>,
|
trait RsaOtherPrimesInfoExt {
|
||||||
key: &str,
|
fn from_value(value: &serde_json::Value) -> Result<RsaOtherPrimesInfo, Error>;
|
||||||
) -> Result<String, Error> {
|
|
||||||
let s = value
|
|
||||||
.get(key)
|
|
||||||
.ok_or(Error::Data)?
|
|
||||||
.as_str()
|
|
||||||
.ok_or(Error::Data)?;
|
|
||||||
Ok(s.to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_jwk_bool(
|
impl RsaOtherPrimesInfoExt for RsaOtherPrimesInfo {
|
||||||
value: &serde_json::Map<String, serde_json::Value>,
|
fn from_value(value: &serde_json::Value) -> Result<RsaOtherPrimesInfo, Error> {
|
||||||
key: &str,
|
let serde_json::Value::Object(object) = value else {
|
||||||
) -> Result<bool, Error> {
|
|
||||||
let b = value
|
|
||||||
.get(key)
|
|
||||||
.ok_or(Error::Data)?
|
|
||||||
.as_bool()
|
|
||||||
.ok_or(Error::Data)?;
|
|
||||||
Ok(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn usage_from_str(op: &str) -> Result<KeyUsage, Error> {
|
|
||||||
let usage = match op {
|
|
||||||
"encrypt" => KeyUsage::Encrypt,
|
|
||||||
"decrypt" => KeyUsage::Decrypt,
|
|
||||||
"sign" => KeyUsage::Sign,
|
|
||||||
"verify" => KeyUsage::Verify,
|
|
||||||
"deriveKey" => KeyUsage::DeriveKey,
|
|
||||||
"deriveBits" => KeyUsage::DeriveBits,
|
|
||||||
"wrapKey" => KeyUsage::WrapKey,
|
|
||||||
"unwrapKey" => KeyUsage::UnwrapKey,
|
|
||||||
_ => {
|
|
||||||
return Err(Error::Data);
|
return Err(Error::Data);
|
||||||
},
|
};
|
||||||
};
|
|
||||||
Ok(usage)
|
let mut rsa_other_primes_info: RsaOtherPrimesInfo = Default::default();
|
||||||
|
for (key, value) in object {
|
||||||
|
match key.as_str() {
|
||||||
|
"r" => {
|
||||||
|
rsa_other_primes_info.r =
|
||||||
|
Some(DOMString::from(value.as_str().ok_or(Error::Data)?))
|
||||||
|
},
|
||||||
|
"d" => {
|
||||||
|
rsa_other_primes_info.d =
|
||||||
|
Some(DOMString::from(value.as_str().ok_or(Error::Data)?))
|
||||||
|
},
|
||||||
|
"t" => {
|
||||||
|
rsa_other_primes_info.t =
|
||||||
|
Some(DOMString::from(value.as_str().ok_or(Error::Data)?))
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
// Additional members can be present in the JWK; if not understood by
|
||||||
|
// implementations encountering them, they MUST be ignored.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(rsa_other_primes_info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait JsonWebKeyExt {
|
||||||
|
fn parse(cx: JSContext, data: &[u8]) -> Result<JsonWebKey, Error>;
|
||||||
|
fn get_usages_from_key_ops(&self) -> Result<Vec<KeyUsage>, Error>;
|
||||||
|
#[expect(unused)]
|
||||||
|
fn get_rsa_other_primes_info_from_oth(&self) -> Result<&[RsaOtherPrimesInfo], Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsonWebKeyExt for JsonWebKey {
|
||||||
|
/// <https://w3c.github.io/webcrypto/#concept-parse-a-jwk>
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
fn parse(cx: JSContext, data: &[u8]) -> Result<JsonWebKey, Error> {
|
||||||
|
// Step 1. Let data be the sequence of bytes to be parsed.
|
||||||
|
// (It is given as a method paramter.)
|
||||||
|
|
||||||
|
// Step 2. Let json be the Unicode string that results from interpreting data according to UTF-8.
|
||||||
|
let json = String::from_utf8_lossy(data);
|
||||||
|
|
||||||
|
// Step 3. Convert json to UTF-16.
|
||||||
|
let json: Vec<_> = json.encode_utf16().collect();
|
||||||
|
|
||||||
|
// Step 4. Let result be the object literal that results from executing the JSON.parse
|
||||||
|
// internal function in the context of a new global object, with text argument set to a
|
||||||
|
// JavaScript String containing json.
|
||||||
|
rooted!(in(*cx) let mut result = UndefinedValue());
|
||||||
|
unsafe {
|
||||||
|
if !JS_ParseJSON(*cx, json.as_ptr(), json.len() as u32, result.handle_mut()) {
|
||||||
|
return Err(Error::JSFailed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5. Let key be the result of converting result to the IDL dictionary type of JsonWebKey.
|
||||||
|
let key = match JsonWebKey::new(cx, result.handle()) {
|
||||||
|
Ok(ConversionResult::Success(key)) => key,
|
||||||
|
Ok(ConversionResult::Failure(error)) => {
|
||||||
|
return Err(Error::Type(error.to_string()));
|
||||||
|
},
|
||||||
|
Err(()) => {
|
||||||
|
return Err(Error::JSFailed);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 6. If the kty field of key is not defined, then throw a DataError.
|
||||||
|
if key.kty.is_none() {
|
||||||
|
return Err(Error::Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 7. Result key.
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_usages_from_key_ops(&self) -> Result<Vec<KeyUsage>, Error> {
|
||||||
|
let mut usages = vec![];
|
||||||
|
for op in self.key_ops.as_ref().ok_or(Error::Data)? {
|
||||||
|
usages.push(KeyUsage::from_str(op).map_err(|_| Error::Data)?);
|
||||||
|
}
|
||||||
|
Ok(usages)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_rsa_other_primes_info_from_oth(&self) -> Result<&[RsaOtherPrimesInfo], Error> {
|
||||||
|
self.oth.as_deref().ok_or(Error::Data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue