Skip to content

Re-add ChaCha20-Poly1305 when the Modern Algorithms proposal settles; preserved aead-internal-nonce design #272

Description

@lann

The package's scope narrowed to WebCrypto-standard compatibility: the ChaCha interfaces (Modern Algorithms proposal-stage, not spec), the aead-internal-nonce kind, and bytes.constant-time-equal were cut — extensions to WebCrypto don't benefit from conforming to WebCrypto's shape, and belong in a sibling package if anywhere.

Re-add trigger: the Modern Algorithms proposal settles

When the W3C "Modern Algorithms in the Web Cryptography API" proposal merges into the spec, one-shot ChaCha20-Poly1305 becomes standard surface and re-enters under the ordinary additive path. Design facts already settled by the first implementation (recover from history, pre-cut):

  • the alg-less oct JWK moved once to follow the proposal's registered "C20P" — re-check the registration at re-add;
  • the 12-byte-nonce rule renders invalid-nonce;
  • jco served it by feature detection (Node ≥ 24.18 experimental); browser coverage was declared per-engine in targets.toml;
  • it was the in-guest provider's recommended AEAD (class A+B vs AES-GCM's C) — the provider README's classification narrative should regain that row.

XChaCha20-Poly1305 is NOT part of this trigger: it is absent from the proposal and from every platform WebCrypto, and does not return to this package.

Preserved design: the aead-internal-nonce WIT

Recorded here per the scope decision, in case a future spec development (or a sibling extension package) wants it. Motivations it carried: nonce-misuse resistance (the caller-nonce aead kind's standing hazard), and SP 800-38D approved-mode sealing (the FIPS-profile trajectory — which is correspondingly weakened in the direction notes by this cut). Key semantics: implementation-generated nonces, iv ‖ ciphertext ‖ tag wire format, a per-key nonce budget surfacing error.key-exhausted (an error case that leaves the variant with the cut and would need restoring).

The aead-internal-nonce kind (from wit/webcrypto.wit)

/// The `aead-internal-nonce` primitive kind: single-message authenticated
/// encryption with the nonce generated within the implementation (SP
/// 800-38D §8's "IV generated within the cryptographic module").
///
/// This is the misuse-resistant counterpart of `aead`: the nonce never
/// appears in the API, so nonce reuse — the most common catastrophic AEAD
/// failure — is unrepresentable. Each `seal` draws a fresh nonce that is
/// unique per key and carries it in the sealed output, so every sealed
/// message is self-contained.
///
/// Keys are a distinct resource from `aead.aead-key`, minted by separate
/// per-algorithm interfaces: the nonce discipline is part of the key
/// capability, so one key can never mix implementation-chosen and
/// caller-chosen nonces. Prefer this kind unless interop requires an
/// externally specified nonce layout; among its algorithms, prefer
/// XChaCha20-Poly1305 for high-volume keys (see the minting interfaces'
/// nonce-budget notes).
///
/// Like `aead`, this is a single-message primitive: `open` buffers and
/// verifies before it releases any plaintext. The streaming,
/// extractability, and getter contracts in `README.md` apply.
interface aead-internal-nonce {
    use types.{error};
    use wrapping.{wrap-input};

    /// Mint-time policy for an `internal-nonce-key`. Grants nothing by
    /// default; see `README.md`, "Key-options contract". The vocabulary is
    /// seal/open only: this kind has no WebCrypto mapping, so no platform
    /// usage vocabulary reaches beyond its own operations.
    resource internal-nonce-key-options {
        constructor();
        /// Whether the minted key may `seal`. Disabled by default.
        can-seal: func(allowed: bool);
        /// Whether the minted key may `open`. Disabled by default.
        can-open: func(allowed: bool);
        /// Whether the minted key's material may be exported. Disabled by
        /// default.
        extractable: func(allowed: bool);
    }

    /// An internal-nonce AEAD key: an unforgeable capability, bound to one
    /// algorithm at creation.
    resource internal-nonce-key {
        /// Encrypt and authenticate `plaintext` under a fresh
        /// implementation-generated nonce with the associated data `aad`.
        ///
        /// The returned stream is a self-contained sealed message:
        /// everything `open` needs besides the key and `aad` travels
        /// in-band. The key's minting interface documents the wire format.
        /// Drain the returned stream concurrently with feeding `plaintext`
        /// (see `README.md`, "Streaming contract").
        ///
        /// Fails with `error.key-exhausted` once the implementation can no
        /// longer guarantee nonce uniqueness for this key.
        seal: async func(aad: list<u8>, plaintext: stream<u8>) -> result<stream<u8>, error>;

        /// Decrypt and verify a sealed message, as produced by `seal` under
        /// the same algorithm, with the associated data `aad`.
        ///
        /// Security:
        /// - `ok(stream)` resolves only after the sealed stream is fully
        ///   drained and the tag verified: it *is* the authentication
        ///   statement, and unverified plaintext is never observable.
        /// - Any failure — a bad tag, wrong associated data, or input too
        ///   short to carry the wire format — reports
        ///   `error.authentication-failed` with no detail.
        open: async func(aad: list<u8>, sealed: stream<u8>) -> result<stream<u8>, error>;

        /// The registry name of the algorithm family this key is bound to,
        /// e.g. `"AES-GCM"` (spelled as for `aead-key.algorithm-name`).
        algorithm-name: func() -> string;

        /// The key length in bits, e.g. `256`.
        algorithm-length: func() -> u32;

        /// The number of further `seal` invocations this key's nonce budget
        /// (see `README.md`, "Terminology") permits, or `none` when the
        /// implementation enforces no budget. A hint for key-rotation
        /// scheduling: monotonically non-increasing, and `some(0)` means
        /// the next `seal` fails `error.key-exhausted`. Implementations
        /// MAY decrement faster than one per seal (a policy or accounting
        /// choice), so it is not an exact invocation count.
        ///
        /// There are deliberately no wire-layout getters here (nonce or tag
        /// size): the sealed message's layout is the minting interface's
        /// contract, and a consumer parsing it already knows that
        /// interface's documented constants.
        seals-remaining: func() -> option<u64>;

        /// Whether the key material may be exported.
        extractable: func() -> bool;

        /// Whether this key permits `seal`. A refused operation fails
        /// `error.not-permitted`.
        can-seal: func() -> bool;

        /// Whether this key permits `open`. See `can-seal`.
        can-open: func() -> bool;

        /// The raw key material. Fails with `error.not-extractable` unless
        /// the key was created with `extractable` true. The nonce budget
        /// does not travel with the material; see the minting interfaces'
        /// import docs.
        export-key-raw: async func() -> result<list<u8>, error>;

        /// The key as an `oct` JWK, behind the same extractability gate
        /// as `export-key-raw`; algorithms with no registered JWK form
        /// fail `error.unsupported`, as on `aead-key.export-key-jwk`. The
        /// nonce budget does not travel with the material.
        export-key-jwk: async func() -> result<string, error>;

        /// This key's raw material as a `wrap-input`, for wrapping
        /// under another key (see the `wrapping` interface). Behind the
        /// same extractability gate as `export-key-raw`; the material
        /// itself never reaches the caller. As with the exports, the
        /// nonce budget does not travel with the material.
        to-wrap-input-raw: async func() -> result<wrap-input, error>;

        /// The JWK serialization as a `wrap-input`, behind the same
        /// gate; algorithms with no registered JWK form fail
        /// `error.unsupported`, as on `export-key-jwk`.
        to-wrap-input-jwk: async func() -> result<wrap-input, error>;
    }
}

aes-gcm-internal-nonce (from wit/aes.wit)

/// AES-GCM internal-nonce key minting: `aead-internal-nonce` keys using the
/// SP 800-38D §8.2.2 RBG-based IV construction (96 random bits per seal).
///
/// Sealed messages are `iv ‖ ciphertext ‖ tag` (12-byte IV, 16-byte tag).
///
/// Security:
/// - A key's nonce budget (see `README.md`, "Terminology") is 2^32 `seal`
///   invocations (§8.2.2's repeat-probability bound). Implementations
///   SHOULD enforce it with `error.key-exhausted`; a FIPS 140-3 profile
///   MUST. For workloads that could approach the bound, prefer
///   `xchacha20-poly1305-internal-nonce`.
///
/// This interface is deliberately separate from `aes-gcm` (caller-nonce
/// minting) so a security profile can serve one nonce discipline without
/// the other at composition time — e.g. a FIPS 140-3 approved-mode
/// provider exports this interface but not `aes-gcm`.
interface aes-gcm-internal-nonce {
    use types.{error};
    use aead-internal-nonce.{internal-nonce-key, internal-nonce-key-options};
    use aes.{aes-variant};
    use wrapping.{unwrap-input};

    /// Import raw key material as an internal-nonce AES-GCM key of the
    /// declared variant (see `aes-gcm.import-key-raw` for the
    /// variant/`raw`-length contract, which applies identically here).
    ///
    /// Security:
    /// - The nonce-uniqueness guarantee is per key *as managed by this
    ///   implementation*: importing the same material into multiple
    ///   implementations divides the nonce budget among them blindly.
    ///   Prefer `generate-key` where interop does not require import.
    ///
    /// An implementation MAY reject imported material per its security
    /// policy (for example, a profile that cannot establish uniqueness for
    /// externally sourced material) with `error.invalid-key` or
    /// `error.unsupported`.
    import-key-raw: async func(%variant: aes-variant, raw: list<u8>, options: internal-nonce-key-options) -> result<internal-nonce-key, error>;

    /// Import key material from an `oct` JWK, subject to
    /// `import-key-raw`'s contract (including the policy-based-rejection
    /// allowance) and the package-wide JWK contract (see
    /// `mac-key.export-key-jwk`).
    import-key-jwk: async func(%variant: aes-variant, jwk: string, options: internal-nonce-key-options) -> result<internal-nonce-key, error>;

    /// Generate a fresh random internal-nonce AES-GCM key of the declared
    /// variant. Fails with `error.unsupported` if this implementation does
    /// not serve the variant.
    generate-key: async func(%variant: aes-variant, options: internal-nonce-key-options) -> result<internal-nonce-key, error>;

    /// Mint a key from unwrapped key material read as raw bytes, subject
    /// to `import-key-raw`'s contract — including the shared-material
    /// caveat and policy-rejection allowance. `input` is consumed; see
    /// `aes-gcm.unwrap-key-raw` for the options model.
    unwrap-key-raw: async func(%variant: aes-variant, input: unwrap-input, options: internal-nonce-key-options) -> result<internal-nonce-key, error>;

    /// Mint a key from unwrapped key material read as an `oct` JWK,
    /// subject to `import-key-jwk`'s contract. `input` is consumed.
    unwrap-key-jwk: async func(%variant: aes-variant, input: unwrap-input, options: internal-nonce-key-options) -> result<internal-nonce-key, error>;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions