Skip to contents

A single SHA-256 over a whole file tells you it changed. A Merkle tree over its chunks tells you WHICH chunk changed, and proves that one chunk belongs to the pinned file without re-reading the rest of it. Use these functions to pin a large data file as a root digest, to find the part of a re-released file that differs, or to hand someone one chunk with a short proof that it is part of the pinned file.

Usage

mrkroot(chunks, scheme = 2L)

mrklvs(chunks, scheme = 2L)

mrkprf(chunks, index, scheme = 2L)

mrkvrf(leaf, proof, root, scheme = NULL)

Arguments

chunks

Chunk contents, in order: a character vector (one chunk per element), a single raw vector (one chunk), or a list of raw vectors and single strings, such as chnkfile() returns.

scheme

The hashing scheme: 2 (the default for building, with leaf and node prefixes) or 1 (no prefixes, as 0.5.12 and earlier built them). For mrkvrf(), NULL (the default) reads the scheme from proof; a number requires that scheme, and a proof that records a different one does not verify.

index

The chunk to prove, one whole number from 1 to the number of chunks.

leaf

The chunk whose membership is being verified: one string or one raw vector.

proof

The list returned by mrkprf().

root

The expected root digest, a 64-character hex string as mrkroot() returns.

Value

mrkroot() a length-1 character vector (64 lowercase hex characters), or NA for no chunks. mrklvs() a character vector of per-chunk digests, one per chunk (character(0) for none). mrkprf() a list with sibling (character, the sibling digests from the leaf upward), side (character, "left" or "right": the side the sibling is on) and scheme (integer). mrkvrf() a length-1 logical.

Details

mrkroot() reduces the chunks to one root digest. mrklvs() returns the per-chunk digests the root is built from, so two capsules can be diffed chunk by chunk. mrkprf() returns the sibling digests on the path from one leaf to the root, and mrkvrf() replays that path.

Each leaf is the SHA-256 of the byte 0x00 followed by a chunk's bytes. Each parent is the SHA-256 of the byte 0x01 followed by the 64 bytes of its left and right child digests (the bytes, not their hex spelling). Levels are paired left to right. This is scheme = 2, the default from 0.5.13 on.

An unpaired node at an odd level is PROMOTED unchanged rather than hashed against a duplicate of itself. Duplicating it would let two different chunk lists produce the same root – the weakness behind CVE-2012-2459 – so promotion is a correctness requirement, not a preference. A promoted node adds no step to a proof, so a proof can be shorter than \(\lceil \log_2 n \rceil\).

The two prefix bytes are the leaf and node domain separation of RFC 6962: without them an interior node, given as the 64 bytes of its two child digests, verifies as a "leaf" with the shorter proof from its parent. Roots and proofs made by 0.5.12 and earlier have no prefixes (scheme = 1, the weaker scheme). Pass scheme = 1 to mrkroot(), mrklvs() or mrkprf() to recompute such a root. mrkprf() records its scheme in the proof, and mrkvrf() reads it from there unless scheme is given. A proof with no recorded scheme was made by 0.5.12 or earlier: it is verified under scheme 1, with a warning. Pass scheme = 2 to mrkvrf() to refuse such a proof, since a scheme-1 check still accepts an interior node as a leaf.

Character chunks are taken as their bytes, so a root computed from character input equals the root of the same bytes as raw vectors. The comparison in mrkvrf() is cdgsteq() (constant time, case-sensitive).

Errors: chunks that is not character, raw or a list of raw vectors and single strings, or that contains NA; index outside 1..length(chunks); a scheme other than 1 or 2; a leaf that is more than one chunk; a proof without sibling and side, or with the two of different lengths.

Examples

chunks <- c("row1,row2", "row3,row4", "row5,row6", "row7,row8")

root <- mrkroot(chunks)
root
#> [1] "3b1537c90e21c2e66f341b946388abbb78bf249fb433f961fba223b67141be31"

# A single chunk's root is just its own leaf digest.
mrkroot("only") == csha256(c(as.raw(0), charToRaw("only")))
#> [1] TRUE

# The leaves are the per-chunk digests, so a diff names the culprit.
before <- mrklvs(chunks)
after <- mrklvs(c(chunks[1:2], "row5,row6-EDITED", chunks[4]))
which(before != after)
#> [1] 3

# Prove chunk 3 belongs, without holding chunks 1, 2 or 4.
pr <- mrkprf(chunks, 3)
pr
#> $sibling
#> [1] "a1d2780e3c438372d14e12c01b280cf9be97a244adfa43671903812ca0deb88f"
#> [2] "28ee5f4ac1a7a56c508b5f1b9cd5dd74abf929b3d0f2e5852280914ddef3cb88"
#> 
#> $side
#> [1] "right" "left" 
#> 
#> $scheme
#> [1] 2
#> 
mrkvrf(chunks[3], pr, root)
#> [1] TRUE

# The proof fails for a chunk that was not in the tree.
mrkvrf("row5,row6-EDITED", pr, root)
#> [1] FALSE

# Any change to any chunk changes the root.
mrkroot(chunks) == mrkroot(c(chunks[1:3], "row7,row8 "))
#> [1] FALSE

# an odd count: the last node is promoted, so its proof has one step
mrkprf(c("a", "b", "c"), 3)
#> $sibling
#> [1] "b137985ff484fb600db93107c77b0365c80d78f5b429ded0fd97361d077999eb"
#> 
#> $side
#> [1] "left"
#> 
#> $scheme
#> [1] 2
#> 

# an index past the end is refused
try(mrkprf(chunks, 5))
#> Error : `index` must be between 1 and the number of chunks

# a root pinned by 0.5.12 or earlier recomputes with scheme = 1, and its
# proofs (which record no scheme) verify with a warning
old <- mrkprf(chunks, 3, scheme = 1)
old$scheme <- NULL
withCallingHandlers(
  mrkvrf(chunks[3], old, mrkroot(chunks, scheme = 1)),
  warning = function(w) invokeRestart("muffleWarning"))
#> [1] TRUE