---
title: "Provenance you can verify"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Provenance you can verify}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
library(rmoriebricklayer)
set.seed(1)
```

A manifest with a SHA-256 in it proves the data was not corrupted. It
proves nothing about who produced it, because anyone who can edit the
data can recompute the digest and write it back.

This vignette covers the three things that close that gap: signing a
manifest, pinning a capsule chunk by chunk, and making the *history* of
runs tamper-evident rather than only each run.

## Signing: a shared secret

The simplest case is one team, one secret. `capsule_sign()` with
`scheme = "hmac"` produces an HMAC-SHA-256 tag: only a key holder can
compute it, so only a key holder can produce a manifest that verifies.

```{r}
manifest <- paste0(
  "source=https://example.org/extract.csv\n",
  "sha256=", core_sha256("id,value\n1,2\n"), "\n",
  "fetched=2026-09-12"
)

sig <- capsule_sign(core_sha256(manifest), key = "team-secret",
                    scheme = "hmac")
capsule_verify(core_sha256(manifest), sig, "team-secret")
```

An edited manifest has a different digest, which the signature does not
cover:

```{r}
edited <- sub("fetched=2026-09-12", "fetched=2026-01-01", manifest)
capsule_verify(core_sha256(edited), sig, "team-secret")
```

Do not carry raw key bytes around by hand. `derive_key()` stretches a
passphrase into a full-width key with PBKDF2:

```{r}
# Fixed here so the vignette is reproducible. For a real key the salt
# comes from random_bytes(16), which reads the operating system's
# CSPRNG rather than R's generator.
salt <- "9f2c41a7d8e05b36"
key <- derive_key("correct horse battery staple", salt)
nchar(key)
```

The salt is not secret and must be stored beside the capsule -- without
the same salt and iteration count, the key cannot be re-derived.

HMAC is symmetric, so anyone who can *verify* can also *sign*. That is
fine within one team and useless for publishing.

## Signing: a public verifier, after quantum

For a verifier who should be able to check but not forge, the signature
has to be asymmetric. The classical options (RSA, Ed25519) fall to
Shor's algorithm.

Worth being precise about what that threatens. Hashing is fine:
Grover's algorithm only halves the security exponent, so SHA-256 retains
roughly 2^128 preimage resistance, and HMAC with a 256-bit key is
likewise fine. **Signatures** are the part that breaks.

`pqc_keygen()` builds a hash-based signing key -- a Merkle tree over
Winternitz one-time keys, the RFC 8391 construction, over the SHA-256
this package already ships. Its security rests on the hash function
alone: no lattice assumption, no elliptic curve, and no new system
dependency.

```{r}
# Small for the vignette; the default height 10 gives 1024 signatures.
signing_key <- pqc_keygen(height = 3)
signing_key
```

Publish the public half. It carries no secret:

```{r}
pub <- signing_public_key(signing_key)
pub
```

```{r}
s1 <- capsule_sign(core_sha256(manifest), signing_key)
capsule_verify(core_sha256(manifest), s1, pub)

# A verifier holding only `pub` cannot forge one.
capsule_verify(core_sha256(edited), s1, pub)
```

### One signature per index, and no more

A Winternitz key signs **once**. Signing two different messages at one
leaf index leaks enough chain material to forge a third. So the key is
stateful, and `capsule_sign()` returns the advanced state for you to
carry forward:

```{r}
signing_key <- s1$key_state
signing_key$next_index

s2 <- capsule_sign(core_sha256("a second manifest"), signing_key)
capsule_verify(core_sha256("a second manifest"), s2, pub)
```

An exhausted key is an error rather than a silent wrap-around:

```{r error = TRUE}
k <- pqc_keygen(height = 1)          # signs exactly 2 messages
k <- capsule_sign("one", k)$key_state
k <- capsule_sign("two", k)$key_state
capsule_sign("three", k)
```

On the standardised schemes: ML-DSA (FIPS 204) and SLH-DSA (FIPS 205)
are implemented here too, at every parameter set both standards define
-- three for ML-DSA, twelve for SLH-DSA -- with no system dependency, so
the list `pqc_backends()` reports is the same on every machine.
`fips_keygen()` takes any of them, `fips_key()` wraps key material that
came from elsewhere, and both are stateless: one key signs any number of
messages.

What makes that defensible is the cross-check rather than care. In
deterministic mode this package and OpenSSL 3.5 produce the same bytes
for every parameter set, which is a test a subtly wrong implementation
cannot pass: it would have to be wrong in exactly the same way, in a
construction where a single misplaced field changes every output.

```{r}
pqc_backends()
```

## Signing something a verifier can check

`capsule_sign()` signs a string, which proves the string was signed and
leaves three things implicit: which key, which scheme, which bytes. A
verifier handed a directory still has to be told all three, and a check
that depends on being told what to check is a formality.

`capsule_attest()` records them inside the signed payload.

```{r}
m <- make_manifest(list(dataset = "otis", rows = 1200L),
                   environment = FALSE)
key <- fips_keygen("ML-DSA-65")
att <- capsule_attest(m, key, note = "counts as published")
capsule_check_attestation(att, m)
```

The check needs the attestation and the manifest, nothing else. Change
either -- or edit the note, which is covered by the signature because it
is part of the digest -- and it fails:

```{r}
m2 <- m
m2$meta$rows <- 1201L
capsule_check_attestation(att, m2)$ok
```

What it does not establish is that the key belongs to whoever you think:
an attestation is only as good as the channel the public key arrived on.
Pass `key_expected` when you know which key should have signed.

## The number in the manifest is the number

A manifest is meant to be checked against later. That only works if it
records what actually happened, at the precision it happened:

```{r}
one_third <- make_manifest(list(x = 1 / 3), environment = FALSE)
back <- bricklayer_json_from_json(manifest_canonical(one_third))
identical(back$meta$x, 1 / 3)
```

Sign `manifest_digest()` rather than the pretty JSON. R lists keep
insertion order, so building `meta` before `results` or the other way
round gives different bytes for the same content; the canonical form
sorts the keys, so the digest depends on what the manifest says and not
on how a script assembled it.

```{r}
identical(
  manifest_digest(make_manifest(list(a = 1, b = 2), environment = FALSE)),
  manifest_digest(make_manifest(list(b = 2, a = 1), environment = FALSE)))
```

## Whether the finding survives

Everything above is about the record being intact. None of it says the
number means anything: a capsule can be signed, hashed, chained and
reproduced byte for byte while reporting an artefact. That is a separate
question and it needs controls that can fail.

```{r}
set.seed(1)
d <- data.frame(x = rnorm(200))
d$y <- 0.8 * d$x + rnorm(200)
capsule_falsify(d, function(z) cor(z$x, z$y), treatment = "x",
                n = 199, seed = 42)
```

Four controls, each for a different way of being wrong: permuting the
exposure destroys the association, so the statistic should fall to its
null; a column of noise cannot matter, so the statistic should not move;
a placebo exposure should show nothing; and random subsets should agree.

They can fail, which is the point. A constant fails the permutation test
and nothing else, because everything else about a constant is perfectly
stable:

```{r}
capsule_falsify(d, function(z) 0.5, treatment = "x", n = 199,
                seed = 42)$controls[, c("control", "passed")]
```

Note what the permutation control reports alongside its p-value: the
smallest p-value the design could have produced. With 199 permutations
that floor is 0.005, and with 19 it is 0.05 -- so a p of 0.05 from 19
permutations is not weak evidence, it is no evidence at all.

## Re-deriving the numbers, not just checking them

`verify_capsule()` reads a manifest and confirms it is consistent with
itself. That catches an edited manifest and cannot catch one that was
wrong when it was written, because a manifest written from the wrong
data is perfectly consistent too.

```{r}
d <- data.frame(x = 1:10)
man <- make_manifest(list(dataset = "demo"), environment = FALSE)
man <- record(man, "mean_x", observed = mean(d$x), expected = 5.5)
man <- record(man, "n", observed = nrow(d), expected = 10)

manifest_recompute(man, d, list(mean_x = function(z) mean(z$x),
                                n = function(z) nrow(z)))
```

Recompute only some of them and the rest are reported as unchecked
rather than passed over, which is the difference between a clean bill of
health and a partial one:

```{r}
manifest_recompute(man, d, list(n = function(z) nrow(z)))
```

For anything stochastic, record the generator state rather than the
seed. `set.seed(1)` reproduces a run only if everything before it does
too -- one extra draw anywhere upstream shifts every later value:

```{r}
set.seed(1)
m2 <- manifest_record_seed(make_manifest(list(a = 1),
                                         environment = FALSE))
first <- runif(3)
invisible(runif(1000))          # any amount of other work
manifest_restore_seed(m2)
identical(runif(3), first)
```

## Saying in advance what you will report

The falsification controls above are worth much more when the statistics
were named before the data was looked at. A declaration makes the two
ways of departing from a plan visible, and they are different failures:

```{r}
plan <- prereg_declare(c(
  ate = "use of force is higher in the exposed division",
  n_rows = "the extract has the row count the source publishes"))

# a declared outcome that was not reported
prereg_check(plan, "n_rows")

# statistics reported that were never declared
prereg_check(plan, c("ate", "n_rows", "by_year", "by_precinct"))
```

That second case is why a nominal p-value needs correcting. Run the
permutation control over several statistics and the smallest is not the
finding:

```{r}
falsify_family(c(ate = 0.02, by_year = 0.3, by_precinct = 0.4,
                 by_shift = 0.6, by_month = 0.7), method = "holm")
```

And for the confounder nobody measured, the E-value says how strong it
would have to be to explain the result away -- with both the exposure
and the outcome:

```{r}
evalue_rr(2, lo = 1.4, hi = 2.9)
```

## One file to hand someone

A bundle carries a digest of every file, the manifest digest, and an
attestation over both. The file digests are inside the signature, which
is the point: a list of hashes that is not itself signed can be
rewritten to match whatever the files now say.

```{r}
dir <- tempfile()
dir.create(dir)
write.csv(data.frame(x = 1:3), file.path(dir, "data.csv"),
          row.names = FALSE)
b <- capsule_bundle(dir, man, key, note = "as published")
capsule_bundle_verify(attr(b, "path"), dir, manifest = man)
```

Files sitting in the directory that the bundle does not mention are
reported as well: a signed list of what should be there says nothing
about what else was put beside it.

```{r}
unlink(dir, recursive = TRUE)
```

## When, not just in what order

The chain proves the order of a sequence of manifests. It does not prove
that any of them existed at a particular time -- the whole chain can be
built in an afternoon and dated however one likes. An RFC 3161
timestamp token from a third party supplies the date, and
`timestamp_verify()` checks three separate things: that the token is
about these bytes, what time it asserts, and that the authority's
signature holds.

```{r eval=FALSE}
res <- timestamp_verify("response.tsr", data = "manifest.json")
res$time
res$checks
```

What it does not check, and this is the difference between it and a
browser's padlock: whether the certificate should be trusted. There is
no chain building, no validity dates, no revocation and no check of the
timeStamping key usage. Pass the certificate you have independently
decided to trust, and read a pass as "this key said so" rather than
"this is true".

## Pinning chunk by chunk

A single digest over a file tells you it changed. A Merkle tree over its
chunks tells you *which* chunk changed, and lets you prove one chunk
belongs without re-reading the rest.

```{r}
chunks <- c("id,value", "1,2", "3,4", "5,6")
root <- merkle_root(chunks)
root
```

```{r}
# Editing one chunk moves exactly one leaf.
edited_chunks <- chunks
edited_chunks[3] <- "3,5"
which(merkle_leaves(chunks) != merkle_leaves(edited_chunks))
```

```{r}
# Prove chunk 3 belongs, holding only it and log2(n) sibling digests.
proof <- merkle_proof(chunks, 3)
proof
merkle_verify(chunks[3], proof, root)
merkle_verify("3,5", proof, root)
```

`chunk_file()` produces the chunks from a file.

An unpaired node at an odd level is *promoted* rather than hashed
against a duplicate of itself. That is a correctness requirement, not a
preference: duplicating the last leaf lets two different chunk lists
produce the same root, which is the CVE-2012-2459 weakness.

```{r}
merkle_root(c("a", "b", "c")) == merkle_root(c("a", "b", "c", "c"))
```

## The history, not just the run

Every manifest above verifies on its own. That says nothing about
whether any were *removed* -- delete an inconvenient run and the
remaining manifests are all still perfectly valid.

A chain fixes that by linking each entry to the digest of the one
before:

```{r}
chain <- chain_new()
chain <- chain_append(chain, "manifest for run 1", label = "run-1")
chain <- chain_append(chain, "manifest for run 2", label = "run-2")
chain <- chain_append(chain, "manifest for run 3", label = "run-3")
chain
```

```{r}
# Deleting an entry from the middle breaks the links, and names where.
tampered <- chain
tampered$entries[[2]] <- NULL
chain_verify(tampered)
```

### Sign the seal, not the head

Two failure modes, and neither single value catches both:

* Deleting from the **middle** leaves the last entry's stored digest
  untouched, so the head is unchanged -- but `chain_verify()` fails.
* Truncating from the **end** leaves a valid prefix that
  `chain_verify()` accepts -- but the head changes.

```{r}
truncated <- chain
truncated$entries[[3]] <- NULL

c(head_unchanged_by_middle_deletion =
    chain_head(tampered) == chain_head(chain),
  links_accept_truncation = chain_verify(truncated)$valid)
```

`chain_seal()` folds the entry count and every link digest into one
value, which covers both. It is the thing to sign:

```{r}
seal_sig <- capsule_sign(chain_seal(chain), signing_key)
capsule_verify(chain_seal(chain), seal_sig, pub)

# The truncated chain seals to a different value.
capsule_verify(chain_seal(truncated), seal_sig, pub)

# A chain whose links disagree has no seal to offer at all.
chain_seal(tampered)
```

## What is verified, and what is not

Every primitive with a published test vector is checked against it:
SHA-512 against FIPS 180-4, HMAC-SHA-256 against RFC 4231, PBKDF2
against the published vectors, BLAKE2b against RFC 7693, CRC-32 against
the ITU V.42 check value.

The XMSS signature scheme is the exception. No official RFC 8391
known-answer vectors ship with the RFC itself, which carries only XDR
formats. The check is instead made against the **reference
implementation**: the whole 2500-byte signature for XMSS-SHA2_10_256 --
index, randomiser, WOTS+ signature and authentication path -- is
byte-identical to what `github.com/XMSS/xmss-reference` produces from
the same key material, and those vectors are embedded in the test suite
so the check needs no network. A signature written here can therefore be
handed to another XMSS implementation as bytes.

It is verified against its security properties as well: a genuine
signature verifies, and every tampering of the message, the signature,
the authentication path, the leaf index or the key fails. The scheme is
not *certified*, which is a statement about process rather than about
the bytes.
