diff options
author | Daniel Adams <70986246+msub2@users.noreply.github.com> | 2024-11-21 01:44:33 -1000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-11-21 11:44:33 +0000 |
commit | 3d8f99c4e65b64e7231a1b5789e397c750e04569 (patch) | |
tree | b93efc896408230307c4bca7c65fb4ccd0bf973b /components/script/dom | |
parent | 80529ef3588a0e7cf32e9783d5cd671118627ec7 (diff) | |
download | servo-3d8f99c4e65b64e7231a1b5789e397c750e04569.tar.gz servo-3d8f99c4e65b64e7231a1b5789e397c750e04569.zip |
crypto: Include `key_ops` in exported JWKs, support JWK for HMAC import (#34317)
* Improve JWK handling, HMAC import
Signed-off-by: Daniel Adams <msub2official@gmail.com>
* Update expectations
Signed-off-by: Daniel Adams <msub2official@gmail.com>
* Fix logic in parse_jwk, properly stringify key_ops
Signed-off-by: Daniel Adams <msub2official@gmail.com>
---------
Signed-off-by: Daniel Adams <msub2official@gmail.com>
Diffstat (limited to 'components/script/dom')
-rw-r--r-- | components/script/dom/subtlecrypto.rs | 154 |
1 files changed, 130 insertions, 24 deletions
diff --git a/components/script/dom/subtlecrypto.rs b/components/script/dom/subtlecrypto.rs index e3638d34b04..cccb068f58d 100644 --- a/components/script/dom/subtlecrypto.rs +++ b/components/script/dom/subtlecrypto.rs @@ -772,7 +772,6 @@ impl SubtleCryptoMethods for SubtleCrypto { }, }; - // TODO: Figure out a way to Send this data so per-algorithm JWK checks can happen let data = match key_data { ArrayBufferViewOrArrayBufferOrJsonWebKey::ArrayBufferView(view) => view.to_vec(), ArrayBufferViewOrArrayBufferOrJsonWebKey::JsonWebKey(json_web_key) => { @@ -951,12 +950,18 @@ impl SubtleCryptoMethods for SubtleCrypto { promise.reject_error(Error::Syntax); return; }; + let Some(key_ops) = key.key_ops else { + promise.reject_error(Error::Syntax); + return; + }; + let key_ops_str = key_ops.iter().map(|op| op.to_string()).collect::<Vec<String>>(); format!("{{ \"kty\": \"oct\", \"k\": \"{}\", \"alg\": \"{}\", - \"ext\": {} - }}", k, alg, ext) + \"ext\": {}, + \"key_ops\": {:?} + }}", k, alg, ext, key_ops_str) .into_bytes() }, }; @@ -1081,7 +1086,7 @@ impl SubtleCryptoMethods for SubtleCrypto { let import_key_bytes = match format { KeyFormat::Raw | KeyFormat::Spki | KeyFormat::Pkcs8 => bytes, KeyFormat::Jwk => { - match parse_jwk(&bytes, normalized_key_algorithm.clone(), extractable) { + match parse_jwk(&bytes, normalized_key_algorithm.clone(), extractable, &key_usages) { Ok(bytes) => bytes, Err(e) => { promise.reject_error(e); @@ -2224,8 +2229,6 @@ impl SubtleCrypto { 192 => Handle::Aes192(data.to_vec()), 256 => Handle::Aes256(data.to_vec()), _ => { - println!("{}", String::from_utf8_lossy(data)); - println!("Bad data length: {}", data.len()); return Err(Error::Data); }, }; @@ -2233,7 +2236,7 @@ impl SubtleCrypto { let name = DOMString::from(alg_name.to_string()); let cx = GlobalScope::get_cx(); - rooted!(in(*cx) let mut algorithm_object = unsafe {JS_NewObject(*cx, ptr::null()) }); + rooted!(in(*cx) let mut algorithm_object = unsafe { JS_NewObject(*cx, ptr::null()) }); assert!(!algorithm_object.is_null()); AesKeyAlgorithm::from_name_and_size( @@ -2278,6 +2281,11 @@ impl SubtleCrypto { }, _ => return Err(Error::Data), }; + let key_ops = key + .usages() + .iter() + .map(|usage| DOMString::from(usage.as_str())) + .collect::<Vec<DOMString>>(); let jwk = JsonWebKey { alg: Some(alg), crv: None, @@ -2287,7 +2295,7 @@ impl SubtleCrypto { e: None, ext: Some(key.Extractable()), k: Some(k), - key_ops: None, + key_ops: Some(key_ops), kty: Some(DOMString::from("oct")), n: None, oth: None, @@ -2319,7 +2327,8 @@ impl SubtleCrypto { // Step 1. If usages contains a value that is not "deriveKey" or "deriveBits", then throw a SyntaxError. if usages .iter() - .any(|usage| !matches!(usage, KeyUsage::DeriveKey | KeyUsage::DeriveBits)) + .any(|usage| !matches!(usage, KeyUsage::DeriveKey | KeyUsage::DeriveBits)) || + usages.is_empty() { return Err(Error::Syntax); } @@ -2370,9 +2379,11 @@ impl SubtleCrypto { ) -> 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. + // Note: This is not explicitly spec'ed, but also throw a SyntaxError if usages is empty if usages .iter() - .any(|usage| !matches!(usage, KeyUsage::Sign | KeyUsage::Verify)) + .any(|usage| !matches!(usage, KeyUsage::Sign | KeyUsage::Verify)) || + usages.is_empty() { return Err(Error::Syntax); } @@ -2383,19 +2394,15 @@ impl SubtleCrypto { // Step 4. let data; match format { - // If format is "raw": - KeyFormat::Raw => { + // Key data has already been extracted in the case of JWK, + // so both raw and jwk can be treated the same here. + KeyFormat::Raw | KeyFormat::Jwk => { // Step 4.1 Let data be the octet string contained in keyData. - data = key_data; + data = key_data.to_vec(); // 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. @@ -2435,7 +2442,7 @@ impl SubtleCrypto { 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()) }); + 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); @@ -2568,7 +2575,8 @@ impl SubtleCrypto { // Step 2. If usages contains a value that is not "deriveKey" or "deriveBits", then throw a SyntaxError. if usages .iter() - .any(|usage| !matches!(usage, KeyUsage::DeriveKey | KeyUsage::DeriveBits)) + .any(|usage| !matches!(usage, KeyUsage::DeriveKey | KeyUsage::DeriveBits)) || + usages.is_empty() { return Err(Error::Syntax); } @@ -3000,9 +3008,13 @@ impl KeyWrapAlgorithm { } /// <https://w3c.github.io/webcrypto/#concept-parse-a-jwk> -fn parse_jwk(bytes: &[u8], alg: ImportKeyAlgorithm, extractable: bool) -> Result<Vec<u8>, Error> { - let jwk_string = String::from_utf8_lossy(bytes).to_string(); - let value = serde_json::from_str(&jwk_string) +fn parse_jwk( + bytes: &[u8], + import_alg: ImportKeyAlgorithm, + extractable: bool, + key_usages: &[KeyUsage], +) -> Result<Vec<u8>, Error> { + let value = serde_json::from_slice(bytes) .map_err(|_| Error::Type("Failed to parse JWK string".into()))?; let serde_json::Value::Object(obj) = value else { return Err(Error::Data); @@ -3014,7 +3026,27 @@ fn parse_jwk(bytes: &[u8], alg: ImportKeyAlgorithm, extractable: bool) -> Result return Err(Error::Data); } - match alg { + // 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. + if let Some(serde_json::Value::Array(key_ops)) = obj.get("key_ops") { + if key_ops.iter().any(|op| { + let op_string = match op { + serde_json::Value::String(op_string) => op_string, + _ => return true, + }; + let usage = match usage_from_str(op_string) { + Ok(usage) => usage, + Err(_) => { + return true; + }, + }; + !key_usages.contains(&usage) + }) { + return Err(Error::Data); + } + } + + match import_alg { ImportKeyAlgorithm::AesCbc | ImportKeyAlgorithm::AesCtr | ImportKeyAlgorithm::AesKw | @@ -3023,6 +3055,63 @@ fn parse_jwk(bytes: &[u8], alg: ImportKeyAlgorithm, extractable: bool) -> Result return Err(Error::Data); } let k = get_jwk_string(&obj, "k")?; + let alg = get_jwk_string(&obj, "alg")?; + + let data = base64::engine::general_purpose::STANDARD_NO_PAD + .decode(k.as_bytes()) + .map_err(|_| Error::Data)?; + + let expected_alg = match (data.len() * 8, &import_alg) { + (128, ImportKeyAlgorithm::AesCbc) => "A128CBC", + (128, ImportKeyAlgorithm::AesCtr) => "A128CTR", + (128, ImportKeyAlgorithm::AesKw) => "A128KW", + (128, ImportKeyAlgorithm::AesGcm) => "A128GCM", + (192, ImportKeyAlgorithm::AesCbc) => "A192CBC", + (192, ImportKeyAlgorithm::AesCtr) => "A192CTR", + (192, ImportKeyAlgorithm::AesKw) => "A192KW", + (192, ImportKeyAlgorithm::AesGcm) => "A192GCM", + (256, ImportKeyAlgorithm::AesCbc) => "A256CBC", + (256, ImportKeyAlgorithm::AesCtr) => "A256CTR", + (256, ImportKeyAlgorithm::AesKw) => "A256KW", + (256, ImportKeyAlgorithm::AesGcm) => "A256GCM", + _ => return Err(Error::Data), + }; + + if alg != expected_alg { + return Err(Error::Data); + } + + if let Some(serde_json::Value::String(use_)) = obj.get("use") { + if use_ != "enc" { + return Err(Error::Data); + } + } + + Ok(data) + }, + ImportKeyAlgorithm::Hmac(params) => { + if kty != "oct" { + return Err(Error::Data); + } + let k = get_jwk_string(&obj, "k")?; + let alg = get_jwk_string(&obj, "alg")?; + + let expected_alg = match params.hash { + DigestAlgorithm::Sha1 => "HS1", + DigestAlgorithm::Sha256 => "HS256", + DigestAlgorithm::Sha384 => "HS384", + DigestAlgorithm::Sha512 => "HS512", + }; + + if alg != expected_alg { + return Err(Error::Data); + } + + if let Some(serde_json::Value::String(use_)) = obj.get("use") { + if use_ != "sign" { + return Err(Error::Data); + } + } base64::engine::general_purpose::STANDARD_NO_PAD .decode(k.as_bytes()) @@ -3055,3 +3144,20 @@ fn get_jwk_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); + }, + }; + Ok(usage) +} |