Skip to main content

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}