diff --git a/rust/core/src/lib.rs b/rust/core/src/lib.rs index c07bb6d4..1df0d6a9 100644 --- a/rust/core/src/lib.rs +++ b/rust/core/src/lib.rs @@ -22,12 +22,15 @@ //! surfaces it as a trap-shaped host error, the guest treats WASI random //! as infallible. //! -//! ## Class-D policy: ECDSA signing and the RSA private-key families are not compiled for wasm +//! ## Class-D policy: ECDSA signing and the RSA private-key families are withheld from wasm //! //! ECDSA signing handles a per-signature secret nonce whose timing leakage //! is key-recovering, and RSA private-key operations leak key material -//! through timing unless constant-time end to end — class D in -//! polymorph-webcrypto-guest-provider's timing-channel classification. The +//! through timing unless constant-time end to end. The in-guest provider +//! withholds both under class D in its timing-channel classification — +//! for ECDSA, a conservative exclusion pending signing-path assurance +//! rather than a claim that constant-time ECDSA is infeasible in wasm +//! (see that crate's README for the distinction from Ed25519). The //! load-bearing enforcement is the in-guest provider's world, which never //! exports `ecdsa-sign`, the `rsa-sign` interfaces, or (with working //! implementations) the RSA-OAEP operations: a composition that diff --git a/rust/core/src/sig.rs b/rust/core/src/sig.rs index 26bbadf4..8fed44f6 100644 --- a/rust/core/src/sig.rs +++ b/rust/core/src/sig.rs @@ -807,11 +807,8 @@ impl std::fmt::Debug for SigPublic { } /// The private key backing a [`SigningKeyMaterial`]. The ECDSA and RSA -/// arms exist only on non-wasm targets: ECDSA signing is class D -/// (per-signature secret nonce; small timing leaks are key-recovering), -/// and RSA private-key operations are class D outright (the Marvin attack -/// lineage), so their code is structurally absent from every wasm build -/// (see the crate doc). +/// arms exist only on non-wasm targets — both are withheld under class D +/// (see the crate doc for the policy and the enforcement mechanics). enum SigPrivate { Ed25519(ed25519_dalek::SigningKey), #[cfg(not(target_family = "wasm"))] diff --git a/rust/guest-provider/README.md b/rust/guest-provider/README.md index 56f14a37..f3faadb6 100644 --- a/rust/guest-provider/README.md +++ b/rust/guest-provider/README.md @@ -54,15 +54,16 @@ JIT), weighted by how **forgiving of failure** the construction is | **A — structurally constant-time** | None beyond a correct compiler: no secret-dependent branches or memory indices, only add/xor/rotate. Nothing for a JIT to miscompile into a leak. | SHA-2, SHA-3, BLAKE2/3, HMAC, HKDF | Export freely. | | **B — CT given a constant-time multiplier and benign lowering** | Constant-latency hardware multiply; JIT lowers select/cmov without branches. This is where the two-compiler problem lives. | GHASH, X25519/Ed25519 | Export with the CT-variant implementation; document. | | **C — CT only via costly variants** | The *fast* implementation leaks (secret-indexed tables); a bitsliced/fixsliced variant is CT at a several-fold cost. | AES | Export **only** the CT variant. | -| **D — not realistically CT in portable wasm** | Heroic implementation effort with near-zero leak tolerance: bignum branches, secret-dependent allocation, catastrophic small leaks (nonce bits → key recovery; remote-exploitable history). | RSA private-key ops, ECDSA signing, classic DH | **Never exported by this provider.** | +| **D — withheld from this provider** | Known variable-time hazards in the available implementation, or signing-path assurance not established. ECDSA signing is a conservative policy exclusion, not an inherent constant-time limitation — see below. | RSA private-key ops, ECDSA signing, classic DH | **Never exported by this provider.** | Class D is enforced structurally, not by documentation: this provider simply does not export those algorithm interfaces, so a composition that needs them -**fails at `wac plug` time** rather than running quietly degraded — and one -level deeper, the shared `polymorph-webcrypto-core` compiles no ECDSA signing -code for wasm targets at all (`#[cfg(not(target_family = "wasm"))]`), so the -class-D code is absent from this component's binary, not merely unexported. -Choose a host-side provider for them. +**fails at `wac plug` time** rather than running quietly degraded. The shared +`polymorph-webcrypto-core` cfg-gates its own ECDSA and RSA private-key arms out +of wasm targets; RSA's signing backend is additionally dependency-gated out of the +wasm build entirely, while ECDSA's absence from the final `.wasm` rests on +dead-code elimination (see `rust/core/src/lib.rs`'s crate doc for the +distinction). Choose a host-side provider for ECDSA or RSA signing. `just conformance-ct::class-d` is that enforcement's gate: it asserts that the conformance signing guest, whose world imports `ecdsa-sign`, does not @@ -120,11 +121,25 @@ what marks where secrets flow. | ECDH P-256/P-384 (key agreement) | B | `p256`/`p384` (RustCrypto: complete Renes–Costello–Batina formulas, constant-time field and scalar arithmetic, no secret-dependent branches or indices; strict point validation at import) | Constant-latency integer multiply; JIT does not pathologically rewrite straight-line arithmetic. | | SHA-2 digests (256/384/512) | exempt (secret-free) | `sha2` | The `digest` primitive is unkeyed — hashing public data carries no secret to leak. | | Checked SHA-1 digests (`sha1-checked`) | exempt (secret-free) | `sha1-checked` (sha1dc counter-cryptanalysis; both postures) | Unkeyed, like SHA-2; the collision detection branches only on the input, which the digest kind treats as public. | -| Ed25519 (sign + verify) | B | `ed25519-dalek` (complete addition laws, no per-signature secret nonce, constant-time scalar arithmetic) | Constant-latency integer multiply; JIT does not pathologically rewrite straight-line arithmetic. | -| ECDSA P-256/P-384 (**verify only**) | exempt (secret-free) | `p256`/`p384` verification — public keys and public signatures | Signing is class D (per-signature secret nonce; small leaks are key-recovering) and its interface (`ecdsa-sign`) is **not exported**; compositions requiring it fail at `wac plug` time. | +| Ed25519 (sign + verify) | B | `ed25519-dalek` (complete addition laws, constant-time scalar arithmetic; a deterministic per-message secret nonce) | Constant-latency integer multiply; LLVM and the runtime preserve secret-independent control flow and memory access; `timing-lab/` does not currently probe Ed25519 signing. | +| ECDSA P-256/P-384 (**verify only**) | exempt (secret-free) | `p256`/`p384` verification — public keys and public signatures | Signing carries a per-signature secret nonce and is withheld under this provider's class-D policy; its interface (`ecdsa-sign`) is **not exported**, so compositions requiring it fail at `wac plug` time. | | RSASSA-PKCS1-v1_5 / RSA-PSS (**verify only**) | exempt (secret-free) | `rsa` crate verification — public keys and public signatures | Signing and decryption are class D (per-message secrets and blinded private-key ops; the `rsa` crate's private-key operations additionally carry RUSTSEC-2023-0071, the Marvin timing sidechannel) — the RSA private-key interfaces (`rsassa-pkcs1-v15-sign`, `rsa-pss-sign`) are **not exported**. | | RSA-OAEP (**neither half exported**) | D (decrypt); encrypt has no secret-free half | None — the `public-encryption` kind is exported with uninhabited key resources | Decryption is class D and the attack lineage's prime target (blinded private-key ops; the Marvin sidechannel, RUSTSEC-2023-0071). Encryption is *not* secret-free, unlike signature verification: the plaintext is the secret, and it transits general-purpose bignum arithmetic with no constant-time variant — so the kind has no exportable half at all. Exporting the kind makes compositions requiring `rsa-oaep-encrypt` or `rsa-oaep-decrypt` fail at `wac plug` time. | +Ed25519 and ECDSA signing both handle a per-message secret nonce scalar; +Ed25519's determinism (deriving it from a secret prefix and the message, +[RFC 8032, section 5.1.6](https://www.rfc-editor.org/rfc/rfc8032.html#section-5.1.6)) +removes the need for fresh per-signature randomness, not the risk of +leaking that nonce through timing. The pinned P-256 and P-384 crates +already use fixed-width, constant-time-designed field and scalar +arithmetic, including for nonce-scalar inversion. ECDSA signing's +class-D policy is a conservative exclusion pending signing-path +assurance, not a measured safety difference from Ed25519: Ed25519's +class-B classification rests on its source design (complete addition +laws, constant-time scalar arithmetic) plus the stated compiler and +runtime assumptions, and neither classification claims a proven +end-to-end constant-time guarantee. + AES-GCM (fixsliced, class C + B) is the package's only AEAD, and in-guest it is a heroic implementation working against the algorithm's nature: the constant-time property rests on a costly bitsliced countermeasure rather diff --git a/rust/guest-provider/src/provider.rs b/rust/guest-provider/src/provider.rs index c9af7a9d..46c1ab0f 100644 --- a/rust/guest-provider/src/provider.rs +++ b/rust/guest-provider/src/provider.rs @@ -1672,11 +1672,9 @@ options_resource! { /// An exported `verifying-key`: public material bound to its algorithm /// (and its curve, digest, or salt-length parameterization) at minting. /// The ECDSA and RSA arms exist for *verification only* — secret-free, so -/// exempt from the timing-channel classes; ECDSA signing is class D, its -/// interface is not exported, and the shared core compiles no ECDSA -/// signing code for wasm targets. The RSA private-key interfaces -/// (`rsassa-pkcs1-v15-sign`, `rsa-pss-sign`, `rsa-oaep-decrypt`) are -/// likewise class D and not exported. +/// exempt from the timing-channel classes. ECDSA signing and RSA +/// private-key interfaces are withheld under class D (see this crate's +/// README for the policy). pub struct VerifyingKey { public: SigPublic, } @@ -1721,10 +1719,8 @@ impl GuestVerifyingKey for VerifyingKey { } /// An exported `signing-key`: the shared core's signing-key material. On -/// this wasm target the core mints only Ed25519 signing keys -/// (constant-time by construction); ECDSA signing is class D, its -/// interface is not exported, and the core compiles no ECDSA signing code -/// for wasm targets. +/// this wasm target the core mints only Ed25519 signing keys (see this +/// crate's README for the timing-channel policy). pub struct SigningKey { material: SigningKeyMaterial, } diff --git a/wit/README.md b/wit/README.md index 94035927..ab5871cd 100644 --- a/wit/README.md +++ b/wit/README.md @@ -255,14 +255,14 @@ NOT include any of it. ## Timing-channel policy -Some algorithms leak key material through execution timing when the -implementation shares a timing domain with an observer. In particular, -ECDSA signing handles a per-signature secret nonce whose timing leakage is -key-recovering. Providers that execute inside an attacker-observable timing -domain should not export such interfaces; a composition that requires one -then fails at composition (`wac plug`) time rather than at run time. This -repository's in-guest provider documents its classification and policy in -`rust/guest-provider/README.md`. +Execution timing can reveal secret material when an implementation shares +a timing domain with an observer. Both Ed25519 and ECDSA signing use secret +nonce scalars; deterministic generation does not remove their secrecy +requirement. Resistance to timing leakage depends on the implementation, +compiler, runtime, and hardware, not the algorithm alone. Providers should +withhold interfaces that cannot meet their deployment's timing requirements. +This repository's in-guest provider documents its classification and +composition-time enforcement in `rust/guest-provider/README.md`. ## Portability contract diff --git a/wit/ecdsa.wit b/wit/ecdsa.wit index 44425e7b..5209f54e 100644 --- a/wit/ecdsa.wit +++ b/wit/ecdsa.wit @@ -79,11 +79,10 @@ interface ecdsa-verify { /// ECDSA signing-key minting (FIPS 186-5). /// /// Security: -/// - ECDSA signing handles a per-signature secret nonce whose timing -/// leakage is key-recovering. Providers in attacker-observable timing -/// domains do not export this interface, and compositions requiring it -/// then fail at composition time (see `README.md`, "Timing-channel -/// policy"). Host-backed providers serve it. +/// - ECDSA signing handles a per-signature secret nonce. Leakage of that +/// nonce can expose the signing key. Providers must assess their +/// implementation against the deployment's timing requirements (see +/// `README.md`, "Timing-channel policy"). /// /// Signing keys import as PKCS#8 or an EC private JWK — the platform /// pass-through formats — and never as bare scalars (no platform door; diff --git a/wit/ed25519.wit b/wit/ed25519.wit index b913527a..cb7734e3 100644 --- a/wit/ed25519.wit +++ b/wit/ed25519.wit @@ -51,9 +51,16 @@ interface ed25519-verify { /// Ed25519 signing-key minting (RFC 8032). /// /// Split from `ed25519-verify` so a provider can serve verification alone. -/// Ed25519 signing is constant-time by construction (no per-signature -/// secret nonce, complete addition laws), so providers in shared timing -/// domains can serve it (see `README.md`, "Timing-channel policy"). +/// +/// Security: +/// - Signing derives a per-message secret nonce scalar deterministically +/// from a secret prefix and the message +/// ([RFC 8032, section 5.1.6](https://www.rfc-editor.org/rfc/rfc8032.html#section-5.1.6)). +/// Determinism removes the need for fresh per-signature randomness; it +/// does not protect the nonce from timing leakage, which can expose the +/// signing key. Providers must assess their implementation against the +/// deployment's timing requirements (see `README.md`, "Timing-channel +/// policy"). /// /// Signing keys import as PKCS#8 or an OKP private JWK — the platform /// pass-through formats — and never as a bare seed (see `README.md`,