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}