Skip to content

gnufood/canaad

canaad

CI crates.io npm MSRV License
Deterministic AAD for AEAD.

Reference Spec

How it works

canaad has two layers:

  • Default-profile layer — validates the standard field set (v, tenant, resource, purpose, optional ts and x_* extensions), then produces an RFC 8785 canonical byte string. Use canonicalize_default / canonicalizeDefault in your application code.

  • Generic-object layer — applies core rules only (size limit, duplicate-key rejection, object assertion, JCS canonicalization) without requiring any specific fields. Use canonicalize_object / canonicalizeObject when building custom profiles on top of canaad.

// the same context always produces the same bytes
let aad = AadContext::new("acme", "/doc/123", "encrypt")?
    .canonicalize()?; // → Vec<u8>, pass as AAD to your cipher

canaad ships as a Rust crate, CLI, and WASM package — all produce identical output for the same input.

Install

Artifact Platform
canaad-core Rust canaad-core = "1.0" in Cargo.toml
canaad-cli CLI see below
canaad-wasm Browser / Worker npm install @gnufoo/canaad

Rust

[dependencies]
canaad-core = "1.0"

CLI

curl -fsSL https://releases.gnu.foo/canaad/install.sh | sh
# or: cargo install canaad-cli

JavaScript / WASM

npm install @gnufoo/canaad

Usage

Rust

use canaad_core::{AadContext, canonicalize_default, canonicalize_object};

// Default profile: validates v, tenant, resource, purpose
let aad = canonicalize_default(r#"{"v":1,"tenant":"acme","resource":"/doc/123","purpose":"encrypt"}"#)?;

// Or build from scratch (timestamp is optional)
let aad = AadContext::new("acme", "/doc/123", "encrypt")?
    .with_timestamp(1700000000)?
    .canonicalize()?;

// Generic object: core rules only, no required fields
let aad = canonicalize_object(r#"{"z":"last","a":"first"}"#)?;

CLI

echo '{"v":1,"tenant":"acme","resource":"/doc/123","purpose":"encrypt"}' | canaad canonicalize
canaad validate input.json
canaad hash input.json -o hex

JavaScript

import init, { AadBuilder, canonicalizeDefault, canonicalizeObject } from '@gnufoo/canaad';

await init(); // call once at startup

// Default profile
const aad = new AadBuilder()
    .tenant("acme")
    .resource("/doc/123")
    .purpose("encrypt")
    .timestamp(1700000000) // optional
    .build(); // → Uint8Array — pass directly as AAD to your AEAD call

// Or canonicalize existing JSON with the default profile
const aad2 = canonicalizeDefault('{"v":1,"tenant":"acme","resource":"/doc/123","purpose":"encrypt"}');

// Generic object: core rules only, no required fields
const aad3 = canonicalizeObject('{"z":"last","a":"first"}');

Numbers only — no BigInt. build() throws on NaN, Infinity, negative, or fractional values.

Development

Prerequisites: Rust 1.70+, just, wasm-pack (WASM only), Rust nightly + miri component (miri only).

just ci          # full check suite (mirrors CI): lint, test, audit, msrv
just ci-local    # run CI workflow locally via act (requires Docker)
just test        # native tests only
just build-wasm  # rebuild pkg/ from crates/canaad-wasm
just miri        # memory safety (nightly)

License

MIT OR Apache-2.0

About

Reference implementation of my AAD canonicalization spec

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Contributing

Security policy

Stars

Watchers

Forks

Contributors

Languages