ledvar-core 0.1.0

Reference implementation of the Ledvar protocol: data model, canonical content-addressed hashing, and well-formedness.
Documentation
<p align="center">
  <img src="assets/ledvar-256.png" alt="Ledvar" width="88">
</p>

# ledvar-rs

> The Rust reference implementation of the [Ledvar protocol]https://github.com/ledvar/ledvar.

**Documentation:** [ledvar.org](https://ledvar.org)

Ledvar represents the state of a system as a canonical, content-addressed tree
and compares two such trees to reveal — exactly and reproducibly — what changed.
This workspace is the reference implementation: a small library, an optional
companion, and a CLI. It is **domain-blind** (security, cloud, anything that can
be a tree) and **dependency-light** in the core.

## Crates

| Crate | Kind | What it is |
|---|---|---|
| [`ledvar-core`]crates/ledvar-core | lib | The protocol core: data model, canonical hashing, well-formedness. Deps: `serde`, `sha2`. |
| [`ledvar-diff`]crates/ledvar-diff | lib | The optional companion: `StateStatus` + the comparison + result types. |
| [`ledvar`]crates/ledvar | bin | The CLI: `validate`, `hash`, `diff`, `canon`, `schema`. |

The three are versioned **lock-step**.

## Build & install

Requirements: a Rust toolchain (stable, **1.96+**). No system dependencies — the whole
workspace is pure Rust (nothing to `apt install`); `cargo` fetches all crate dependencies.

```bash
cargo build --release                                 # build the workspace
cargo install --path crates/ledvar                    # install the `ledvar` CLI (JSON only)
cargo install --path crates/ledvar --features yaml    # ...with YAML support
```

The core depends only on `serde` and `sha2`; `yaml` is the only optional feature
(see [Format support](#format-support-v01)).

## CLI

```bash
cargo run -p ledvar -- validate snapshot.json
cargo run -p ledvar -- hash     snapshot.json
cargo run -p ledvar -- diff     previous.json current.json              # human-readable (git-style + - ~)
cargo run -p ledvar -- diff     current.json  --baseline                # cold start: every node is Baseline
cargo run -p ledvar --features yaml -- diff previous.json current.json --out yaml   # YAML needs the feature
cargo run -p ledvar -- canon    snapshot.json                          # the exact bytes that get hashed
cargo run -p ledvar -- schema --out json                               # reference schema (or --out proto)
```

Input and output are **independent axes** (`--in` / `--out`) — e.g. read JSON,
write YAML. `--in auto` detects by file extension, and the hashing is
format-independent.

### Format support (v0.1)

| Format | read (`--in`) | write (`--out`) | notes |
|---|:---:|:---:|---|
| `json` ||| always available; the canonical interchange format |
| `yaml` ||| **opt-in: build with `--features yaml`** |
| `xml` |**not implemented** |**not implemented** | |
| `proto` |**not implemented** |**not implemented** | |

> **YAML is opt-in behind the `yaml` Cargo feature — for two reasons.** The default build is JSON-only.
> **(1) No unsafe/C in the default build:** the archived/unmaintained `serde_yaml` was dropped; with the
> feature, YAML uses the maintained `serde_yaml_ng`, which pulls `unsafe-libyaml`, so it stays out of the
> default. **(2) YAML would coerce string values:** YAML 1.1 turns bare `no`/`yes`/`on`/`off`/`true`/…
> into booleans, but every Ledvar value is a **string** (SPEC §4.4) — an unquoted `off` in a config
> value would silently become `false` and change its hash. If you enable YAML, **quote** any value that
> could be read as a bool/number/null. Selecting `--in/--out yaml` without the feature returns a clear
> error telling you to rebuild with `--features yaml`. JSON is the canonical data format.
>
> *How this reference reads YAML:* it deserializes plain scalars as **raw text**`0600` stays
> `"0600"`, `4.50` stays `"4.50"`, and bare `null`/`~` become the literal strings — so no value is
> corrupted on input. A different YAML reader that resolves scalar types by the YAML core schema
> *before* mapping to strings could reject the very same document this one accepts. That is a difference
> in **input acceptance**, never in the hash (identical accepted input always hashes identically), and
> quoting your values sidesteps it entirely.

> **XML and Protocol Buffers are NOT implemented yet** — only the format
> *dispatch* exists. Selecting them returns a clear "not implemented yet" error.
> They are deferred until a real consumer needs them: adding a serde-native format
> (like YAML) is one match arm, but XML and proto need a custom mapping and a
> defined snapshot/diff representation first.

## Library

```rust
use ledvar_core::Node;
use ledvar_diff::diff;

let id = node.identity_key()?;     // SHA-256 of the canonical path
let ch = node.content_hash()?;     // SHA-256 of the canonical content
let result = diff(Some(&previous), &current)?;
// Every hashing entry point validates first (SPEC §9): ill-formed input
// yields an `Err`, never a hash.
```

## Design notes

- [`docs/canonical-hashing.md`]docs/canonical-hashing.md — why hashing is
  hand-rolled and streaming (and the one known caveat about non-BMP characters).

## Conformance

`conformance/` holds golden vectors mirrored from the protocol repo (including the cross-domain and
`node-astral*` code-point-vs-UTF-16 vectors). The tests assert that every hash and `StateStatus`
matches them exactly:

```bash
cargo test
```

Keep the mirror honest with `conformance/sync-check.sh` — run it to verify (exit 1 on drift), or
`--write` to copy the vectors from the protocol repo. Point it at the protocol with `LEDVAR_PROTOCOL_DIR`
when it isn't checked out beside this repo.

> **Interim.** This copy (and the sync script) are temporary. After launch, once the protocol's
> conformance vectors are published on crates.io, this directory is removed and the vectors are consumed
> from that crate instead — single source of truth, no manual sync.

## Verifying a release

Every release binary ships with two companion files: a `.sha256` and a `.asc`. They answer different
questions, and only one of them protects you from an attacker.

| File | Proves |
|---|---|
| `.sha256` | **integrity** — the download did not get corrupted on the way |
| `.asc` | **authenticity** — the file is genuinely the maintainer's and nobody swapped it |

A checksum alone protects against nothing hostile: whoever can replace the binary can replace the
checksum next to it. The signature can't be forged without the private key.

```bash
# 1. import the project's public key — ONCE
#    from the site:            https://ledvar.org/ledvar-public-key.asc
#    or the protocol repo:     https://github.com/ledvar/ledvar/blob/main/ledvar-public-key.asc
gpg --import ledvar-public-key.asc

# 2. confirm you imported the RIGHT key (compare against the fingerprint published on both)
gpg --fingerprint 911CCB7A80D349FB
#   750F 73CA 0EC5 9FB3 D945  CA12 911C CB7A 80D3 49FB

# 3. verify the file you downloaded
gpg --verify ledvar-v0.1.0-x86_64-unknown-linux-musl.tar.gz.asc \
             ledvar-v0.1.0-x86_64-unknown-linux-musl.tar.gz
```

Expected: `Good signature from "Maykon Luiz Matos Araújo <maykon.lma@gmail.com>"`.

> Step 2 is not optional. A signature only means something once you know the key is the right one —
> otherwise an attacker hands you a fake binary *and* a fake key that signs it perfectly. That is why
> the fingerprint is published in two independent places: compromising one is not enough.

Releases are signed **locally** — the private key never reaches CI — and are **immutable** once
published: a release that is live can never have its binaries replaced afterwards.

## License

Apache-2.0. See [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE).