cred: isolate scripts in WASI and move RSA signing to AWS-LC

This commit is contained in:
2026-11-06 22:22:49 +00:00
parent 5b3e1393f5
commit 86c5d8a4e4
217 changed files with 155392 additions and 391 deletions
+80 -80
View File
@@ -8,14 +8,28 @@ use ed25519_dalek::{Signer, SigningKey};
use hmac::{Hmac, Mac};
use rsa::{
pkcs1::DecodeRsaPrivateKey,
pkcs8::{DecodePrivateKey, EncodePublicKey},
pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey},
traits::PublicKeyParts,
};
use serde_json::json;
use sha2::{Digest, Sha256};
use sha2::{Sha384, Sha512};
use sha2::{Sha256, Sha384, Sha512};
use zeroize::Zeroizing;
#[derive(Default, serde::Deserialize)]
#[serde(default)]
struct Jwk {
kty: Zeroizing<String>,
crv: Zeroizing<String>,
k: Option<Zeroizing<String>>,
d: Option<Zeroizing<String>>,
x: Option<Zeroizing<String>>,
y: Option<Zeroizing<String>>,
n: Option<Zeroizing<String>>,
e: Option<Zeroizing<String>>,
p: Option<Zeroizing<String>>,
q: Option<Zeroizing<String>>,
}
pub(super) enum Key {
Ed(SigningKey),
Ec(p256::ecdsa::SigningKey),
@@ -51,7 +65,7 @@ pub(super) fn read(value: &str, format: &str, passphrase: Option<&str>) -> Resul
};
match key.key_data() {
ssh_key::private::KeypairData::Ed25519(k) => {
Key::Ed(SigningKey::from_bytes(&k.private.to_bytes()))
Key::Ed(SigningKey::from_bytes(&Zeroizing::new(k.private.to_bytes())))
}
ssh_key::private::KeypairData::Ecdsa(
k @ ssh_key::private::EcdsaKeypair::NistP256 { .. },
@@ -63,11 +77,16 @@ pub(super) fn read(value: &str, format: &str, passphrase: Option<&str>) -> Resul
// ssh-key 0.6.7's TryFrom<&RsaKeypair> accidentally passes p twice.
// Convert the parsed components explicitly, preserving p and q.
let int = |n: &ssh_key::Mpint| rsa::BigUint::try_from(n).map_err(|_| invalid_key());
let n = int(&k.public.n)?;
let e = int(&k.public.e)?;
let mut d = Zeroizing::new(int(&k.private.d)?);
let mut p = Zeroizing::new(int(&k.private.p)?);
let mut q = Zeroizing::new(int(&k.private.q)?);
let key = rsa::RsaPrivateKey::from_components(
int(&k.public.n)?,
int(&k.public.e)?,
int(&k.private.d)?,
vec![int(&k.private.p)?, int(&k.private.q)?],
n,
e,
std::mem::take(&mut *d),
vec![std::mem::take(&mut *p), std::mem::take(&mut *q)],
)
.map_err(|_| invalid_key())?;
key.validate().map_err(|_| invalid_key())?;
@@ -94,40 +113,39 @@ pub(super) fn read(value: &str, format: &str, passphrase: Option<&str>) -> Resul
_ => return Err(invalid_key()),
}
} else if value.starts_with('{') {
let j: serde_json::Value = serde_json::from_str(value).map_err(|_| invalid_key())?;
let bytes = |name: &str| -> Result<Zeroizing<Vec<u8>>> {
decode(j[name].as_str().ok_or_else(invalid_key)?)
let j: Jwk = serde_json::from_str(value).map_err(|_| invalid_key())?;
let bytes = |value: &Option<Zeroizing<String>>| -> Result<Zeroizing<Vec<u8>>> {
decode(value.as_ref().ok_or_else(invalid_key)?)
};
match j["kty"].as_str() {
Some("oct") => Key::Mac(bytes("k")?),
Some("OKP") if j["crv"] == "Ed25519" => {
let d = bytes("d")?;
match j.kty.as_str() {
"oct" => Key::Mac(bytes(&j.k)?),
"OKP" if j.crv.as_str() == "Ed25519" => {
let d = bytes(&j.d)?;
let k = SigningKey::from_bytes(d.as_slice().try_into().map_err(|_| invalid_key())?);
if bytes("x")?.as_slice() != k.verifying_key().as_bytes() {
if bytes(&j.x)?.as_slice() != k.verifying_key().as_bytes() {
return Err(invalid_key());
}
Key::Ed(k)
}
Some("EC") if j["crv"] == "P-256" => {
"EC" if j.crv.as_str() == "P-256" => {
let k =
p256::ecdsa::SigningKey::from_slice(&bytes("d")?).map_err(|_| invalid_key())?;
p256::ecdsa::SigningKey::from_slice(&bytes(&j.d)?).map_err(|_| invalid_key())?;
let point = k.verifying_key().to_encoded_point(false);
if bytes("x")?.as_slice() != point.x().ok_or_else(invalid_key)?.as_slice()
|| bytes("y")?.as_slice() != point.y().ok_or_else(invalid_key)?.as_slice()
if bytes(&j.x)?.as_slice() != point.x().ok_or_else(invalid_key)?.as_slice()
|| bytes(&j.y)?.as_slice() != point.y().ok_or_else(invalid_key)?.as_slice()
{
return Err(invalid_key());
}
Key::Ec(k)
}
Some("RSA") => {
let int = |name| -> Result<rsa::BigUint> {
Ok(rsa::BigUint::from_bytes_be(&bytes(name)?))
};
"RSA" => {
// Decode every component before creating private integers.
let [n, e, d, p, q] = [
bytes(&j.n)?, bytes(&j.e)?, bytes(&j.d)?, bytes(&j.p)?, bytes(&j.q)?,
];
let int = |bytes: &[u8]| rsa::BigUint::from_bytes_be(bytes);
let k = rsa::RsaPrivateKey::from_components(
int("n")?,
int("e")?,
int("d")?,
vec![int("p")?, int("q")?],
int(&n), int(&e), int(&d), vec![int(&p), int(&q)],
)
.map_err(|_| invalid_key())?;
k.validate().map_err(|_| invalid_key())?;
@@ -190,7 +208,7 @@ pub(super) fn sign_key(key: &Key, format: &str, payload: &[u8]) -> Result<Vec<u8
let s: p256::ecdsa::Signature = k.sign(payload);
s.to_bytes().to_vec()
}
Key::Rsa(k) => rsa_signature(k, format, payload, &mut rsa::rand_core::OsRng)?,
Key::Rsa(k) => rsa_signature(k, format, payload)?,
Key::Mac(k) => {
macro_rules! mac {
($h:ty) => {{
@@ -207,26 +225,25 @@ pub(super) fn sign_key(key: &Key, format: &str, payload: &[u8]) -> Result<Vec<u8
}
})
}
fn rsa_signature(
key: &rsa::RsaPrivateKey,
format: &str,
payload: &[u8],
rng: &mut impl rsa::rand_core::CryptoRngCore,
) -> Result<Vec<u8>> {
if format == "rsa-sha2-512" {
key.sign_with_rng(
rng,
rsa::Pkcs1v15Sign::new::<Sha512>(),
&Sha512::digest(payload),
)
fn rsa_signature(key: &rsa::RsaPrivateKey, format: &str, payload: &[u8]) -> Result<Vec<u8>> {
// SecretDocument zeroizes the DER after AWS-LC imports the private key.
let der = key.to_pkcs8_der().map_err(|_| invalid_key())?;
let key = aws_lc_rs::signature::RsaKeyPair::from_pkcs8(der.as_bytes())
.map_err(|_| invalid_key())?;
let algorithm = if format == "rsa-sha2-512" {
&aws_lc_rs::signature::RSA_PKCS1_SHA512
} else {
key.sign_with_rng(
rng,
rsa::Pkcs1v15Sign::new::<Sha256>(),
&Sha256::digest(payload),
)
}
.map_err(|_| invalid_key())
&aws_lc_rs::signature::RSA_PKCS1_SHA256
};
let mut signature = vec![0; key.public_modulus_len()];
key.sign(
algorithm,
&aws_lc_rs::rand::SystemRandom::new(),
payload,
&mut signature,
)
.map_err(|_| invalid_key())?;
Ok(signature)
}
fn string(out: &mut Vec<u8>, bytes: &[u8]) {
@@ -322,41 +339,24 @@ pub(super) fn public(key: &Key, output: &str) -> Result<String> {
}
#[cfg(test)]
mod blinding_tests {
mod tests {
use super::*;
use rsa::rand_core::{CryptoRng, OsRng, RngCore};
struct Counting(usize);
impl CryptoRng for Counting {}
impl RngCore for Counting {
fn next_u32(&mut self) -> u32 {
self.0 += 4;
OsRng.next_u32()
}
fn next_u64(&mut self) -> u64 {
self.0 += 8;
OsRng.next_u64()
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0 += dest.len();
OsRng.fill_bytes(dest);
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> std::result::Result<(), rsa::rand_core::Error> {
self.0 += dest.len();
OsRng.try_fill_bytes(dest)
}
}
#[test]
fn rsa_uses_blinding_randomness() {
let key = rsa::RsaPrivateKey::new(&mut OsRng, 1024).unwrap();
let mut rng = Counting(0);
for format in ["rs256", "rsa-sha2-256", "rsa-sha2-512"] {
let before = rng.0;
rsa_signature(&key, format, b"synthetic", &mut rng).unwrap();
assert!(
rng.0 > before,
"RSA signing must consume blinding randomness"
);
fn rsa_backend_rejects_weak_keys() {
let key = rsa::RsaPrivateKey::new(&mut rsa::rand_core::OsRng, 1024).unwrap();
assert!(rsa_signature(&key, "rsa-sha2-256", b"synthetic").is_err());
}
#[test]
fn jwk_private_fields_have_zeroizing_ownership() {
let jwk: Jwk = serde_json::from_str(r#"{"kty":"oct","k":"SYNTHETIC"}"#).unwrap();
for field in [
&jwk.k, &jwk.d, &jwk.p, &jwk.q, &jwk.x, &jwk.y, &jwk.n, &jwk.e,
] {
let _: &Option<Zeroizing<String>> = field;
}
let _: &Zeroizing<String> = &jwk.kty;
let _: &Zeroizing<String> = &jwk.crv;
}
}
+1 -1
View File
@@ -33,4 +33,4 @@ pub mod script;
pub mod keys;
pub mod sign;
pub mod oauth2;
pub mod v8;
pub mod quickjs;
+4 -3
View File
@@ -156,9 +156,7 @@ impl Cache {
http: &impl Http,
deadline: Instant,
) -> Result<Token, Error> {
// Baseline preserves the old unchecked policy until the regression runs.
let _ = allowed(item, request);
if Instant::now() >= deadline {
if !allowed(item, request) || Instant::now() >= deadline {
return Err(Error);
}
let key = (item.id.clone(), item.version);
@@ -183,6 +181,9 @@ impl Cache {
item: item.id.clone(),
};
let response = http.send_once(request, deadline).await?;
if !response.status.is_success() {
return Err(Error);
}
let token = decode(&response.body)?;
let mut state = self.state.lock().map_err(|_| Error)?;
if state.epoch != epoch || Instant::now() >= deadline {
+275
View File
@@ -0,0 +1,275 @@
//! Credential scripts run in a fresh, memory-limited WASI instance. Only two calls run at once.
use super::script::{Error, ErrorKind, Limits, Script};
use std::{
sync::{Arc, Condvar, Mutex, OnceLock, mpsc},
time::{Duration, Instant},
};
use wasmtime::{
Config, Engine, Store, StoreLimits, StoreLimitsBuilder, UpdateDeadline,
component::{Component, Linker, ResourceTable},
};
use wasmtime_wasi::{
WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView,
clocks::HostWallClock,
p2::{
bindings::sync::CommandPre,
pipe::{MemoryInputPipe, MemoryOutputPipe},
},
random::Deterministic,
};
use zeroize::Zeroizing;
struct State {
wasi: WasiCtx,
table: ResourceTable,
limits: StoreLimits,
}
impl WasiView for State {
fn ctx(&mut self) -> WasiCtxView<'_> {
WasiCtxView {
ctx: &mut self.wasi,
table: &mut self.table,
}
}
}
struct FixedClock;
impl HostWallClock for FixedClock {
fn resolution(&self) -> Duration {
Duration::from_millis(1)
}
fn now(&self) -> Duration {
Duration::ZERO
}
}
struct Compiled {
engine: Engine,
pre: CommandPre<State>,
active: Mutex<usize>,
available: Condvar,
}
struct Slot<'a>(&'a Compiled);
impl Drop for Slot<'_> {
fn drop(&mut self) {
*self.0.active.lock().unwrap() -= 1;
self.0.available.notify_one();
}
}
impl Compiled {
fn new() -> Result<Arc<Self>, Error> {
let mut config = Config::new();
config.epoch_interruption(true);
// The cache also keeps warm test runs from compiling the component again.
config.cache(wasmtime::Cache::new(Default::default()).ok());
let engine = Engine::new(&config).map_err(|_| failure(ErrorKind::Engine))?;
let component = Component::new(&engine, include_bytes!("../../cred-script/script.wasm"))
.map_err(|_| failure(ErrorKind::Engine))?;
let mut linker = Linker::new(&engine);
wasmtime_wasi::p2::add_to_linker_sync(&mut linker)
.map_err(|_| failure(ErrorKind::Engine))?;
let pre = CommandPre::new(
linker
.instantiate_pre(&component)
.map_err(|_| failure(ErrorKind::Engine))?,
)
.map_err(|_| failure(ErrorKind::Engine))?;
Ok(Arc::new(Self {
engine,
pre,
active: Mutex::new(0),
available: Condvar::new(),
}))
}
fn acquire(&self, deadline: Instant) -> Result<Slot<'_>, Error> {
let mut active = self.active.lock().map_err(|_| failure(ErrorKind::Engine))?;
while *active >= 2 {
let remaining = deadline
.checked_duration_since(Instant::now())
.ok_or_else(|| failure(ErrorKind::Limit))?;
(active, _) = self
.available
.wait_timeout(active, remaining)
.map_err(|_| failure(ErrorKind::Engine))?;
}
if Instant::now() >= deadline {
return Err(failure(ErrorKind::Limit));
}
*active += 1;
Ok(Slot(self))
}
}
static COMPILED: OnceLock<Result<Arc<Compiled>, Error>> = OnceLock::new();
/// Construct once at startup. Compilation is cached; each evaluation gets a fresh Store.
#[derive(Clone)]
pub struct QuickJs {
compiled: Result<Arc<Compiled>, Error>,
}
impl Default for QuickJs {
fn default() -> Self {
Self {
compiled: COMPILED.get_or_init(Compiled::new).clone(),
}
}
}
fn failure(kind: ErrorKind) -> Error {
Error {
kind,
message: match kind {
ErrorKind::Limit => "script time, memory or stack limit exceeded",
ErrorKind::Syntax => "invalid script or JSON",
ErrorKind::Throw => "script threw or rejected",
ErrorKind::NonJson => "result is not JSON",
ErrorKind::Engine => "script engine failure",
}
.into(),
}
}
impl Script for QuickJs {
fn evaluate(
&self,
source: &str,
input: &str,
export: Option<&str>,
limits: Limits,
) -> Result<String, Error> {
let compiled = self.compiled.as_ref().map_err(Clone::clone)?;
let time = limits.max_time_ms.unwrap_or(100).clamp(1, 1000);
let heap = limits
.max_memory_bytes
.unwrap_or(8 * 1024 * 1024)
.clamp(256 * 1024, 32 * 1024 * 1024);
let deadline = Instant::now() + Duration::from_millis(time);
let _slot = compiled.acquire(deadline)?;
if source
.len()
.saturating_add(input.len())
.saturating_add(export.map_or(0, str::len))
> heap
{
return Err(failure(ErrorKind::Limit));
}
// WASI's synchronous adapter must not enter a caller's Tokio runtime.
std::thread::scope(|scope| {
let worker = std::thread::Builder::new()
.name("cred-script".into())
.spawn_scoped(scope, move || {
#[derive(serde::Serialize)]
struct Request<'a> {
source: &'a str,
input: &'a str,
export: Option<&'a str>,
max_time_ms: u64,
max_memory_bytes: usize,
}
let wire = Zeroizing::new(
serde_json::to_vec(&Request {
source,
input,
export,
max_time_ms: time,
max_memory_bytes: heap,
})
.map_err(|_| failure(ErrorKind::Engine))?,
);
let memory = heap + 8 * 1024 * 1024;
let output = MemoryOutputPipe::new(memory);
let mut wasi = WasiCtxBuilder::new();
wasi.stdin(MemoryInputPipe::new(bytes::Bytes::from_owner(wire)))
.stdout(output.clone())
.wall_clock(FixedClock)
.secure_random(Deterministic::new(vec![0]))
.insecure_random(Deterministic::new(vec![0]))
.insecure_random_seed(0)
.allow_tcp(false)
.allow_udp(false)
.allow_ip_name_lookup(false);
let mut store = Store::new(
&compiled.engine,
State {
wasi: wasi.build(),
table: ResourceTable::new(),
limits: StoreLimitsBuilder::new()
.memory_size(memory)
.memories(1)
.trap_on_grow_failure(true)
.build(),
},
);
store.limiter(|state| &mut state.limits);
store.epoch_deadline_callback(move |_| {
if Instant::now() >= deadline {
Err(wasmtime::Trap::Interrupt.into())
} else {
Ok(UpdateDeadline::Continue(1))
}
});
store.set_epoch_deadline(1);
let (done, wait) = mpsc::channel();
let engine = compiled.engine.clone();
let watchdog = std::thread::Builder::new()
.name("cred-script-deadline".into())
.spawn(move || {
if wait
.recv_timeout(deadline.saturating_duration_since(Instant::now()))
.is_err()
{
engine.increment_epoch();
}
})
.map_err(|_| failure(ErrorKind::Engine))?;
let result = compiled
.pre
.instantiate(&mut store)
.and_then(|command| command.wasi_cli_run().call_run(&mut store));
let _ = done.send(());
let _ = watchdog.join();
if Instant::now() >= deadline || !matches!(result, Ok(Ok(()))) {
return Err(failure(ErrorKind::Limit));
}
#[derive(serde::Deserialize)]
struct Response {
result: Option<String>,
kind: Option<String>,
}
let response: Response = serde_json::from_slice(&output.contents())
.map_err(|_| failure(ErrorKind::Engine))?;
if Instant::now() >= deadline {
return Err(failure(ErrorKind::Limit));
}
if let Some(result) = response.result {
return Ok(result);
}
Err(failure(match response.kind.as_deref() {
Some("Limit") => ErrorKind::Limit,
Some("Syntax") => ErrorKind::Syntax,
Some("Throw") => ErrorKind::Throw,
Some("NonJson") => ErrorKind::NonJson,
_ => ErrorKind::Engine,
}))
})
.map_err(|_| failure(ErrorKind::Engine))?;
worker.join().map_err(|_| failure(ErrorKind::Engine))?
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn concurrency_is_bounded_and_queue_uses_the_deadline() {
let runtime = QuickJs::default();
let compiled = runtime.compiled.as_ref().unwrap();
let deadline = Instant::now() + Duration::from_secs(1);
let first = compiled.acquire(deadline).unwrap();
let second = compiled.acquire(deadline).unwrap();
assert!(
matches!(compiled.acquire(Instant::now()), Err(error) if error.kind == ErrorKind::Limit)
);
drop(first);
let third = compiled.acquire(deadline).unwrap();
assert_eq!(*compiled.active.lock().unwrap(), 2);
drop((second, third));
}
}
-212
View File
@@ -1,212 +0,0 @@
//! Fresh, bounded V8 isolates for credential scripts. Each call owns a dedicated worker thread.
use super::script::{Error, ErrorKind, Limits, Script};
use deno_core::{JsRuntime, RuntimeOptions, v8};
use zeroize::Zeroizing;
use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc,
},
time::Duration,
};
#[derive(Default)]
pub struct V8;
fn failure(kind: ErrorKind) -> Error {
Error {
kind,
message: match kind {
ErrorKind::Limit => "script time, memory or stack limit exceeded",
ErrorKind::Syntax => "invalid script or JSON",
ErrorKind::Throw => "script threw or rejected",
ErrorKind::NonJson => "result is not JSON",
ErrorKind::Engine => "script engine failure",
}
.into(),
}
}
impl Script for V8 {
fn evaluate(
&self,
source: &str,
input: &str,
export: Option<&str>,
limits: Limits,
) -> Result<String, Error> {
let source = source.to_owned();
let input = Zeroizing::new(input.to_owned());
let export = export.unwrap_or("default").to_owned();
std::thread::spawn(move || evaluate(source, input, export, limits))
.join()
.map_err(|_| failure(ErrorKind::Engine))?
}
}
fn evaluate(
source: String,
input: Zeroizing<String>,
export: String,
limits: Limits,
) -> Result<String, Error> {
let heap = limits
.max_memory_bytes
.unwrap_or(8 * 1024 * 1024)
.clamp(256 * 1024, 32 * 1024 * 1024);
// V8 uses Tokio for internal delayed tasks, even without guest extensions.
let executor = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.map_err(|_| failure(ErrorKind::Engine))?;
let _entered = executor.enter();
let mut runtime = JsRuntime::new(RuntimeOptions {
// Bootstrap before imposing the caller's budget: V8 cannot recover from
// bootstrap OOM because the runtime cannot install its callback yet.
create_params: Some(v8::CreateParams::default().heap_limits(0, 32 * 1024 * 1024)),
..Default::default()
});
if runtime.v8_isolate().get_heap_statistics().total_heap_size() > heap {
return Err(failure(ErrorKind::Limit));
}
// Removing a temporary callback sets the heap limit after bootstrap.
runtime.add_near_heap_limit_callback(|current, _| current);
runtime.remove_near_heap_limit_callback(heap);
let handle = runtime.v8_isolate().thread_safe_handle();
let limited = Arc::new(AtomicBool::new(false));
let flag = limited.clone();
let heap_handle = handle.clone();
runtime.add_near_heap_limit_callback(move |current, _| {
flag.store(true, Ordering::SeqCst);
heap_handle.terminate_execution();
// V8 needs emergency headroom to unwind after termination.
current + 16 * 1024 * 1024
});
let (done, wait) = mpsc::channel();
let flag = limited.clone();
let watchdog = std::thread::spawn(move || {
if wait
.recv_timeout(Duration::from_millis(
limits.max_time_ms.unwrap_or(100).clamp(1, 1000),
))
.is_err()
{
flag.store(true, Ordering::SeqCst);
handle.terminate_execution();
}
});
let task = async {
runtime
.execute_script("sandbox.js", BOOTSTRAP)
.map_err(|_| failure(ErrorKind::Engine))?;
let input = {
deno_core::scope!(scope, &mut runtime);
v8::tc_scope!(let scope, scope);
let text = v8::String::new(scope, &input).ok_or_else(|| failure(ErrorKind::Limit))?;
let value = v8::json::parse(scope, text).ok_or_else(|| failure(ErrorKind::Syntax))?;
v8::Global::new(scope, value)
};
let spec = deno_core::ModuleSpecifier::parse("file:///credential.js").unwrap();
let module = runtime
.load_main_es_module_from_code(&spec, source)
.await
.map_err(|_| failure(ErrorKind::Syntax))?;
let evaluated = runtime.mod_evaluate(module);
runtime
.with_event_loop_promise(evaluated, Default::default())
.await
.map_err(|e| {
if e.to_string().contains("call stack") {
failure(ErrorKind::Limit)
} else {
failure(ErrorKind::Throw)
}
})?;
let ns = runtime
.get_module_namespace(module)
.map_err(|_| failure(ErrorKind::Engine))?;
{
deno_core::scope!(scope, &mut runtime);
let ns = v8::Local::new(scope, ns);
let global = scope.get_current_context().global(scope);
let key =
v8::String::new(scope, "__module").ok_or_else(|| failure(ErrorKind::Limit))?;
global.set(scope, key.into(), ns.into());
let input = v8::Local::new(scope, input);
let key = v8::String::new(scope, "__input").ok_or_else(|| failure(ErrorKind::Limit))?;
global.set(scope, key.into(), input);
}
let code = format!(
r#"(async()=>{{
const f=__module[{}];
if(typeof f!=='function') return JSON.stringify({{kind:'Engine'}});
let value;
try {{ value=await f(__input); }} catch(e) {{ const message=String(e?.message); return JSON.stringify({{kind: e instanceof RangeError && message.includes('call stack') ? 'Limit':'Throw'}}); }}
try {{ const text=JSON.stringify(value); return JSON.stringify(text===undefined?{{kind:'NonJson'}}:{{text}}); }} catch(e) {{ return JSON.stringify({{kind:e instanceof RangeError && String(e.message).includes('call stack') ? 'Limit':'NonJson'}}); }}
}})()"#,
serde_json::to_string(&export).unwrap()
);
let value = runtime
.execute_script("call.js", code)
.map_err(|_| failure(ErrorKind::Throw))?;
let resolve = runtime.resolve(value);
let value = runtime
.with_event_loop_promise(resolve, Default::default())
.await
.map_err(|_| failure(ErrorKind::Engine))?;
let text = {
deno_core::scope!(scope, &mut runtime);
v8::Local::new(scope, value).to_rust_string_lossy(scope)
};
let result: serde_json::Value =
serde_json::from_str(&text).map_err(|_| failure(ErrorKind::Engine))?;
match result["text"].as_str() {
Some(s) => Ok(s.to_owned()),
None => Err(failure(match result["kind"].as_str() {
Some("NonJson") => ErrorKind::NonJson,
Some("Throw") => ErrorKind::Throw,
Some("Limit") => ErrorKind::Limit,
_ => ErrorKind::Engine,
})),
}
};
let result = executor.block_on(task);
let _ = done.send(());
let _ = watchdog.join();
if limited.load(Ordering::SeqCst) {
Err(failure(ErrorKind::Limit))
} else {
result
}
}
const BOOTSTRAP: &str = r#"
delete globalThis.Date; delete Math.random;
for (const name of ['Deno','__bootstrap','console','performance']) delete globalThis[name];
const alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
globalThis.btoa = value => {
const s=String(value); let out='';
for(let i=0;i<s.length;i+=3){
const a=s.charCodeAt(i), b=s.charCodeAt(i+1), c=s.charCodeAt(i+2);
if(a>255||b>255||c>255) throw Error('invalid base64 input');
const n=(a<<16)|((b||0)<<8)|(c||0);
out+=alphabet[(n>>>18)&63]+alphabet[(n>>>12)&63]+(i+1<s.length?alphabet[(n>>>6)&63]:'=')+(i+2<s.length?alphabet[n&63]:'=');
} return out;
};
globalThis.atob = value => {
let s=String(value).replace(/[\t\n\f\r ]/g,'');
if(s.length%4===0) s=s.replace(/==?$/,'');
if(s.length%4===1||/[^A-Za-z0-9+/]/.test(s)) throw Error('invalid base64 input');
let out='', bits=0, n=0;
for(const c of s){n=(n<<6)|alphabet.indexOf(c);bits+=6;if(bits>=8){bits-=8;out+=String.fromCharCode((n>>>bits)&255);}}
return out;
};
"#;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn script_input_zeroizes_on_drop() {
let _: fn(String, zeroize::Zeroizing<String>, String, Limits) -> Result<String, Error> =
evaluate;
}
}