polymorph_webcrypto_wasmtime/lib.rs
1//! Wasmtime host implementation of the `polymorph:webcrypto` interfaces, backed by
2//! the pure-Rust [RustCrypto](https://github.com/RustCrypto) crates.
3//!
4//! This crate factors the host-agnostic part of the Wasmtime WebCrypto host
5//! out of the demo binaries so any host can satisfy the `polymorph:webcrypto`
6//! imports with one call to [`add_to_linker`]. It is an async (component-model
7//! async) implementation modeled after [`wasmtime_wasi_http::p3`]: a host
8//! embeds a [`WasiWebcryptoCtx`] in its store state, implements
9//! [`WasiWebcryptoView`] to expose it alongside the store's [`ResourceTable`],
10//! and calls [`add_to_linker`] to satisfy the full `polymorph:webcrypto`
11//! package surface with RustCrypto implementations.
12//!
13//! [`wasmtime_wasi_http::p3`]: https://docs.rs/wasmtime-wasi-http
14
15pub mod bindings;
16mod host;
17mod limits;
18pub mod standalone;
19mod streams;
20
21use wasmtime::component::{HasData, Linker, ResourceTable};
22
23/// Configuration and per-store state for the WebCrypto host.
24///
25/// This is intentionally minimal (mirroring `wasmtime_wasi_http`'s
26/// `WasiHttpCtx`); it exists so hosts have a stable place to grow
27/// configuration without changing the [`WasiWebcryptoView`] shape.
28///
29/// # Input-buffering limits
30///
31/// Every stream-taking operation buffers its whole input host-side (the
32/// single-message contract), and the component-model async ABI lets a guest
33/// run many calls concurrently — so without limits a guest could make the
34/// host retain unbounded memory. Two limits bound that retention (wasmtime's
35/// per-call lift budget, [`Store::set_hostcall_fuel`], bounds each stream
36/// *delivery*; these bound what operations *accumulate*):
37///
38/// - **Per call** ([`set_per_call_buffer_limit`]): the most one operation
39/// may buffer. Inputs beyond it are drained and discarded (this host
40/// drains to completion rather than exercising the streaming contract's
41/// early-close-on-error permission) and the operation fails with a
42/// recoverable `error.other`.
43/// Defaults to ¼ of the store's hostcall fuel at admission time.
44/// - **Total** ([`set_total_buffer_limit`]): the admission pool. Each
45/// operation reserves its per-call bound before draining and waits
46/// (FIFO) for capacity when the pool is full, releasing when its buffers
47/// are gone — including the returned output stream. Defaults to 1× the
48/// store's hostcall fuel, so untouched configurations retain at most the
49/// embedder's one configured number, at a default concurrency of four
50/// operations.
51///
52/// Admission shares one pool per context, so an operation may wait on
53/// *unrelated* operations' completion. The package states the caller's side
54/// of this as the making-progress rule (see the `mac-key` docs): feed each
55/// in-flight operation's input, and drain each returned stream as it becomes
56/// available, without waiting on another operation. A caller that defers
57/// either can deadlock against the bound, and no implementation can rescue
58/// it.
59///
60/// # Minted-resource retention
61///
62/// Minted resources — keys, IKM, passwords, derivations, options, digests —
63/// live in the store's table until the guest drops them, so unbounded
64/// minting is unbounded host retention even with every operation bounded.
65/// A third limit bounds it:
66///
67/// - **Retention** ([`set_retention_limit`]): the retention pool. Every
68/// mint charges a fixed per-resource floor (which bounds resource
69/// *count*) plus its variable-length material bytes, holds the charge for
70/// the resource's lifetime, and releases it when the resource drops.
71/// Defaults to 16 MiB.
72///
73/// Retention admission is fail-fast, never waiting: its capacity frees only
74/// when the guest drops a resource, which may never happen. A mint past the
75/// budget fails with a recoverable `error.other` — drop resources and
76/// retry. The `*-options.new` constructors, whose WIT signatures carry no
77/// error channel, trap instead, the same class as a table-push failure.
78///
79/// What is bounded is what this host *accumulates*: the stream buffers, the
80/// output stream until its bytes are read or dropped, and minted resources.
81/// What is not bounded is the `list<u8>` parameters — `aad`, `nonce`, and
82/// the minting interfaces' `raw` — which the canonical ABI lifts before the
83/// host function runs, so they are already in host memory when admission is
84/// reached. Bounding those needs a hold *before* the call starts, which the
85/// component model provides to a component callee (`backpressure.{inc,dec}`)
86/// and does not expose to a host import. (The in-guest provider, which could
87/// use it, deliberately does not: it has essentially one caller, so its
88/// instance memory limit is its bound — see polymorph-webcrypto-guest-provider's `buffer` module.)
89/// Also outside the pools: each operation's transient working set — in
90/// `seal`/`open` the buffered input and the constructed output coexist
91/// until the input drops, so peak use briefly reaches about twice the
92/// reservation plus the tag, and `derive-bits` builds its full output
93/// before the ABI lifts it.
94///
95/// Each pool's budget is resolved **once**, at its first use, and belongs
96/// to the pool from then on (see `limits.rs` for why the budget is the
97/// pool's, not each acquirer's). Configure the limits before the first
98/// crypto call; changing them afterwards retunes the per-call limit but
99/// not the pools.
100///
101/// Cloning the context gives the clone its own pools, since the pools are
102/// parameterized by the budgets the context carries.
103///
104/// [`set_per_call_buffer_limit`]: WasiWebcryptoCtx::set_per_call_buffer_limit
105/// [`set_total_buffer_limit`]: WasiWebcryptoCtx::set_total_buffer_limit
106/// [`set_retention_limit`]: WasiWebcryptoCtx::set_retention_limit
107/// [`Store::set_hostcall_fuel`]: wasmtime::Store::set_hostcall_fuel
108#[derive(Debug, Default)]
109#[non_exhaustive]
110pub struct WasiWebcryptoCtx {
111 /// The most one operation may buffer, in bytes; `None` defaults to ¼ of
112 /// the store's hostcall fuel.
113 per_call_buffer_limit: Option<u64>,
114 /// The admission pool, in bytes; `None` defaults to the store's
115 /// hostcall fuel.
116 total_buffer_limit: Option<u64>,
117 /// The minted-resource retention pool, in bytes; `None` defaults to
118 /// [`DEFAULT_RETENTION_LIMIT`].
119 retention_limit: Option<u64>,
120 /// The admission pool, created on first use with the budget resolved
121 /// from this context and the store's hostcall fuel.
122 pool: std::sync::OnceLock<std::sync::Arc<crate::limits::BufferPool>>,
123 /// The retention pool, created on first mint with the budget resolved
124 /// from this context.
125 retention_pool: std::sync::OnceLock<std::sync::Arc<crate::limits::BufferPool>>,
126}
127
128/// The default minted-resource retention budget, in bytes. A constant
129/// rather than a share of the store's hostcall fuel: mints must resolve it
130/// in contexts that have no store access.
131const DEFAULT_RETENTION_LIMIT: u64 = 16 * 1024 * 1024;
132
133/// Cloning a context gives the clone its **own** pools.
134///
135/// The pools bound aggregate retention against ceilings that each context
136/// carries separately, so sharing one pool between contexts configured
137/// differently would let the larger ceiling admit against the smaller
138/// context's accounting — exceeding the bound it was asked to enforce.
139/// Independent pools keep each context's limit meaning what it says; a
140/// single bound across several contexts is not something this type can
141/// express.
142impl Clone for WasiWebcryptoCtx {
143 fn clone(&self) -> Self {
144 Self {
145 per_call_buffer_limit: self.per_call_buffer_limit,
146 total_buffer_limit: self.total_buffer_limit,
147 retention_limit: self.retention_limit,
148 pool: std::sync::OnceLock::new(),
149 retention_pool: std::sync::OnceLock::new(),
150 }
151 }
152}
153
154impl WasiWebcryptoCtx {
155 /// Create a new, default context.
156 pub fn new() -> Self {
157 Self::default()
158 }
159
160 /// Set the most one operation may buffer, in bytes. `None` (the
161 /// default) derives ¼ of the store's hostcall fuel at admission time.
162 pub fn set_per_call_buffer_limit(&mut self, limit: Option<u64>) {
163 self.per_call_buffer_limit = limit;
164 }
165
166 /// Set the total input-buffering admission pool, in bytes. `None` (the
167 /// default) derives the store's hostcall fuel at admission time.
168 pub fn set_total_buffer_limit(&mut self, limit: Option<u64>) {
169 self.total_buffer_limit = limit;
170 }
171
172 /// Set the minted-resource retention pool, in bytes: the most the
173 /// guest's live resources (keys, derivations, options, …) may retain
174 /// host-side, charged per mint and released per drop. `None` (the
175 /// default) is 16 MiB. A limit below the per-resource floor admits no
176 /// mint at all.
177 pub fn set_retention_limit(&mut self, limit: Option<u64>) {
178 self.retention_limit = limit;
179 }
180
181 /// The effective `(per-call, total)` limits given the store's hostcall
182 /// fuel, clamped so a reservation always fits an empty pool and no
183 /// limit is zero.
184 pub(crate) fn buffer_limits(&self, hostcall_fuel: u64) -> (u64, u64) {
185 let total = self.total_buffer_limit.unwrap_or(hostcall_fuel).max(1);
186 let per_call = self
187 .per_call_buffer_limit
188 .unwrap_or(hostcall_fuel / 4)
189 .clamp(1, total);
190 (per_call, total)
191 }
192
193 /// The admission pool, created on first use with `total` as its budget.
194 /// Later calls reuse the pool that exists: the budget is the pool's, not
195 /// each acquisition's.
196 pub(crate) fn pool(&self, total: u64) -> &std::sync::Arc<crate::limits::BufferPool> {
197 self.pool.get_or_init(|| crate::limits::pool(total))
198 }
199
200 /// The effective retention limit, floored so the pool is never empty by
201 /// construction.
202 pub(crate) fn retention_limit_bytes(&self) -> u64 {
203 self.retention_limit
204 .unwrap_or(DEFAULT_RETENTION_LIMIT)
205 .max(1)
206 }
207
208 /// Charge one mint's retention (the per-resource floor plus
209 /// `material_bytes`) against the retention pool, fail-fast: `None` when
210 /// the budget cannot fit the charge now. The reservation releases when
211 /// dropped — it travels in the minted resource.
212 pub(crate) fn charge_retention(
213 &self,
214 material_bytes: usize,
215 ) -> Option<crate::limits::Reservation> {
216 let pool = self
217 .retention_pool
218 .get_or_init(|| crate::limits::pool(self.retention_limit_bytes()));
219 crate::limits::charge(pool, material_bytes)
220 }
221}
222
223/// A borrowed view into a host's [`WasiWebcryptoCtx`] and its
224/// [`ResourceTable`].
225///
226/// Returned by [`WasiWebcryptoView::webcrypto`], this is the [`HasData::Data`]
227/// the generated host bindings operate on.
228pub struct WasiWebcryptoCtxView<'a> {
229 /// Mutable reference to the WebCrypto host context.
230 pub ctx: &'a mut WasiWebcryptoCtx,
231 /// Mutable reference to the table used to manage host resources.
232 pub table: &'a mut ResourceTable,
233}
234
235/// A trait that provides access to the [`WasiWebcryptoCtx`] host state.
236///
237/// Implement this for your store's data type so [`add_to_linker`] can wire the
238/// `polymorph:webcrypto` imports onto your linker.
239pub trait WasiWebcryptoView: Send {
240 /// Return a [`WasiWebcryptoCtxView`] from a mutable reference to `self`.
241 fn webcrypto(&mut self) -> WasiWebcryptoCtxView<'_>;
242}
243
244/// The type for which this crate implements the `polymorph:webcrypto` interfaces.
245/// Used as the [`HasData`] marker for the generated bindings.
246pub struct WasiWebcrypto;
247
248impl HasData for WasiWebcrypto {
249 type Data<'a> = WasiWebcryptoCtxView<'a>;
250}
251
252/// A resource this host mints: a payload plus the retention reservation
253/// charged for it (see [`WasiWebcryptoCtx`], "Minted-resource retention").
254/// [`minted_resources!`] implements it, placing the reservation in a
255/// private field so it releases exactly when the resource leaves the
256/// store's table.
257pub(crate) trait Minted: Sized {
258 /// What a mint computes; any other fields take their declared
259 /// defaults.
260 type Payload;
261
262 /// The variable-length bytes the resource retains beyond the
263 /// per-resource floor, measured for the retention charge.
264 fn payload_bytes(payload: &Self::Payload) -> usize;
265
266 /// Assemble the resource around its charged reservation.
267 fn minted(payload: Self::Payload, retention: crate::limits::Reservation) -> Self;
268}
269
270/// Declare the minted resource types: the `#[payload]` field, any
271/// defaulted extra fields, and the hidden retention reservation, with the
272/// [`Minted`] impl assembling them. `#[payload(retains = method)]` names
273/// the payload method measuring the retention charge's variable part (the
274/// default is floor-only); anything more complex than a method call
275/// belongs on the payload type, not in a declaration.
276macro_rules! minted_resources {
277 ($(
278 $(#[$attr:meta])*
279 pub struct $name:ident {
280 #[payload $((retains = $measure:ident))?]
281 $(#[$pattr:meta])*
282 $payload:ident: $pty:ty
283 $(, $(#[$fattr:meta])* $field:ident: $fty:ty = $default:expr)* $(,)?
284 }
285 )*) => {$(
286 $(#[$attr])*
287 pub struct $name {
288 $(#[$pattr])*
289 pub(crate) $payload: $pty,
290 $($(#[$fattr])* pub(crate) $field: $fty,)*
291 _retention: crate::limits::Reservation,
292 }
293
294 impl Minted for $name {
295 type Payload = $pty;
296
297 fn payload_bytes(payload: &Self::Payload) -> usize {
298 let _ = payload;
299 0 $(+ payload.$measure())?
300 }
301
302 fn minted(payload: Self::Payload, retention: crate::limits::Reservation) -> Self {
303 Self {
304 $payload: payload,
305 $($field: $default,)*
306 _retention: retention,
307 }
308 }
309 }
310 )*};
311}
312
313minted_resources! {
314 /// A `mac-key-options` resource: mint-time policy under construction.
315 /// Constructed with the WIT defaults (nothing granted), mutated by the
316 /// setters, consumed by a mint.
317 #[derive(Debug)]
318 pub struct MacKeyOptions {
319 #[payload]
320 policy: polymorph_webcrypto_core::MacPolicy,
321 }
322
323 /// An `aead-key-options` resource. See [`MacKeyOptions`].
324 #[derive(Debug)]
325 pub struct AeadKeyOptions {
326 #[payload]
327 policy: polymorph_webcrypto_core::AeadPolicy,
328 }
329
330 /// A `cipher-key-options` resource. See [`MacKeyOptions`].
331 #[derive(Debug)]
332 pub struct CipherKeyOptions {
333 #[payload]
334 policy: polymorph_webcrypto_core::CipherPolicy,
335 }
336
337 /// A `signing-key-options` resource. See [`MacKeyOptions`].
338 #[derive(Debug)]
339 pub struct SigningKeyOptions {
340 #[payload]
341 policy: polymorph_webcrypto_core::SigningPolicy,
342 }
343
344 /// A `kw-key-options` resource. See [`MacKeyOptions`].
345 #[derive(Debug)]
346 pub struct KwKeyOptions {
347 #[payload]
348 policy: polymorph_webcrypto_core::KwPolicy,
349 }
350
351 /// A `decryption-key-options` resource. See [`MacKeyOptions`].
352 #[derive(Debug)]
353 pub struct DecryptionKeyOptions {
354 #[payload]
355 policy: polymorph_webcrypto_core::TransportPolicy,
356 }
357
358 /// Backing type for the `public-encryption.encryption-key` resource.
359 ///
360 /// Public material only — encryption and wrapping are grant-free, and
361 /// there is no extractability gate (the exports are unconditional).
362 #[derive(Debug)]
363 pub struct EncryptionKey {
364 #[payload]
365 material: polymorph_webcrypto_core::EncryptionKeyMaterial,
366 }
367
368 /// Backing type for the `public-encryption.decryption-key` resource.
369 ///
370 /// `decrypt`/`unwrap` are one-shot and stateless per call, so the key
371 /// carries no per-operation state. The mint-time policy gates the two
372 /// operations and the private exports.
373 #[derive(Debug)]
374 pub struct DecryptionKey {
375 #[payload]
376 material: polymorph_webcrypto_core::DecryptionKeyMaterial,
377 }
378
379 /// Backing type for the `key-wrap.kw-key` resource: the AES-KW
380 /// key-encryption key's material.
381 #[derive(Debug)]
382 pub struct KwKey {
383 #[payload(retains = byte_len)]
384 material: polymorph_webcrypto_core::KwKeyMaterial,
385 }
386
387 /// Backing type for the `wrapping.wrap-input` resource: one key's
388 /// serialized material awaiting encryption under a wrapping key,
389 /// consumed by the wrap operations.
390 #[derive(Debug)]
391 pub struct WrapInput {
392 #[payload(retains = byte_len)]
393 material: polymorph_webcrypto_core::WrapInputMaterial,
394 }
395
396 /// Backing type for the `wrapping.unwrap-input` resource: decrypted
397 /// key material awaiting a typed mint, consumed by the unwrap mints.
398 #[derive(Debug)]
399 pub struct UnwrapInput {
400 #[payload(retains = byte_len)]
401 material: polymorph_webcrypto_core::UnwrapInputMaterial,
402 }
403
404 /// A `derive-options` resource. See [`MacKeyOptions`].
405 #[derive(Debug)]
406 pub struct DeriveOptions {
407 #[payload]
408 policy: polymorph_webcrypto_core::DerivePolicy,
409 }
410
411 /// An `agreement-key-options` resource. See [`MacKeyOptions`].
412 #[derive(Debug)]
413 pub struct AgreementKeyOptions {
414 #[payload]
415 policy: polymorph_webcrypto_core::AgreementPolicy,
416 }
417
418 /// Backing type for the `key-agreement.public-key` resource: public
419 /// material only, exchangeable and secret-free.
420 #[derive(Debug)]
421 pub struct AgreementPublicKey {
422 #[payload]
423 material: polymorph_webcrypto_core::AgreementPublicMaterial,
424 }
425
426 /// Backing type for the `key-agreement.secret-key` resource. `agree` is
427 /// one-shot and stateless per call; the derivation state lives in the
428 /// `derive-input` it mints.
429 #[derive(Debug)]
430 pub struct AgreementSecretKey {
431 #[payload]
432 material: polymorph_webcrypto_core::AgreementSecretMaterial,
433 }
434
435 /// Backing type for the `hkdf.ikm` resource: input keying material, never
436 /// readable through the API under any grant.
437 #[derive(Debug)]
438 pub struct Ikm {
439 #[payload(retains = byte_len)]
440 material: polymorph_webcrypto_core::IkmMaterial,
441 }
442
443 /// Backing type for the `pbkdf2.password` resource: a password, never
444 /// readable through the API under any grant.
445 #[derive(Debug)]
446 pub struct Password {
447 #[payload(retains = byte_len)]
448 material: polymorph_webcrypto_core::PasswordMaterial,
449 }
450
451 /// Backing type for the `derivation.derive-input` resource: a
452 /// parameterized derivation, run eagerly (the extract step runs at
453 /// `prepare`, so this retains the PRK rather than the base secret).
454 #[derive(Debug)]
455 pub struct DeriveInput {
456 #[payload(retains = byte_len)]
457 material: polymorph_webcrypto_core::DeriveInputMaterial,
458 }
459
460 /// Backing type for the `mac.mac-key` resource.
461 ///
462 /// Holds the shared core's HMAC key material (raw bytes zeroized on drop,
463 /// the bound SHA-2 variant, and extractability); `sign`/`verify` are
464 /// one-shot and stateless per call, so the key carries no per-operation
465 /// state. `extractable` gates `export-key-raw` only — the material necessarily
466 /// lives host-side either way.
467 #[derive(Debug)]
468 pub struct MacKey {
469 #[payload(retains = byte_len)]
470 material: polymorph_webcrypto_core::MacKeyMaterial,
471 }
472
473 /// Backing type for the `aead.aead-key` resource.
474 ///
475 /// Holds the shared core's AEAD key material (the ready-to-use cipher bound
476 /// to its algorithm at minting, raw bytes zeroized on drop, and
477 /// extractability). `seal`/`open` are stateless per call, so the key
478 /// carries no per-operation state.
479 #[derive(Debug)]
480 pub struct AeadKey {
481 #[payload(retains = byte_len)]
482 material: polymorph_webcrypto_core::AeadKeyMaterial,
483 }
484
485 /// Backing type for the `cipher.cipher-key` resource: the unauthenticated
486 /// AES modes' key material.
487 pub struct CipherKey {
488 #[payload(retains = byte_len)]
489 material: polymorph_webcrypto_core::CipherKeyMaterial,
490 }
491
492 /// Backing type for the `digest.digest` resource.
493 ///
494 /// A digest holds no key material — just the algorithm it is bound to
495 /// (a SHA-2 variant, or checked SHA-1 in a collision posture); `compute`
496 /// is one-shot and stateless per call, so the resource is reusable and
497 /// carries no per-operation state.
498 #[derive(Debug)]
499 pub struct Digest {
500 #[payload]
501 /// The digest algorithm this resource is bound to.
502 variant: polymorph_webcrypto_core::DigestKind,
503 }
504
505 /// Backing type for the `signature.verifying-key` resource.
506 ///
507 /// Public material only — verification is secret-free, and there is no
508 /// extractability gate (`%export` always succeeds).
509 #[derive(Debug)]
510 pub struct VerifyingKey {
511 #[payload]
512 /// The public key, bound to its algorithm (and, for ECDSA, its
513 /// curve/digest variant) at minting.
514 public: polymorph_webcrypto_core::SigPublic,
515 }
516
517 /// Backing type for the `signature.signing-key` resource.
518 ///
519 /// `sign` is one-shot and stateless per call, so the key carries no
520 /// per-operation state. `extractable` gates `%export` only.
521 #[derive(Debug)]
522 pub struct SigningKey {
523 #[payload]
524 material: polymorph_webcrypto_core::SigningKeyMaterial,
525 }
526}
527
528// `Debug` derives on the key-holding types print through the shared
529// core's material types, whose hand-written `Debug` impls redact all key
530// material — a key reaching a log line cannot leak (asserted by the
531// `debug_redacts_key_material` tests here and in the core).
532
533/// Add the `polymorph:webcrypto` interfaces implemented by this crate — `types`,
534/// the primitive kinds (`mac`, `aead`, `digest`, `signature`), and
535/// the algorithm minting interfaces — to the provided [`Linker`].
536///
537/// The store's data type `T` must implement [`WasiWebcryptoView`]. The
538/// engine's [`Config`](wasmtime::Config) must have
539/// `wasm_component_model_async` enabled, since the key-minting and
540/// stream-carrying functions use the component-model async ABI.
541///
542/// # Example
543///
544/// ```no_run
545/// use wasmtime::component::{Linker, ResourceTable};
546/// use wasmtime::{Engine, Result};
547/// use polymorph_webcrypto_wasmtime::{
548/// add_to_linker, WasiWebcryptoCtx, WasiWebcryptoCtxView, WasiWebcryptoView,
549/// };
550///
551/// struct MyState {
552/// webcrypto: WasiWebcryptoCtx,
553/// table: ResourceTable,
554/// }
555///
556/// impl WasiWebcryptoView for MyState {
557/// fn webcrypto(&mut self) -> WasiWebcryptoCtxView<'_> {
558/// WasiWebcryptoCtxView {
559/// ctx: &mut self.webcrypto,
560/// table: &mut self.table,
561/// }
562/// }
563/// }
564///
565/// fn wire(linker: &mut Linker<MyState>) -> Result<()> {
566/// add_to_linker(linker)
567/// }
568/// ```
569///
570/// The `@unstable`-gated interfaces — `sha1-checked`,
571/// the RSA signing pair, and RSA-OAEP decryption-key minting (see
572/// `wit/README.md`, "Stability gates") — are
573/// **not** added: a guest whose world imports them
574/// fails to instantiate against this default. Opt in with
575/// [`add_to_linker_with_options`].
576pub fn add_to_linker<T>(linker: &mut Linker<T>) -> wasmtime::Result<()>
577where
578 T: WasiWebcryptoView + 'static,
579{
580 add_to_linker_with_options(linker, &LinkOptions::default())
581}
582
583/// Which `@unstable`-gated interfaces [`add_to_linker_with_options`] adds.
584/// Every flag defaults to off; this host implements all of them, so a flag
585/// is embedder policy, not capability.
586#[derive(Clone, Debug, Default)]
587pub struct LinkOptions {
588 sha1_checked: bool,
589 rsa_sign: bool,
590 rsa_oaep_decrypt: bool,
591}
592
593impl LinkOptions {
594 /// Serve `polymorph:webcrypto/sha1-checked`.
595 pub fn sha1_checked(&mut self, enabled: bool) -> &mut Self {
596 self.sha1_checked = enabled;
597 self
598 }
599
600 /// Serve `polymorph:webcrypto/rsassa-pkcs1-v15-sign` and
601 /// `polymorph:webcrypto/rsa-pss-sign`.
602 pub fn rsa_sign(&mut self, enabled: bool) -> &mut Self {
603 self.rsa_sign = enabled;
604 self
605 }
606
607 /// Serve `polymorph:webcrypto/rsa-oaep-decrypt`.
608 pub fn rsa_oaep_decrypt(&mut self, enabled: bool) -> &mut Self {
609 self.rsa_oaep_decrypt = enabled;
610 self
611 }
612}
613
614/// [`add_to_linker`], with the `@unstable`-gated interfaces `options`
615/// selects also served.
616pub fn add_to_linker_with_options<T>(
617 linker: &mut Linker<T>,
618 options: &LinkOptions,
619) -> wasmtime::Result<()>
620where
621 T: WasiWebcryptoView + 'static,
622{
623 bindings::webcrypto::types::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
624 bindings::webcrypto::mac::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
625 bindings::webcrypto::aead::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
626 bindings::webcrypto::wrapping::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
627 bindings::webcrypto::key_wrap::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
628 bindings::webcrypto::aes_kw::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
629 bindings::webcrypto::digest::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
630 bindings::webcrypto::derivation::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
631 bindings::webcrypto::key_agreement::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
632 bindings::webcrypto::x25519::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
633 bindings::webcrypto::ecdh::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
634 bindings::webcrypto::hkdf::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
635 bindings::webcrypto::hkdf_sha2::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
636 bindings::webcrypto::hkdf_sha1::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
637 bindings::webcrypto::pbkdf2::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
638 bindings::webcrypto::pbkdf2_sha2::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
639 bindings::webcrypto::pbkdf2_sha1::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
640 bindings::webcrypto::hmac_sha2::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
641 bindings::webcrypto::hmac_sha1::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
642 bindings::webcrypto::aes_gcm::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
643 bindings::webcrypto::cipher::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
644 bindings::webcrypto::aes_cbc::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
645 bindings::webcrypto::aes_ctr::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
646 bindings::webcrypto::sha2::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
647 // The generated `add_to_linker`s for the gated interfaces consult
648 // their `LinkOptions` and add nothing when the flag is off.
649 bindings::webcrypto::sha1_checked::add_to_linker::<_, WasiWebcrypto>(
650 linker,
651 bindings::webcrypto::sha1_checked::LinkOptions::default()
652 .sha1_checked(options.sha1_checked),
653 T::webcrypto,
654 )?;
655 bindings::webcrypto::signature::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
656 bindings::webcrypto::ed25519_verify::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
657 bindings::webcrypto::ed25519_sign::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
658 bindings::webcrypto::ecdsa_verify::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
659 bindings::webcrypto::ecdsa_sign::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
660 bindings::webcrypto::rsa::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
661 bindings::webcrypto::rsassa_pkcs1_v15_verify::add_to_linker::<_, WasiWebcrypto>(
662 linker,
663 T::webcrypto,
664 )?;
665 bindings::webcrypto::rsa_pss_verify::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
666 bindings::webcrypto::rsassa_pkcs1_v15_sign::add_to_linker::<_, WasiWebcrypto>(
667 linker,
668 bindings::webcrypto::rsassa_pkcs1_v15_sign::LinkOptions::default()
669 .rsa_sign(options.rsa_sign),
670 T::webcrypto,
671 )?;
672 bindings::webcrypto::rsa_pss_sign::add_to_linker::<_, WasiWebcrypto>(
673 linker,
674 bindings::webcrypto::rsa_pss_sign::LinkOptions::default().rsa_sign(options.rsa_sign),
675 T::webcrypto,
676 )?;
677 bindings::webcrypto::public_encryption::add_to_linker::<_, WasiWebcrypto>(
678 linker,
679 T::webcrypto,
680 )?;
681 bindings::webcrypto::rsa_oaep_encrypt::add_to_linker::<_, WasiWebcrypto>(linker, T::webcrypto)?;
682 bindings::webcrypto::rsa_oaep_decrypt::add_to_linker::<_, WasiWebcrypto>(
683 linker,
684 bindings::webcrypto::rsa_oaep_decrypt::LinkOptions::default()
685 .rsa_oaep_decrypt(options.rsa_oaep_decrypt),
686 T::webcrypto,
687 )?;
688 Ok(())
689}
690
691#[cfg(test)]
692mod tests {
693 use super::WasiWebcryptoCtx;
694
695 /// The untouched defaults derive from the store's hostcall fuel — ¼
696 /// per call, 1× total — floored at 1 when the derivation rounds to
697 /// zero.
698 #[test]
699 fn buffer_limits_default_derivation() {
700 let ctx = WasiWebcryptoCtx::new();
701 assert_eq!(ctx.buffer_limits(8), (2, 8));
702 assert_eq!(ctx.buffer_limits(0), (1, 1));
703 }
704
705 /// The setters govern the derived limits, with per-call clamped into
706 /// the pool's bound.
707 #[test]
708 fn buffer_limits_reflect_configuration() {
709 let mut ctx = WasiWebcryptoCtx::new();
710 ctx.set_per_call_buffer_limit(Some(16));
711 ctx.set_total_buffer_limit(Some(64));
712 assert_eq!(ctx.buffer_limits(1024), (16, 64));
713 ctx.set_per_call_buffer_limit(Some(128));
714 assert_eq!(ctx.buffer_limits(1024), (64, 64));
715 }
716
717 /// A clone carries the configured limits but gets its own pools (the
718 /// `Clone` impl's contract: budgets are per-context).
719 #[test]
720 fn clone_preserves_limits_with_a_fresh_pool() {
721 let mut ctx = WasiWebcryptoCtx::new();
722 ctx.set_per_call_buffer_limit(Some(3));
723 ctx.set_total_buffer_limit(Some(9));
724 ctx.set_retention_limit(Some(crate::limits::RETENTION_FLOOR * 3 / 2));
725 let pool = ctx.pool(9).clone();
726 let charge = ctx.charge_retention(0).expect("within the fresh budget");
727 let clone = ctx.clone();
728 assert_eq!(clone.buffer_limits(1024), (3, 9));
729 assert_eq!(
730 clone.retention_limit_bytes(),
731 crate::limits::RETENTION_FLOOR * 3 / 2
732 );
733 assert!(!std::sync::Arc::ptr_eq(&pool, clone.pool(9)));
734 // The clone's retention pool is fresh: the original's outstanding
735 // charge does not count against it.
736 assert!(clone.charge_retention(0).is_some());
737 assert!(ctx.charge_retention(0).is_none(), "the original is spent");
738 drop(charge);
739 }
740
741 /// Minting charges the retention pool and dropping the reservation
742 /// releases it, with the budget resolved once at first charge.
743 #[test]
744 fn retention_charges_release_on_drop() {
745 let mut ctx = WasiWebcryptoCtx::new();
746 ctx.set_retention_limit(Some(crate::limits::RETENTION_FLOOR * 2));
747 let first = ctx.charge_retention(0).expect("floor fits");
748 let second = ctx.charge_retention(0).expect("two floors fit");
749 assert!(ctx.charge_retention(0).is_none(), "budget is spent");
750 drop(first);
751 let third = ctx.charge_retention(0).expect("released capacity readmits");
752 drop((second, third));
753 // Raising the limit after the pool resolved does not retune it.
754 ctx.set_retention_limit(Some(1_000_000));
755 assert!(ctx.charge_retention(1_000).is_none());
756 }
757}