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