Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions examples/crypto-demo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ publish = false
crate-type = ["cdylib"]

[features]
# The RSA-OAEP key-transport check, forwarding to the SDK's
# `rsa-oaep-decrypt`. Off by default, exactly like that feature and the
# WIT gate behind it: enabling it adds the withheld-by-default RSA op
# interfaces to the component's imports, which only hosts that
# deliberately serve them can satisfy — the in-guest provider does not
# (class D), so the default build is what stays composable. The wasmtime
# and jco demo legs opt in.
rsa-oaep = ["polymorph-webcrypto-guest/rsa-oaep-decrypt"]
# The checks whose imports the in-guest provider withholds (class D):
# the RSA-OAEP key transport and the ECDSA signing-key mints. Off by
# default, exactly like the SDK feature and WIT gate behind the RSA
# half: enabling it adds withheld interfaces to the component's imports,
# which only hosts that deliberately serve them can satisfy — the
# default build is what stays composable. The wasmtime and jco demo legs
# run the opt-in artifact, named after the conformance architecture's
# host-only suite.
host-only = ["polymorph-webcrypto-guest/rsa-oaep-decrypt"]

[dependencies]
# Failure reporting inside the checks (`context`/`ensure!`); rendered with
Expand Down
417 changes: 401 additions & 16 deletions examples/crypto-demo/src/lib.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion examples/jco-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"description": "crypto-demo driver for the jco host (js/jco/webcrypto.js): transpiles the guest with jco and runs it on Node 24+.",
"scripts": {
"build:component": "cd ../.. && just demo::build-component",
"transpile": "node ../../scripts/jco-transpile.mjs transpile ../crypto-demo/build/crypto-demo-rsa-oaep.component.wasm --name crypto-demo --async-mode jspi --map 'polymorph:webcrypto/*@0.1.0=../../../js/jco/webcrypto.js#*' -o generated",
"transpile": "node ../../scripts/jco-transpile.mjs transpile ../crypto-demo/build/crypto-demo-host-only.component.wasm --name crypto-demo --async-mode jspi --map 'polymorph:webcrypto/*@0.1.0=../../../js/jco/webcrypto.js#*' -o generated",
"start": "node --experimental-wasm-jspi src/run.mjs",
"test": "node --experimental-wasm-jspi src/run.mjs"
},
Expand Down
8 changes: 4 additions & 4 deletions examples/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@

import '../justfile.shared.just'

# Run the Wasmtime (native, RustCrypto) host demo — the opt-in rsa-oaep
# build: this host serves the withheld-by-default RSA op interfaces.
# Run the Wasmtime (native, RustCrypto) host demo — the opt-in host-only
# build: this host serves the interfaces the in-guest provider withholds.
wasmtime: build-component
cargo run --release --manifest-path '{{root}}/Cargo.toml' --bin wasmtime-demo-host -- \
crypto-demo/build/crypto-demo-rsa-oaep.component.wasm
crypto-demo/build/crypto-demo-host-only.component.wasm

# Compose the fully in-guest demo: the crypto-demo guest's polymorph:webcrypto
# imports are satisfied by the in-guest provider's exports (`wac plug`), then
# the CLI driver (async wasi:cli/run) is plugged on top, yielding one
# self-contained component in target/crypto-demo-composed.wasm. The default
# build is the composable one — the opt-in `rsa-oaep` artifact imports
# build is the composable one — the opt-in host-only artifact imports
# interfaces the provider withholds (class D) and cannot plug.
compose: build-component (_compose "crypto-demo" "examples/crypto-demo/build/crypto-demo.component.wasm" "crypto-demo-driver")

Expand Down
4 changes: 2 additions & 2 deletions examples/wasmtime-demo/tests/demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ fn run(dir: &Path, program: &str, args: &[&str]) {
}

/// Build the guest component through `just demo::build-component` — the single
/// definition of that build — and return the opt-in rsa-oaep artifact's
/// definition of that build — and return the opt-in host-only artifact's
/// path (this host serves the withheld-by-default interfaces). The build
/// runs once per test binary: the tests run in parallel, and a concurrent
/// rebuild's `wasm-tools component new -o` truncates the component file in
/// place while another test may be loading it.
fn build_component(workspace_root: &Path) -> PathBuf {
static BUILD: Once = Once::new();
BUILD.call_once(|| run(workspace_root, "just", &["build-component"]));
workspace_root.join("examples/crypto-demo/build/crypto-demo-rsa-oaep.component.wasm")
workspace_root.join("examples/crypto-demo/build/crypto-demo-host-only.component.wasm")
}

#[tokio::test(flavor = "multi_thread")]
Expand Down
28 changes: 14 additions & 14 deletions justfile.shared.just
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ chromium-enabled := if env('GITHUB_ACTIONS', '') == 'true' { '1' } else if env('

# Build the crypto-demo guest component into examples/crypto-demo/build/ —
# both artifacts. The default build is crypto-demo.component.wasm, the
# artifact the in-guest composition plugs. The `--features rsa-oaep`
# build adds the key-transport check and with it the withheld-by-default
# RSA op interfaces, so it exists as the separate
# crypto-demo-rsa-oaep.component.wasm, run only by the hosts that
# deliberately serve those interfaces (the wasmtime-demo tests, the jco
# demo leg). Sequential on purpose: the two builds share the cargo
# target dir. The outputs are renamed into place: `wasm-tools component
# new -o` truncates in place, so a direct write would expose an empty or
# partial component to a concurrent reader (the wasmtime-demo tests load
# these paths).
# artifact the in-guest composition plugs. The `--features host-only`
# build adds the checks whose imports the in-guest provider withholds
# (class D: the RSA-OAEP transport, the ECDSA signing mints), so it
# exists as the separate crypto-demo-host-only.component.wasm, run only
# by the hosts that deliberately serve those interfaces (the
# wasmtime-demo tests, the jco demo leg). Sequential on purpose: the two
# builds share the cargo target dir. The outputs are renamed into place:
# `wasm-tools component new -o` truncates in place, so a direct write
# would expose an empty or partial component to a concurrent reader (the
# wasmtime-demo tests load these paths).
[private]
build-component:
#!/usr/bin/env bash
Expand All @@ -39,12 +39,12 @@ build-component:
-o examples/crypto-demo/build/crypto-demo.component.wasm.tmp
mv -f examples/crypto-demo/build/crypto-demo.component.wasm.tmp \
examples/crypto-demo/build/crypto-demo.component.wasm
cargo build --release -p crypto-demo --features rsa-oaep --target wasm32-unknown-unknown
cargo build --release -p crypto-demo --features host-only --target wasm32-unknown-unknown
wasm-tools component new \
target/wasm32-unknown-unknown/release/crypto_demo.wasm \
-o examples/crypto-demo/build/crypto-demo-rsa-oaep.component.wasm.tmp
mv -f examples/crypto-demo/build/crypto-demo-rsa-oaep.component.wasm.tmp \
examples/crypto-demo/build/crypto-demo-rsa-oaep.component.wasm
-o examples/crypto-demo/build/crypto-demo-host-only.component.wasm.tmp
mv -f examples/crypto-demo/build/crypto-demo-host-only.component.wasm.tmp \
examples/crypto-demo/build/crypto-demo-host-only.component.wasm

# Build the in-guest provider component (RustCrypto entirely in-guest; it
# exports the polymorph:webcrypto surface) into
Expand Down
96 changes: 95 additions & 1 deletion rust/guest/src/ecdsa.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! `ecdsa-verify` / `ecdsa-sign` key creation.

use crate::{bindings, Error, SigningKey, SigningKeyOptions, VerifyingKey};
use crate::{bindings, Error, SigningKey, SigningKeyOptions, UnwrapInput, VerifyingKey};

pub use crate::bindings::ecdsa_verify::EcdsaVariant;

Expand All @@ -14,6 +14,35 @@ pub async fn import_verifying_key_raw(
))
}

/// Import a public key as an X.509 SubjectPublicKeyInfo (DER). The curve
/// must be named by OID and match the declared variant's, or the import
/// fails [`Error::InvalidKey`]; whether a *compressed* point encoding is
/// accepted is implementation-defined — do not rely on either behavior
/// (see the WIT `import-verifying-key-spki` doc).
pub async fn import_verifying_key_spki(
variant: EcdsaVariant,
spki: impl Into<Vec<u8>>,
) -> Result<VerifyingKey, Error> {
Ok(VerifyingKey::from_raw(
bindings::ecdsa_verify::import_verifying_key_spki(variant, spki.into()).await?,
))
}

/// Import a public key as an EC public JWK (`kty: "EC"`, with `crv`,
/// `x`, and `y`; as JSON text). The JWK's `crv` must match the declared
/// variant's curve, and an `alg` member, when present, must be the
/// curve's JOSE signature alg (`"ES256"` for P-256, `"ES384"` for
/// P-384). See the WIT `mac-key.export-key-jwk` doc for the package-wide
/// JWK contract.
pub async fn import_verifying_key_jwk(
variant: EcdsaVariant,
jwk: impl Into<String>,
) -> Result<VerifyingKey, Error> {
Ok(VerifyingKey::from_raw(
bindings::ecdsa_verify::import_verifying_key_jwk(variant, jwk.into()).await?,
))
}

/// Generate a fresh random signing key of the declared variant, returning
/// both halves.
pub async fn generate_key(
Expand All @@ -26,3 +55,68 @@ pub async fn generate_key(
VerifyingKey::from_raw(verifying),
))
}

/// Import a signing key as a PKCS#8 PrivateKeyInfo (DER, the SEC1
/// private-key body). The encoded curve must match the declared
/// variant's ([`Error::InvalidKey`]); an embedded public key, when
/// present, is validated against the scalar and never trusted on its
/// own. Returns only the signing key; the public half is imported
/// separately (there is no derive from a private import — see the WIT
/// `ecdsa-sign` interface doc).
pub async fn import_signing_key_pkcs8(
variant: EcdsaVariant,
pkcs8: impl Into<Vec<u8>>,
options: SigningKeyOptions,
) -> Result<SigningKey, Error> {
Ok(SigningKey::from_raw(
bindings::ecdsa_sign::import_signing_key_pkcs8(variant, pkcs8.into(), options.lower())
.await?,
))
}

/// Import a signing key as an EC private JWK (`kty: "EC"`, with `crv`,
/// `d`, and the mandatory public coordinates `x`/`y`; as JSON text).
/// `crv` and `alg` are validated as in [`import_verifying_key_jwk`].
///
/// Security: implementations MAY validate `x`/`y` against `d`, and never
/// trust them for any operation.
pub async fn import_signing_key_jwk(
variant: EcdsaVariant,
jwk: impl Into<String>,
options: SigningKeyOptions,
) -> Result<SigningKey, Error> {
Ok(SigningKey::from_raw(
bindings::ecdsa_sign::import_signing_key_jwk(variant, jwk.into(), options.lower()).await?,
))
}

/// Mint a signing key from unwrapped key material read as a PKCS#8
/// PrivateKeyInfo, subject to [`import_signing_key_pkcs8`]'s contract.
/// Consumes the [`UnwrapInput`]; the minted key's usages and
/// extractability come from `options` alone.
pub async fn unwrap_signing_key_pkcs8(
variant: EcdsaVariant,
input: UnwrapInput,
options: SigningKeyOptions,
) -> Result<SigningKey, Error> {
Ok(SigningKey::from_raw(
bindings::ecdsa_sign::unwrap_signing_key_pkcs8(variant, input.into_raw(), options.lower())
.await?,
))
}

/// Mint a signing key from unwrapped key material read as an EC private
/// JWK, subject to [`import_signing_key_jwk`]'s contract plus the
/// unwrap-path `use`/`key_ops` checks (see the WIT `README.md`, "JWK
/// contract"). Consumes the [`UnwrapInput`]; see
/// [`unwrap_signing_key_pkcs8`] for the options model.
pub async fn unwrap_signing_key_jwk(
variant: EcdsaVariant,
input: UnwrapInput,
options: SigningKeyOptions,
) -> Result<SigningKey, Error> {
Ok(SigningKey::from_raw(
bindings::ecdsa_sign::unwrap_signing_key_jwk(variant, input.into_raw(), options.lower())
.await?,
))
}
82 changes: 80 additions & 2 deletions rust/guest/src/ed25519.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
//! `ed25519-verify` / `ed25519-sign` key creation.

use crate::{bindings, Error, SigningKey, SigningKeyOptions, VerifyingKey};
use crate::{bindings, Error, SigningKey, SigningKeyOptions, UnwrapInput, VerifyingKey};

/// Import a 32-byte raw public key.
/// Import a 32-byte raw public key (RFC 8032 encoding). Material of any
/// other length fails [`Error::InvalidKey`]; a non-canonical or
/// small-order encoding is rejected here or at verification, per the WIT
/// interface's verification criterion.
pub async fn import_verifying_key_raw(raw: impl Into<Vec<u8>>) -> Result<VerifyingKey, Error> {
Ok(VerifyingKey::from_raw(
bindings::ed25519_verify::import_verifying_key_raw(raw.into()).await?,
))
}

/// Import a public key as an X.509 SubjectPublicKeyInfo (DER, RFC 8410).
/// The embedded point is subject to the same strict criterion as
/// [`import_verifying_key_raw`].
pub async fn import_verifying_key_spki(spki: impl Into<Vec<u8>>) -> Result<VerifyingKey, Error> {
Ok(VerifyingKey::from_raw(
bindings::ed25519_verify::import_verifying_key_spki(spki.into()).await?,
))
}

/// Import a public key as an RFC 8037 OKP public JWK (`kty: "OKP"`,
/// `crv: "Ed25519"`, `x`; as JSON text). An `alg` member, when present,
/// must be `"Ed25519"` or `"EdDSA"`. The same strict point criterion as
/// [`import_verifying_key_raw`] applies; see the WIT
/// `mac-key.export-key-jwk` doc for the package-wide JWK contract.
pub async fn import_verifying_key_jwk(jwk: impl Into<String>) -> Result<VerifyingKey, Error> {
Ok(VerifyingKey::from_raw(
bindings::ed25519_verify::import_verifying_key_jwk(jwk.into()).await?,
))
}

/// Generate a fresh random signing key, returning both halves.
pub async fn generate_key(options: SigningKeyOptions) -> Result<(SigningKey, VerifyingKey), Error> {
let (signing, verifying) = bindings::ed25519_sign::generate_key(options.lower()).await?;
Expand All @@ -17,3 +40,58 @@ pub async fn generate_key(options: SigningKeyOptions) -> Result<(SigningKey, Ver
VerifyingKey::from_raw(verifying),
))
}

/// Import a signing key as a PKCS#8 PrivateKeyInfo (DER, RFC 8410: the
/// 32-byte seed in a CurvePrivateKey). Returns only the signing key; the
/// public half is imported separately (there is no derive from a private
/// import — see the WIT `ed25519-sign` interface doc).
pub async fn import_signing_key_pkcs8(
pkcs8: impl Into<Vec<u8>>,
options: SigningKeyOptions,
) -> Result<SigningKey, Error> {
Ok(SigningKey::from_raw(
bindings::ed25519_sign::import_signing_key_pkcs8(pkcs8.into(), options.lower()).await?,
))
}

/// Import a signing key as an RFC 8037 OKP private JWK (`kty: "OKP"`,
/// `crv: "Ed25519"`, with `x` and `d` both required; as JSON text). An
/// `alg` member, when present, must be `"Ed25519"` or `"EdDSA"`.
///
/// Security: implementations MAY reject a JWK whose `x` is not the
/// public key of `d`, and never trust `x` for any operation.
pub async fn import_signing_key_jwk(
jwk: impl Into<String>,
options: SigningKeyOptions,
) -> Result<SigningKey, Error> {
Ok(SigningKey::from_raw(
bindings::ed25519_sign::import_signing_key_jwk(jwk.into(), options.lower()).await?,
))
}

/// Mint a signing key from unwrapped key material read as a PKCS#8
/// PrivateKeyInfo, subject to [`import_signing_key_pkcs8`]'s contract.
/// Consumes the [`UnwrapInput`]; the minted key's usages and
/// extractability come from `options` alone.
pub async fn unwrap_signing_key_pkcs8(
input: UnwrapInput,
options: SigningKeyOptions,
) -> Result<SigningKey, Error> {
Ok(SigningKey::from_raw(
bindings::ed25519_sign::unwrap_signing_key_pkcs8(input.into_raw(), options.lower()).await?,
))
}

/// Mint a signing key from unwrapped key material read as an OKP private
/// JWK, subject to [`import_signing_key_jwk`]'s contract plus the
/// unwrap-path `use`/`key_ops` checks (see the WIT `README.md`, "JWK
/// contract"). Consumes the [`UnwrapInput`]; see
/// [`unwrap_signing_key_pkcs8`] for the options model.
pub async fn unwrap_signing_key_jwk(
input: UnwrapInput,
options: SigningKeyOptions,
) -> Result<SigningKey, Error> {
Ok(SigningKey::from_raw(
bindings::ed25519_sign::unwrap_signing_key_jwk(input.into_raw(), options.lower()).await?,
))
}
Loading
Loading