diff --git a/examples/crypto-demo/Cargo.toml b/examples/crypto-demo/Cargo.toml index bfa8abf..ec551f4 100644 --- a/examples/crypto-demo/Cargo.toml +++ b/examples/crypto-demo/Cargo.toml @@ -7,6 +7,16 @@ publish = false [lib] 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"] + [dependencies] # Failure reporting inside the checks (`context`/`ensure!`); rendered with # `{:#}` at the WIT `result` boundary. diff --git a/examples/crypto-demo/src/lib.rs b/examples/crypto-demo/src/lib.rs index 141853e..2396057 100644 --- a/examples/crypto-demo/src/lib.rs +++ b/examples/crypto-demo/src/lib.rs @@ -13,6 +13,14 @@ //! conformance suites' job (`conformance/`), which gate the same targets. //! The `check(...)` names below are the inventory, and the integration //! tests assert the expected summary. +//! +//! The `rsa-oaep` cargo feature (off by default, like the SDK feature it +//! forwards to) adds the key-transport check. A component's imports are +//! derived from the calls it makes, and the in-guest provider withholds +//! every RSA op interface (rust/guest-provider/README.md, class D), so +//! the default build is what composes with it — the hosts that +//! deliberately serve those interfaces (the Wasmtime test embedding, the +//! jco host on Node) run the opt-in build. wit_bindgen::generate!({ path: "wit", @@ -134,6 +142,74 @@ const RSA_PSS_SIG: [u8; 256] = hexlower!( 00bc0ec320711b211fde92e57feb9013c3609342495ec0d7cabdec21e54acc38" ); +// --- Wycheproof rsa_oaep_2048_sha256_mgf1sha256_test.json: group 1's key +// (the same 2048-bit modulus as RSA_SPKI above) as PKCS#8 and as the +// file's full-CRT private JWK, a public JWK built from its n/e, and +// tcId 2's known answer (twenty 0x00 bytes, empty label). Compiled +// only under the opt-in `rsa-oaep` feature: the default build stays +// composable with the in-guest provider, which withholds these +// interfaces. ------------------------------------------------------ + +#[cfg(feature = "rsa-oaep")] +const OAEP_PKCS8: [u8; 1217] = hexlower!( + "308204bd020100300d06092a864886f70d0101010500048204a7308204a30201\ + 000282010100a2b451a07d0aa5f96e455671513550514a8a5b462ebef717094f\ + a1fee82224e637f9746d3f7cafd31878d80325b6ef5a1700f65903b469429e89\ + d6eac8845097b5ab393189db92512ed8a7711a1253facd20f79c15e8247f3d3e\ + 42e46e48c98e254a2fe9765313a03eff8f17e1a029397a1fa26a8dce26f490ed\ + 81299615d9814c22da610428e09c7d9658594266f5c021d0fceca08d945a12be\ + 82de4d1ece6b4c03145b5d3495d4ed5411eb878daf05fd7afc3e09ada0f11264\ + 22f590975a1969816f48698bcbba1b4d9cae79d460d8f9f85e7975005d9bc22c\ + 4e5ac0f7c1a45d12569a62807d3b9a02e5a530e773066f453d1f5b4c2e9cf782\ + 0283f742b9d502030100010282010024cdc62317f5d72a6f6ba6cc9632899b01\ + d1ff28867d72f61688995bc855a4e420a8405250089bdb13cf8e09543827b748\ + b9d27fbb2b4d9e20af8c5a6a862796d1a4cc18ad16ea678bc1bd4a83bbbe9c5e\ + 57453b5ce7388e41a3ba4ce2b77b4438a229e954f720dae0353dc088ac8a76b2\ + 6dc276f8e1b7851ddd6398ad16ff2e78195123b9b036e945c38c9d12434f6df7\ + 6fe22359eb3e1ac9c011678fc926fad3ae475a4fffff55feb2d147e9c894f4c0\ + e29a599e762462482d968bf42780945fc0d2c31c573c4431b8f4fe8b8c67bec8\ + 15abd44f7a86edca1c2308737358d2c2ae5e2e0e2dadf730980262377e58b13b\ + 7d9992060a0bc870ccfdb4a9319ee102818100dc431050f782e894fb5248247d\ + 98cb7d58b8d1e24f3b55d041c56e4de086b0d5bb028bda42eeb5d234d5681e58\ + 09d415e6a289ad4cfbf78f978f6c35814f50eebff1c5b80a69f788e81e6bab5d\ + daa78369d659d143ec6f17e79813a575cfad9c569156b90113e2e9110ad9e7b4\ + 8a1c9348a6e653321191290ea36cfb3a5b18f102818100bd1a81e7977f989812\ + 2273ae3222b598ea5fb19eb4eabc38308a5e32196603b2e500ffb79f5b886816\ + 611debc472fac45544070beb057c941378a6868af3b7a03d3f9880ec47d5e089\ + b94fbde542aba9ae8d72c57088d7abf5b131f39098f7bc160f90536abc9492fd\ + 4e06f3ed7299d4b97bb03677207d95669f140cfbc20f2502818100a94b528b28\ + f291599121d91952ffd1c7f21d7c1479d99d478885fb161870ee1218bf084726\ + 12dbe5497e8d9c650688e09c786961ae3e2c354dc48ae34514759c4c23c45884\ + 88961dc06b414e61c0e1e7fbbd2923d31532fe289f96da220711e58c14019808\ + e00414276933bb07e4efb9b4a9b37656917205209f33f09515d7c10281803af0\ + e72a933aef09ff2503df78bafed531c02ff1a2bc437c540cdcbd4ad35435cf51\ + 1763596543480629b114ca7f780ff7efa32ea0cb6e000d6d9ea1f2ef71fd9cf9\ + 948422a165557e37e755edfe70d90b920502eb478bc98a63f788ce3a0f856d6e\ + de7251a383bfa8fa480a81a925af7b3cc538c4bab8c9f7597ffb68011d8d0281\ + 802640fbfbcfefb163ee7a87b6483a66ee41f956d90fa8a7939bfc042ee0924b\ + 1b7993d0445f758d51933e85179c0320b0c968b48a91c38b5be923e1097c0c56\ + 2f88d42294b6a2759bafa5428a74f1270874e45f6fcc60f21602de5eccd143cf\ + 31241f5921b5ad3983fb54ef17be3b285367e50c999c67247b552fe4bfce945f\ + 7b" +); +#[cfg(feature = "rsa-oaep")] +const OAEP_PRIVATE_JWK: &str = r#"{"kty":"RSA","alg":"RSA-OAEP-256","n":"orRRoH0KpfluRVZxUTVQUUqKW0YuvvcXCU-h_ugiJOY3-XRtP3yv0xh42AMltu9aFwD2WQO0aUKeidbqyIRQl7WrOTGJ25JRLtincRoSU_rNIPecFegkfz0-QuRuSMmOJUov6XZTE6A-_48X4aApOXofomqNzib0kO2BKZYV2YFMItphBCjgnH2WWFlCZvXAIdD87KCNlFoSvoLeTR7Oa0wDFFtdNJXU7VQR64eNrwX9evw-Ca2g8RJkIvWQl1oZaYFvSGmLy7obTZyuedRg2Pn4Xnl1AF2bwixOWsD3waRdElaaYoB9O5oC5aUw53MGb0U9H1tMLpz3ggKD90K51Q","e":"AQAB","kid":"none","d":"JM3GIxf11ypva6bMljKJmwHR_yiGfXL2FoiZW8hVpOQgqEBSUAib2xPPjglUOCe3SLnSf7srTZ4gr4xaaoYnltGkzBitFupni8G9SoO7vpxeV0U7XOc4jkGjukzit3tEOKIp6VT3INrgNT3AiKyKdrJtwnb44beFHd1jmK0W_y54GVEjubA26UXDjJ0SQ09t92_iI1nrPhrJwBFnj8km-tOuR1pP__9V_rLRR-nIlPTA4ppZnnYkYkgtlov0J4CUX8DSwxxXPEQxuPT-i4xnvsgVq9RPeobtyhwjCHNzWNLCrl4uDi2t9zCYAmI3flixO32ZkgYKC8hwzP20qTGe4Q","p":"3EMQUPeC6JT7UkgkfZjLfVi40eJPO1XQQcVuTeCGsNW7AovaQu610jTVaB5YCdQV5qKJrUz794-Xj2w1gU9Q7r_xxbgKafeI6B5rq13ap4Np1lnRQ-xvF-eYE6V1z62cVpFWuQET4ukRCtnntIock0im5lMyEZEpDqNs-zpbGPE","q":"vRqB55d_mJgSInOuMiK1mOpfsZ606rw4MIpeMhlmA7LlAP-3n1uIaBZhHevEcvrEVUQHC-sFfJQTeKaGivO3oD0_mIDsR9XgiblPveVCq6mujXLFcIjXq_WxMfOQmPe8Fg-QU2q8lJL9Tgbz7XKZ1Ll7sDZ3IH2VZp8UDPvCDyU","dp":"qUtSiyjykVmRIdkZUv_Rx_IdfBR52Z1HiIX7Fhhw7hIYvwhHJhLb5Ul-jZxlBojgnHhpYa4-LDVNxIrjRRR1nEwjxFiEiJYdwGtBTmHA4ef7vSkj0xUy_iifltoiBxHljBQBmAjgBBQnaTO7B-TvubSps3ZWkXIFIJ8z8JUV18E","dq":"OvDnKpM67wn_JQPfeLr-1THAL_GivEN8VAzcvUrTVDXPURdjWWVDSAYpsRTKf3gP9--jLqDLbgANbZ6h8u9x_Zz5lIQioWVVfjfnVe3-cNkLkgUC60eLyYpj94jOOg-FbW7eclGjg7-o-kgKgaklr3s8xTjEurjJ91l_-2gBHY0","qi":"JkD7-8_vsWPueoe2SDpm7kH5VtkPqKeTm_wELuCSSxt5k9BEX3WNUZM-hRecAyCwyWi0ipHDi1vpI-EJfAxWL4jUIpS2onWbr6VCinTxJwh05F9vzGDyFgLeXszRQ88xJB9ZIbWtOYP7VO8XvjsoU2flDJmcZyR7VS_kv86UX3s"}"#; +#[cfg(feature = "rsa-oaep")] +const OAEP_PUBLIC_JWK: &str = r#"{"kty":"RSA","alg":"RSA-OAEP-256","n":"orRRoH0KpfluRVZxUTVQUUqKW0YuvvcXCU-h_ugiJOY3-XRtP3yv0xh42AMltu9aFwD2WQO0aUKeidbqyIRQl7WrOTGJ25JRLtincRoSU_rNIPecFegkfz0-QuRuSMmOJUov6XZTE6A-_48X4aApOXofomqNzib0kO2BKZYV2YFMItphBCjgnH2WWFlCZvXAIdD87KCNlFoSvoLeTR7Oa0wDFFtdNJXU7VQR64eNrwX9evw-Ca2g8RJkIvWQl1oZaYFvSGmLy7obTZyuedRg2Pn4Xnl1AF2bwixOWsD3waRdElaaYoB9O5oC5aUw53MGb0U9H1tMLpz3ggKD90K51Q","e":"AQAB"}"#; +#[cfg(feature = "rsa-oaep")] +const OAEP_MESSAGE: [u8; 20] = hexlower!("0000000000000000000000000000000000000000"); +#[cfg(feature = "rsa-oaep")] +const OAEP_CIPHERTEXT: [u8; 256] = hexlower!( + "207180c340658b5154ae45d2e4e7326a0997c683a26b595e536a29333c4b6614\ + 9af85e029d5419a39e3a147b221516ffd86b6b4b66c3e0c4c49fe8c57a2f5c37\ + b8704b9b592b80db9cd788a4ed51ab4f0a1cbed63bd18d1f06a22f225866b0c2\ + c417cb23473b7ba4250b1353bd2e5b4f0f937cd2efe5fa38db3c295f7748b970\ + 088657db4aa9a76e1ee6fbff166ec1861d00d085326c7384bdd1bc2f400d4f74\ + dbdfadaf3fdc46073e668573e02030b9eb5af58eb540c66677a771194479ec00\ + 98d858a2ea45d0ba1e6b32440dfbac745000554d51a17684ca964b02a74d479f\ + 1d432ef763ef4059715a4348cfe36a215359712f25b6977903be4adb92febbf6" +); + struct Component; impl Guest for Component { @@ -172,6 +248,8 @@ impl Guest for Component { ) .await?; check("rsa-verify-known-answer", rsa_verify_known_answer().await).await?; + #[cfg(feature = "rsa-oaep")] + check("rsa-oaep-key-transport", rsa_oaep_key_transport().await).await?; check("hkdf-rfc5869-derive", hkdf_derive().await).await?; check("hkdf-sha1-derive", hkdf_sha1_derive().await).await?; check("pbkdf2-rfc7914-derive", pbkdf2_derive().await).await?; @@ -694,6 +772,83 @@ async fn rsa_verify_known_answer() -> Result<()> { ) } +/// The RSA-OAEP key-transport tour, one call per `rsa_oaep` constructor: +/// the Wycheproof known answer decrypts under both private-key imports +/// (PKCS#8 and the file's full-CRT JWK), the public half imported as the +/// SPKI the signature checks carry (same modulus) and as a public JWK +/// encrypts back to the same private key, a ciphertext decrypts only +/// under the label it was encrypted with, and a generated pair +/// round-trips (RFC 8017's OAEP is randomized, so generation has no +/// known-answer form — the round trip is the check). +#[cfg(feature = "rsa-oaep")] +async fn rsa_oaep_key_transport() -> Result<()> { + use polymorph_webcrypto_guest::rsa_oaep::{self, RsaModulus, RsaVariant}; + use polymorph_webcrypto_guest::DecryptionKeyOptions; + + let grants = DecryptionKeyOptions { + decrypt: true, + unwrap: false, + extractable: false, + }; + + // rsa_oaep_2048_sha256_mgf1sha256_test.json tcId 2, under both + // private-key encodings. + let dec = rsa_oaep::import_decryption_key_pkcs8(RsaVariant::Sha256, OAEP_PKCS8, grants) + .await + .context("rsa-oaep-decrypt import-decryption-key-pkcs8")?; + let plaintext = dec.decrypt(None, OAEP_CIPHERTEXT).await?; + ensure!( + plaintext == OAEP_MESSAGE, + "OAEP known answer (PKCS#8 import): got {}", + hex(&plaintext) + ); + let dec_jwk = rsa_oaep::import_decryption_key_jwk(RsaVariant::Sha256, OAEP_PRIVATE_JWK, grants) + .await + .context("rsa-oaep-decrypt import-decryption-key-jwk")?; + let plaintext = dec_jwk.decrypt(None, OAEP_CIPHERTEXT).await?; + ensure!( + plaintext == OAEP_MESSAGE, + "OAEP known answer (JWK import): got {}", + hex(&plaintext) + ); + + // The public half arrives independently of the private key, in both + // import encodings; each encrypts to the vector's private key. + let enc = rsa_oaep::import_encryption_key_spki(RsaVariant::Sha256, RSA_SPKI) + .await + .context("rsa-oaep-encrypt import-encryption-key-spki")?; + let ct = enc.encrypt(None, *b"oaep spki transport").await?; + ensure!( + dec.decrypt(None, ct).await? == b"oaep spki transport", + "SPKI-imported public key's ciphertext did not round-trip" + ); + let enc_jwk = rsa_oaep::import_encryption_key_jwk(RsaVariant::Sha256, OAEP_PUBLIC_JWK) + .await + .context("rsa-oaep-encrypt import-encryption-key-jwk")?; + let label = b"transport label"; + let ct = enc_jwk.encrypt(Some(label), *b"oaep jwk transport").await?; + ensure!( + dec.decrypt(Some(label), ct.clone()).await? == b"oaep jwk transport", + "JWK-imported public key's ciphertext did not round-trip" + ); + expect_error!( + dec.decrypt(Some(b"wrong label"), ct).await, + Error::AuthenticationFailed, + "a ciphertext decrypted under a label it was not encrypted with", + )?; + + // A generated pair round-trips. + let (dec_gen, enc_gen) = rsa_oaep::generate_key(RsaVariant::Sha256, RsaModulus::M2048, grants) + .await + .context("rsa-oaep-decrypt generate-key")?; + let ct = enc_gen.encrypt(None, *b"generated pair").await?; + ensure!( + dec_gen.decrypt(None, ct).await? == b"generated pair", + "generated OAEP pair failed to round-trip" + ); + Ok(()) +} + // --- derivation -------------------------------------------------------------- /// HKDF-SHA-256 against RFC 5869 test case 1, through the SDK wrappers: diff --git a/examples/jco-demo/package.json b/examples/jco-demo/package.json index 5c04c8f..4871254 100644 --- a/examples/jco-demo/package.json +++ b/examples/jco-demo/package.json @@ -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.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-rsa-oaep.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" }, diff --git a/examples/justfile b/examples/justfile index f73cc24..cc26602 100644 --- a/examples/justfile +++ b/examples/justfile @@ -4,15 +4,18 @@ import '../justfile.shared.just' -# Run the Wasmtime (native, RustCrypto) host demo. +# Run the Wasmtime (native, RustCrypto) host demo — the opt-in rsa-oaep +# build: this host serves the withheld-by-default RSA op interfaces. wasmtime: build-component cargo run --release --manifest-path '{{root}}/Cargo.toml' --bin wasmtime-demo-host -- \ - crypto-demo/build/crypto-demo.component.wasm + crypto-demo/build/crypto-demo-rsa-oaep.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. +# self-contained component in target/crypto-demo-composed.wasm. The default +# build is the composable one — the opt-in `rsa-oaep` 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") # In-guest integration test: run the composed demo under `wasmtime` — the diff --git a/examples/wasmtime-demo/tests/demo.rs b/examples/wasmtime-demo/tests/demo.rs index 70d70be..40752ea 100644 --- a/examples/wasmtime-demo/tests/demo.rs +++ b/examples/wasmtime-demo/tests/demo.rs @@ -23,14 +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 component path. The build runs -/// once per test binary: the tests run in parallel, and a concurrent +/// definition of that build — and return the opt-in rsa-oaep 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.component.wasm") + workspace_root.join("examples/crypto-demo/build/crypto-demo-rsa-oaep.component.wasm") } #[tokio::test(flavor = "multi_thread")] diff --git a/justfile b/justfile index 98f5abe..ff1c77f 100644 --- a/justfile +++ b/justfile @@ -50,7 +50,7 @@ fmt-check: # Run clippy across all crates (the wasm crates on their wasm targets). clippy: cargo clippy --workspace --exclude crypto-demo --exclude polymorph-webcrypto-guest-provider --exclude crypto-demo-driver --exclude timing-lab -- -D warnings - cargo clippy -p crypto-demo --target wasm32-unknown-unknown -- -D warnings + cargo clippy -p crypto-demo --all-features --target wasm32-unknown-unknown -- -D warnings cargo clippy -p polymorph-webcrypto-guest-provider --target wasm32-wasip2 -- -D warnings cargo clippy -p crypto-demo-driver --target wasm32-wasip2 -- -D warnings cargo clippy -p timing-lab --target wasm32-wasip2 -- -D warnings diff --git a/justfile.shared.just b/justfile.shared.just index 06a58a0..2ad8be2 100644 --- a/justfile.shared.just +++ b/justfile.shared.just @@ -15,22 +15,36 @@ root := `git rev-parse --show-toplevel` firefox-enabled := if env('GITHUB_ACTIONS', '') == 'true' { '1' } else if env('WPT_PARITY_FIREFOX', '') == '1' { '1' } else { '' } chromium-enabled := if env('GITHUB_ACTIONS', '') == 'true' { '1' } else if env('WPT_PARITY_CHROMIUM', '') == '1' { '1' } else { '' } -# Build the crypto-demo guest component into examples/crypto-demo/build/. -# The output is 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 this path). +# 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). [private] build-component: #!/usr/bin/env bash set -euo pipefail cd '{{root}}' - cargo build --release -p crypto-demo --target wasm32-unknown-unknown mkdir -p examples/crypto-demo/build + cargo build --release -p crypto-demo --target wasm32-unknown-unknown wasm-tools component new \ target/wasm32-unknown-unknown/release/crypto_demo.wasm \ -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 + 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 # Build the in-guest provider component (RustCrypto entirely in-guest; it # exports the polymorph:webcrypto surface) into