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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .claude/skills/misuse-resistance/EVIDENCE.md
Original file line number Diff line number Diff line change
@@ -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: 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: 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. |

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.
129 changes: 129 additions & 0 deletions .claude/skills/misuse-resistance/RUST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# 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. 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<u8> }

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. 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<Ciphertext, EncryptError> {
let nonce = self.fresh_nonce()?;
let sealed = self.seal(&nonce, plaintext, aad)?;
Ok(Ciphertext { nonce, sealed })
}

fn fresh_nonce(&self) -> Result<[u8; 12], EncryptError> { todo!("draw from the CSPRNG") }
fn seal(&self, _nonce: &[u8; 12], _plaintext: &[u8], _aad: &[u8]) -> Result<Vec<u8>, EncryptError> { 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.
- **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
pub struct VerifyingKey { /* public key */ }

pub struct Unverified { message: Vec<u8>, signature: [u8; 64] }
pub struct Verified(Vec<u8>);

pub struct BadSignature;

impl Unverified {
/// Anyone may hold a message they received; only `verify` turns it into a `Verified`.
pub fn new(message: Vec<u8>, 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<Verified, BadSignature> {
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. `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() }
```

- **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: <reason>")]` 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.
65 changes: 65 additions & 0 deletions .claude/skills/misuse-resistance/SKILL.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading