use std::borrow::Cow;
use deno_core::v8;
use deno_core::webidl::ContextFn;
use deno_core::webidl::WebIdlConverter;
use deno_core::webidl::WebIdlError;
use deno_error::JsErrorBox;
use crate::CryptoError;
use crate::KeyData;
use crate::SignArg;
use crate::crypto_key::CryptoKeyType;
use crate::ed25519::ed25519_sign_into;
use crate::key::Algorithm;
use crate::key::CryptoHash;
use crate::key::CryptoNamedCurve;
use crate::mldsa::mldsa_sign;
use crate::shared::ShaHash;
use crate::sign_key_sync;
use crate::subtle_encrypt::extract_name_and_obj;
use crate::subtle_encrypt::v8_str;
use crate::subtle_key::SubtleKey;
pub enum SubtleSignParams {
RsassaPkcs1v15,
RsaPss {
salt_length: u32,
},
Ecdsa {
hash: String,
},
Hmac,
Kmac {
name: &'static str,
output_length: u32,
customization: Option<Vec<u8>>,
},
Ed25519,
MlDsa {
variant: u8,
context: Option<Vec<u8>>,
},
SlhDsa {
variant: crate::slhdsa::SlhDsaVariantId,
context: Option<Vec<u8>>,
},
Unknown(String),
}
impl SubtleSignParams {
pub fn canonical_name(&self) -> &str {
match self {
Self::RsassaPkcs1v15 => "RSASSA-PKCS1-v1_5",
Self::RsaPss { .. } => "RSA-PSS",
Self::Ecdsa { .. } => "ECDSA",
Self::Hmac => "HMAC",
Self::Kmac { name, .. } => name,
Self::Ed25519 => "Ed25519",
Self::MlDsa { variant, .. } => match variant {
0 => "ML-DSA-44",
1 => "ML-DSA-65",
_ => "ML-DSA-87",
},
Self::SlhDsa { variant, .. } => crate::slhdsa::params(*variant).name,
Self::Unknown(n) => n,
}
}
}
impl<'a> WebIdlConverter<'a> for SubtleSignParams {
type Options = ();
fn convert<'b>(
scope: &mut v8::PinScope<'a, '_>,
value: v8::Local<'a, v8::Value>,
prefix: Cow<'static, str>,
context: ContextFn<'b>,
_options: &Self::Options,
) -> Result<Self, WebIdlError> {
let (name_str, maybe_obj) =
extract_name_and_obj(scope, value, prefix.clone(), context.borrowed())?;
let Some(canonical) = canonical_sign_name(&name_str) else {
return Ok(Self::Unknown(name_str));
};
match canonical {
"RSASSA-PKCS1-v1_5" => Ok(Self::RsassaPkcs1v15),
"RSA-PSS" => {
let obj =
maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?;
let salt_length = read_required_u32(
scope,
obj,
"saltLength",
prefix.clone(),
&context,
)?;
Ok(Self::RsaPss { salt_length })
}
"ECDSA" => {
let obj =
maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?;
let hash =
read_required_hash(scope, obj, "hash", prefix.clone(), &context)?;
Ok(Self::Ecdsa { hash })
}
"HMAC" => Ok(Self::Hmac),
"KMAC128" | "KMAC256" => {
let obj =
maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?;
let output_length = read_required_u32(
scope,
obj,
"outputLength",
prefix.clone(),
&context,
)?;
let customization = read_optional_buffer_source(
scope,
obj,
"customization",
prefix.clone(),
&context,
)?;
Ok(Self::Kmac {
name: canonical,
output_length,
customization,
})
}
"Ed25519" => Ok(Self::Ed25519),
"ML-DSA-44" | "ML-DSA-65" | "ML-DSA-87" => {
let variant = match canonical {
"ML-DSA-44" => 0,
"ML-DSA-65" => 1,
_ => 2,
};
let context_bytes = match maybe_obj {
Some(o) => read_optional_buffer_source(
scope,
o,
"context",
prefix.clone(),
&context,
)?,
None => None,
};
Ok(Self::MlDsa {
variant,
context: context_bytes,
})
}
_ if let Some(variant) = crate::slhdsa::variant_from_name(canonical) => {
let context_bytes = match maybe_obj {
Some(o) => read_optional_buffer_source(
scope,
o,
"context",
prefix.clone(),
&context,
)?,
None => None,
};
Ok(Self::SlhDsa {
variant,
context: context_bytes,
})
}
_ => unreachable!(),
}
}
}
fn canonical_sign_name(name: &str) -> Option<&'static str> {
const NAMES: &[&str] = &[
"RSASSA-PKCS1-v1_5",
"RSA-PSS",
"ECDSA",
"HMAC",
"KMAC128",
"KMAC256",
"Ed25519",
"ML-DSA-44",
"ML-DSA-65",
"ML-DSA-87",
"SLH-DSA-SHA2-128s",
"SLH-DSA-SHA2-128f",
"SLH-DSA-SHA2-192s",
"SLH-DSA-SHA2-192f",
"SLH-DSA-SHA2-256s",
"SLH-DSA-SHA2-256f",
"SLH-DSA-SHAKE-128s",
"SLH-DSA-SHAKE-128f",
"SLH-DSA-SHAKE-192s",
"SLH-DSA-SHAKE-192f",
"SLH-DSA-SHAKE-256s",
"SLH-DSA-SHAKE-256f",
];
NAMES.iter().copied().find(|n| n.eq_ignore_ascii_case(name))
}
fn missing_dict(
prefix: Cow<'static, str>,
context: &ContextFn<'_>,
) -> WebIdlError {
WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error("Algorithm requires a parameter dictionary"),
)
}
pub(crate) fn read_required_u32<'a, 'b>(
scope: &mut v8::PinScope<'a, '_>,
obj: v8::Local<'a, v8::Object>,
field: &'static str,
prefix: Cow<'static, str>,
context: &ContextFn<'b>,
) -> Result<u32, WebIdlError> {
let key = v8_str(scope, field);
let val = obj
.get(scope, key.into())
.unwrap_or_else(|| v8::undefined(scope).into());
if val.is_undefined() {
return Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!("required dictionary member '{field}'")),
));
}
let n = val.number_value(scope).ok_or_else(|| {
WebIdlError::other(
prefix.clone(),
context.borrowed(),
JsErrorBox::type_error(format!("'{field}' must be convertible to u32")),
)
})?;
if !n.is_finite() || n.trunc() < 0.0 || n.trunc() > u32::MAX as f64 {
return Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!(
"'{field}' is outside the accepted range for [EnforceRange] unsigned long"
)),
));
}
Ok(n.trunc() as u32)
}
pub(crate) fn read_required_hash<'a, 'b>(
scope: &mut v8::PinScope<'a, '_>,
obj: v8::Local<'a, v8::Object>,
field: &'static str,
prefix: Cow<'static, str>,
context: &ContextFn<'b>,
) -> Result<String, WebIdlError> {
let key = v8_str(scope, field);
let val = obj
.get(scope, key.into())
.unwrap_or_else(|| v8::undefined(scope).into());
if val.is_undefined() {
return Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!("required dictionary member '{field}'")),
));
}
if val.is_string() {
Ok(val.to_rust_string_lossy(scope))
} else if let Ok(obj) = v8::Local::<v8::Object>::try_from(val) {
let name_key = v8_str(scope, "name");
let name_val = obj
.get(scope, name_key.into())
.unwrap_or_else(|| v8::undefined(scope).into());
let s = name_val.to_string(scope).ok_or_else(|| {
WebIdlError::other(
prefix.clone(),
context.borrowed(),
JsErrorBox::type_error(format!("'{field}.name' is not a DOMString")),
)
})?;
Ok(s.to_rust_string_lossy(scope))
} else {
Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!(
"'{field}' must be a HashAlgorithmIdentifier"
)),
))
}
}
pub(crate) fn read_optional_buffer_source<'a, 'b>(
scope: &mut v8::PinScope<'a, '_>,
obj: v8::Local<'a, v8::Object>,
field: &'static str,
prefix: Cow<'static, str>,
context: &ContextFn<'b>,
) -> Result<Option<Vec<u8>>, WebIdlError> {
let key = v8_str(scope, field);
let val = obj
.get(scope, key.into())
.unwrap_or_else(|| v8::undefined(scope).into());
if val.is_undefined() || val.is_null() {
return Ok(None);
}
if let Ok(view) = v8::Local::<v8::ArrayBufferView>::try_from(val) {
if let Some(ab) = view.buffer(scope) {
let ab_val: v8::Local<v8::Value> = ab.into();
if ab_val.is_shared_array_buffer() {
return Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!(
"'{field}' is a view on a SharedArrayBuffer, which is not allowed"
)),
));
}
}
let byte_length = view.byte_length();
if byte_length == 0 {
return Ok(Some(Vec::new()));
}
let byte_offset = view.byte_offset();
let ab = view.buffer(scope).ok_or_else(|| {
WebIdlError::other(
prefix.clone(),
context.borrowed(),
JsErrorBox::type_error(format!(
"'{field}' backing ArrayBuffer is detached"
)),
)
})?;
unsafe {
let base = ab.data().unwrap().as_ptr() as *const u8;
return Ok(Some(
std::slice::from_raw_parts(base.add(byte_offset), byte_length).to_vec(),
));
}
}
if val.is_shared_array_buffer() {
return Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!(
"'{field}' is a SharedArrayBuffer, which is not allowed"
)),
));
}
if let Ok(ab) = v8::Local::<v8::ArrayBuffer>::try_from(val) {
let byte_length = ab.byte_length();
if byte_length == 0 {
return Ok(Some(Vec::new()));
}
unsafe {
let base = ab.data().unwrap().as_ptr() as *const u8;
return Ok(Some(std::slice::from_raw_parts(base, byte_length).to_vec()));
}
}
Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!("'{field}' is not a BufferSource")),
))
}
const SUPPORTED_NAMED_CURVES: &[&str] = &["P-256", "P-384", "P-521"];
pub fn run(
params: SubtleSignParams,
key: SubtleKey,
data: Vec<u8>,
) -> Result<Vec<u8>, CryptoError> {
if params.canonical_name() != key.algorithm_name {
return Err(invalid_access(format!(
"Signing algorithm '{}' does not match key algorithm",
params.canonical_name()
)));
}
if !key.has_usage("sign") {
return Err(invalid_access(
"The requested operation is not valid for the provided key".to_string(),
));
}
match params {
SubtleSignParams::RsassaPkcs1v15 => {
if key.key_type != CryptoKeyType::Private {
return Err(invalid_access("Key type not supported".to_string()));
}
let hash = key.algorithm_hash.ok_or_else(|| {
op_error("RSASSA-PKCS1-v1_5 key is missing 'hash'".to_string())
})?;
let key_data: KeyData = (&key.raw).into();
let args = SignArg::new(
Algorithm::RsassaPkcs1v15,
None,
Some(sha_to_crypto_hash(hash)),
None,
);
sign_key_sync(key_data, args, &data)
}
SubtleSignParams::RsaPss { salt_length } => {
if key.key_type != CryptoKeyType::Private {
return Err(invalid_access("Key type not supported".to_string()));
}
let hash = key
.algorithm_hash
.ok_or_else(|| op_error("RSA-PSS key is missing 'hash'".to_string()))?;
let key_data: KeyData = (&key.raw).into();
let args = SignArg::new(
Algorithm::RsaPss,
Some(salt_length),
Some(sha_to_crypto_hash(hash)),
None,
);
sign_key_sync(key_data, args, &data)
}
SubtleSignParams::Ecdsa { hash } => {
let hash =
crate::subtle_generate_key::sha_from_name(&hash).ok_or_else(|| {
not_supported(format!("Unrecognized hash algorithm: {hash}"))
})?;
if key.key_type != CryptoKeyType::Private {
return Err(invalid_access("Key type not supported".to_string()));
}
let curve_name =
key.algorithm_named_curve.as_deref().ok_or_else(|| {
op_error("ECDSA key is missing 'namedCurve'".to_string())
})?;
if !SUPPORTED_NAMED_CURVES.contains(&curve_name) {
return Err(not_supported("Curve not supported".to_string()));
}
let named_curve = parse_named_curve(curve_name)
.ok_or_else(|| not_supported("Curve not supported".to_string()))?;
let key_data: KeyData = (&key.raw).into();
let args = SignArg::new(
Algorithm::Ecdsa,
None,
Some(sha_to_crypto_hash(hash)),
Some(named_curve),
);
sign_key_sync(key_data, args, &data)
}
SubtleSignParams::Hmac => {
let hash = key
.algorithm_hash
.ok_or_else(|| op_error("HMAC key is missing 'hash'".to_string()))?;
let key_data: KeyData = (&key.raw).into();
let args = SignArg::new(
Algorithm::Hmac,
None,
Some(sha_to_crypto_hash(hash)),
None,
);
sign_key_sync(key_data, args, &data)
}
SubtleSignParams::Kmac {
output_length,
customization,
..
} => run_kmac(&key, &data, output_length, customization),
SubtleSignParams::Ed25519 => {
if key.key_type != CryptoKeyType::Private {
return Err(invalid_access("Key type not supported".to_string()));
}
const SIGNATURE_LEN: usize = 32 * 2;
let mut signature = vec![0u8; SIGNATURE_LEN];
if !ed25519_sign_into(key.raw.bytes(), &data, &mut signature) {
return Err(op_error("Failed to sign".to_string()));
}
Ok(signature)
}
SubtleSignParams::MlDsa { variant, context } => {
if key.key_type != CryptoKeyType::Private {
return Err(invalid_access("Key type not supported".to_string()));
}
mldsa_sign(
variant,
key.raw.expanded_private_key(),
&data,
context.as_deref(),
)
.map_err(|e| CryptoError::Other(JsErrorBox::from_err(e)))
}
SubtleSignParams::SlhDsa { variant, context } => {
if key.key_type != CryptoKeyType::Private {
return Err(invalid_access("Key type not supported".to_string()));
}
crate::slhdsa::sign(
variant,
key.raw.expanded_private_key(),
&data,
context.as_deref(),
)
.map_err(|e| CryptoError::Other(JsErrorBox::from_err(e)))
}
SubtleSignParams::Unknown(name) => Err(not_supported(format!(
"Algorithm '{name}' is not supported"
))),
}
}
pub(crate) fn run_kmac(
key: &SubtleKey,
data: &[u8],
output_length_bits: u32,
customization: Option<Vec<u8>>,
) -> Result<Vec<u8>, CryptoError> {
if key.key_type != CryptoKeyType::Secret {
return Err(invalid_access("Key type not supported".to_string()));
}
if output_length_bits == 0 || !output_length_bits.is_multiple_of(8) {
return Err(op_error("Invalid KMAC outputLength".to_string()));
}
let customization = customization.unwrap_or_default();
let mut mac = match key.algorithm_name.as_str() {
"KMAC128" => tiny_keccak::Kmac::v128(key.raw.bytes(), &customization),
"KMAC256" => tiny_keccak::Kmac::v256(key.raw.bytes(), &customization),
_ => {
return Err(invalid_access(
"KMAC operation does not match key algorithm".to_string(),
));
}
};
tiny_keccak::Hasher::update(&mut mac, data);
let mut out = vec![0u8; (output_length_bits / 8) as usize];
tiny_keccak::Hasher::finalize(mac, &mut out);
Ok(out)
}
fn sha_to_crypto_hash(h: ShaHash) -> CryptoHash {
match h {
ShaHash::Sha1 => CryptoHash::Sha1,
ShaHash::Sha256 => CryptoHash::Sha256,
ShaHash::Sha384 => CryptoHash::Sha384,
ShaHash::Sha512 => CryptoHash::Sha512,
ShaHash::Sha3_256 => CryptoHash::Sha3_256,
ShaHash::Sha3_384 => CryptoHash::Sha3_384,
ShaHash::Sha3_512 => CryptoHash::Sha3_512,
}
}
fn parse_named_curve(name: &str) -> Option<CryptoNamedCurve> {
match name {
"P-256" => Some(CryptoNamedCurve::P256),
"P-384" => Some(CryptoNamedCurve::P384),
"P-521" => Some(CryptoNamedCurve::P521),
_ => None,
}
}
fn invalid_access(msg: String) -> CryptoError {
CryptoError::Other(JsErrorBox::new("DOMExceptionInvalidAccessError", msg))
}
fn op_error(msg: String) -> CryptoError {
CryptoError::Other(JsErrorBox::new("DOMExceptionOperationError", msg))
}
fn not_supported(msg: String) -> CryptoError {
CryptoError::Other(JsErrorBox::new("DOMExceptionNotSupportedError", msg))
}