From d122ab2a7b3bf95de33936d4a007003ae9eb3b8f Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 20 Oct 2026 20:51:02 +1100 Subject: [PATCH 1/4] docs: add misuse-resistance and vitaminc agent skills, grounded in usability research Most crypto failures are callers misusing correct libraries. These skills give agents the research-backed design rules for building misuse-resistant APIs, and the vitaminc types and recipes for using them, so agent-written code takes the secure path by default. --- .claude/skills/misuse-resistance/EVIDENCE.md | 29 +++ .claude/skills/misuse-resistance/RUST.md | 109 ++++++++++ .claude/skills/misuse-resistance/SKILL.md | 65 ++++++ .claude/skills/vitaminc/RECIPES.md | 186 +++++++++++++++++ .claude/skills/vitaminc/SKILL.md | 58 ++++++ .gitignore | 3 + docs/research/misuse-resistance.md | 197 +++++++++++++++++++ 7 files changed, 647 insertions(+) create mode 100644 .claude/skills/misuse-resistance/EVIDENCE.md create mode 100644 .claude/skills/misuse-resistance/RUST.md create mode 100644 .claude/skills/misuse-resistance/SKILL.md create mode 100644 .claude/skills/vitaminc/RECIPES.md create mode 100644 .claude/skills/vitaminc/SKILL.md create mode 100644 docs/research/misuse-resistance.md diff --git a/.claude/skills/misuse-resistance/EVIDENCE.md b/.claude/skills/misuse-resistance/EVIDENCE.md new file mode 100644 index 00000000..2edc8681 --- /dev/null +++ b/.claude/skills/misuse-resistance/EVIDENCE.md @@ -0,0 +1,29 @@ +# Evidence + +The studies behind each rule in [SKILL.md](SKILL.md). Cite these when a rule is challenged in review. Fuller notes, with the source of each figure, are in the vitaminc repository at `docs/research/misuse-resistance.md`. + +## Why the API carries the burden + +- **Lazar et al. 2014**, *Why does cryptographic software fail?* (APSys). Of 269 crypto CVEs, 83% were applications misusing libraries; 17% were in the libraries. +- **Oliveira et al. 2014**, *It's the psychology stupid* (ACSAC). Security is not part of developers' everyday heuristics. Primed developers found about twice as many vulnerabilities. +- **Acar et al. 2016**, *You get where you're looking for* (IEEE S&P). Developers using Stack Overflow produced the most functional and least secure code, and rated insecure solutions as secure. +- **Perry et al. 2023**, *Do users write more insecure code with AI assistants?* (CCS). Participants with an AI assistant wrote less secure code, including for encryption, and were more confident it was secure. +- **Elsayed et al. 2026**, *An empirical security evaluation of LLM-generated cryptographic Rust code* (arXiv:2604.27001). 23.3% of 240 samples compiled; of those, 57% had vulnerabilities, chiefly nonce reuse and hallucinated APIs. + +## Per rule + +| Rule | Evidence | +|---|---| +| 1. Secure by default | Fischer et al. 2021 (CCS): re-ranking search results by security raised secure and functional outcomes with no behaviour change. Green & Smith 2016: defaults safe and never ambiguous. | +| 2. Tasks, not primitives | Nadi et al. 2016 (ICSE): Java developers found crypto APIs too low-level and asked for task-based ones. Bernstein et al. 2012: NaCl's `crypto_box` offers no algorithm choices. | +| 3. Own the periphery | Acar et al. 2017 (IEEE S&P): calls into the library were mostly secure; failures came from key storage, password-based derivation, and insecure inputs copied from examples. Egele et al. 2013 (CCS): 88% of 11,748 Android apps misused crypto, through mistakes such as ECB mode, constant keys and constant IVs. | +| 4. Secrets as types | Blessing et al. 2021: 37.2% of vulnerabilities in crypto libraries were memory-safety issues. | +| 5. Invalid states unrepresentable | Russell's hard-to-misuse scale: "won't compile" outranks every runtime and documentation measure. Georgiev et al. 2012 (CCS): certificate validation broke where libraries exposed confusing options. | +| 6. Promise only what you deliver | Oliveira et al. 2018 (SOUPS) and Brun et al. 2023 (TOSEM): APIs with misleading names or hidden preconditions caused vulnerabilities regardless of experience. Albertini et al. 2022 (USENIX Security): callers assumed AEAD key commitment that the primitive did not give. | +| 7. Errors reveal nothing | Padding-oracle and partitioning-oracle attacks (Len, Grubbs & Ristenpart 2021) turn distinguishable failures into plaintext recovery. | +| 8. Warn at point of use | Gorski et al. 2018 (SOUPS): API warnings nearly doubled secure outcomes; code that triggered a warning was 15 times more likely to be fixed. Gorski et al. 2020 (CHI): the five parts of a warning. Vance et al. 2018 (CHI): repeated identical warnings stop being processed. | +| 9. Secure, complete examples | Acar et al. 2017: copied code was three times more likely to work, and insecure inputs in examples were copied with it. Fischer et al. 2017 (IEEE S&P): insecure Stack Overflow snippets propagated into Play Store apps. | +| 10. Keep it small | Blessing et al. 2021: library complexity correlated strongly with vulnerability count. | +| 11. Evolve loudly | Holmwood 2022 (linux.conf.au): Keyczar's replacement by Tink as the model of a breaking change with a migration path. | + +Experience does not protect against footguns: Acar et al. 2017 found Python, security and library experience did not predict success, and Brun et al. 2023 found long-time professionals made as many mistakes as novices. diff --git a/.claude/skills/misuse-resistance/RUST.md b/.claude/skills/misuse-resistance/RUST.md new file mode 100644 index 00000000..6d46ff05 --- /dev/null +++ b/.claude/skills/misuse-resistance/RUST.md @@ -0,0 +1,109 @@ +# Misuse resistance in Rust + +Techniques for reaching each rung of the ladder in [SKILL.md](SKILL.md). The snippets use only `std` so they show the pattern; in production, reach for a vetted crate for the secret type itself (`vitaminc-protected`, or `zeroize` plus `subtle`). Cryptographic internals are `todo!()`, so a copied snippet fails loudly instead of shipping a placeholder: do the same in your own examples. + +## Rung 1: impossible to get wrong + +Remove the parameter. If the library generates the nonce, no caller can reuse one. + +```rust +pub struct Ciphertext { nonce: [u8; 12], sealed: Vec } + +pub struct Cipher { /* key, rng */ } + +impl Cipher { + // No `nonce` argument: the cipher draws a fresh one per call. + pub fn encrypt(&self, plaintext: &[u8], aad: &[u8]) -> Ciphertext { + let nonce = self.fresh_nonce(); + let sealed = self.seal(&nonce, plaintext, aad); + Ciphertext { nonce, sealed } + } + + fn fresh_nonce(&self) -> [u8; 12] { todo!("draw from the CSPRNG") } + fn seal(&self, _nonce: &[u8; 12], _plaintext: &[u8], _aad: &[u8]) -> Vec { todo!("AEAD seal") } +} +``` + +Make the valid value the only value. Private fields plus one constructor mean every instance passed the check. A blanket implementation over a sealed trait means no downstream type can supply its own, divergent, implementation. + +```rust +mod sealed { pub trait Sealed {} } + +/// Implemented only inside this crate. +pub trait Algorithm: sealed::Sealed { const NAME: &'static str; } + +pub struct Aes256Gcm; +impl sealed::Sealed for Aes256Gcm {} +impl Algorithm for Aes256Gcm { const NAME: &'static str = "AES-256-GCM"; } +``` + +## Rung 2: won't compile + +Put the invariant in the type. + +- **Newtypes over raw bytes.** `Key([u8; 32])`, never `&[u8]`. A key, a nonce and a ciphertext should not be interchangeable. +- **Const generics for lengths.** A `[u8; 32]` argument makes the length check infallible and removes the error path. Take runtime-length material through a separate, fallible `try_from_bytes` that rejects weak inputs. +- **Typestate.** Encode protocol steps as types, so the unsafe order does not type-check: + +```rust +pub struct Unverified(Vec); +pub struct Verified(Vec); + +pub struct BadSignature; + +impl Unverified { + pub fn verify(self, signature_is_valid: impl FnOnce(&[u8]) -> bool) -> Result { + if signature_is_valid(&self.0) { Ok(Verified(self.0)) } else { Err(BadSignature) } + } +} + +/// Only verified messages can be processed. There is no path from `Unverified` +/// to `Verified` except `verify`. +pub fn process(message: Verified) -> usize { message.0.len() } +``` + +- **Consume one-shot values.** Take `self` by value for anything that must be used once (a nonce, a one-time key, a request token), so a second use is a move error. +- **Withhold dangerous traits.** A secret type implements no `Copy` (a copy has no owner to wipe it on drop), no `Default` (a default key is an all-zero key), no `Deref`, no derived `PartialEq` (it is not constant-time), no `Display`, and no `Serialize` unless through an explicitly named export type. +- **Post-monomorphisation checks.** A `const` assertion can reject a generic parameter at compile time. Use a named associated `const` item, not an inline `const { }` block: trybuild does not report inline-const failures. +- **Test the guarantee.** Every rung-2 claim gets a compile-fail test (trybuild) showing the misuse does not compile. Without one, a refactor can silently drop it to rung 6 or 7. + +## Rung 3: compiles with a warning + +- `#[must_use]` on results, tags, and anything whose silent drop is a bug. +- `#[deprecated(note = "use X instead: ")]` names the replacement. +- Ship a `clippy.toml` snippet with `disallowed-methods` / `disallowed-types` for downstream users (for example, banning a non-cryptographic RNG in key-handling code). + +## Rung 4: the simplest use is correct + +- The shortest constructor yields the secure configuration. Options exist only to make it stricter, or sit behind an explicitly named alternative. +- Prefer one function with no choices over a builder with secure defaults: a builder invites changing them. + +## Rung 5: the name says how to use it + +Name escape hatches so they are obvious and greppable: `risky_unwrap`, `risky_ref`, a `hazmat` module, an `insecure-*` feature, `*_unchecked`. Obvious, not easy: a caller who needs a legacy algorithm should find the door, and a reviewer should find every use with one search. + +## Rung 6: fails loudly at runtime + +- Return `Result` with an error that reveals nothing about the secret. Cipher failures are one opaque error: a wrong key, a wrong AAD and a tampered ciphertext are indistinguishable. +- Refuse weak input (short keys, empty contexts) at construction, once, then carry the proof in the type. +- Return errors rather than panicking on conditions that depend on a secret: a panic is an observable side channel. + +## Secret types: the full checklist + +A type holding a secret: + +- wipes on drop (`Zeroize` + `ZeroizeOnDrop`, or a wrapper that does it); +- prints an opaque `Debug`, no `Display`; +- exposes its contents only through named, greppable methods, and offers `map`-style combinators so most code never unwraps; +- compares in constant time, if at all; +- serialises only through an explicit export type; +- for process-lifetime secrets, lives in memory locked against swap and core dumps, since nothing drops a static or a killed process. + +## Errors + +- `Display` and `Debug` on error types never include plaintext, keys, or values derived from them. Report the path or position (`field "email"`), not the value. Redact messages that come from user-supplied types. +- Mark error enums `#[non_exhaustive]` so variants can be added without a breaking change. + +## Features + +Optional functionality goes behind cargo features. Dangerous functionality goes behind a feature whose name says so, off by default. diff --git a/.claude/skills/misuse-resistance/SKILL.md b/.claude/skills/misuse-resistance/SKILL.md new file mode 100644 index 00000000..656e8dad --- /dev/null +++ b/.claude/skills/misuse-resistance/SKILL.md @@ -0,0 +1,65 @@ +--- +name: misuse-resistance +description: Misuse-resistant API design for cryptography and sensitive data. Use when designing or reviewing a public API that handles keys, secrets, ciphertext, nonces or randomness; when writing docs or examples for a security library; or when adding warnings or errors to one. +--- + +# Misuse resistance + +A **footgun** is an API whose obvious use is insecure. Most cryptographic failures are footguns fired at the call site: in one study, 83% of crypto CVEs were applications misusing correct libraries. The people firing them, developers and coding agents alike, optimise for code that works, copy examples verbatim, and rate their insecure code as secure. Security therefore has to come from the API itself: the shortest path that works must be the secure path. + +Design for two outcomes at once, **secure** and **functional**. An API that is secure but too hard to finish a task with sends its users to Stack Overflow or a model for a footgun. (Keyczar produced the most secure solutions of five libraries tested, and the fewest working ones.) + +## The ladder + +Ranks the ways a rule can be enforced, best first. Put every rule on the highest rung you can reach. Adapted from Rusty Russell's hard-to-misuse scale. + +1. **Impossible to get wrong.** The wrong thing cannot be expressed: there is no nonce parameter to reuse. +2. **Won't compile.** +3. **Compiles with a warning**: a lint, `#[must_use]`, a deprecation. +4. **The simplest use is correct**: the default is secure. +5. **The name says how to use it**: `risky_unwrap`, a `hazmat` module. +6. **Fails loudly at runtime**, on first use. +7. **Documented.** +8. **Documented, but the obvious use is wrong.** A footgun. + +Below rung 6, a mistake ships silently. Treat rungs 7 and 8 as bugs to fix. + +## Rules + +Each rule states the target. The studies behind each are in [EVIDENCE.md](EVIDENCE.md). + +1. **Secure by default.** The zero-configuration call is the secure call. Callers choose an algorithm, mode, nonce, IV, padding or key size only when the task genuinely needs it, and then every choice offered is safe. +2. **Offer tasks, not primitives.** Name operations for what the caller is doing ("encrypt this record for this tenant"). Primitives live in a lower, clearly named layer. +3. **Own the periphery.** The library provides, typed, everything a caller would otherwise improvise: key generation, key loading and storage, randomness, nonces, password-based derivation, secret comparison, encoding, and wiping. A secure core call fed an improvised input is still a vulnerability, and this is where most real failures are. +4. **Make secrets a type.** A secret lives in a type that wipes on drop, prints opaquely, compares in constant time if at all, and leaves only through a named, greppable escape hatch. +5. **Make invalid states unrepresentable.** Parse once into a type that proves validity (key length, non-empty context, verified signature, authenticated ciphertext) and accept only that type. +6. **Promise only what you deliver.** Names and types claim exactly the guarantee provided. Where a guarantee stops (an AEAD that does not commit to its key, a position that is not authenticated), say so at the type, where the caller looks. +7. **Errors reveal nothing.** Errors never carry plaintext, keys or secret-derived values. Cryptographic failures are one opaque error: a wrong key, a wrong AAD and tampering look the same. Context comes from paths and positions. +8. **Warn at the point of use.** A footgun that cannot be removed (a legacy algorithm, a compatibility mode) warns when used, or requires an explicit opt-in. A warning carries a classification, a title, the code location, and a link to the secure alternative. Keep warnings rare and specific so they are not tuned out. Escape hatches are obvious but effortful: a named feature, a `hazmat` module, a `risky_` prefix. +9. **Every example is secure and complete.** Each example is the production path, inputs included: real key generation, real randomness, real error handling. An example labelled "insecure, for illustration" is copied anyway, so publish only secure ones. Compile-check every example in CI, and use `todo!()` for anything elided so a copied snippet fails loudly. +10. **Keep it small.** Fewer features, options and dependencies. Complexity tracks vulnerability count. +11. **Evolve loudly.** Deprecations name their replacement, and migrations are mechanical. A breaking change is acceptable when the upgrade path is smooth. + +When rules conflict with each other or with other goals (flexibility, performance, `no_std`), name the trade-off, choose explicitly, and record the choice where reviewers will find it: an ADR or the type's doc comment. + +## Designing an API + +1. Write the task as the caller would state it, then list every input the caller must supply. +2. For each input, list its footguns: wrong source, wrong length, reuse, leaking through logs, `Debug` or errors, non-constant-time comparison, accidental serialisation, wrong order of steps. +3. Place each footgun on the ladder, then raise it as far as it will go. Removing the input altogether (rule 3) beats every other rung. +4. Write the caller's secure example before the implementation. If it is longer or harder than the insecure code a caller could write with other tools, simplify the API until it is not. +5. Add a compile-fail test for every rung-2 guarantee and a test for every rung-6 refusal. + +Done when every caller-supplied input has its footgun list, every footgun sits on rung 6 or higher, and each has a test. + +## Reviewing an API or change + +Check every public item in scope (type, function, trait, feature flag, example, doc comment) against all eleven rules. For each finding, report: the item, the rule, the rung it sits on now, the rung it could reach, and the concrete misuse (these inputs lead to this insecure outcome). Before reporting a finding, check the project's ADRs and design notes: a documented, deliberate trade-off is not a footgun. + +Done when every public item in scope has been checked against every rule, with items that have no findings listed as such. + +## Rust and vitaminc + +For Rust techniques at each rung (newtypes, typestate, sealed traits, const generics, withheld traits, compile-fail tests), read [RUST.md](RUST.md). + +In Rust, the `vitaminc` skill provides ready-made types for rules 3, 4 and 5: secret wrappers, randomness, encryption, key wrapping and keyed hashing. Reach for them before writing your own. diff --git a/.claude/skills/vitaminc/RECIPES.md b/.claude/skills/vitaminc/RECIPES.md new file mode 100644 index 00000000..a3bce69b --- /dev/null +++ b/.claude/skills/vitaminc/RECIPES.md @@ -0,0 +1,186 @@ +# vitaminc recipes + +One recipe per task. Each is complete, compiles, and is the production path: copy it as written. Imports use the individual crates; with the facade crate, `vitaminc_protected::X` is `vitaminc::protected::X`, and so on. + +## Hold a secret + +Wrap the secret the moment it exists, and keep it wrapped until the last call that needs it. + +```rust +use vitaminc_protected::{Controlled, Protected}; + +/// Read a token from the environment straight into a `Protected`. +fn api_token() -> Result, std::env::VarError> { + Protected::generate_ok(|| std::env::var("API_TOKEN")) +} + +/// Transform without unwrapping: the closure sees the value, the result stays protected. +fn token_len(token: Protected) -> Protected { + token.map(|t| t.len()) +} + +/// At the boundary to an API that takes a plain `&str`, borrow with `risky_ref`. +/// Keep the borrow inside one call, never store it. +fn send(token: &Protected, client: impl Fn(&str)) { + client(token.risky_ref()); +} +``` + +## Compare secrets + +`Protected` has no `PartialEq`. Compare through `Equatable`, which is constant-time. + +```rust +use vitaminc_protected::{Equatable, Protected}; + +fn tag_matches( + presented: &Equatable>, + expected: &Equatable>, +) -> bool { + presented == expected +} + +let a: Equatable> = Equatable::new([7; 32]); +let b: Equatable> = Equatable::new([7; 32]); +assert!(tag_matches(&a, &b)); +``` + +## Generate random values + +```rust +use vitaminc_protected::Protected; +use vitaminc_random::{Generatable, RandomError, SafeRand, SeedableRng}; + +fn random_secret() -> Result, RandomError> { + let mut rng = SafeRand::from_entropy()?; + Protected::random(&mut rng) +} + +/// A uniform index in `0..len`, for choosing from a list. +fn random_index(rng: &mut SafeRand, len: u32) -> u32 { + rng.next_below(len) +} +``` + +Derive `Generatable` to generate a whole struct of random fields: + +```rust +use vitaminc_random::{Generatable, RandomError, SafeRand, SeedableRng}; + +#[derive(Generatable)] +struct SessionSecrets { + signing: [u8; 32], + encryption: [u8; 32], +} + +fn new_session() -> Result { + SessionSecrets::random(&mut SafeRand::from_entropy()?) +} +``` + +## Generate a password + +```rust +use vitaminc_password::{AlphaNumericPassword, Password}; +use vitaminc_random::{Generatable, RandomError, SafeRand, SeedableRng}; + +fn new_passwords() -> Result<(Password<20>, AlphaNumericPassword<16>), RandomError> { + let mut rng = SafeRand::from_entropy()?; + Ok((Generatable::random(&mut rng)?, Generatable::random(&mut rng)?)) +} +``` + +## Encrypt a value or a struct + +Generate the key, build a cipher, and encrypt under associated data (AAD) naming the record the ciphertext belongs to. Decryption must present the same AAD, so a ciphertext copied onto another record fails to open. Nonces are generated for you. + +```rust +use vitaminc_encrypt::{decrypt, encrypt, Aes256Cipher, Decrypt, Encrypt, Key}; +use vitaminc_random::{Generatable, SafeRand, SeedableRng}; + +#[derive(Debug, PartialEq, Encrypt, Decrypt)] +struct User { + #[aead(passthrough)] // stored in the clear: only for non-sensitive fields + id: u64, + email: String, + date_of_birth: String, +} + +fn round_trip() -> Result<(), Box> { + let key = Key::random(&mut SafeRand::from_entropy()?)?; + let cipher = Aes256Cipher::new(&key)?; + + let user = User { id: 42, email: "ada@example.com".into(), date_of_birth: "1815-12-10".into() }; + let aad = format!("users/{}", user.id); + + let ciphertext = encrypt(&user, &cipher, aad.as_str())?; + let back: User = decrypt(&cipher, ciphertext, aad.as_str())?; + assert_eq!(back, user); + + // The same ciphertext under another record's AAD does not open. + let ciphertext = encrypt(&user, &cipher, aad.as_str())?; + assert!(decrypt::(&cipher, ciphertext, "users/43").is_err()); + Ok(()) +} +round_trip().unwrap(); +``` + +To encrypt a single secret, pass the `Protected` itself. It is walked inside the wrapper and decrypts back into one: + +```rust +use vitaminc_encrypt::{decrypt, encrypt, Aes256Cipher, Key}; +use vitaminc_protected::Protected; +use vitaminc_random::{Generatable, SafeRand, SeedableRng}; + +fn store_token(token: Protected) -> Result<(), Box> { + let cipher = Aes256Cipher::new(&Key::random(&mut SafeRand::from_entropy()?)?)?; + let ciphertext = encrypt(token, &cipher, "integrations/github/token")?; + let _back: Protected = decrypt(&cipher, ciphertext, "integrations/github/token")?; + Ok(()) +} +``` + +## Wrap a data key under a key-encryption key + +```rust +use vitaminc_encrypt::{Aes256Cipher, Key}; +use vitaminc_random::{Generatable, SafeRand, SeedableRng}; + +fn rotate_data_key() -> Result<(), Box> { + let mut rng = SafeRand::from_entropy()?; + let kek = Aes256Cipher::new(&Key::random(&mut rng)?)?; + + let dek = Key::random(&mut rng)?; + let wrapped = dek.wrap(&kek, "tenants/acme/dek/1")?; // store this + + let dek = Key::from_wrapped(&kek, wrapped, "tenants/acme/dek/1")?; + let _data_cipher = Aes256Cipher::new(&dek)?; + Ok(()) +} +``` + +## Keyed hash, blind index, or equality term + +An unkeyed hash of a low-entropy value (an email, a phone number) is reversible by guessing. Derive a keyed term instead, under a context naming the index and its version. Writes and queries must use the same input type and context. + +```rust +use vitaminc_hmac::HmacSha256Prf; +use vitaminc_prf::{PrfKeyInit, PrfValue}; +use vitaminc_protected::Protected; + +async fn email_term(prf: &HmacSha256Prf, email: &str) -> Result<[u8; 32], Box> { + Ok(email.prf_with_context(prf, "users/email/exact/v1").await?) +} + +fn index_key_from_secret_store(bytes: Vec) -> Result> { + // Rejects keys shorter than the minimum length instead of deriving weak terms. + Ok(HmacSha256Prf::try_from_bytes(Protected::new(bytes))?) +} + +let prf = index_key_from_secret_store(vec![7; 32]).unwrap(); +let a = futures::executor::block_on(email_term(&prf, "ada@example.com")).unwrap(); +let b = futures::executor::block_on(email_term(&prf, "ada@example.com")).unwrap(); +assert_eq!(a, b); +``` + +`vec![7; 32]` stands in for bytes fetched from your secret store; generate the real key with `Protected::<[u8; 32]>::random` and pass it to `HmacSha256Prf::new`. diff --git a/.claude/skills/vitaminc/SKILL.md b/.claude/skills/vitaminc/SKILL.md new file mode 100644 index 00000000..c37b2b4b --- /dev/null +++ b/.claude/skills/vitaminc/SKILL.md @@ -0,0 +1,58 @@ +--- +name: vitaminc +description: Using the vitaminc Rust crates for secrets and cryptography. Use when Rust code holds keys, passwords, tokens or other secrets; generates random values or passwords; encrypts or decrypts values or structs; wraps keys; derives keyed hashes or searchable index terms; or compares secrets. +--- + +# vitaminc + +vitaminc is a suite of Rust crates that replace common cryptography footguns with types. When Rust code touches a secret, use the vitaminc type for the job instead of a raw value or a hand-rolled routine. Code written this way is checked by the compiler rather than by the reviewer. + +For the reasoning behind these types, and for designing APIs of your own, see the `misuse-resistance` skill. + +## Install + +Use the facade crate with a feature per capability, and import through it (`vitaminc::protected::Protected`): + +```sh +cargo add vitaminc --features protected,random,encrypt +``` + +Or add individual crates (`cargo add vitaminc-protected`). All crates release together under one version. Deriving `Encrypt`/`Decrypt` also needs a direct `vitaminc-aead` dependency, because the generated code names that crate. + +## Task map + +| Task | Use | In place of | +|---|---|---| +| Hold a key, token, password or other secret | `Protected` (`protected`) | `String`, `Vec`, `[u8; N]`, `Zeroizing` | +| Hold a key for the whole process | `Locked` (`protected`) | a `static` or lazily initialised byte array | +| Compare secrets or tags | `Equatable>`, `TimingSafeEq` (`protected`) | `==` | +| Hash secret input | `ProtectedDigest` (`protected`) | a digest fed raw secret bytes | +| Require a non-empty label or context | `NonEmpty`, `nonempty!` (`protected`) | `&str` | +| Generate random bytes, keys or structs | `SafeRand` + `Generatable` (`random`) | `rand::thread_rng`, `rand::random` | +| Pick a random index | `SafeRand::next_below` (`random`) | `rng.gen::() % n` | +| Generate a password | `Password`, `AlphaNumericPassword`, `AlphaPassword` (`vitaminc-password`) | sampling a charset by hand | +| Encrypt a value or struct | `encrypt` / `decrypt`, `Aes256Cipher`, `#[derive(Encrypt, Decrypt)]` (`encrypt`) | an AEAD crate with nonces managed by hand | +| Wrap a data key under a key-encryption key | `Key::wrap` / `Key::from_wrapped` (`encrypt`) | encrypting key bytes as ordinary data | +| Keyed hash, blind index, equality term | `HmacSha256Prf` + `PrfValue::prf_with_context` (`hmac`, `prf`) | an unkeyed hash, or HMAC with no context | +| HMAC with the key held in AWS KMS | `AwsKmsHmac` (`aws-kms`) | fetching the key into process memory | +| Order-revealing or normalised index terms | `vitaminc-ore`, `vitaminc-normalize` (`ore`, `normalize`) | encoding values for comparison by hand | + +Working code for each common task is in [RECIPES.md](RECIPES.md). Read the recipe before writing the code, and copy it as written. For rows without a recipe, read the crate's docs on docs.rs. + +## Rules + +- **Wrap at the source.** A secret becomes `Protected` the moment it is read or generated (`Protected::generate_ok`, `Protected::random`) and stays wrapped until the last call that needs it. Transform it with `map`, `zip` and `flatten`, which keep the result wrapped. +- **Unwrap only at a boundary.** `risky_unwrap` and `risky_ref` hand the plain value to code outside vitaminc, along with the duty to wipe it and keep it out of logs. Use them only at the call into an external API, inside that one call. +- **Bind every ciphertext to its place.** Pass associated data naming what the ciphertext belongs to, such as `"users/42/email"`, as the third argument to `encrypt` and `decrypt`. A ciphertext moved to another record then fails to open. `()` is for values that truly belong nowhere. +- **Name every derivation.** Give `prf_with_context` a context naming the index and its version, such as `"users/email/exact/v1"`, and use the same input type and context for writes and for queries. +- **Keys come from a generator, a key service or an unwrap**: `Key::random(&mut SafeRand)`, a KMS, or `Key::from_wrapped`. A literal key such as `Key::from([0u8; 32])` is a test fixture. +- **Log the error kind, never the value.** Cipher failures are deliberately the opaque `Unspecified`: a wrong key, a wrong AAD and a tampered ciphertext look the same. + +## Behaviour to know + +- `#[aead(passthrough)]` stores a field in the clear by design. Use it for identifiers and other non-sensitive fields only. +- In an encrypted sequence, each element is bound to the sequence but not to its index, and the element count is not authenticated. If order or completeness matters, put it in the value or the associated data. +- `Aes256Cipher` is not `Sync` (its nonce generator is a `RefCell`). Build one per thread or per task rather than sharing one. +- `Protected` is never `Copy`. Duplicate it with an explicit `clone()`; each clone is wiped when dropped. +- `Locked` is best-effort by default: when the OS refuses to lock memory, the value still exists and `lock_error()` says why. Call `require_locked()`, or set `LockPolicy::Strict` at startup, when locking is mandatory. +- The `Password` string conversions (`into_protected_string`, `into_unprotected_string`) are unimplemented and panic in current releases. Check docs.rs before relying on them. diff --git a/.gitignore b/.gitignore index 56de360b..88adfcab 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ # Bytecode from `mise run pr:gate:has_rust_changes:test`. __pycache__/ + +# Per-developer Claude Code settings. +.claude/settings.local.json diff --git a/docs/research/misuse-resistance.md b/docs/research/misuse-resistance.md new file mode 100644 index 00000000..ba857bf9 --- /dev/null +++ b/docs/research/misuse-resistance.md @@ -0,0 +1,197 @@ +# Misuse resistance: research notes + +Research base for a misuse-resistance skill (or two) in this repo. Starts from the sources Lindsay Holmwood cited at linux.conf.au 2022, then widens to the rest of the field. Each finding ends with what it implies for API design, so the skill can cite evidence for its rules rather than assert them. + +## The talk + +**Footguns and factorisation: how to make users of your cryptographic library successful.** Lindsay Holmwood, linux.conf.au 2022, 16 January 2022 (online). + +- Video: https://www.youtube.com/watch?v=raCRgJJvyRY +- Slides: https://speakerdeck.com/auxesis/footguns-and-factorisation-how-to-make-users-of-your-cryptographic-library-successful +- Abstract: https://linux.conf.au/schedule/presentation/49/ + +The talk's closing takeaways: + +1. Developers focus on functionality first and performance second, and do not prioritise security. Library authors have to do the hard work up front. +2. Help developers prioritise security with targeted warnings, with frictionless misuse-resistant APIs that work and are secure, and with copious secure code examples. +3. Most security bugs happen on the periphery of a cryptography library, inside the applications that use it. + +Two audience questions were left open and are worth carrying into the skill. Lindsay said he had no good answer for improving an API while keeping compatibility (beyond warnings plus a smooth upgrade path, as Keyczar to Tink did). He also said research on whether static types help secure API usage exists but is "contradictory", and never followed up on it publicly. + +## Sources cited in the talk + +### Developers do not think about security unless primed + +Oliveira, D., Rosenthal, M., Morin, N., Yeh, K. C., Cappos, J., & Zhuang, Y. (2014). *It's the psychology stupid: how heuristics explain software vulnerabilities and how priming can illuminate developer's blind spots.* ACSAC 2014, pp. 296–305. + +- Developers were shown six vulnerable snippets (buffer overflow, TLS setup, XSS, TOCTOU, SQL injection, brute-force exhaustion) under three conditions: control, primed ("could a developer experience unexpected results?"), and explicit ("this code has a vulnerability"). +- Primed developers found security problems at almost twice the rate of unprimed ones. +- Security is not part of the heuristics developers use day to day. They focus on functionality and performance. +- The authors recommend that tools developers already use (IDEs, editors, compilers, static analysis) prime developers on the spot, while they are coding. + +*Implication:* do not rely on developers knowing or remembering security rules. Put the signal where the code is written: in types, compile errors and warnings. + +### Stack Overflow gives functional, insecure code + +Acar, Y., Backes, M., Fahl, S., Kim, D., Mazurek, M. L., & Stransky, C. (2016). *You get where you're looking for: The impact of information sources on code security.* IEEE S&P 2016, pp. 289–305. + +- Android developers did four security tasks, each group restricted to one source: Stack Overflow, official docs, books, or free choice. +- Stack Overflow and books gave the highest functional success. Official docs alone gave the lowest, but the most secure results. +- Security success was low (about 38–40%) on secure networking, inter-component communication and storage. Developers rated their own insecure solutions as secure. +- The authors recommend documentation that combines the usefulness of Q&A forums with the security awareness of books and official docs. + +*Implication:* official docs must answer task-shaped questions ("how do I store a key?") with copyable code, or developers go elsewhere. Maintainers should mine Stack Overflow (and now LLM transcripts) for questions their docs fail to answer. + +### Search ranking pushes insecure code up + +Fischer, F., Stachelscheid, Y., & Grossklags, J. (2021). *The effect of Google search on software security: Unobtrusive security interventions via content re-ranking.* ACM CCS 2021, pp. 3070–3084. + +- There is at least a 22.8% chance that one of the top three Google results for a crypto question leads to insecure Stack Overflow code, and insecure results get more clicks. +- Re-ranking results by security made participants submit code that was both more secure and more functional, with no change in developer behaviour. + +*Implication:* interventions that require no behaviour change work best. The analogue for a library is making the default path the secure path, so a developer who does nothing special gets the secure result. + +### Most crypto bugs are in the code that calls the library + +Lazar, D., Chen, H., Wang, X., & Zeldovich, N. (2014). *Why does cryptographic software fail? A case study and open problems.* APSys 2014, pp. 1–7. + +- Analysed 269 CVEs tagged as cryptographic issues (CWE-310), 2011 to 2014, by impact (plaintext disclosure, man-in-the-middle, brute force, side channel) and layer (primitive, protocol, application). +- 83% of the bugs came from applications misusing crypto libraries; 17% were in the libraries themselves. +- Application-layer bugs accounted for all plaintext disclosures, 86% of man-in-the-middle and 79% of brute-force issues. Side channels occurred only in primitives and protocols. + +*Implication:* the API surface is where security is won or lost. A correct primitive behind a misusable interface still produces insecure systems. + +### Simplified APIs are more secure, but not enough on their own + +Acar, Y., Backes, M., Fahl, S., Garfinkel, S., Kim, D., Mazurek, M. L., & Stransky, C. (2017). *Comparing the usability of cryptographic APIs.* IEEE S&P 2017, pp. 154–171. + +The talk's centrepiece, and the paper Lindsay recommended reading if you read only one. + +- 256 Python developers did symmetric (key generation, encrypt/decrypt) and asymmetric (key generation, encrypt/decrypt, certificate validation) tasks with one of cryptography.io, Keyczar, PyNaCl, M2Crypto or PyCrypto, using only that library's docs. +- Libraries with simplified interfaces (the first three explicitly aim for usability) produced more secure results. Keyczar solutions were far more likely to be secure, but few were functional at all. +- About 20% of functional solutions were insecure, yet their authors believed they were secure. +- Python experience, security background and library experience did not predict success. +- Code copied from Stack Overflow was about three times more likely to be functional on symmetric tasks. +- The calls into the library were generally secure. Failures came from **peripheral features** the library left to the caller: key storage, password-based key derivation, and insecure inputs copied from examples. Developers lacked the knowledge to see the danger in those inputs. +- Documentation quality and the availability of working examples for peripheral tasks made or broke secure use. + +*Implication:* misuse resistance must cover the whole task, not just the core call. Identify what callers implicitly depend on (key generation, key storage, randomness, nonce handling, comparisons, encoding) and either wrap it in the library with secure types or document it exhaustively. Every example in the docs must be secure. A comment saying "insecure, do not use" above an example does not stop people copying it. + +### Warnings in the API double secure outcomes + +Gorski, P. L., Lo Iacono, L., Wermke, D., Stransky, C., Möller, S., Acar, Y., & Fahl, S. (2018). *Developers deserve security warnings, too: On the effect of integrated security advice on cryptographic API misuse.* SOUPS 2018, pp. 265–281. + +- Python developers did symmetric key generation, encryption and key storage with either stock PyCrypto or a patched version that printed a warning when an insecure primitive or parameter was used. +- Functional success was the same in both groups. Secure-and-functional success nearly doubled with warnings. +- Developers whose code triggered a warning were 15 times more likely to convert it to a secure solution. +- Participants rated both versions equally usable. + +*Implication:* changing the ergonomics improves security even when the API shape cannot change. This is the priming result from Oliveira et al. delivered at the point of use. + +Gorski, P. L., Acar, Y., Lo Iacono, L., & Fahl, S. (2020). *Listen to developers! A participatory design study on security warnings for cryptographic APIs.* CHI 2020, pp. 1–13. + +- Focus groups with 25 professional developers designed console warnings for crypto APIs. +- Five elements: message classification, title, code location, link to an external resource, and colour. +- Developers want concise messages with detail linked externally, and context-dependent detail. + +*Implication:* a warning or error should name the problem, point at the code, and link to a page that explains the secure alternative. Provide an obvious (not easy) way to opt out, because some callers are forced into legacy parameters by systems they do not control. + +### Further reading listed in the slides + +- Oliveira, D. S., Lin, T., Rahman, M. S., et al. (2018). *API blindspots: Why experienced developers write vulnerable code.* SOUPS 2018. Java APIs with "blindspots" (misleading names, hidden preconditions) reduced developers' ability to spot vulnerabilities, regardless of experience. +- Brun, Y., Lin, T., Somerville, J. E., Myers, E. M., & Ebner, N. (2023). *Blindspots in Python and Java APIs result in vulnerable code.* ACM TOSEM. Replication with 129 more developers and 22 Python APIs. The effect held in both languages and was stronger in Python. Long-time professionals made as many mistakes as novices. Lindsay pointed to these two papers when asked what the common developer mistakes are: mostly insecure inputs, copied from examples. +- Vance, A., et al. (2018). *What do we really know about how habituation to warnings occurs over time? A longitudinal fMRI study of habituation and polymorphic warnings.* CHI 2018. Repeated identical warnings stop being processed; varying them slows habituation. Relevant to how noisy warnings should be. +- Lo Iacono, L., & Gorski, P. L. (2017). *I do and I understand. Not yet true for security APIs. So sad.* EuroUSEC 2017. +- Mindermann, K., Keck, P., & Wagner, S. (2018). *How usable are Rust cryptography APIs?* IEEE QRS 2018, pp. 143–154. Only about half of the major Rust crypto libraries explicitly aimed at usability and misuse resistance. In a 28-student experiment, participants did better with rust-crypto than with ring, which the authors had expected to be more usable. Recommends better docs, higher-level wrappers over low-level libraries, and better examples. +- Blessing, J., Specter, M. A., & Weitzner, D. J. (2021). *You really shouldn't roll your own crypto: An empirical study of vulnerabilities in cryptographic libraries.* arXiv:2107.04940. Across eight libraries, only 27.2% of vulnerabilities were cryptographic issues and 37.2% were memory safety issues. Complexity correlated strongly with insecurity. (Lindsay referred to this as a 2011 paper in the talk; it is 2021.) + +## Wider research + +### Measuring misuse in the wild + +- Egele, M., Brumley, D., Fratantonio, Y., & Kruegel, C. (2013). *An empirical study of cryptographic misuse in Android applications.* ACM CCS 2013. 10,327 of 11,748 apps (88%) made at least one mistake, such as ECB mode, constant IVs, constant keys or low PBKDF iteration counts. +- Nadi, S., Krüger, S., Mezini, M., & Bodden, E. (2016). *Jumping through hoops: Why do Java developers struggle with cryptography APIs?* ICSE 2016. Developers were confident choosing concepts (encryption vs signatures) but found the APIs too low-level, and wanted task-based APIs that hide algorithm choices. +- Fischer, F., Böttinger, K., Xiao, H., Stransky, C., Acar, Y., Backes, M., & Fahl, S. (2017). *Stack Overflow considered harmful? The impact of copy&paste on Android application security.* IEEE S&P 2017. Insecure crypto snippets from Stack Overflow were found copied into a large share of Play Store apps. +- Georgiev, M., et al. (2012). *The most dangerous code in the world: Validating SSL certificates in non-browser software.* ACM CCS 2012. Certificate validation was broken in many applications because the libraries exposed confusing options with unsafe defaults. +- Patnaik, N., Hallett, J., & Rashid, A. (2019). *Usability smells: An analysis of developers' struggle with crypto libraries.* SOUPS 2019. Analysed about 2,400 Stack Overflow questions on seven libraries, mapped the issues to Green and Smith's principles, and named four "usability smells" where the principles are not observed. +- Hazhirpasand, M., Nierstrasz, O., & Ghafari, M. (2021). *Dazed and confused: What's wrong with crypto libraries?* PST 2021. + +### Design principles + +- Green, M., & Smith, M. (2016). *Developers are not the enemy! The need for usable security APIs.* IEEE Security & Privacy 14(5), pp. 40–46. Ten principles for usable security APIs, including: integrate crypto into standard APIs so most developers never touch crypto APIs; be powerful enough for non-security needs too; be easy to learn without crypto expertise; do not break the developer's mental model; be usable without documentation; be hard to misuse, with incorrect use producing visible errors; make defaults safe and never ambiguous. The full list is in the paper and should be checked against the original before the skill quotes it. +- Rusty Russell's "hard to misuse" scale (Ottawa Linux Symposium keynote, 2003; blog posts 2008). Ranks interfaces from "impossible to get wrong" and "compiler/linker won't let you get it wrong" at the top, down through "simplest use is correct", "name tells you how to use it", and "do it right or it breaks at runtime", to "the obvious use is wrong" at the bottom. The point is that "hard to misuse" is a stronger goal than "easy to use". Summary: https://technovelty.org/badcode/rusty-russells-hard-to-misuse-rules.html +- Bernstein, D. J., Lange, T., & Schwabe, P. (2012). *The security impact of a new cryptographic library.* LATINCRYPT 2012. The design rationale for NaCl: one high-level `crypto_box` call with no algorithm choices, no data-dependent branches or indices on secrets, and no padding oracles by construction. +- Rogaway, P., & Shrimpton, T. (2006). *A provable-security treatment of the key-wrap problem.* EUROCRYPT 2006. Introduces misuse-resistant authenticated encryption (SIV), the origin of the term in cryptography: nonce reuse degrades gracefully instead of failing catastrophically. See also RFC 8452 (AES-GCM-SIV). +- Albertini, A., Duong, T., Gueron, S., Kölbl, S., Luykx, A., & Schmieg, S. (2022). *How to abuse and fix authenticated encryption without key commitment.* USENIX Security 2022. A different kind of misuse: callers assume AEAD properties (key commitment) that the primitive does not give. The API must not let callers believe in guarantees it lacks. +- Type-driven design outside the crypto literature: Yaron Minsky's "make illegal states unrepresentable", and Alexis King's "Parse, don't validate" (2019). These are the language-level tools for climbing Rusty Russell's scale: newtypes, typestate, sealed traits, and constructors that are the only way to obtain a valid value. + +### Tooling + +- Krüger, S., Späth, J., Ali, K., Bodden, E., & Mezini, M. (2018/2021). *CrySL: An extensible approach to validating the correct usage of cryptographic APIs.* ECOOP 2018 / IEEE TSE 2021. Specification language and static analysis (CogniCrypt) for crypto API rules. Shows how much of the rule set a library could encode in its types instead. +- Kafader, S., & Ghafari, M. (2021). *FluentCrypto: Cryptography in easy mode.* ICSME 2021. A task-based fluent API over Node's crypto module. +- Zhang, Y., et al. *Automatic detection of Java cryptographic API misuses: Are we there yet?* No single detector was best, and developers rejected detector reports more often than they accepted them. Evidence for preventing misuse in the API rather than detecting it afterwards. + +### AI assistants and LLM-generated code + +Directly relevant because the skill's readers include agents, and agents now write much of the code that calls crypto libraries. + +- Perry, N., Srivastava, M., Kumar, D., & Boneh, D. (2023). *Do users write more insecure code with AI assistants?* ACM CCS 2023. Participants with an AI assistant wrote significantly less secure code, including for string encryption, and were more likely to believe it was secure. This is the Acar et al. confidence gap, now amplified. +- *An investigation into misuse of Java security APIs by large language models.* ASIACCS 2024 (arXiv:2404.03823). ChatGPT-generated code misused Java security APIs frequently. +- Elsayed, M., Fulton, K., & Yang, J. (2026). *An empirical security evaluation of LLM-generated cryptographic Rust code.* EASE 2026 workshop (arXiv:2604.27001). 240 Rust samples for AES-256-GCM and ChaCha20-Poly1305 from three models. Only 23.3% compiled. A crypto-specific analyser flagged vulnerabilities in 57% of the compiled samples. Systematic failures were nonce reuse and hallucinated APIs. General-purpose tools (CodeQL) missed the AEAD misuses. +- LLMs as misuse detectors: *Beyond static pattern matching? Rethinking automatic cryptographic API misuse detection in the era of LLMs* (ISSTA 2025, arXiv:2407.16576). Raw LLM reports were over half false positives; constrained scope plus self-validation reached about 90% detection. + +*Implication:* models reproduce the same footguns humans do, add hallucinated APIs, and are confidently wrong. Two consequences for this project. First, misuse resistance in types matters more, because the compiler is the one reviewer an agent cannot talk past. Second, a skill is a priming intervention in the Oliveira sense: it puts security context in front of the agent at the moment it writes code. + +## Distilled principles + +Each principle names its evidence. These are candidates for the skill's rules. + +1. **Make the secure path the default and the shortest path.** Developers optimise for "it works". No-behaviour-change interventions work best. (Oliveira 2014; Fischer 2021; Green & Smith 2016) +2. **Climb the hard-to-misuse scale: compile error over runtime error over warning over documentation.** Prefer "won't compile" to "breaks at runtime" to "warns" to "documented". (Russell; Green & Smith "incorrect use produces visible errors") +3. **Own the periphery.** Key generation, key storage, randomness, nonces, password-based derivation, comparison and encoding are where real systems fail. Provide them, typed, in the library. (Acar 2017; Lazar 2014) +4. **Offer tasks, not primitives.** No algorithm, mode, IV or padding choices at the call site unless the caller genuinely needs them. (Nadi 2016; Bernstein 2012) +5. **Do not let callers believe in guarantees the API lacks.** Name and type things for what they actually promise. (API blindspots: Oliveira 2018, Brun 2023; Albertini 2022) +6. **Warn at the point of use, with an obvious but effortful escape hatch.** Name the problem, locate it, link to the fix. Avoid habituation from noisy repeated warnings. (Gorski 2018, 2020; Vance 2018) +7. **Every example is secure, complete, and copyable.** Developers and models copy examples verbatim, including their inputs. (Acar 2016, 2017; Fischer 2017) +8. **Do not trust self-assessed security, human or model.** Verify with tests, test vectors and review. (Acar 2016, 2017; Perry 2023; Elsayed 2026) +9. **Keep it small.** Complexity correlates with vulnerabilities. (Blessing 2021) +10. **Evolve with warnings and a smooth migration path.** Breaking changes are acceptable if the upgrade path is smooth (Keyczar to Tink). (Holmwood 2022 Q&A) + +## Where vitaminc already applies these + +Snapshot from the crate READMEs, October 2026. Check the code before the skill relies on it. + +| Principle | vitaminc today | +|---|---| +| Own the periphery: secret handling | `vitaminc-protected`: `Protected` zeroizes on drop, keeps secrets out of `Debug`, constrains comparison and serialisation | +| Own the periphery: randomness | `vitaminc-random`: `SafeRand` and the `Generatable` derive | +| Own the periphery: passwords | `vitaminc-password`: compile-time length, `Protected` storage, no heap until converted | +| Own the periphery: nonces | `vitaminc-encrypt`: automatic random nonce per encryption; 256-bit keys only | +| Compile error over runtime error | `vitaminc-hmac`: `new` takes `Protected<[u8; 32]>`, so key length is guaranteed by the type and construction cannot fail; runtime-length keys go through a separate fallible `try_from_bytes` | +| No guarantees the API lacks | `vitaminc-context`: one encoding for AAD and PRF context, equal by construction because the views are blanket impls that cannot be hand-written | +| Tasks, not primitives | `vitaminc-aead`: describe a value once as `Encrypt` and walk it into a `Sink`; structural binding rules live once in the sink, not in each caller | +| Typed outputs | `vitaminc-ore` / `vitaminc-normalize`: sealed `OrderPlaintext` only the crate can build; frozen domain labels so values that only look alike do not collide | +| Sensitivity in traits | `vitaminc-traits`: inputs and outputs typed so sensitivity can be reasoned about; all types bound by `Controlled` | + +Gaps worth checking against the principles: runtime warnings (principle 6) do not appear to be used anywhere, and whether examples in every README and doc comment meet principle 7 has not been audited. + +## Follow-up: replicate Acar et al. 2017 with agents + +Acar et al. 2017 has never been replicated with Tink (Keyczar's successor), and the only Rust study (Mindermann 2018) used 28 students. Agents now write a large share of the code that calls crypto libraries, so agents are the participants that matter. + +Plan, to come back to: use the skill tester to run the paper's tasks (symmetric key generation, encryption and decryption; asymmetric key generation, encryption and decryption, certificate validation; plus key storage and password-based key derivation, the periphery where the paper found most failures) with agents as participants. Vary the model, the prompt, and the context (no docs, library docs only, with and without the skills below). Compare: + +- Tink +- vitaminc on its own +- Stack Encrypt, once it comes together + +Measure what the paper measured: functional success, secure success (scored separately, with a crypto-specific rubric rather than a general-purpose analyser, per Elsayed 2026), and the agent's self-assessed security. Also record hallucinated APIs, the failure mode the paper's human participants did not have. + +## Proposal for the skill(s) + +The goals split cleanly into two audiences with different needs, so two skills make sense: + +1. **`misuse-resistance`** (general, for any library author: vitaminc maintainers and outside developers). The distilled principles with their evidence, Rust techniques for each rung of the hard-to-misuse scale (newtypes, typestate, sealed traits, private constructors, `#[must_use]`, compile-fail tests with trybuild), a review checklist, and guidance on docs and examples. Language-agnostic principles first, Rust idioms second, so it is useful outside Rust. +2. **`vitaminc`** (for developers adopting vitaminc in their own code). Maps common tasks and footguns to vitaminc capabilities: "I need to hold a key" leads to `Protected`, "I need random bytes" leads to `SafeRand`, "I need to encrypt a struct" leads to `vitaminc-encrypt` / `vitaminc-aead`. Every example secure and copyable, per principle 7. The first skill links to this one wherever vitaminc provides the secure wrapper. + +Internal development uses both: the first when designing vitaminc's own APIs, the second to keep vitaminc's examples and docs honest. From da5eb546a5eb5a2c38a8c5d952a5cb6e7c204caa Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Wed, 21 Oct 2026 11:58:09 +1100 Subject: [PATCH 2/4] docs(skills): fix misuse in the skills' own examples, found by a skill-improver review loop The typestate example let the caller pass the verification check, so |_| true got past it; the caller now passes a key and the library checks. The encrypt and key-wrap recipes made a fresh key per call while being labelled the production path; the cipher is now built once at startup from a data key generated once per tenant and stored wrapped under AAD naming that tenant, not one hard-coded tenant's. The Generatable recipe keeps its random secrets in Protected. SKILL.md no longer lists a KMS as a key source vitaminc can load from, says passthrough fields are unauthenticated, and stops sending password generation to a type whose value cannot be read. --- .claude/skills/misuse-resistance/RUST.md | 21 +++- .claude/skills/vitaminc/RECIPES.md | 120 ++++++++++++++--------- .claude/skills/vitaminc/SKILL.md | 8 +- 3 files changed, 96 insertions(+), 53 deletions(-) diff --git a/.claude/skills/misuse-resistance/RUST.md b/.claude/skills/misuse-resistance/RUST.md index 6d46ff05..4ff8e174 100644 --- a/.claude/skills/misuse-resistance/RUST.md +++ b/.claude/skills/misuse-resistance/RUST.md @@ -46,19 +46,30 @@ Put the invariant in the type. - **Typestate.** Encode protocol steps as types, so the unsafe order does not type-check: ```rust -pub struct Unverified(Vec); +pub struct VerifyingKey { /* public key */ } + +pub struct Unverified { message: Vec, signature: [u8; 64] } pub struct Verified(Vec); pub struct BadSignature; impl Unverified { - pub fn verify(self, signature_is_valid: impl FnOnce(&[u8]) -> bool) -> Result { - if signature_is_valid(&self.0) { Ok(Verified(self.0)) } else { Err(BadSignature) } + // The caller supplies a key, never the check: the library owns the + // verification, so there is no predicate to stub out with `|_| true`. + pub fn verify(self, key: &VerifyingKey) -> Result { + key.check(&self.message, &self.signature)?; + Ok(Verified(self.message)) + } +} + +impl VerifyingKey { + fn check(&self, _message: &[u8], _signature: &[u8; 64]) -> Result<(), BadSignature> { + todo!("signature verification") } } -/// Only verified messages can be processed. There is no path from `Unverified` -/// to `Verified` except `verify`. +/// Only verified messages can be processed. `Verified` has a private field, so +/// the only way to obtain one is a signature check the library performs. pub fn process(message: Verified) -> usize { message.0.len() } ``` diff --git a/.claude/skills/vitaminc/RECIPES.md b/.claude/skills/vitaminc/RECIPES.md index a3bce69b..3bfcfa34 100644 --- a/.claude/skills/vitaminc/RECIPES.md +++ b/.claude/skills/vitaminc/RECIPES.md @@ -65,12 +65,13 @@ fn random_index(rng: &mut SafeRand, len: u32) -> u32 { Derive `Generatable` to generate a whole struct of random fields: ```rust +use vitaminc_protected::Protected; use vitaminc_random::{Generatable, RandomError, SafeRand, SeedableRng}; #[derive(Generatable)] struct SessionSecrets { - signing: [u8; 32], - encryption: [u8; 32], + signing: Protected<[u8; 32]>, + encryption: Protected<[u8; 32]>, } fn new_session() -> Result { @@ -80,25 +81,36 @@ fn new_session() -> Result { ## Generate a password -```rust -use vitaminc_password::{AlphaNumericPassword, Password}; -use vitaminc_random::{Generatable, RandomError, SafeRand, SeedableRng}; +Draw each character uniformly with `next_below`, and build the string inside `Protected::generate` so it is wrapped from the start. Reserve the full capacity first, so the string never reallocates and leaves an unwiped copy behind. -fn new_passwords() -> Result<(Password<20>, AlphaNumericPassword<16>), RandomError> { - let mut rng = SafeRand::from_entropy()?; - Ok((Generatable::random(&mut rng)?, Generatable::random(&mut rng)?)) +```rust +use vitaminc_protected::{Controlled, Protected}; +use vitaminc_random::SafeRand; + +const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + +fn new_password(rng: &mut SafeRand, len: usize) -> Protected { + Protected::generate(|| { + let mut password = String::with_capacity(len); + for _ in 0..len { + password.push(CHARSET[rng.next_below(CHARSET.len() as u32) as usize] as char); + } + password + }) } ``` ## Encrypt a value or a struct -Generate the key, build a cipher, and encrypt under associated data (AAD) naming the record the ciphertext belongs to. Decryption must present the same AAD, so a ciphertext copied onto another record fails to open. Nonces are generated for you. +Build the cipher once at startup from a key that outlives the process (see "Wrap a data key" below), and pass it to every call. A key generated inside the function that encrypts is lost when it returns, and so is everything sealed under it. Encrypt under associated data (AAD) naming the record the ciphertext belongs to. Decryption must present the same AAD, so a ciphertext copied onto another record fails to open. Nonces are generated for you. ```rust -use vitaminc_encrypt::{decrypt, encrypt, Aes256Cipher, Decrypt, Encrypt, Key}; -use vitaminc_random::{Generatable, SafeRand, SeedableRng}; +use vitaminc_encrypt::{ + decrypt, encrypt, Aes256Cipher, AesCipherText, DecodeError, Decrypt, DecryptError, Encrypt, + EncryptError, Unspecified, +}; -#[derive(Debug, PartialEq, Encrypt, Decrypt)] +#[derive(Encrypt, Decrypt)] struct User { #[aead(passthrough)] // stored in the clear: only for non-sensitive fields id: u64, @@ -106,56 +118,76 @@ struct User { date_of_birth: String, } -fn round_trip() -> Result<(), Box> { - let key = Key::random(&mut SafeRand::from_entropy()?)?; - let cipher = Aes256Cipher::new(&key)?; - - let user = User { id: 42, email: "ada@example.com".into(), date_of_birth: "1815-12-10".into() }; - let aad = format!("users/{}", user.id); - - let ciphertext = encrypt(&user, &cipher, aad.as_str())?; - let back: User = decrypt(&cipher, ciphertext, aad.as_str())?; - assert_eq!(back, user); +/// Seal a user for storage. `cipher` is built once at startup and shared by every call. +fn seal_user(cipher: &Aes256Cipher, user: &User) -> Result> { + encrypt(user, cipher, format!("users/{}", user.id).as_str()) +} - // The same ciphertext under another record's AAD does not open. - let ciphertext = encrypt(&user, &cipher, aad.as_str())?; - assert!(decrypt::(&cipher, ciphertext, "users/43").is_err()); - Ok(()) +/// Open the record stored under `id`. Take `id` from the lookup, not from the stored +/// value: the passthrough copy is unauthenticated. Under another record's id this fails. +fn open_user( + cipher: &Aes256Cipher, + id: u64, + stored: AesCipherText, +) -> Result> { + decrypt(cipher, stored, format!("users/{id}").as_str()) } -round_trip().unwrap(); ``` To encrypt a single secret, pass the `Protected` itself. It is walked inside the wrapper and decrypts back into one: ```rust -use vitaminc_encrypt::{decrypt, encrypt, Aes256Cipher, Key}; +use vitaminc_encrypt::{ + decrypt, encrypt, Aes256Cipher, AesCipherText, DecodeError, DecryptError, EncryptError, + Unspecified, +}; use vitaminc_protected::Protected; -use vitaminc_random::{Generatable, SafeRand, SeedableRng}; -fn store_token(token: Protected) -> Result<(), Box> { - let cipher = Aes256Cipher::new(&Key::random(&mut SafeRand::from_entropy()?)?)?; - let ciphertext = encrypt(token, &cipher, "integrations/github/token")?; - let _back: Protected = decrypt(&cipher, ciphertext, "integrations/github/token")?; - Ok(()) +fn seal_token(cipher: &Aes256Cipher, token: Protected) -> Result> { + encrypt(token, cipher, "integrations/github/token") +} + +fn open_token( + cipher: &Aes256Cipher, + stored: AesCipherText, +) -> Result, DecryptError> { + decrypt(cipher, stored, "integrations/github/token") } ``` ## Wrap a data key under a key-encryption key +Generate the data key once, when its owner (a tenant, a table) is created, and store only the wrapped form. At every startup, unwrap it and build the cipher the encrypt recipes take. `kek` is the key-encryption key the service holds for its whole lifetime; it is never generated per call. It is built from 32 bytes held in the secret store, passed straight to `Key::from`. + +Wrap each data key under AAD naming its owner. Under one KEK, a wrapped key copied onto another tenant's record then fails to unwrap rather than opening as that tenant's key. + ```rust -use vitaminc_encrypt::{Aes256Cipher, Key}; -use vitaminc_random::{Generatable, SafeRand, SeedableRng}; +use vitaminc_encrypt::{Aes256Cipher, AesCipherText, Key}; +use vitaminc_random::{Generatable, SafeRand}; -fn rotate_data_key() -> Result<(), Box> { - let mut rng = SafeRand::from_entropy()?; - let kek = Aes256Cipher::new(&Key::random(&mut rng)?)?; +/// Run at startup, on the root key's bytes fetched from the secret store. +fn key_encryption_cipher(root_key: [u8; 32]) -> Result> { + Ok(Aes256Cipher::new(&Key::from(root_key))?) +} - let dek = Key::random(&mut rng)?; - let wrapped = dek.wrap(&kek, "tenants/acme/dek/1")?; // store this +/// Run once, when the tenant is created. Store the result with the tenant. +fn new_data_key( + kek: &Aes256Cipher, + rng: &mut SafeRand, + tenant_id: &str, +) -> Result> { + let dek = Key::random(rng)?; + Ok(dek.wrap(kek, format!("tenants/{tenant_id}/dek/1").as_str())?) +} - let dek = Key::from_wrapped(&kek, wrapped, "tenants/acme/dek/1")?; - let _data_cipher = Aes256Cipher::new(&dek)?; - Ok(()) +/// Run at every startup, on the stored result of `new_data_key` for the same tenant. +fn data_cipher( + kek: &Aes256Cipher, + tenant_id: &str, + wrapped: AesCipherText, +) -> Result> { + let dek = Key::from_wrapped(kek, wrapped, format!("tenants/{tenant_id}/dek/1").as_str())?; + Ok(Aes256Cipher::new(&dek)?) } ``` diff --git a/.claude/skills/vitaminc/SKILL.md b/.claude/skills/vitaminc/SKILL.md index c37b2b4b..35c4842e 100644 --- a/.claude/skills/vitaminc/SKILL.md +++ b/.claude/skills/vitaminc/SKILL.md @@ -30,7 +30,7 @@ Or add individual crates (`cargo add vitaminc-protected`). All crates release to | Require a non-empty label or context | `NonEmpty`, `nonempty!` (`protected`) | `&str` | | Generate random bytes, keys or structs | `SafeRand` + `Generatable` (`random`) | `rand::thread_rng`, `rand::random` | | Pick a random index | `SafeRand::next_below` (`random`) | `rng.gen::() % n` | -| Generate a password | `Password`, `AlphaNumericPassword`, `AlphaPassword` (`vitaminc-password`) | sampling a charset by hand | +| Generate a password | `SafeRand::next_below` per character, built inside `Protected::generate` (`random`, `protected`) | `rand`'s `Alphanumeric` sampler, or `vitaminc-password`, whose value cannot be read yet | | Encrypt a value or struct | `encrypt` / `decrypt`, `Aes256Cipher`, `#[derive(Encrypt, Decrypt)]` (`encrypt`) | an AEAD crate with nonces managed by hand | | Wrap a data key under a key-encryption key | `Key::wrap` / `Key::from_wrapped` (`encrypt`) | encrypting key bytes as ordinary data | | Keyed hash, blind index, equality term | `HmacSha256Prf` + `PrfValue::prf_with_context` (`hmac`, `prf`) | an unkeyed hash, or HMAC with no context | @@ -45,14 +45,14 @@ Working code for each common task is in [RECIPES.md](RECIPES.md). Read the recip - **Unwrap only at a boundary.** `risky_unwrap` and `risky_ref` hand the plain value to code outside vitaminc, along with the duty to wipe it and keep it out of logs. Use them only at the call into an external API, inside that one call. - **Bind every ciphertext to its place.** Pass associated data naming what the ciphertext belongs to, such as `"users/42/email"`, as the third argument to `encrypt` and `decrypt`. A ciphertext moved to another record then fails to open. `()` is for values that truly belong nowhere. - **Name every derivation.** Give `prf_with_context` a context naming the index and its version, such as `"users/email/exact/v1"`, and use the same input type and context for writes and for queries. -- **Keys come from a generator, a key service or an unwrap**: `Key::random(&mut SafeRand)`, a KMS, or `Key::from_wrapped`. A literal key such as `Key::from([0u8; 32])` is a test fixture. +- **Keys come from a generator, an unwrap or the secret store**: generate a data key once with `Key::random(&mut SafeRand)`, store it wrapped, and load it with `Key::from_wrapped`. The root key-encryption key is the one key built from bytes: fetch its 32 bytes from the secret store at startup and pass them straight to `Key::from`, keeping no other copy. No vitaminc API fetches an AES key from a KMS (`AwsKmsHmac` computes HMACs only), so the secret store is the caller's code. A key written into the source, such as `Key::from([0u8; 32])`, is a test fixture. - **Log the error kind, never the value.** Cipher failures are deliberately the opaque `Unspecified`: a wrong key, a wrong AAD and a tampered ciphertext look the same. ## Behaviour to know -- `#[aead(passthrough)]` stores a field in the clear by design. Use it for identifiers and other non-sensitive fields only. +- `#[aead(passthrough)]` stores a field in the clear by design: it is neither encrypted nor authenticated, so it can be read, edited or removed in storage while every encrypted field beside it still decrypts. Use it only for non-sensitive fields, treat what it returns as untrusted input, and never let it decide authorization, tenancy or which record to trust. A value that must be readable but tamper-evident belongs in the associated data. - In an encrypted sequence, each element is bound to the sequence but not to its index, and the element count is not authenticated. If order or completeness matters, put it in the value or the associated data. - `Aes256Cipher` is not `Sync` (its nonce generator is a `RefCell`). Build one per thread or per task rather than sharing one. - `Protected` is never `Copy`. Duplicate it with an explicit `clone()`; each clone is wiped when dropped. - `Locked` is best-effort by default: when the OS refuses to lock memory, the value still exists and `lock_error()` says why. Call `require_locked()`, or set `LockPolicy::Strict` at startup, when locking is mandatory. -- The `Password` string conversions (`into_protected_string`, `into_unprotected_string`) are unimplemented and panic in current releases. Check docs.rs before relying on them. +- `vitaminc-password` is not usable yet. `Password`, `AlphaNumericPassword` and `AlphaPassword` generate a value but give no way to read it: their string conversions (`into_protected_string`, `into_unprotected_string`) are unimplemented and panic. Use the password recipe in RECIPES.md instead. From 2a541d17cd46642192ecdbe4287311ab3f6de86d Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Wed, 21 Oct 2026 11:59:52 +1100 Subject: [PATCH 3/4] docs(skills): close the minor review findings that would mislead a reader The random-index recipe refused nothing although next_below panics on zero; it now returns None for an empty list. The password recipe refuses lengths under 16 characters. The encrypt recipe told readers to share one cipher with every call, but Aes256Cipher is !Sync; it now says one per thread or task. The PRF key loader takes its bytes already in Protected, the root-key loader says why it must take a plain array and to keep no other copy, and the random recipes drop an unused SeedableRng import. In RUST.md the rung-1 encrypt returns an error rather than being infallible, as rule 9 asks, and states the random-nonce message limit; the const-generics bullet is renamed for what it shows, and the typestate example can construct an Unverified. EVIDENCE.md qualifies the Acar three-times figure and the Blessing link to rule 4, and the research notes now list Len, Grubbs and Ristenpart 2021, which rule 7 cites. --- .claude/skills/misuse-resistance/EVIDENCE.md | 4 +- .claude/skills/misuse-resistance/RUST.md | 27 +++++++++----- .claude/skills/vitaminc/RECIPES.md | 39 ++++++++++++-------- docs/research/misuse-resistance.md | 1 + 4 files changed, 45 insertions(+), 26 deletions(-) diff --git a/.claude/skills/misuse-resistance/EVIDENCE.md b/.claude/skills/misuse-resistance/EVIDENCE.md index 2edc8681..b210357d 100644 --- a/.claude/skills/misuse-resistance/EVIDENCE.md +++ b/.claude/skills/misuse-resistance/EVIDENCE.md @@ -17,12 +17,12 @@ The studies behind each rule in [SKILL.md](SKILL.md). Cite these when a rule is | 1. Secure by default | Fischer et al. 2021 (CCS): re-ranking search results by security raised secure and functional outcomes with no behaviour change. Green & Smith 2016: defaults safe and never ambiguous. | | 2. Tasks, not primitives | Nadi et al. 2016 (ICSE): Java developers found crypto APIs too low-level and asked for task-based ones. Bernstein et al. 2012: NaCl's `crypto_box` offers no algorithm choices. | | 3. Own the periphery | Acar et al. 2017 (IEEE S&P): calls into the library were mostly secure; failures came from key storage, password-based derivation, and insecure inputs copied from examples. Egele et al. 2013 (CCS): 88% of 11,748 Android apps misused crypto, through mistakes such as ECB mode, constant keys and constant IVs. | -| 4. Secrets as types | Blessing et al. 2021: 37.2% of vulnerabilities in crypto libraries were memory-safety issues. | +| 4. Secrets as types | Blessing et al. 2021: across eight crypto libraries, 37.2% of vulnerabilities were memory-safety issues and only 27.2% cryptographic ones, so how secret material is held matters as much as the algorithm. (Indirect: the study does not test secret types.) | | 5. Invalid states unrepresentable | Russell's hard-to-misuse scale: "won't compile" outranks every runtime and documentation measure. Georgiev et al. 2012 (CCS): certificate validation broke where libraries exposed confusing options. | | 6. Promise only what you deliver | Oliveira et al. 2018 (SOUPS) and Brun et al. 2023 (TOSEM): APIs with misleading names or hidden preconditions caused vulnerabilities regardless of experience. Albertini et al. 2022 (USENIX Security): callers assumed AEAD key commitment that the primitive did not give. | | 7. Errors reveal nothing | Padding-oracle and partitioning-oracle attacks (Len, Grubbs & Ristenpart 2021) turn distinguishable failures into plaintext recovery. | | 8. Warn at point of use | Gorski et al. 2018 (SOUPS): API warnings nearly doubled secure outcomes; code that triggered a warning was 15 times more likely to be fixed. Gorski et al. 2020 (CHI): the five parts of a warning. Vance et al. 2018 (CHI): repeated identical warnings stop being processed. | -| 9. Secure, complete examples | Acar et al. 2017: copied code was three times more likely to work, and insecure inputs in examples were copied with it. Fischer et al. 2017 (IEEE S&P): insecure Stack Overflow snippets propagated into Play Store apps. | +| 9. Secure, complete examples | Acar et al. 2017: code copied from Stack Overflow was about three times more likely to be functional on the symmetric tasks, and insecure inputs in examples were copied with it. Fischer et al. 2017 (IEEE S&P): insecure Stack Overflow snippets propagated into Play Store apps. | | 10. Keep it small | Blessing et al. 2021: library complexity correlated strongly with vulnerability count. | | 11. Evolve loudly | Holmwood 2022 (linux.conf.au): Keyczar's replacement by Tink as the model of a breaking change with a migration path. | diff --git a/.claude/skills/misuse-resistance/RUST.md b/.claude/skills/misuse-resistance/RUST.md index 4ff8e174..a601a9a8 100644 --- a/.claude/skills/misuse-resistance/RUST.md +++ b/.claude/skills/misuse-resistance/RUST.md @@ -4,23 +4,27 @@ Techniques for reaching each rung of the ladder in [SKILL.md](SKILL.md). The sni ## Rung 1: impossible to get wrong -Remove the parameter. If the library generates the nonce, no caller can reuse one. +Remove the parameter. If the library generates the nonce, no caller can reuse one. A random 96-bit nonce still bounds how much one key may seal: past about 2³² messages the chance of a repeat is no longer negligible (NIST SP 800-38D), so a long-lived key must be rotated, or the cipher must use a longer nonce (XChaCha20-Poly1305) or a nonce-misuse-resistant mode (AES-GCM-SIV). The caller still cannot get the nonce wrong; the library owns that limit too. ```rust pub struct Ciphertext { nonce: [u8; 12], sealed: Vec } pub struct Cipher { /* key, rng */ } +/// Opaque: says that sealing failed, not why. +pub struct EncryptError; + impl Cipher { - // No `nonce` argument: the cipher draws a fresh one per call. - pub fn encrypt(&self, plaintext: &[u8], aad: &[u8]) -> Ciphertext { - let nonce = self.fresh_nonce(); - let sealed = self.seal(&nonce, plaintext, aad); - Ciphertext { nonce, sealed } + // No `nonce` argument: the cipher draws a fresh one per call. Drawing it can + // fail (the OS RNG is unavailable), so the call returns an error, not a panic. + pub fn encrypt(&self, plaintext: &[u8], aad: &[u8]) -> Result { + let nonce = self.fresh_nonce()?; + let sealed = self.seal(&nonce, plaintext, aad)?; + Ok(Ciphertext { nonce, sealed }) } - fn fresh_nonce(&self) -> [u8; 12] { todo!("draw from the CSPRNG") } - fn seal(&self, _nonce: &[u8; 12], _plaintext: &[u8], _aad: &[u8]) -> Vec { todo!("AEAD seal") } + fn fresh_nonce(&self) -> Result<[u8; 12], EncryptError> { todo!("draw from the CSPRNG") } + fn seal(&self, _nonce: &[u8; 12], _plaintext: &[u8], _aad: &[u8]) -> Result, EncryptError> { todo!("AEAD seal") } } ``` @@ -42,7 +46,7 @@ impl Algorithm for Aes256Gcm { const NAME: &'static str = "AES-256-GCM"; } Put the invariant in the type. - **Newtypes over raw bytes.** `Key([u8; 32])`, never `&[u8]`. A key, a nonce and a ciphertext should not be interchangeable. -- **Const generics for lengths.** A `[u8; 32]` argument makes the length check infallible and removes the error path. Take runtime-length material through a separate, fallible `try_from_bytes` that rejects weak inputs. +- **Fixed-size arrays for fixed lengths.** A `[u8; 32]` argument makes the length check infallible and removes the error path; a const generic `[u8; N]` does the same for a family of sizes. Take runtime-length material through a separate, fallible `try_from_bytes` that rejects weak inputs. - **Typestate.** Encode protocol steps as types, so the unsafe order does not type-check: ```rust @@ -54,6 +58,11 @@ pub struct Verified(Vec); pub struct BadSignature; impl Unverified { + /// Anyone may hold a message they received; only `verify` turns it into a `Verified`. + pub fn new(message: Vec, signature: [u8; 64]) -> Self { + Self { message, signature } + } + // The caller supplies a key, never the check: the library owns the // verification, so there is no predicate to stub out with `|_| true`. pub fn verify(self, key: &VerifyingKey) -> Result { diff --git a/.claude/skills/vitaminc/RECIPES.md b/.claude/skills/vitaminc/RECIPES.md index 3bfcfa34..ec2367fb 100644 --- a/.claude/skills/vitaminc/RECIPES.md +++ b/.claude/skills/vitaminc/RECIPES.md @@ -49,16 +49,17 @@ assert!(tag_matches(&a, &b)); ```rust use vitaminc_protected::Protected; -use vitaminc_random::{Generatable, RandomError, SafeRand, SeedableRng}; +use vitaminc_random::{Generatable, RandomError, SafeRand}; fn random_secret() -> Result, RandomError> { let mut rng = SafeRand::from_entropy()?; Protected::random(&mut rng) } -/// A uniform index in `0..len`, for choosing from a list. -fn random_index(rng: &mut SafeRand, len: u32) -> u32 { - rng.next_below(len) +/// A uniform index in `0..len`, for choosing from a list. `next_below` panics on a +/// zero bound, since `0..0` has nothing to draw, so an empty list is refused here. +fn random_index(rng: &mut SafeRand, len: u32) -> Option { + (len > 0).then(|| rng.next_below(len)) } ``` @@ -66,7 +67,7 @@ Derive `Generatable` to generate a whole struct of random fields: ```rust use vitaminc_protected::Protected; -use vitaminc_random::{Generatable, RandomError, SafeRand, SeedableRng}; +use vitaminc_random::{Generatable, RandomError, SafeRand}; #[derive(Generatable)] struct SessionSecrets { @@ -81,7 +82,7 @@ fn new_session() -> Result { ## Generate a password -Draw each character uniformly with `next_below`, and build the string inside `Protected::generate` so it is wrapped from the start. Reserve the full capacity first, so the string never reallocates and leaves an unwiped copy behind. +Draw each character uniformly with `next_below`, and build the string inside `Protected::generate` so it is wrapped from the start. Reserve the full capacity first, so the string never reallocates and leaves an unwiped copy behind. With this 62-character set, 16 characters give about 95 bits; refuse a shorter `len` rather than issue a guessable password. ```rust use vitaminc_protected::{Controlled, Protected}; @@ -89,20 +90,23 @@ use vitaminc_random::SafeRand; const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; -fn new_password(rng: &mut SafeRand, len: usize) -> Protected { - Protected::generate(|| { +fn new_password(rng: &mut SafeRand, len: usize) -> Option> { + if len < 16 { + return None; + } + Some(Protected::generate(|| { let mut password = String::with_capacity(len); for _ in 0..len { password.push(CHARSET[rng.next_below(CHARSET.len() as u32) as usize] as char); } password - }) + })) } ``` ## Encrypt a value or a struct -Build the cipher once at startup from a key that outlives the process (see "Wrap a data key" below), and pass it to every call. A key generated inside the function that encrypts is lost when it returns, and so is everything sealed under it. Encrypt under associated data (AAD) naming the record the ciphertext belongs to. Decryption must present the same AAD, so a ciphertext copied onto another record fails to open. Nonces are generated for you. +Build the cipher at startup from a key that outlives the process (see "Wrap a data key" below), and pass it to every call. `Aes256Cipher` is not `Sync`, so build one per thread or task from the same key rather than sharing one across threads. A key generated inside the function that encrypts is lost when it returns, and so is everything sealed under it. Encrypt under associated data (AAD) naming the record the ciphertext belongs to. Decryption must present the same AAD, so a ciphertext copied onto another record fails to open. Nonces are generated for you. ```rust use vitaminc_encrypt::{ @@ -118,7 +122,7 @@ struct User { date_of_birth: String, } -/// Seal a user for storage. `cipher` is built once at startup and shared by every call. +/// Seal a user for storage. `cipher` is built at startup, one per thread or task. fn seal_user(cipher: &Aes256Cipher, user: &User) -> Result> { encrypt(user, cipher, format!("users/{}", user.id).as_str()) } @@ -165,7 +169,9 @@ Wrap each data key under AAD naming its owner. Under one KEK, a wrapped key copi use vitaminc_encrypt::{Aes256Cipher, AesCipherText, Key}; use vitaminc_random::{Generatable, SafeRand}; -/// Run at startup, on the root key's bytes fetched from the secret store. +/// Run at startup, on the root key's bytes fetched from the secret store. `Key` takes a +/// plain array (it has no constructor from `Protected` yet) and wraps it at once: fetch +/// straight into this call, and keep no other copy of the bytes. fn key_encryption_cipher(root_key: [u8; 32]) -> Result> { Ok(Aes256Cipher::new(&Key::from(root_key))?) } @@ -204,12 +210,15 @@ async fn email_term(prf: &HmacSha256Prf, email: &str) -> Result<[u8; 32], Box) -> Result> { +/// Take the key bytes already wrapped, straight from the secret store's read. +fn index_key_from_secret_store( + bytes: Protected>, +) -> Result> { // Rejects keys shorter than the minimum length instead of deriving weak terms. - Ok(HmacSha256Prf::try_from_bytes(Protected::new(bytes))?) + Ok(HmacSha256Prf::try_from_bytes(bytes)?) } -let prf = index_key_from_secret_store(vec![7; 32]).unwrap(); +let prf = index_key_from_secret_store(Protected::new(vec![7; 32])).unwrap(); let a = futures::executor::block_on(email_term(&prf, "ada@example.com")).unwrap(); let b = futures::executor::block_on(email_term(&prf, "ada@example.com")).unwrap(); assert_eq!(a, b); diff --git a/docs/research/misuse-resistance.md b/docs/research/misuse-resistance.md index ba857bf9..651b64f9 100644 --- a/docs/research/misuse-resistance.md +++ b/docs/research/misuse-resistance.md @@ -122,6 +122,7 @@ Gorski, P. L., Acar, Y., Lo Iacono, L., & Fahl, S. (2020). *Listen to developers - Rusty Russell's "hard to misuse" scale (Ottawa Linux Symposium keynote, 2003; blog posts 2008). Ranks interfaces from "impossible to get wrong" and "compiler/linker won't let you get it wrong" at the top, down through "simplest use is correct", "name tells you how to use it", and "do it right or it breaks at runtime", to "the obvious use is wrong" at the bottom. The point is that "hard to misuse" is a stronger goal than "easy to use". Summary: https://technovelty.org/badcode/rusty-russells-hard-to-misuse-rules.html - Bernstein, D. J., Lange, T., & Schwabe, P. (2012). *The security impact of a new cryptographic library.* LATINCRYPT 2012. The design rationale for NaCl: one high-level `crypto_box` call with no algorithm choices, no data-dependent branches or indices on secrets, and no padding oracles by construction. - Rogaway, P., & Shrimpton, T. (2006). *A provable-security treatment of the key-wrap problem.* EUROCRYPT 2006. Introduces misuse-resistant authenticated encryption (SIV), the origin of the term in cryptography: nonce reuse degrades gracefully instead of failing catastrophically. See also RFC 8452 (AES-GCM-SIV). +- Len, J., Grubbs, P., & Ristenpart, T. (2021). *Partitioning oracle attacks.* USENIX Security 2021. A decryption error that distinguishes a wrong key from other failures lets an attacker test many candidate keys per query, recovering passwords from non-committing AEADs. Behind rule 7: errors reveal nothing. - Albertini, A., Duong, T., Gueron, S., Kölbl, S., Luykx, A., & Schmieg, S. (2022). *How to abuse and fix authenticated encryption without key commitment.* USENIX Security 2022. A different kind of misuse: callers assume AEAD properties (key commitment) that the primitive does not give. The API must not let callers believe in guarantees it lacks. - Type-driven design outside the crypto literature: Yaron Minsky's "make illegal states unrepresentable", and Alexis King's "Parse, don't validate" (2019). These are the language-level tools for climbing Rusty Russell's scale: newtypes, typestate, sealed traits, and constructors that are the only way to obtain a valid value. From fa136db525ea486ca8c9138583efd9b89450a015 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Wed, 21 Oct 2026 12:11:38 +1100 Subject: [PATCH 4/4] test: doctest every Rust block in the agent skills, so a recipe that stops compiling fails CI The skills are Markdown, so nothing compiled their examples, and agents copy them as written. skill-doctests includes each skill file as rustdoc, which makes every Rust block a doctest against the workspace crates; the Test job's plain cargo test picks it up as a workspace member. It is never published or released, like the derive-tests crates. --- Cargo.lock | 13 +++++++++++++ Cargo.toml | 1 + packages/skill-doctests/Cargo.toml | 20 ++++++++++++++++++++ packages/skill-doctests/src/lib.rs | 19 +++++++++++++++++++ release-plz.toml | 7 +++++++ 5 files changed, 60 insertions(+) create mode 100644 packages/skill-doctests/Cargo.toml create mode 100644 packages/skill-doctests/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 129922af..829f2ddd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2199,6 +2199,19 @@ dependencies = [ "libc", ] +[[package]] +name = "skill-doctests" +version = "0.0.0" +dependencies = [ + "futures", + "vitaminc-aead", + "vitaminc-encrypt", + "vitaminc-hmac", + "vitaminc-prf", + "vitaminc-protected", + "vitaminc-random", +] + [[package]] name = "slab" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index d96e0131..638a8c51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "packages/hmac", "packages/random", "packages/random-derives", + "packages/skill-doctests", "packages/traits", "packages/vitaminc", ] diff --git a/packages/skill-doctests/Cargo.toml b/packages/skill-doctests/Cargo.toml new file mode 100644 index 00000000..f379cae2 --- /dev/null +++ b/packages/skill-doctests/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "skill-doctests" +description = "Test-only crate, never published: compiles and runs every Rust block in the agent skills under .claude/skills against the workspace crates." +version = "0.0.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish = false + +# The skill files are Markdown, so nothing else compiles their examples. Each is +# included as rustdoc in src/lib.rs, which makes every Rust block a doctest. +# Doctests link dev-dependencies, so the crates the examples use live here. +[dev-dependencies] +futures = "0.3.31" +vitaminc-aead = { path = "../aead" } +vitaminc-encrypt = { path = "../encrypt" } +vitaminc-hmac = { path = "../hmac" } +vitaminc-prf = { path = "../prf" } +vitaminc-protected = { path = "../protected" } +vitaminc-random = { path = "../random" } diff --git a/packages/skill-doctests/src/lib.rs b/packages/skill-doctests/src/lib.rs new file mode 100644 index 00000000..754cb85a --- /dev/null +++ b/packages/skill-doctests/src/lib.rs @@ -0,0 +1,19 @@ +//! Every Rust block in the agent skills under `.claude/skills`, compiled and run +//! as a doctest against the workspace crates. Agents copy these examples as +//! written, so an API change that breaks one fails `cargo test` here rather than +//! shipping a recipe that no longer compiles. +//! +//! Each skill file is one module below. A new skill file with Rust blocks gets a +//! module of its own. + +#[doc = include_str!("../../../.claude/skills/misuse-resistance/SKILL.md")] +pub mod misuse_resistance {} + +#[doc = include_str!("../../../.claude/skills/misuse-resistance/RUST.md")] +pub mod misuse_resistance_rust {} + +#[doc = include_str!("../../../.claude/skills/vitaminc/SKILL.md")] +pub mod vitaminc {} + +#[doc = include_str!("../../../.claude/skills/vitaminc/RECIPES.md")] +pub mod vitaminc_recipes {} diff --git a/release-plz.toml b/release-plz.toml index 125eff72..1fe661fd 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -115,3 +115,10 @@ publish = false name = "derive-tests-umbrella-only" release = false publish = false + +# Test-only workspace member: doctests every Rust block in .claude/skills. Never +# released, for the same reasons as the derive-tests crates above. +[[package]] +name = "skill-doctests" +release = false +publish = false