From c25ea6845535081841ccfc46cb22d5e9a836d897 Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Tue, 8 Sep 2026 13:09:24 -0400 Subject: [PATCH] docs: record CBC unpadding timing-policy exception (#405) --- AGENTS.md | 3 ++- rust/core/src/cipher.rs | 13 +++++++------ rust/guest-provider/README.md | 11 ++++++++++- wit/aes.wit | 9 +++++---- wit/webcrypto.wit | 3 ++- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8872fbaf..4b71416f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -252,7 +252,8 @@ component — see the conventions note in that file's header). `rust/guest-provider/README.md` carries the timing-channel classification (classes A–D) and this provider's policy: only class A–C algorithms are exported, -always via constant-time-variant implementations; class D algorithm +via constant-time-variant implementations with the recorded CBC-unpadding +exception; class D algorithm interfaces (RSA private-key ops, ECDSA signing, …) are **never** exported by the in-guest provider, so compositions requiring them fail at `wac plug` time. Secret-free operations (hashing public data, signature *verification*) diff --git a/rust/core/src/cipher.rs b/rust/core/src/cipher.rs index 43febadc..61cb16ee 100644 --- a/rust/core/src/cipher.rs +++ b/rust/core/src/cipher.rs @@ -345,12 +345,13 @@ impl CipherKeyMaterial { /// CBC-decrypt and unpad (the `cbc` crate over the keyed block /// cipher). Uniform failure: wrong-shape ciphertext and bad padding - /// are indistinguishable — every failure, including the crate's - /// `UnpadError`, renders as the mode's one fixed message. - /// `block-padding`'s unpad reads the final block with data-dependent - /// branches; what that timing can distinguish is bounded by the - /// uniform error and recorded in the in-guest provider's - /// timing-channel classification. + /// are indistinguishable in the *returned error* — every failure, + /// including the crate's `UnpadError`, renders as the mode's one + /// fixed message. `block-padding`'s unpad reads the final block with + /// data-dependent branches; a uniform returned error does not imply + /// uniform execution timing, and this path is not constant-time. See + /// the in-guest provider's timing-channel classification for the + /// accepted residual risk. fn cbc_decrypt(&self, iv: [u8; BLOCK], ciphertext: &[u8]) -> Result, Error> { if ciphertext.is_empty() || !ciphertext.len().is_multiple_of(BLOCK) { return Err(self.mode.decrypt_failed()); diff --git a/rust/guest-provider/README.md b/rust/guest-provider/README.md index 56f14a37..896fcc45 100644 --- a/rust/guest-provider/README.md +++ b/rust/guest-provider/README.md @@ -114,7 +114,7 @@ what marks where secrets flow. | --- | --- | --- | --- | | HMAC-SHA-2 (256/384/512) and HMAC-SHA-1 | A | `hmac` + `sha2`/`sha1` (pure ARX-style arithmetic; constant-time `verify_slice`) | None beyond compiler correctness. SHA-1 appears only inside the HMAC-family constructions (`hmac-sha1`, `hkdf-sha1`, `pbkdf2-sha1`), where collision resistance is not load-bearing. | | AES-GCM (128/256) | C + B | `aes-gcm` with the soft **fixsliced** AES backend (bitsliced, table-free) + masked-multiply GHASH | Constant-latency integer multiply; JIT does not pathologically rewrite straight-line arithmetic. | -| AES-CBC / AES-CTR (128/256, the `cipher` kind) | C | The same fixsliced `aes` block cipher; CBC chaining, arbitrary-width wrapping CTR, and the branch-free PKCS#7 unpad are assembled here | As AES-GCM's AES half. The CBC padding *verdict* is API-visible by design (WebCrypto parity; one uniform error) — the unpad accumulates it without early exits, so timing adds nothing beyond the verdict itself. | +| AES-CBC / AES-CTR (128/256, the `cipher` kind) | C (AES core; CBC unpadding excepted) | The same fixsliced `aes` block cipher; `cbc` owns CBC chaining, and arbitrary-width wrapping CTR is assembled locally | As AES-GCM's AES half. CBC's PKCS#7 unpadding is variable-time; see the exception below. | | AES-KW (128/256, the `key-wrap` kind) | C | `aes-kw` (RFC 3394) over the same fixsliced `aes` block cipher | As AES-GCM's AES half. The unwrap *verdict* is API-visible by design (one detail-free `authentication-failed` for malformed lengths and bad ICVs alike); the ICV comparison is a fixed-size compare of non-secret-length data. | | X25519 key agreement | B | `x25519-dalek` (curve25519-dalek's constant-time Montgomery ladder: limb-based multiply-accumulate, no secret-dependent branches or indices; the all-zero contributory check compares in constant time) | Constant-latency integer multiply. | | 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. | @@ -125,6 +125,15 @@ what marks where secrets flow. | 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. | +CBC remains exported for fixed-format compatibility with an explicit +exception to the constant-time-variant policy: `block-padding`'s PKCS#7 +unpadding branches on decrypted padding length and stops at the first +mismatch. Equal-sized malformed inputs can take different validation +paths despite returning the same error. Timing can therefore reveal more +than the padding verdict; the uniform error does not remove this risk. +This applies to both decryption and key unwrapping. Prefer AES-GCM where +the format is not fixed; do not use unauthenticated CBC plaintext. + 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/wit/aes.wit b/wit/aes.wit index 62301d4f..9bb990d7 100644 --- a/wit/aes.wit +++ b/wit/aes.wit @@ -127,10 +127,11 @@ interface aes-gcm { /// influence future plaintexts (CWE-329; the BEAST class), not merely /// unique: generate it fresh and randomly per message. /// - CBC with visible decryption failures is padding-oracle-prone by -/// nature. The `cipher` kind's uniform-failure rule bounds what this -/// API reveals to the verdict itself; protocols that surface the -/// verdict per message to an active attacker remain at risk. Prefer -/// `aes-gcm` anywhere the format is not already fixed. +/// nature. The `cipher` kind's uniform-failure rule gives every +/// malformed input the same returned error; a uniform returned error +/// does not guarantee uniform execution timing, and protocols that +/// surface the verdict per message to an active attacker remain at +/// risk. Prefer `aes-gcm` anywhere the format is not already fixed. interface aes-cbc { use types.{error}; use cipher.{cipher-key, cipher-key-options}; diff --git a/wit/webcrypto.wit b/wit/webcrypto.wit index 3780741c..3739f887 100644 --- a/wit/webcrypto.wit +++ b/wit/webcrypto.wit @@ -394,7 +394,8 @@ interface aead { /// condition (for AES-CBC, a bad final padding block among them) fails /// `error.other` with no distinguishing detail. Implementations MUST /// NOT reveal *why* a decryption failed — a distinguishable padding -/// verdict is a padding-oracle amplifier. +/// verdict is a padding-oracle amplifier. This is a contract on the +/// returned error, not a guarantee of uniform execution timing. /// - IV discipline is the algorithm's contract (unpredictability for CBC, /// per-key uniqueness for CTR counter blocks); the minting interface /// documents it. The caller owns it, as with `aead` nonces.