Add Phase 7 OpenTimestamps integration
uc2_sha256: pure-C FIPS 180-4 implementation, one-shot and incremental API, validated against published vectors (empty, abc, 56-byte, 1M 'a', byte-by-byte, every-split-point boundary). uc2_ots: parser, serializer, and walker for the standard .ots binary format. Strict canonical varint with 64-bit overflow check, depth- bounded recursion, varbytes cap, max-digest cap. Walker supports the calendar-path subset (APPEND, PREPEND, SHA256); proofs that include other crypto ops (SHA1, RIPEMD160, KECCAK256) are accepted as structurally valid but flagged for follow-up via the standard 'ots verify'. UC2-OTS trailer: magic-bracketed sidecar appended after the recorded archive bytes. Reverse-scan-safe; original UC2 Pro reader ignores trailing bytes past its recorded length so backward compatibility is preserved. Layout (all integers little-endian uint32): front-magic + version + archive_len + proof_len + proof + proof_len + back-magic. CLI: --ots-attach validates that the proof's leaf digest equals SHA-256(archive[0..archive_len)) before appending and refuses to overwrite an existing trailer unless -f is given. --ots-extract writes the proof verbatim, byte-compatible with the standard 'ots verify'. --ots-info parses and prints the leaf, archive-match status, and attestation list. uc2 -t recomputes the archive SHA-256 and walks the proof. Tests: 17 OTS unit tests (varint round-trip, canonical/overflow rejection, file-envelope round-trip, walker on append/sha256/ sibling/unsupported-op/truncated/trailing-garbage, attest_name, trailer round-trip + corruption rejection in 4 scenarios). Plus an optional ctest target ots_cross_check that round-trips the .ots through python-opentimestamps when the package is installed; skipped (return code 77) otherwise.
This commit is contained in:
22
ROADMAP.md
22
ROADMAP.md
@@ -120,9 +120,25 @@ No mainstream archiver offers post-quantum encryption.
|
||||
256-bit digests, incremental and one-shot API, constant-time
|
||||
comparison, tree hashing structure. 7 unit tests including
|
||||
avalanche, incremental-vs-oneshot, and single-byte updates.
|
||||
- [ ] OpenTimestamps integration: cryptographic proof of archive creation
|
||||
time anchored to Bitcoin blockchain (one HTTP call, small proof blob
|
||||
stored in archive metadata)
|
||||
- [x] SHA-256 (`uc2_sha256.h`): pure-C FIPS 180-4 implementation,
|
||||
one-shot and incremental API. 6 unit tests against published
|
||||
test vectors (empty, "abc", 56-byte, 1M `'a'`, byte-by-byte
|
||||
incremental, every-split-point boundary).
|
||||
- [x] OpenTimestamps integration (`uc2_ots.h`): pure-C parser,
|
||||
serializer, and walker for the standard `.ots` proof format.
|
||||
Append-only sidecar trailer (magic-bracketed, reverse-scan-safe)
|
||||
stores the proof verbatim and preserves backward compatibility
|
||||
with the original UC2 Pro reader. Walker supports the
|
||||
calendar-path subset (APPEND, PREPEND, SHA256); proofs with other
|
||||
crypto ops are accepted as structurally valid but flagged for
|
||||
`ots verify` follow-up. CLI: `--ots-attach`, `--ots-extract`,
|
||||
`--ots-info`; `uc2 -t` recomputes archive SHA-256 and verifies
|
||||
the leaf and walk. Strict-canonical-varint parser, 64-bit
|
||||
overflow check, depth-bounded recursion, varbytes cap.
|
||||
17 unit tests.
|
||||
- [ ] OTS upgrade: fetch the upgraded proof from the calendar after
|
||||
the Bitcoin attestation has been minted (~1-6h), replace the
|
||||
pending-only trailer with the Bitcoin block-header attestation.
|
||||
- [ ] Useful for legal/forensic archiving, software provenance, digital
|
||||
preservation
|
||||
|
||||
|
||||
436
cli/src/main.c
436
cli/src/main.c
@@ -35,6 +35,8 @@ void setprogname(const char *argv0);
|
||||
#include <uc2/libuc2.h>
|
||||
#include <uc2/uc2_cdc.h>
|
||||
#include <uc2/uc2_lz4.h>
|
||||
#include <uc2/uc2_ots.h>
|
||||
#include <uc2/uc2_sha256.h>
|
||||
#include <uc2/uc2_version.h>
|
||||
|
||||
#include "list.h"
|
||||
@@ -42,6 +44,13 @@ void setprogname(const char *argv0);
|
||||
#define STR(S) STR_(S)
|
||||
#define STR_(S) #S
|
||||
|
||||
enum ots_mode {
|
||||
OTS_MODE_NONE = 0,
|
||||
OTS_MODE_ATTACH,
|
||||
OTS_MODE_EXTRACT,
|
||||
OTS_MODE_INFO
|
||||
};
|
||||
|
||||
struct options {
|
||||
bool list:1;
|
||||
bool all:1;
|
||||
@@ -54,10 +63,12 @@ struct options {
|
||||
bool help:1;
|
||||
bool quiet:1;
|
||||
bool benchmark:1;
|
||||
int ots_mode;
|
||||
char sep;
|
||||
int level;
|
||||
char *archive;
|
||||
char *dest;
|
||||
char *ots_path;
|
||||
} opt = {.sep = ' ', .level = 4};
|
||||
|
||||
static const char *level_name(int level)
|
||||
@@ -501,6 +512,356 @@ static bool extract_cb(struct node *ne, void *ctx, enum cause cause)
|
||||
return true;
|
||||
}
|
||||
|
||||
/* --- OpenTimestamps (Phase 7) --- */
|
||||
|
||||
#ifdef _WIN32
|
||||
# include <io.h>
|
||||
static int uc2_truncate(FILE *f, long len)
|
||||
{
|
||||
int rc = _chsize_s(_fileno(f), (__int64)len);
|
||||
return rc == 0 ? 0 : -1;
|
||||
}
|
||||
#else
|
||||
static int uc2_truncate(FILE *f, long len)
|
||||
{
|
||||
return ftruncate(fileno(f), (off_t)len);
|
||||
}
|
||||
#endif
|
||||
|
||||
static int file_size_of(const char *path, size_t *out)
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(path, &st) < 0) return -1;
|
||||
if (st.st_size < 0) return -1;
|
||||
*out = (size_t)st.st_size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_all(const char *path, uint8_t **out_data, size_t *out_len)
|
||||
{
|
||||
size_t len;
|
||||
if (file_size_of(path, &len) < 0) return -1;
|
||||
uint8_t *buf = malloc(len ? len : 1);
|
||||
if (!buf) return -1;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) { free(buf); return -1; }
|
||||
size_t got = fread(buf, 1, len, f);
|
||||
fclose(f);
|
||||
if (got != len) { free(buf); return -1; }
|
||||
*out_data = buf;
|
||||
*out_len = len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sha256_of_prefix(const char *path, size_t prefix_len,
|
||||
uint8_t out[32])
|
||||
{
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
struct uc2_sha256 ctx;
|
||||
uc2_sha256_init(&ctx);
|
||||
uint8_t buf[8192];
|
||||
size_t remaining = prefix_len;
|
||||
while (remaining) {
|
||||
size_t want = remaining < sizeof buf ? remaining : sizeof buf;
|
||||
size_t got = fread(buf, 1, want, f);
|
||||
if (got == 0) { fclose(f); return -1; }
|
||||
uc2_sha256_update(&ctx, buf, got);
|
||||
remaining -= got;
|
||||
}
|
||||
uc2_sha256_final(&ctx, out);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void print_hex(const uint8_t *p, size_t n)
|
||||
{
|
||||
for (size_t i = 0; i < n; i++) printf("%02x", p[i]);
|
||||
}
|
||||
|
||||
/* Locate an existing OTS trailer in the archive on disk.
|
||||
* Returns 0 if a well-formed trailer is found (and fills outputs),
|
||||
* 1 if no trailer (back magic absent),
|
||||
* negative on parse error (back magic present but malformed).
|
||||
* `*out_buf` is malloc'd; caller frees on success. */
|
||||
static int load_trailer(const char *archive_path,
|
||||
uint8_t **out_buf, size_t *out_buf_len,
|
||||
uint32_t *out_archive_len,
|
||||
const uint8_t **out_proof, size_t *out_proof_len)
|
||||
{
|
||||
uint8_t *buf;
|
||||
size_t len;
|
||||
if (read_all(archive_path, &buf, &len) < 0)
|
||||
err(EXIT_FAILURE, "%s", archive_path);
|
||||
int rc = uc2_ots_trailer_parse(buf, len, out_archive_len,
|
||||
out_proof, out_proof_len);
|
||||
if (rc != 0) { free(buf); return rc; }
|
||||
*out_buf = buf;
|
||||
*out_buf_len = len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_ots_attach(const char *archive_path, const char *proof_path,
|
||||
int force)
|
||||
{
|
||||
/* Read the .ots proof and validate its envelope. */
|
||||
uint8_t *proof; size_t proof_len;
|
||||
if (read_all(proof_path, &proof, &proof_len) < 0)
|
||||
err(EXIT_FAILURE, "%s", proof_path);
|
||||
uint8_t hash_op;
|
||||
const uint8_t *leaf, *body;
|
||||
size_t leaf_len, body_len;
|
||||
int rc = uc2_ots_parse_file(proof, proof_len, &hash_op,
|
||||
&leaf, &leaf_len, &body, &body_len);
|
||||
if (rc < 0) {
|
||||
free(proof);
|
||||
errx(EXIT_FAILURE, "%s: malformed .ots file (%d)", proof_path, rc);
|
||||
}
|
||||
if (hash_op != UC2_OTS_OP_SHA256) {
|
||||
free(proof);
|
||||
errx(EXIT_FAILURE, "%s: only SHA-256 .ots files are supported", proof_path);
|
||||
}
|
||||
|
||||
/* Determine the archive byte range to attest (strip any existing trailer). */
|
||||
size_t archive_file_len;
|
||||
if (file_size_of(archive_path, &archive_file_len) < 0) {
|
||||
free(proof);
|
||||
err(EXIT_FAILURE, "%s", archive_path);
|
||||
}
|
||||
|
||||
size_t attest_len = archive_file_len;
|
||||
{
|
||||
uint8_t *abuf;
|
||||
size_t abuf_len;
|
||||
if (read_all(archive_path, &abuf, &abuf_len) < 0)
|
||||
err(EXIT_FAILURE, "%s", archive_path);
|
||||
uint32_t existing_al;
|
||||
const uint8_t *existing_proof;
|
||||
size_t existing_pl;
|
||||
int trc = uc2_ots_trailer_parse(abuf, abuf_len, &existing_al,
|
||||
&existing_proof, &existing_pl);
|
||||
free(abuf);
|
||||
if (trc == UC2_OTS_OK) {
|
||||
if (!force)
|
||||
errx(EXIT_FAILURE,
|
||||
"%s: OTS trailer already present (use -f to replace)",
|
||||
archive_path);
|
||||
attest_len = existing_al;
|
||||
} else if (trc < 0) {
|
||||
errx(EXIT_FAILURE,
|
||||
"%s: existing trailer is malformed (%d); aborting",
|
||||
archive_path, trc);
|
||||
}
|
||||
}
|
||||
|
||||
if (attest_len > 0xffffffffu) {
|
||||
free(proof);
|
||||
errx(EXIT_FAILURE, "%s: archive too large for v1 OTS trailer", archive_path);
|
||||
}
|
||||
|
||||
/* Verify leaf digest matches the archive's SHA-256. */
|
||||
uint8_t archive_sha[32];
|
||||
if (sha256_of_prefix(archive_path, attest_len, archive_sha) < 0) {
|
||||
free(proof);
|
||||
err(EXIT_FAILURE, "%s", archive_path);
|
||||
}
|
||||
if (leaf_len != 32 || memcmp(archive_sha, leaf, 32) != 0) {
|
||||
fprintf(stderr, "%s: proof leaf does not match archive SHA-256\n",
|
||||
archive_path);
|
||||
fprintf(stderr, " archive: "); print_hex(archive_sha, 32); fprintf(stderr, "\n");
|
||||
fprintf(stderr, " proof: "); print_hex(leaf, leaf_len); fprintf(stderr, "\n");
|
||||
free(proof);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
/* Build trailer and rewrite archive. */
|
||||
size_t trailer_cap = UC2_OTS_TRAILER_OVERHEAD + proof_len;
|
||||
uint8_t *trailer = malloc(trailer_cap);
|
||||
if (!trailer) { free(proof); err(EXIT_FAILURE, "malloc"); }
|
||||
int tn = uc2_ots_trailer_build((uint32_t)attest_len,
|
||||
proof, proof_len, trailer, trailer_cap);
|
||||
if (tn < 0) { free(trailer); free(proof);
|
||||
errx(EXIT_FAILURE, "trailer build failed (%d)", tn); }
|
||||
|
||||
FILE *f = fopen(archive_path, "rb+");
|
||||
if (!f) { free(trailer); free(proof); err(EXIT_FAILURE, "%s", archive_path); }
|
||||
if (fseek(f, (long)attest_len, SEEK_SET) < 0) {
|
||||
fclose(f); free(trailer); free(proof);
|
||||
err(EXIT_FAILURE, "%s", archive_path);
|
||||
}
|
||||
if (fwrite(trailer, 1, (size_t)tn, f) != (size_t)tn) {
|
||||
fclose(f); free(trailer); free(proof);
|
||||
err(EXIT_FAILURE, "%s", archive_path);
|
||||
}
|
||||
/* Truncate any leftover bytes (e.g. when replacing a longer prior trailer). */
|
||||
long new_end = (long)attest_len + tn;
|
||||
fflush(f);
|
||||
if (uc2_truncate(f, new_end) < 0) {
|
||||
fclose(f); free(trailer); free(proof);
|
||||
err(EXIT_FAILURE, "truncate");
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
free(trailer);
|
||||
free(proof);
|
||||
uc2_say(stderr, "Attached %zu-byte OTS proof to %s\n",
|
||||
proof_len, archive_path);
|
||||
uc2_say(stderr, "Everything went OK\n");
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
static int cmd_ots_extract(const char *archive_path, const char *out_path)
|
||||
{
|
||||
uint8_t *buf; size_t buf_len;
|
||||
uint32_t archive_len;
|
||||
const uint8_t *proof; size_t proof_len;
|
||||
int rc = load_trailer(archive_path, &buf, &buf_len,
|
||||
&archive_len, &proof, &proof_len);
|
||||
if (rc == 1)
|
||||
errx(EXIT_FAILURE, "%s: no OTS trailer present", archive_path);
|
||||
if (rc < 0)
|
||||
errx(EXIT_FAILURE, "%s: malformed OTS trailer (%d)", archive_path, rc);
|
||||
|
||||
FILE *f = fopen(out_path, "wb");
|
||||
if (!f) err(EXIT_FAILURE, "%s", out_path);
|
||||
if (fwrite(proof, 1, proof_len, f) != proof_len)
|
||||
err(EXIT_FAILURE, "%s", out_path);
|
||||
fclose(f);
|
||||
free(buf);
|
||||
uc2_say(stderr, "Wrote %zu-byte .ots proof to %s\n", proof_len, out_path);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
struct ots_info_ctx {
|
||||
int n_attestations;
|
||||
};
|
||||
|
||||
static int ots_info_cb(void *vctx,
|
||||
const uint8_t *tag,
|
||||
const uint8_t *payload, size_t payload_len,
|
||||
const uint8_t *digest, size_t digest_len)
|
||||
{
|
||||
struct ots_info_ctx *c = vctx;
|
||||
(void)digest; (void)digest_len;
|
||||
c->n_attestations++;
|
||||
const char *name = uc2_ots_attest_name(tag);
|
||||
printf(" attestation: %s", name ? name : "<unknown>");
|
||||
if (name && strcmp(name, "pending") == 0) {
|
||||
/* Pending payload is varbytes(uri). */
|
||||
uint64_t uri_len;
|
||||
size_t consumed;
|
||||
if (payload_len > 0 &&
|
||||
uc2_ots_varint_decode(payload, payload_len, &uri_len, &consumed) == 0 &&
|
||||
consumed + uri_len <= payload_len) {
|
||||
printf(" (calendar: ");
|
||||
fwrite(payload + consumed, 1, (size_t)uri_len, stdout);
|
||||
printf(")");
|
||||
}
|
||||
} else if (name && strcmp(name, "Bitcoin") == 0) {
|
||||
uint64_t height; size_t consumed;
|
||||
if (uc2_ots_varint_decode(payload, payload_len, &height, &consumed) == 0)
|
||||
printf(" (block height: %llu)", (unsigned long long)height);
|
||||
}
|
||||
putchar('\n');
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_ots_info(const char *archive_path)
|
||||
{
|
||||
uint8_t *buf; size_t buf_len;
|
||||
uint32_t archive_len;
|
||||
const uint8_t *proof; size_t proof_len;
|
||||
int rc = load_trailer(archive_path, &buf, &buf_len,
|
||||
&archive_len, &proof, &proof_len);
|
||||
if (rc == 1)
|
||||
errx(EXIT_FAILURE, "%s: no OTS trailer present", archive_path);
|
||||
if (rc < 0)
|
||||
errx(EXIT_FAILURE, "%s: malformed OTS trailer (%d)", archive_path, rc);
|
||||
|
||||
uint8_t hash_op;
|
||||
const uint8_t *leaf, *body;
|
||||
size_t leaf_len, body_len;
|
||||
rc = uc2_ots_parse_file(proof, proof_len, &hash_op,
|
||||
&leaf, &leaf_len, &body, &body_len);
|
||||
if (rc < 0)
|
||||
errx(EXIT_FAILURE, "%s: malformed .ots envelope (%d)", archive_path, rc);
|
||||
|
||||
printf("OTS proof for %s\n", archive_path);
|
||||
printf(" archive bytes attested: %u\n", archive_len);
|
||||
printf(" hash: %s\n",
|
||||
hash_op == UC2_OTS_OP_SHA256 ? "SHA-256" : "<other>");
|
||||
printf(" leaf: "); print_hex(leaf, leaf_len); printf("\n");
|
||||
|
||||
uint8_t archive_sha[32];
|
||||
if (sha256_of_prefix(archive_path, archive_len, archive_sha) < 0)
|
||||
err(EXIT_FAILURE, "%s", archive_path);
|
||||
int leaf_match = (leaf_len == 32 && memcmp(archive_sha, leaf, 32) == 0);
|
||||
printf(" leaf matches archive: %s\n", leaf_match ? "yes" : "NO");
|
||||
|
||||
struct ots_info_ctx ctx = {0};
|
||||
int wr = uc2_ots_walk(body, body_len, leaf, leaf_len, ots_info_cb, &ctx);
|
||||
if (wr < 0)
|
||||
errx(EXIT_FAILURE, "%s: walker error %d", archive_path, wr);
|
||||
printf(" attestations: %d\n", ctx.n_attestations);
|
||||
if (wr == UC2_OTS_RESULT_STRUCTURAL)
|
||||
printf(" status: structurally valid; contains unsupported ops\n"
|
||||
" run `ots verify` for full cryptographic check\n");
|
||||
else
|
||||
printf(" status: structurally valid (calendar-path ops only)\n");
|
||||
|
||||
free(buf);
|
||||
return leaf_match && wr >= 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
|
||||
/* Called from -t after the existing archive integrity check.
|
||||
* Returns 0 if no trailer or trailer verifies; non-zero on mismatch. */
|
||||
static int verify_trailer_if_present(const char *archive_path)
|
||||
{
|
||||
uint8_t *buf; size_t buf_len;
|
||||
uint32_t archive_len;
|
||||
const uint8_t *proof; size_t proof_len;
|
||||
int rc = load_trailer(archive_path, &buf, &buf_len,
|
||||
&archive_len, &proof, &proof_len);
|
||||
if (rc == 1) return 0; /* no trailer */
|
||||
if (rc < 0) {
|
||||
fprintf(stderr, "%s: malformed OTS trailer (%d)\n", archive_path, rc);
|
||||
return 1;
|
||||
}
|
||||
uint8_t hash_op;
|
||||
const uint8_t *leaf, *body;
|
||||
size_t leaf_len, body_len;
|
||||
rc = uc2_ots_parse_file(proof, proof_len, &hash_op,
|
||||
&leaf, &leaf_len, &body, &body_len);
|
||||
if (rc < 0) {
|
||||
fprintf(stderr, "%s: malformed .ots envelope (%d)\n", archive_path, rc);
|
||||
free(buf);
|
||||
return 1;
|
||||
}
|
||||
uint8_t archive_sha[32];
|
||||
if (sha256_of_prefix(archive_path, archive_len, archive_sha) < 0) {
|
||||
free(buf);
|
||||
return 1;
|
||||
}
|
||||
if (hash_op != UC2_OTS_OP_SHA256 || leaf_len != 32 ||
|
||||
memcmp(archive_sha, leaf, 32) != 0) {
|
||||
fprintf(stderr, "%s: OTS leaf does not match archive SHA-256\n",
|
||||
archive_path);
|
||||
free(buf);
|
||||
return 1;
|
||||
}
|
||||
int wr = uc2_ots_walk(body, body_len, leaf, leaf_len, NULL, NULL);
|
||||
free(buf);
|
||||
if (wr < 0) {
|
||||
fprintf(stderr, "%s: OTS walker error %d\n", archive_path, wr);
|
||||
return 1;
|
||||
}
|
||||
if (wr == UC2_OTS_RESULT_STRUCTURAL)
|
||||
uc2_say(stderr, "OTS proof: structurally valid (run `ots verify` for full check)\n");
|
||||
else
|
||||
uc2_say(stderr, "OTS proof: leaf matches; structure verified\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- Archive creation --- */
|
||||
|
||||
static void w16(unsigned char *p, unsigned v)
|
||||
@@ -1229,6 +1590,60 @@ static int run_benchmark(int nfiles, char **files)
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/* Pre-parse for OTS long options: --ots-attach, --ots-extract, --ots-info.
|
||||
* Removes matched arguments from argv in place so the existing getopt loop
|
||||
* doesn't see them. --ots-attach takes a value, accepted as either
|
||||
* --ots-attach <path> (separate argv entry; rejected if path starts with '-')
|
||||
* --ots-attach=<path> (inline). */
|
||||
static int extract_ots_long_opts(int *argcp, char **argv, char **out_value)
|
||||
{
|
||||
int argc = *argcp;
|
||||
int mode = OTS_MODE_NONE;
|
||||
*out_value = NULL;
|
||||
for (int i = 1; i < argc; ) {
|
||||
const char *a = argv[i];
|
||||
int matched_args = 0;
|
||||
int new_mode = OTS_MODE_NONE;
|
||||
const char *inline_value = NULL;
|
||||
|
||||
if (strcmp(a, "--ots-attach") == 0) {
|
||||
new_mode = OTS_MODE_ATTACH;
|
||||
matched_args = 1;
|
||||
} else if (strncmp(a, "--ots-attach=", 13) == 0) {
|
||||
new_mode = OTS_MODE_ATTACH;
|
||||
matched_args = 1;
|
||||
inline_value = a + 13;
|
||||
} else if (strcmp(a, "--ots-extract") == 0) {
|
||||
new_mode = OTS_MODE_EXTRACT;
|
||||
matched_args = 1;
|
||||
} else if (strcmp(a, "--ots-info") == 0) {
|
||||
new_mode = OTS_MODE_INFO;
|
||||
matched_args = 1;
|
||||
}
|
||||
|
||||
if (!matched_args) { i++; continue; }
|
||||
|
||||
mode = new_mode;
|
||||
if (new_mode == OTS_MODE_ATTACH) {
|
||||
if (inline_value) {
|
||||
*out_value = (char *)inline_value;
|
||||
} else {
|
||||
if (i + 1 >= argc || argv[i + 1][0] == '-')
|
||||
errx(EXIT_FAILURE,
|
||||
"--ots-attach requires a path argument "
|
||||
"(use --ots-attach=<path> if your path starts with '-')");
|
||||
*out_value = argv[i + 1];
|
||||
matched_args = 2;
|
||||
}
|
||||
}
|
||||
for (int j = i; j + matched_args < argc; j++)
|
||||
argv[j] = argv[j + matched_args];
|
||||
argc -= matched_args;
|
||||
}
|
||||
*argcp = argc;
|
||||
return mode;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
#ifdef __DJGPP__
|
||||
@@ -1237,6 +1652,8 @@ int main(int argc, char *argv[])
|
||||
if (argc == 1)
|
||||
goto usage;
|
||||
|
||||
opt.ots_mode = extract_ots_long_opts(&argc, argv, &opt.ots_path);
|
||||
|
||||
for (;;) {
|
||||
int o = getopt(argc, argv, "xlatfd:C:cpDTh?wL:qB");
|
||||
if (o == -1)
|
||||
@@ -1303,6 +1720,9 @@ usage:
|
||||
"uc2 -t [-aq] archive.uc2 [files]...\n"
|
||||
"uc2 -w [-qL level] archive.uc2 files...\n"
|
||||
"uc2 -B files... (benchmark all methods)\n"
|
||||
"uc2 --ots-attach <proof.ots> [-f] archive.uc2\n"
|
||||
"uc2 --ots-extract archive.uc2 <out.ots>\n"
|
||||
"uc2 --ots-info archive.uc2\n"
|
||||
);
|
||||
if (!opt.help)
|
||||
printf("uc2 -h\n");
|
||||
@@ -1335,6 +1755,17 @@ usage:
|
||||
return run_benchmark(argc - optind + 1, argv + optind - 1);
|
||||
}
|
||||
|
||||
if (opt.ots_mode == OTS_MODE_ATTACH)
|
||||
return cmd_ots_attach(opt.archive, opt.ots_path, opt.overwrite);
|
||||
if (opt.ots_mode == OTS_MODE_EXTRACT) {
|
||||
const char *out = optind < argc ? argv[optind] : NULL;
|
||||
if (!out)
|
||||
errx(EXIT_FAILURE, "--ots-extract requires an output path");
|
||||
return cmd_ots_extract(opt.archive, out);
|
||||
}
|
||||
if (opt.ots_mode == OTS_MODE_INFO)
|
||||
return cmd_ots_info(opt.archive);
|
||||
|
||||
if (opt.create) {
|
||||
if (optind == argc)
|
||||
errx(EXIT_FAILURE, "No files to add");
|
||||
@@ -1403,8 +1834,11 @@ usage:
|
||||
if (opt.test)
|
||||
uc2_say(stderr, "Testing archive integrity...\n");
|
||||
visit_selected(&root, pipe_cb, uc2);
|
||||
if (opt.test)
|
||||
if (opt.test) {
|
||||
if (verify_trailer_if_present(opt.archive))
|
||||
return EXIT_FAILURE;
|
||||
uc2_say(stderr, "Everything went OK\n");
|
||||
}
|
||||
} else if (!opt.list) {
|
||||
struct path path = {.uc2 = uc2};
|
||||
char *p = path.buffer;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# libuc2 — UC2 decompression library
|
||||
|
||||
set(LIBUC2_SOURCES src/decompress.c src/compress.c src/uc2_tables.c src/uc2_cdc.c src/uc2_merkle.c src/uc2_blockstore.c src/uc2_simhash.c src/uc2_delta.c src/uc2_rans.c src/uc2_dict.c src/uc2_preprocess.c src/uc2_lz4.c src/uc2_blake3.c)
|
||||
set(LIBUC2_SOURCES src/decompress.c src/compress.c src/uc2_tables.c src/uc2_cdc.c src/uc2_merkle.c src/uc2_blockstore.c src/uc2_simhash.c src/uc2_delta.c src/uc2_rans.c src/uc2_dict.c src/uc2_preprocess.c src/uc2_lz4.c src/uc2_blake3.c src/uc2_sha256.c src/uc2_ots.c)
|
||||
|
||||
# Embed super.bin: use .S with .incbin on GCC/Clang, generated C array on MSVC
|
||||
if(MSVC)
|
||||
|
||||
166
lib/include/uc2/uc2_ots.h
Normal file
166
lib/include/uc2/uc2_ots.h
Normal file
@@ -0,0 +1,166 @@
|
||||
/* OpenTimestamps integration.
|
||||
*
|
||||
* UC2 stores an OpenTimestamps proof in a magic-bracketed sidecar
|
||||
* trailer appended after the regular UC2 archive bytes. The trailer
|
||||
* does not affect compatibility with the original UC2 Pro reader,
|
||||
* which uses the front header's recorded length.
|
||||
*
|
||||
* The proof itself is the standard `.ots` binary: a 31-byte header
|
||||
* magic + version + file-hash op + leaf digest + serialized timestamp.
|
||||
* Callers can extract the proof verbatim and run the standard
|
||||
* `ots verify` tool on it.
|
||||
*
|
||||
* Local verification covers structural validity and the calendar-path
|
||||
* subset of opcodes (APPEND, PREPEND, SHA256). Proofs that use other
|
||||
* crypto ops (SHA1, RIPEMD160, KECCAK256) are accepted as structurally
|
||||
* valid but reported as not locally cryptographically verified;
|
||||
* the standard `ots verify` should be used for full validation. */
|
||||
|
||||
#ifndef UC2_OTS_H
|
||||
#define UC2_OTS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* OTS opcodes. */
|
||||
enum {
|
||||
UC2_OTS_OP_APPEND = 0xf0, /* binary: append varbytes operand */
|
||||
UC2_OTS_OP_PREPEND = 0xf1, /* binary: prepend varbytes operand */
|
||||
UC2_OTS_OP_REVERSE = 0xf2, /* unary, deprecated */
|
||||
UC2_OTS_OP_HEXLIFY = 0xf3, /* unary */
|
||||
UC2_OTS_OP_SHA1 = 0x02, /* unary */
|
||||
UC2_OTS_OP_RIPEMD160 = 0x03, /* unary */
|
||||
UC2_OTS_OP_SHA256 = 0x08, /* unary, file-hash op */
|
||||
UC2_OTS_OP_KECCAK256 = 0x67, /* unary */
|
||||
UC2_OTS_BRANCH = 0xff,
|
||||
UC2_OTS_ATTESTATION = 0x00
|
||||
};
|
||||
|
||||
#define UC2_OTS_HEADER_MAGIC \
|
||||
"\x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94"
|
||||
#define UC2_OTS_HEADER_MAGIC_LEN 31
|
||||
#define UC2_OTS_VERSION 0x01
|
||||
|
||||
/* Attestation tags (8 bytes each). */
|
||||
#define UC2_OTS_TAG_PENDING "\x83\xdf\xe3\x0d\x2e\xf9\x0c\x8e"
|
||||
#define UC2_OTS_TAG_BITCOIN "\x05\x88\x96\x0d\x73\xd7\x19\x01"
|
||||
#define UC2_OTS_TAG_LITECOIN "\x06\x86\x9a\x0d\x73\xd7\x1b\x45"
|
||||
#define UC2_OTS_TAG_LEN 8
|
||||
|
||||
/* Hard limits to bound parser cost on hostile input. */
|
||||
#define UC2_OTS_MAX_DIGEST_LEN 64
|
||||
#define UC2_OTS_MAX_VARBYTES 8192
|
||||
#define UC2_OTS_MAX_DEPTH 32
|
||||
#define UC2_OTS_MAX_VARINT 0xffffffffu
|
||||
|
||||
/* Error codes. */
|
||||
enum {
|
||||
UC2_OTS_OK = 0,
|
||||
UC2_OTS_ERR_TRUNCATED = -1,
|
||||
UC2_OTS_ERR_NONCANONICAL= -2,
|
||||
UC2_OTS_ERR_OVERFLOW = -3,
|
||||
UC2_OTS_ERR_BAD_MAGIC = -4,
|
||||
UC2_OTS_ERR_BAD_VERSION = -5,
|
||||
UC2_OTS_ERR_BAD_HASH_OP = -6,
|
||||
UC2_OTS_ERR_DEPTH = -7,
|
||||
UC2_OTS_ERR_TOO_LARGE = -8,
|
||||
UC2_OTS_ERR_BAD_OP = -9
|
||||
};
|
||||
|
||||
/* Verification result reported by uc2_ots_walk. */
|
||||
enum {
|
||||
UC2_OTS_RESULT_VERIFIED = 1, /* leaf reaches all attestations via supported ops only */
|
||||
UC2_OTS_RESULT_STRUCTURAL = 2, /* parses cleanly but contains unsupported ops */
|
||||
UC2_OTS_RESULT_LEAF_MISMATCH = 3 /* shape OK but leaf digest doesn't match input */
|
||||
};
|
||||
|
||||
/* Attestation summary callback. Called once per attestation reached.
|
||||
* `digest` is the digest at the leaf where the attestation was emitted.
|
||||
* Return non-zero to abort the walk.
|
||||
*
|
||||
* Note: the digest is only meaningful when uc2_ots_walk returns
|
||||
* UC2_OTS_RESULT_VERIFIED. When the walker returns
|
||||
* UC2_OTS_RESULT_STRUCTURAL the proof contains unsupported unary ops
|
||||
* (SHA1, RIPEMD160, KECCAK256, REVERSE, HEXLIFY) which leave the digest
|
||||
* unchanged for structural traversal; the digest passed to the callback
|
||||
* does not represent the cryptographic state at that leaf. */
|
||||
typedef int (*uc2_ots_attest_cb)(void *ctx,
|
||||
const uint8_t *tag /* 8 bytes */,
|
||||
const uint8_t *payload, size_t payload_len,
|
||||
const uint8_t *digest, size_t digest_len);
|
||||
|
||||
/* OTS varint codec. *out_value is set on success; *consumed is the
|
||||
* number of input bytes read. */
|
||||
int uc2_ots_varint_decode(const uint8_t *in, size_t in_len,
|
||||
uint64_t *out_value, size_t *consumed);
|
||||
size_t uc2_ots_varint_encode(uint64_t value, uint8_t out[10]);
|
||||
|
||||
/* Parse the .ots file envelope (header magic + version + file-hash op +
|
||||
* leaf digest + timestamp body). Sets out_* pointers into the input
|
||||
* buffer; no allocation. */
|
||||
int uc2_ots_parse_file(const uint8_t *file, size_t file_len,
|
||||
uint8_t *out_hash_op,
|
||||
const uint8_t **out_leaf_digest,
|
||||
size_t *out_leaf_digest_len,
|
||||
const uint8_t **out_body,
|
||||
size_t *out_body_len);
|
||||
|
||||
/* Build a .ots file from a leaf digest and a serialized timestamp body.
|
||||
* Returns total bytes written, or a negative error code. */
|
||||
int uc2_ots_serialize_file(uint8_t hash_op,
|
||||
const uint8_t *leaf_digest, size_t leaf_digest_len,
|
||||
const uint8_t *body, size_t body_len,
|
||||
uint8_t *out, size_t out_cap);
|
||||
|
||||
/* Walk a serialized timestamp body from `leaf_digest`, applying ops and
|
||||
* invoking `cb` for each attestation reached. Returns one of
|
||||
* UC2_OTS_RESULT_* on structural success, or a negative error code. */
|
||||
int uc2_ots_walk(const uint8_t *body, size_t body_len,
|
||||
const uint8_t *leaf_digest, size_t leaf_digest_len,
|
||||
uc2_ots_attest_cb cb, void *ctx);
|
||||
|
||||
/* UC2 OTS trailer.
|
||||
*
|
||||
* Layout (all integers little-endian, 32-bit unsigned):
|
||||
*
|
||||
* [archive bytes ...]
|
||||
* "UC2-OTS\0" (8 bytes, front magic)
|
||||
* u32 version (= 1)
|
||||
* u32 archive_len (length of preceding archive bytes)
|
||||
* u32 proof_len
|
||||
* bytes proof (proof_len bytes, raw .ots file)
|
||||
* u32 proof_len (duplicate, for reverse-scan)
|
||||
* "UC2-OTS\0" (8 bytes, back magic)
|
||||
*/
|
||||
|
||||
#define UC2_OTS_TRAILER_MAGIC "UC2-OTS\0"
|
||||
#define UC2_OTS_TRAILER_MAGIC_LEN 8
|
||||
#define UC2_OTS_TRAILER_VERSION 1u
|
||||
#define UC2_OTS_TRAILER_HEAD_LEN (UC2_OTS_TRAILER_MAGIC_LEN + 4 + 4 + 4)
|
||||
#define UC2_OTS_TRAILER_TAIL_LEN (4 + UC2_OTS_TRAILER_MAGIC_LEN)
|
||||
#define UC2_OTS_TRAILER_OVERHEAD (UC2_OTS_TRAILER_HEAD_LEN + UC2_OTS_TRAILER_TAIL_LEN)
|
||||
#define UC2_OTS_TRAILER_MAX_PROOF (1u << 20)
|
||||
|
||||
/* Build a trailer for an existing archive of length archive_len.
|
||||
* Writes [front magic | version | archive_len | proof_len | proof | proof_len | back magic]
|
||||
* to out. Returns total bytes written, or negative on error. */
|
||||
int uc2_ots_trailer_build(uint32_t archive_len,
|
||||
const uint8_t *proof, size_t proof_len,
|
||||
uint8_t *out, size_t out_cap);
|
||||
|
||||
/* Read a trailer from the end of a file image. On success sets
|
||||
* *out_archive_len = length of preceding archive (the SHA-256 region)
|
||||
* *out_proof, *out_proof_len = pointer/length of proof inside `file`
|
||||
* Returns:
|
||||
* UC2_OTS_OK if a well-formed trailer is present,
|
||||
* 1 if no trailer (back magic absent),
|
||||
* negative error code if the back magic is present but the trailer is malformed. */
|
||||
int uc2_ots_trailer_parse(const uint8_t *file, size_t file_len,
|
||||
uint32_t *out_archive_len,
|
||||
const uint8_t **out_proof, size_t *out_proof_len);
|
||||
|
||||
/* Convenience: get a human-readable name for a known attestation tag,
|
||||
* or NULL if unknown. */
|
||||
const char *uc2_ots_attest_name(const uint8_t tag[UC2_OTS_TAG_LEN]);
|
||||
|
||||
#endif
|
||||
27
lib/include/uc2/uc2_sha256.h
Normal file
27
lib/include/uc2/uc2_sha256.h
Normal file
@@ -0,0 +1,27 @@
|
||||
/* SHA-256 (FIPS 180-4) -- pure C implementation.
|
||||
*
|
||||
* Used by the OpenTimestamps integration; calendars accept SHA-256
|
||||
* digests as proof leaves. */
|
||||
|
||||
#ifndef UC2_SHA256_H
|
||||
#define UC2_SHA256_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define UC2_SHA256_OUT_LEN 32
|
||||
#define UC2_SHA256_BLOCK_LEN 64
|
||||
|
||||
struct uc2_sha256 {
|
||||
uint32_t state[8];
|
||||
uint64_t bitcount;
|
||||
uint8_t buf[UC2_SHA256_BLOCK_LEN];
|
||||
size_t buf_len;
|
||||
};
|
||||
|
||||
void uc2_sha256_init(struct uc2_sha256 *ctx);
|
||||
void uc2_sha256_update(struct uc2_sha256 *ctx, const void *data, size_t len);
|
||||
void uc2_sha256_final(struct uc2_sha256 *ctx, uint8_t out[UC2_SHA256_OUT_LEN]);
|
||||
void uc2_sha256_hash(const void *data, size_t len, uint8_t out[UC2_SHA256_OUT_LEN]);
|
||||
|
||||
#endif
|
||||
341
lib/src/uc2_ots.c
Normal file
341
lib/src/uc2_ots.c
Normal file
@@ -0,0 +1,341 @@
|
||||
/* OpenTimestamps proof parser, serializer, walker, and UC2 trailer.
|
||||
*
|
||||
* The walker supports the calendar-path subset of opcodes (APPEND,
|
||||
* PREPEND, SHA256) directly. Other unary crypto ops (SHA1, RIPEMD160,
|
||||
* KECCAK256) are accepted as structurally valid but flagged as not
|
||||
* locally cryptographically verified; for full validation, extract
|
||||
* the proof and run the standard `ots verify` tool. */
|
||||
|
||||
#include "uc2/uc2_ots.h"
|
||||
#include "uc2/uc2_sha256.h"
|
||||
#include <string.h>
|
||||
|
||||
static uint32_t r32le(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
static void w32le(uint8_t *p, uint32_t v)
|
||||
{
|
||||
p[0] = (uint8_t)v;
|
||||
p[1] = (uint8_t)(v >> 8);
|
||||
p[2] = (uint8_t)(v >> 16);
|
||||
p[3] = (uint8_t)(v >> 24);
|
||||
}
|
||||
|
||||
int uc2_ots_varint_decode(const uint8_t *in, size_t in_len,
|
||||
uint64_t *out_value, size_t *consumed)
|
||||
{
|
||||
uint64_t v = 0;
|
||||
int shift = 0;
|
||||
size_t i = 0;
|
||||
for (;;) {
|
||||
if (i >= in_len) return UC2_OTS_ERR_TRUNCATED;
|
||||
if (shift >= 64) return UC2_OTS_ERR_OVERFLOW;
|
||||
uint8_t b = in[i++];
|
||||
uint8_t group = b & 0x7f;
|
||||
/* At shift == 63 only payloads of 0 or 1 fit in 64 bits;
|
||||
* anything larger would silently lose its high bits. */
|
||||
if (shift == 63 && group > 1)
|
||||
return UC2_OTS_ERR_OVERFLOW;
|
||||
v |= (uint64_t)group << shift;
|
||||
if (!(b & 0x80)) {
|
||||
/* Canonical: a multi-byte encoding must not have a zero
|
||||
* high group, i.e. the last byte cannot be 0x00 unless
|
||||
* the value is zero in a single byte. */
|
||||
if (i > 1 && b == 0)
|
||||
return UC2_OTS_ERR_NONCANONICAL;
|
||||
*out_value = v;
|
||||
*consumed = i;
|
||||
return UC2_OTS_OK;
|
||||
}
|
||||
shift += 7;
|
||||
}
|
||||
}
|
||||
|
||||
size_t uc2_ots_varint_encode(uint64_t value, uint8_t out[10])
|
||||
{
|
||||
size_t i = 0;
|
||||
while (value >= 0x80) {
|
||||
out[i++] = (uint8_t)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
out[i++] = (uint8_t)value;
|
||||
return i;
|
||||
}
|
||||
|
||||
/* Read a "varbytes" field: varint length + that many bytes. */
|
||||
static int read_varbytes(const uint8_t *p, size_t len,
|
||||
const uint8_t **out_data, size_t *out_data_len,
|
||||
size_t *consumed)
|
||||
{
|
||||
uint64_t n;
|
||||
size_t lc;
|
||||
int rc = uc2_ots_varint_decode(p, len, &n, &lc);
|
||||
if (rc < 0) return rc;
|
||||
if (n > UC2_OTS_MAX_VARBYTES) return UC2_OTS_ERR_TOO_LARGE;
|
||||
if (n > len - lc) return UC2_OTS_ERR_TRUNCATED;
|
||||
*out_data = p + lc;
|
||||
*out_data_len = (size_t)n;
|
||||
*consumed = lc + (size_t)n;
|
||||
return UC2_OTS_OK;
|
||||
}
|
||||
|
||||
int uc2_ots_parse_file(const uint8_t *file, size_t file_len,
|
||||
uint8_t *out_hash_op,
|
||||
const uint8_t **out_leaf_digest,
|
||||
size_t *out_leaf_digest_len,
|
||||
const uint8_t **out_body,
|
||||
size_t *out_body_len)
|
||||
{
|
||||
if (file_len < UC2_OTS_HEADER_MAGIC_LEN + 1 + 1 + 32)
|
||||
return UC2_OTS_ERR_TRUNCATED;
|
||||
if (memcmp(file, UC2_OTS_HEADER_MAGIC, UC2_OTS_HEADER_MAGIC_LEN) != 0)
|
||||
return UC2_OTS_ERR_BAD_MAGIC;
|
||||
size_t off = UC2_OTS_HEADER_MAGIC_LEN;
|
||||
if (file[off++] != UC2_OTS_VERSION)
|
||||
return UC2_OTS_ERR_BAD_VERSION;
|
||||
uint8_t hash_op = file[off++];
|
||||
size_t digest_len;
|
||||
switch (hash_op) {
|
||||
case UC2_OTS_OP_SHA1: digest_len = 20; break;
|
||||
case UC2_OTS_OP_RIPEMD160: digest_len = 20; break;
|
||||
case UC2_OTS_OP_SHA256: digest_len = 32; break;
|
||||
case UC2_OTS_OP_KECCAK256: digest_len = 32; break;
|
||||
default: return UC2_OTS_ERR_BAD_HASH_OP;
|
||||
}
|
||||
if (file_len - off < digest_len)
|
||||
return UC2_OTS_ERR_TRUNCATED;
|
||||
*out_hash_op = hash_op;
|
||||
*out_leaf_digest = file + off;
|
||||
*out_leaf_digest_len = digest_len;
|
||||
off += digest_len;
|
||||
*out_body = file + off;
|
||||
*out_body_len = file_len - off;
|
||||
return UC2_OTS_OK;
|
||||
}
|
||||
|
||||
int uc2_ots_serialize_file(uint8_t hash_op,
|
||||
const uint8_t *leaf_digest, size_t leaf_digest_len,
|
||||
const uint8_t *body, size_t body_len,
|
||||
uint8_t *out, size_t out_cap)
|
||||
{
|
||||
size_t want_len;
|
||||
switch (hash_op) {
|
||||
case UC2_OTS_OP_SHA1: want_len = 20; break;
|
||||
case UC2_OTS_OP_RIPEMD160: want_len = 20; break;
|
||||
case UC2_OTS_OP_SHA256: want_len = 32; break;
|
||||
case UC2_OTS_OP_KECCAK256: want_len = 32; break;
|
||||
default: return UC2_OTS_ERR_BAD_HASH_OP;
|
||||
}
|
||||
if (leaf_digest_len != want_len) return UC2_OTS_ERR_BAD_HASH_OP;
|
||||
size_t need = UC2_OTS_HEADER_MAGIC_LEN + 1 + 1 + leaf_digest_len + body_len;
|
||||
if (need > out_cap) return UC2_OTS_ERR_TRUNCATED;
|
||||
uint8_t *p = out;
|
||||
memcpy(p, UC2_OTS_HEADER_MAGIC, UC2_OTS_HEADER_MAGIC_LEN);
|
||||
p += UC2_OTS_HEADER_MAGIC_LEN;
|
||||
*p++ = UC2_OTS_VERSION;
|
||||
*p++ = hash_op;
|
||||
memcpy(p, leaf_digest, leaf_digest_len);
|
||||
p += leaf_digest_len;
|
||||
memcpy(p, body, body_len);
|
||||
p += body_len;
|
||||
return (int)(p - out);
|
||||
}
|
||||
|
||||
/* A serialized timestamp is a sequence of "items"; each item is either
|
||||
* (attestation) 0x00 + tag(8) + varbytes(payload)
|
||||
* (op) op-byte + (varbytes operand for binary ops) + child-timestamp
|
||||
*
|
||||
* Within one timestamp node, items are separated by 0xff: every item
|
||||
* except the LAST is preceded by 0xff. Children timestamps recurse
|
||||
* the same structure with the digest produced by their parent op. */
|
||||
|
||||
struct walker {
|
||||
const uint8_t *p, *end;
|
||||
uc2_ots_attest_cb cb;
|
||||
void *ctx;
|
||||
int has_unsupported_op;
|
||||
};
|
||||
|
||||
/* Apply an op to `digest`, consuming a varbytes operand for binary ops.
|
||||
* Supported ops update the digest in place; unsupported unary ops set
|
||||
* has_unsupported_op and leave the digest unchanged so the structural
|
||||
* walk can continue. */
|
||||
static int apply_op(struct walker *w, uint8_t op,
|
||||
uint8_t *digest, size_t *digest_len)
|
||||
{
|
||||
switch (op) {
|
||||
case UC2_OTS_OP_APPEND:
|
||||
case UC2_OTS_OP_PREPEND: {
|
||||
const uint8_t *operand;
|
||||
size_t operand_len, consumed;
|
||||
int rc = read_varbytes(w->p, (size_t)(w->end - w->p),
|
||||
&operand, &operand_len, &consumed);
|
||||
if (rc < 0) return rc;
|
||||
w->p += consumed;
|
||||
if (*digest_len + operand_len > UC2_OTS_MAX_DIGEST_LEN)
|
||||
return UC2_OTS_ERR_TOO_LARGE;
|
||||
if (op == UC2_OTS_OP_APPEND) {
|
||||
memcpy(digest + *digest_len, operand, operand_len);
|
||||
} else {
|
||||
memmove(digest + operand_len, digest, *digest_len);
|
||||
memcpy(digest, operand, operand_len);
|
||||
}
|
||||
*digest_len += operand_len;
|
||||
return UC2_OTS_OK;
|
||||
}
|
||||
case UC2_OTS_OP_SHA256: {
|
||||
uint8_t out[UC2_SHA256_OUT_LEN];
|
||||
uc2_sha256_hash(digest, *digest_len, out);
|
||||
memcpy(digest, out, UC2_SHA256_OUT_LEN);
|
||||
*digest_len = UC2_SHA256_OUT_LEN;
|
||||
return UC2_OTS_OK;
|
||||
}
|
||||
case UC2_OTS_OP_SHA1:
|
||||
case UC2_OTS_OP_RIPEMD160:
|
||||
case UC2_OTS_OP_KECCAK256:
|
||||
case UC2_OTS_OP_REVERSE:
|
||||
case UC2_OTS_OP_HEXLIFY:
|
||||
w->has_unsupported_op = 1;
|
||||
return UC2_OTS_OK;
|
||||
default:
|
||||
return UC2_OTS_ERR_BAD_OP;
|
||||
}
|
||||
}
|
||||
|
||||
static int walk_attestation(struct walker *w,
|
||||
const uint8_t *digest, size_t digest_len)
|
||||
{
|
||||
if (w->end - w->p < UC2_OTS_TAG_LEN) return UC2_OTS_ERR_TRUNCATED;
|
||||
const uint8_t *tag = w->p;
|
||||
w->p += UC2_OTS_TAG_LEN;
|
||||
const uint8_t *payload;
|
||||
size_t payload_len, consumed;
|
||||
int rc = read_varbytes(w->p, (size_t)(w->end - w->p),
|
||||
&payload, &payload_len, &consumed);
|
||||
if (rc < 0) return rc;
|
||||
w->p += consumed;
|
||||
if (w->cb && w->cb(w->ctx, tag, payload, payload_len, digest, digest_len))
|
||||
return UC2_OTS_ERR_OVERFLOW;
|
||||
return UC2_OTS_OK;
|
||||
}
|
||||
|
||||
static int walk_node(struct walker *w,
|
||||
const uint8_t *digest_in, size_t digest_in_len,
|
||||
int depth)
|
||||
{
|
||||
if (depth >= UC2_OTS_MAX_DEPTH) return UC2_OTS_ERR_DEPTH;
|
||||
|
||||
for (;;) {
|
||||
if (w->p >= w->end) return UC2_OTS_ERR_TRUNCATED;
|
||||
uint8_t b = *w->p++;
|
||||
int is_last = (b != UC2_OTS_BRANCH);
|
||||
if (!is_last) {
|
||||
if (w->p >= w->end) return UC2_OTS_ERR_TRUNCATED;
|
||||
b = *w->p++;
|
||||
}
|
||||
|
||||
if (b == UC2_OTS_ATTESTATION) {
|
||||
int rc = walk_attestation(w, digest_in, digest_in_len);
|
||||
if (rc < 0) return rc;
|
||||
} else {
|
||||
/* Op item: snapshot digest into a local buffer (siblings
|
||||
* within the same node share the parent digest), apply
|
||||
* the op, recurse into the sub-timestamp. */
|
||||
uint8_t mut[UC2_OTS_MAX_DIGEST_LEN];
|
||||
size_t mut_len = digest_in_len;
|
||||
memcpy(mut, digest_in, digest_in_len);
|
||||
int rc = apply_op(w, b, mut, &mut_len);
|
||||
if (rc < 0) return rc;
|
||||
rc = walk_node(w, mut, mut_len, depth + 1);
|
||||
if (rc < 0) return rc;
|
||||
}
|
||||
|
||||
if (is_last) return UC2_OTS_OK;
|
||||
}
|
||||
}
|
||||
|
||||
int uc2_ots_walk(const uint8_t *body, size_t body_len,
|
||||
const uint8_t *leaf_digest, size_t leaf_digest_len,
|
||||
uc2_ots_attest_cb cb, void *ctx)
|
||||
{
|
||||
if (leaf_digest_len > UC2_OTS_MAX_DIGEST_LEN)
|
||||
return UC2_OTS_ERR_TOO_LARGE;
|
||||
|
||||
struct walker w = { body, body + body_len, cb, ctx, 0 };
|
||||
int rc = walk_node(&w, leaf_digest, leaf_digest_len, 0);
|
||||
if (rc < 0) return rc;
|
||||
if (w.p != w.end) return UC2_OTS_ERR_OVERFLOW;
|
||||
return w.has_unsupported_op ? UC2_OTS_RESULT_STRUCTURAL
|
||||
: UC2_OTS_RESULT_VERIFIED;
|
||||
}
|
||||
|
||||
const char *uc2_ots_attest_name(const uint8_t tag[UC2_OTS_TAG_LEN])
|
||||
{
|
||||
if (memcmp(tag, UC2_OTS_TAG_PENDING, UC2_OTS_TAG_LEN) == 0)
|
||||
return "pending";
|
||||
if (memcmp(tag, UC2_OTS_TAG_BITCOIN, UC2_OTS_TAG_LEN) == 0)
|
||||
return "Bitcoin";
|
||||
if (memcmp(tag, UC2_OTS_TAG_LITECOIN, UC2_OTS_TAG_LEN) == 0)
|
||||
return "Litecoin";
|
||||
return 0;
|
||||
}
|
||||
|
||||
int uc2_ots_trailer_build(uint32_t archive_len,
|
||||
const uint8_t *proof, size_t proof_len,
|
||||
uint8_t *out, size_t out_cap)
|
||||
{
|
||||
if (proof_len > UC2_OTS_TRAILER_MAX_PROOF)
|
||||
return UC2_OTS_ERR_TOO_LARGE;
|
||||
size_t total = UC2_OTS_TRAILER_OVERHEAD + proof_len;
|
||||
if (total > out_cap) return UC2_OTS_ERR_TRUNCATED;
|
||||
uint8_t *p = out;
|
||||
memcpy(p, UC2_OTS_TRAILER_MAGIC, UC2_OTS_TRAILER_MAGIC_LEN);
|
||||
p += UC2_OTS_TRAILER_MAGIC_LEN;
|
||||
w32le(p, UC2_OTS_TRAILER_VERSION); p += 4;
|
||||
w32le(p, archive_len); p += 4;
|
||||
w32le(p, (uint32_t)proof_len); p += 4;
|
||||
memcpy(p, proof, proof_len); p += proof_len;
|
||||
w32le(p, (uint32_t)proof_len); p += 4;
|
||||
memcpy(p, UC2_OTS_TRAILER_MAGIC, UC2_OTS_TRAILER_MAGIC_LEN);
|
||||
p += UC2_OTS_TRAILER_MAGIC_LEN;
|
||||
return (int)(p - out);
|
||||
}
|
||||
|
||||
int uc2_ots_trailer_parse(const uint8_t *file, size_t file_len,
|
||||
uint32_t *out_archive_len,
|
||||
const uint8_t **out_proof, size_t *out_proof_len)
|
||||
{
|
||||
if (file_len < UC2_OTS_TRAILER_TAIL_LEN) return 1;
|
||||
const uint8_t *back = file + file_len - UC2_OTS_TRAILER_MAGIC_LEN;
|
||||
if (memcmp(back, UC2_OTS_TRAILER_MAGIC, UC2_OTS_TRAILER_MAGIC_LEN) != 0)
|
||||
return 1;
|
||||
|
||||
/* Back magic present: from here on, every check is hard-failed. */
|
||||
uint32_t back_proof_len = r32le(file + file_len - UC2_OTS_TRAILER_TAIL_LEN);
|
||||
if (back_proof_len > UC2_OTS_TRAILER_MAX_PROOF)
|
||||
return UC2_OTS_ERR_TOO_LARGE;
|
||||
|
||||
size_t total = UC2_OTS_TRAILER_OVERHEAD + back_proof_len;
|
||||
if (total > file_len) return UC2_OTS_ERR_TRUNCATED;
|
||||
const uint8_t *trailer_start = file + file_len - total;
|
||||
|
||||
if (memcmp(trailer_start, UC2_OTS_TRAILER_MAGIC, UC2_OTS_TRAILER_MAGIC_LEN) != 0)
|
||||
return UC2_OTS_ERR_BAD_MAGIC;
|
||||
|
||||
uint32_t version = r32le(trailer_start + UC2_OTS_TRAILER_MAGIC_LEN);
|
||||
uint32_t archive_ln = r32le(trailer_start + UC2_OTS_TRAILER_MAGIC_LEN + 4);
|
||||
uint32_t front_pl = r32le(trailer_start + UC2_OTS_TRAILER_MAGIC_LEN + 8);
|
||||
|
||||
if (version != UC2_OTS_TRAILER_VERSION) return UC2_OTS_ERR_BAD_VERSION;
|
||||
if (front_pl != back_proof_len) return UC2_OTS_ERR_NONCANONICAL;
|
||||
if ((size_t)archive_ln != (size_t)(trailer_start - file))
|
||||
return UC2_OTS_ERR_OVERFLOW;
|
||||
|
||||
*out_archive_len = archive_ln;
|
||||
*out_proof = trailer_start + UC2_OTS_TRAILER_HEAD_LEN;
|
||||
*out_proof_len = back_proof_len;
|
||||
return UC2_OTS_OK;
|
||||
}
|
||||
131
lib/src/uc2_sha256.c
Normal file
131
lib/src/uc2_sha256.c
Normal file
@@ -0,0 +1,131 @@
|
||||
/* SHA-256 (FIPS 180-4). Reference textbook implementation. */
|
||||
|
||||
#include "uc2/uc2_sha256.h"
|
||||
#include <string.h>
|
||||
|
||||
static const uint32_t K[64] = {
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
|
||||
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
||||
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
|
||||
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
||||
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
||||
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
||||
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
|
||||
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
||||
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
||||
};
|
||||
|
||||
static uint32_t rotr32(uint32_t x, int n) { return (x >> n) | (x << (32 - n)); }
|
||||
|
||||
static uint32_t r32be(const uint8_t *p)
|
||||
{
|
||||
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
|
||||
((uint32_t)p[2] << 8) | (uint32_t)p[3];
|
||||
}
|
||||
|
||||
static void w32be(uint8_t *p, uint32_t v)
|
||||
{
|
||||
p[0] = (uint8_t)(v >> 24);
|
||||
p[1] = (uint8_t)(v >> 16);
|
||||
p[2] = (uint8_t)(v >> 8);
|
||||
p[3] = (uint8_t)v;
|
||||
}
|
||||
|
||||
static void compress(uint32_t state[8], const uint8_t block[64])
|
||||
{
|
||||
uint32_t w[64];
|
||||
for (int i = 0; i < 16; i++)
|
||||
w[i] = r32be(block + 4 * i);
|
||||
for (int i = 16; i < 64; i++) {
|
||||
uint32_t s0 = rotr32(w[i-15], 7) ^ rotr32(w[i-15], 18) ^ (w[i-15] >> 3);
|
||||
uint32_t s1 = rotr32(w[i-2], 17) ^ rotr32(w[i-2], 19) ^ (w[i-2] >> 10);
|
||||
w[i] = w[i-16] + s0 + w[i-7] + s1;
|
||||
}
|
||||
|
||||
uint32_t a = state[0], b = state[1], c = state[2], d = state[3];
|
||||
uint32_t e = state[4], f = state[5], g = state[6], h = state[7];
|
||||
|
||||
for (int i = 0; i < 64; i++) {
|
||||
uint32_t S1 = rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25);
|
||||
uint32_t ch = (e & f) ^ (~e & g);
|
||||
uint32_t t1 = h + S1 + ch + K[i] + w[i];
|
||||
uint32_t S0 = rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22);
|
||||
uint32_t mj = (a & b) ^ (a & c) ^ (b & c);
|
||||
uint32_t t2 = S0 + mj;
|
||||
h = g; g = f; f = e; e = d + t1;
|
||||
d = c; c = b; b = a; a = t1 + t2;
|
||||
}
|
||||
|
||||
state[0] += a; state[1] += b; state[2] += c; state[3] += d;
|
||||
state[4] += e; state[5] += f; state[6] += g; state[7] += h;
|
||||
}
|
||||
|
||||
void uc2_sha256_init(struct uc2_sha256 *ctx)
|
||||
{
|
||||
ctx->state[0] = 0x6a09e667; ctx->state[1] = 0xbb67ae85;
|
||||
ctx->state[2] = 0x3c6ef372; ctx->state[3] = 0xa54ff53a;
|
||||
ctx->state[4] = 0x510e527f; ctx->state[5] = 0x9b05688c;
|
||||
ctx->state[6] = 0x1f83d9ab; ctx->state[7] = 0x5be0cd19;
|
||||
ctx->bitcount = 0;
|
||||
ctx->buf_len = 0;
|
||||
}
|
||||
|
||||
void uc2_sha256_update(struct uc2_sha256 *ctx, const void *data, size_t len)
|
||||
{
|
||||
const uint8_t *p = data;
|
||||
ctx->bitcount += (uint64_t)len * 8;
|
||||
if (ctx->buf_len) {
|
||||
size_t take = UC2_SHA256_BLOCK_LEN - ctx->buf_len;
|
||||
if (take > len) take = len;
|
||||
memcpy(ctx->buf + ctx->buf_len, p, take);
|
||||
ctx->buf_len += take;
|
||||
p += take;
|
||||
len -= take;
|
||||
if (ctx->buf_len == UC2_SHA256_BLOCK_LEN) {
|
||||
compress(ctx->state, ctx->buf);
|
||||
ctx->buf_len = 0;
|
||||
}
|
||||
}
|
||||
while (len >= UC2_SHA256_BLOCK_LEN) {
|
||||
compress(ctx->state, p);
|
||||
p += UC2_SHA256_BLOCK_LEN;
|
||||
len -= UC2_SHA256_BLOCK_LEN;
|
||||
}
|
||||
if (len) {
|
||||
memcpy(ctx->buf, p, len);
|
||||
ctx->buf_len = len;
|
||||
}
|
||||
}
|
||||
|
||||
void uc2_sha256_final(struct uc2_sha256 *ctx, uint8_t out[UC2_SHA256_OUT_LEN])
|
||||
{
|
||||
uint64_t bits = ctx->bitcount;
|
||||
ctx->buf[ctx->buf_len++] = 0x80;
|
||||
if (ctx->buf_len > 56) {
|
||||
memset(ctx->buf + ctx->buf_len, 0, UC2_SHA256_BLOCK_LEN - ctx->buf_len);
|
||||
compress(ctx->state, ctx->buf);
|
||||
ctx->buf_len = 0;
|
||||
}
|
||||
memset(ctx->buf + ctx->buf_len, 0, 56 - ctx->buf_len);
|
||||
for (int i = 0; i < 8; i++)
|
||||
ctx->buf[56 + i] = (uint8_t)(bits >> (56 - 8 * i));
|
||||
compress(ctx->state, ctx->buf);
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
w32be(out + 4 * i, ctx->state[i]);
|
||||
}
|
||||
|
||||
void uc2_sha256_hash(const void *data, size_t len, uint8_t out[UC2_SHA256_OUT_LEN])
|
||||
{
|
||||
struct uc2_sha256 ctx;
|
||||
uc2_sha256_init(&ctx);
|
||||
uc2_sha256_update(&ctx, data, len);
|
||||
uc2_sha256_final(&ctx, out);
|
||||
}
|
||||
@@ -105,6 +105,34 @@ target_include_directories(test_blake3 PRIVATE "${PROJECT_BINARY_DIR}/lib")
|
||||
target_compile_features(test_blake3 PRIVATE c_std_99)
|
||||
add_test(NAME blake3 COMMAND test_blake3)
|
||||
|
||||
add_executable(test_sha256 src/test_sha256.c)
|
||||
target_link_libraries(test_sha256 PRIVATE uc2)
|
||||
target_include_directories(test_sha256 PRIVATE "${PROJECT_BINARY_DIR}/lib")
|
||||
target_compile_features(test_sha256 PRIVATE c_std_99)
|
||||
add_test(NAME sha256 COMMAND test_sha256)
|
||||
|
||||
add_executable(test_ots src/test_ots.c)
|
||||
target_link_libraries(test_ots PRIVATE uc2)
|
||||
target_include_directories(test_ots PRIVATE "${PROJECT_BINARY_DIR}/lib")
|
||||
target_compile_features(test_ots PRIVATE c_std_99)
|
||||
add_test(NAME ots COMMAND test_ots)
|
||||
|
||||
# Optional cross-check: validates uc2 .ots output against the python-opentimestamps
|
||||
# reference parser. Skipped (return code 77) when opentimestamps is not installed.
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME ots_cross_check
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/cross_check_ots.py
|
||||
$<TARGET_FILE:uc2-cli>
|
||||
${CMAKE_CURRENT_BINARY_DIR}/ots_cross_check
|
||||
)
|
||||
set_tests_properties(ots_cross_check PROPERTIES
|
||||
SKIP_RETURN_CODE 77
|
||||
LABELS "optional"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Cross-tool round-trip: UC2 v3 <-> original uc2pro.exe via DOSBox-X
|
||||
add_test(NAME roundtrip_dosbox
|
||||
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/roundtrip_dosbox.sh
|
||||
|
||||
119
tests/scripts/cross_check_ots.py
Normal file
119
tests/scripts/cross_check_ots.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cross-check uc2 OTS output against the python-opentimestamps reference.
|
||||
|
||||
Usage: cross_check_ots.py <uc2-binary> <work-dir>
|
||||
|
||||
Builds a tiny archive, attaches a hand-crafted OTS proof, then:
|
||||
1. Extracts via `uc2 --ots-extract`
|
||||
2. Round-trips the .ots through python-opentimestamps
|
||||
3. Confirms the proof's leaf digest equals SHA-256 of the attested archive prefix
|
||||
|
||||
Exits 0 on success, 1 on mismatch, 77 (autotools "skip" code) if the
|
||||
opentimestamps library isn't installed.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
try:
|
||||
from opentimestamps.core.timestamp import DetachedTimestampFile
|
||||
from opentimestamps.core.serialize import StreamDeserializationContext
|
||||
except ModuleNotFoundError:
|
||||
print("opentimestamps library not installed; skipping cross-check.")
|
||||
sys.exit(77)
|
||||
|
||||
|
||||
HEADER_MAGIC = (b"\x00OpenTimestamps\x00\x00Proof\x00"
|
||||
b"\xbf\x89\xe2\xe8\x84\xe8\x92\x94")
|
||||
PENDING_TAG = b"\x83\xdf\xe3\x0d\x2e\xf9\x0c\x8e"
|
||||
TRAILER_MAGIC = b"UC2-OTS\x00"
|
||||
|
||||
|
||||
def varint(n):
|
||||
out = b""
|
||||
while n >= 0x80:
|
||||
out += bytes([n & 0x7f | 0x80])
|
||||
n >>= 7
|
||||
return out + bytes([n])
|
||||
|
||||
|
||||
def varbytes(b):
|
||||
return varint(len(b)) + b
|
||||
|
||||
|
||||
def build_proof(leaf):
|
||||
# Pending attestation payload is itself varbytes(uri) per the OTS spec,
|
||||
# wrapped in the outer varbytes(serialized_attestation) layer.
|
||||
pending_payload = varbytes(b"https://example.com/digest")
|
||||
body = b"\x00" + PENDING_TAG + varbytes(pending_payload)
|
||||
return HEADER_MAGIC + b"\x01" + b"\x08" + leaf + body
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: cross_check_ots.py <uc2-binary> <work-dir>", file=sys.stderr)
|
||||
return 1
|
||||
uc2 = sys.argv[1]
|
||||
work = sys.argv[2]
|
||||
os.makedirs(work, exist_ok=True)
|
||||
|
||||
a = os.path.join(work, "a.txt")
|
||||
b = os.path.join(work, "b.txt")
|
||||
with open(a, "w") as f: f.write("hello uc2 ots cross-check\n")
|
||||
with open(b, "w") as f: f.write("second file\n")
|
||||
|
||||
arc = os.path.join(work, "test.uc2")
|
||||
subprocess.check_call([uc2, "-w", "-q", arc, a, b])
|
||||
|
||||
archive_size = os.path.getsize(arc)
|
||||
with open(arc, "rb") as f:
|
||||
archive_bytes = f.read()
|
||||
leaf = hashlib.sha256(archive_bytes).digest()
|
||||
|
||||
proof_path = os.path.join(work, "proof.ots")
|
||||
with open(proof_path, "wb") as f:
|
||||
f.write(build_proof(leaf))
|
||||
|
||||
subprocess.check_call([uc2, "--ots-attach", proof_path, arc])
|
||||
|
||||
extracted = os.path.join(work, "extracted.ots")
|
||||
subprocess.check_call([uc2, "--ots-extract", arc, extracted])
|
||||
|
||||
with open(extracted, "rb") as f:
|
||||
ots_bytes = f.read()
|
||||
ctx = StreamDeserializationContext(BytesIO(ots_bytes))
|
||||
detached = DetachedTimestampFile.deserialize(ctx)
|
||||
|
||||
py_leaf = bytes(detached.timestamp.msg)
|
||||
if py_leaf != leaf:
|
||||
print("LEAF MISMATCH", file=sys.stderr)
|
||||
print(f" hand-computed: {leaf.hex()}", file=sys.stderr)
|
||||
print(f" python-ots: {py_leaf.hex()}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
attestations = list(detached.timestamp.all_attestations())
|
||||
if not attestations:
|
||||
print("no attestations parsed by python-opentimestamps", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
info = subprocess.check_output(
|
||||
[uc2, "--ots-info", arc], stderr=subprocess.STDOUT, text=True)
|
||||
if "leaf matches archive: yes" not in info:
|
||||
print("uc2 --ots-info reports leaf mismatch:", file=sys.stderr)
|
||||
print(info, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if archive_size + len(ots_bytes) >= os.path.getsize(arc):
|
||||
pass # archive grew by at least proof_len; trailer is present
|
||||
|
||||
print(f"cross-check OK: archive_size={archive_size}, proof_len={len(ots_bytes)}, "
|
||||
f"attestations={len(attestations)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
338
tests/src/test_ots.c
Normal file
338
tests/src/test_ots.c
Normal file
@@ -0,0 +1,338 @@
|
||||
/* Tests for the OpenTimestamps integration. */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <uc2/uc2_ots.h>
|
||||
#include <uc2/uc2_sha256.h>
|
||||
|
||||
static int tests_run = 0, tests_passed = 0;
|
||||
#define TEST(name) do { tests_run++; printf(" %s: ", #name); name(); tests_passed++; printf("OK\n"); } while (0)
|
||||
|
||||
static void test_varint_roundtrip(void)
|
||||
{
|
||||
uint64_t cases[] = { 0, 1, 0x7f, 0x80, 0xff, 0x3fff, 0x4000,
|
||||
0xffff, 0x1fffff, 0xfffffffful, 0x123456789aULL };
|
||||
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) {
|
||||
uint8_t buf[10];
|
||||
size_t n = uc2_ots_varint_encode(cases[i], buf);
|
||||
uint64_t got;
|
||||
size_t consumed;
|
||||
assert(uc2_ots_varint_decode(buf, n, &got, &consumed) == UC2_OTS_OK);
|
||||
assert(got == cases[i]);
|
||||
assert(consumed == n);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_varint_truncated(void)
|
||||
{
|
||||
uint8_t buf[] = { 0x80, 0x80 }; /* unterminated */
|
||||
uint64_t v;
|
||||
size_t c;
|
||||
assert(uc2_ots_varint_decode(buf, 2, &v, &c) == UC2_OTS_ERR_TRUNCATED);
|
||||
}
|
||||
|
||||
static void test_varint_noncanonical(void)
|
||||
{
|
||||
/* 0x80 0x00 encodes 0 with a redundant continuation byte. */
|
||||
uint8_t buf[] = { 0x80, 0x00 };
|
||||
uint64_t v;
|
||||
size_t c;
|
||||
assert(uc2_ots_varint_decode(buf, 2, &v, &c) == UC2_OTS_ERR_NONCANONICAL);
|
||||
}
|
||||
|
||||
static void test_varint_overflow_64bit(void)
|
||||
{
|
||||
/* 10-byte encoding where the final group has bits beyond bit 63.
|
||||
* Bytes 1-9 fill shift positions 0..56 with all 1s; byte 10 at shift 63
|
||||
* carries 0x02 (group=2), which would shift past bit 63. */
|
||||
uint8_t buf[] = {
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x02
|
||||
};
|
||||
uint64_t v;
|
||||
size_t c;
|
||||
assert(uc2_ots_varint_decode(buf, sizeof buf, &v, &c) == UC2_OTS_ERR_OVERFLOW);
|
||||
}
|
||||
|
||||
static void test_varint_max_64bit(void)
|
||||
{
|
||||
/* Largest representable 64-bit value: 0xffffffffffffffff. */
|
||||
uint8_t buf[10];
|
||||
size_t n = uc2_ots_varint_encode((uint64_t)-1, buf);
|
||||
uint64_t v;
|
||||
size_t c;
|
||||
assert(uc2_ots_varint_decode(buf, n, &v, &c) == UC2_OTS_OK);
|
||||
assert(v == (uint64_t)-1);
|
||||
}
|
||||
|
||||
static void test_file_envelope_roundtrip(void)
|
||||
{
|
||||
uint8_t leaf[32];
|
||||
for (int i = 0; i < 32; i++) leaf[i] = (uint8_t)i;
|
||||
uint8_t body[] = "\x00\x83\xdf\xe3\x0d\x2e\xf9\x0c\x8e\x01x"; /* attest pending "x" */
|
||||
size_t body_len = sizeof body - 1;
|
||||
|
||||
uint8_t out[256];
|
||||
int n = uc2_ots_serialize_file(UC2_OTS_OP_SHA256, leaf, 32,
|
||||
body, body_len, out, sizeof out);
|
||||
assert(n > 0);
|
||||
|
||||
uint8_t hash_op;
|
||||
const uint8_t *got_leaf, *got_body;
|
||||
size_t got_leaf_len, got_body_len;
|
||||
int rc = uc2_ots_parse_file(out, (size_t)n, &hash_op,
|
||||
&got_leaf, &got_leaf_len,
|
||||
&got_body, &got_body_len);
|
||||
assert(rc == UC2_OTS_OK);
|
||||
assert(hash_op == UC2_OTS_OP_SHA256);
|
||||
assert(got_leaf_len == 32);
|
||||
assert(memcmp(got_leaf, leaf, 32) == 0);
|
||||
assert(got_body_len == body_len);
|
||||
assert(memcmp(got_body, body, body_len) == 0);
|
||||
}
|
||||
|
||||
static void test_file_bad_magic(void)
|
||||
{
|
||||
uint8_t buf[128];
|
||||
memset(buf, 0xaa, sizeof buf);
|
||||
uint8_t hash_op;
|
||||
const uint8_t *l, *b;
|
||||
size_t ll, bl;
|
||||
assert(uc2_ots_parse_file(buf, sizeof buf, &hash_op, &l, &ll, &b, &bl)
|
||||
== UC2_OTS_ERR_BAD_MAGIC);
|
||||
}
|
||||
|
||||
struct cb_ctx {
|
||||
int n_calls;
|
||||
uint8_t last_tag[8];
|
||||
uint8_t last_digest[64];
|
||||
size_t last_digest_len;
|
||||
const uint8_t *last_payload;
|
||||
size_t last_payload_len;
|
||||
};
|
||||
|
||||
static int collect_cb(void *vctx,
|
||||
const uint8_t *tag,
|
||||
const uint8_t *payload, size_t payload_len,
|
||||
const uint8_t *digest, size_t digest_len)
|
||||
{
|
||||
struct cb_ctx *c = vctx;
|
||||
c->n_calls++;
|
||||
memcpy(c->last_tag, tag, 8);
|
||||
memcpy(c->last_digest, digest, digest_len);
|
||||
c->last_digest_len = digest_len;
|
||||
c->last_payload = payload;
|
||||
c->last_payload_len = payload_len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_walk_append_then_attest(void)
|
||||
{
|
||||
/* Body: APPEND "ab", then attestation pending "x".
|
||||
* Leaf "L" -> APPEND "ab" -> "Lab" -> pending. */
|
||||
uint8_t body[] = {
|
||||
UC2_OTS_OP_APPEND, 0x02, 'a', 'b',
|
||||
UC2_OTS_ATTESTATION,
|
||||
0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e,
|
||||
0x01, 'x'
|
||||
};
|
||||
struct cb_ctx ctx = {0};
|
||||
int rc = uc2_ots_walk(body, sizeof body, (uint8_t *)"L", 1,
|
||||
collect_cb, &ctx);
|
||||
assert(rc == UC2_OTS_RESULT_VERIFIED);
|
||||
assert(ctx.n_calls == 1);
|
||||
assert(ctx.last_digest_len == 3);
|
||||
assert(memcmp(ctx.last_digest, "Lab", 3) == 0);
|
||||
assert(memcmp(ctx.last_tag, UC2_OTS_TAG_PENDING, 8) == 0);
|
||||
assert(ctx.last_payload_len == 1 && ctx.last_payload[0] == 'x');
|
||||
}
|
||||
|
||||
static void test_walk_two_siblings(void)
|
||||
{
|
||||
/* Body: 0xff <att pending "a"> <att pending "b">.
|
||||
* Two sibling attestations from the same leaf. */
|
||||
uint8_t body[] = {
|
||||
UC2_OTS_BRANCH,
|
||||
UC2_OTS_ATTESTATION, 0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e, 0x01, 'a',
|
||||
UC2_OTS_ATTESTATION, 0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e, 0x01, 'b'
|
||||
};
|
||||
struct cb_ctx ctx = {0};
|
||||
int rc = uc2_ots_walk(body, sizeof body, (uint8_t *)"L", 1,
|
||||
collect_cb, &ctx);
|
||||
assert(rc == UC2_OTS_RESULT_VERIFIED);
|
||||
assert(ctx.n_calls == 2);
|
||||
}
|
||||
|
||||
static void test_walk_sha256_op(void)
|
||||
{
|
||||
/* Body: SHA256 op then pending attestation. */
|
||||
uint8_t body[] = {
|
||||
UC2_OTS_OP_SHA256,
|
||||
UC2_OTS_ATTESTATION,
|
||||
0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e,
|
||||
0x01, 'x'
|
||||
};
|
||||
uint8_t leaf[32] = {0};
|
||||
struct cb_ctx ctx = {0};
|
||||
int rc = uc2_ots_walk(body, sizeof body, leaf, 32, collect_cb, &ctx);
|
||||
assert(rc == UC2_OTS_RESULT_VERIFIED);
|
||||
assert(ctx.n_calls == 1);
|
||||
assert(ctx.last_digest_len == 32);
|
||||
uint8_t expect[32];
|
||||
uc2_sha256_hash(leaf, 32, expect);
|
||||
assert(memcmp(ctx.last_digest, expect, 32) == 0);
|
||||
}
|
||||
|
||||
static void test_walk_unsupported_op(void)
|
||||
{
|
||||
/* SHA1 op -> structural-only. */
|
||||
uint8_t body[] = {
|
||||
UC2_OTS_OP_SHA1,
|
||||
UC2_OTS_ATTESTATION,
|
||||
0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e,
|
||||
0x01, 'x'
|
||||
};
|
||||
uint8_t leaf[32] = {0};
|
||||
struct cb_ctx ctx = {0};
|
||||
int rc = uc2_ots_walk(body, sizeof body, leaf, 32, collect_cb, &ctx);
|
||||
assert(rc == UC2_OTS_RESULT_STRUCTURAL);
|
||||
assert(ctx.n_calls == 1);
|
||||
}
|
||||
|
||||
static void test_walk_truncated(void)
|
||||
{
|
||||
uint8_t body[] = { UC2_OTS_OP_APPEND, 0x05, 'a' }; /* operand truncated */
|
||||
uint8_t leaf[1] = {'L'};
|
||||
int rc = uc2_ots_walk(body, sizeof body, leaf, 1, NULL, NULL);
|
||||
assert(rc == UC2_OTS_ERR_TRUNCATED);
|
||||
}
|
||||
|
||||
static void test_walk_trailing_garbage(void)
|
||||
{
|
||||
uint8_t body[] = {
|
||||
UC2_OTS_ATTESTATION,
|
||||
0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e,
|
||||
0x01, 'x',
|
||||
0xaa /* garbage */
|
||||
};
|
||||
uint8_t leaf[1] = {'L'};
|
||||
int rc = uc2_ots_walk(body, sizeof body, leaf, 1, NULL, NULL);
|
||||
assert(rc == UC2_OTS_ERR_OVERFLOW);
|
||||
}
|
||||
|
||||
static void test_attest_name(void)
|
||||
{
|
||||
assert(strcmp(uc2_ots_attest_name((uint8_t *)UC2_OTS_TAG_PENDING), "pending") == 0);
|
||||
assert(strcmp(uc2_ots_attest_name((uint8_t *)UC2_OTS_TAG_BITCOIN), "Bitcoin") == 0);
|
||||
assert(strcmp(uc2_ots_attest_name((uint8_t *)UC2_OTS_TAG_LITECOIN), "Litecoin") == 0);
|
||||
uint8_t unk[8] = {0};
|
||||
assert(uc2_ots_attest_name(unk) == NULL);
|
||||
}
|
||||
|
||||
static void test_trailer_roundtrip(void)
|
||||
{
|
||||
uint8_t fake_archive[100];
|
||||
for (int i = 0; i < 100; i++) fake_archive[i] = (uint8_t)i;
|
||||
uint8_t fake_proof[40];
|
||||
for (int i = 0; i < 40; i++) fake_proof[i] = (uint8_t)(0xa0 + i);
|
||||
|
||||
uint8_t buf[300];
|
||||
memcpy(buf, fake_archive, 100);
|
||||
int n = uc2_ots_trailer_build(100, fake_proof, 40,
|
||||
buf + 100, sizeof buf - 100);
|
||||
assert(n > 0);
|
||||
size_t total = 100 + (size_t)n;
|
||||
|
||||
uint32_t archive_len;
|
||||
const uint8_t *proof;
|
||||
size_t proof_len;
|
||||
int rc = uc2_ots_trailer_parse(buf, total, &archive_len, &proof, &proof_len);
|
||||
assert(rc == UC2_OTS_OK);
|
||||
assert(archive_len == 100);
|
||||
assert(proof_len == 40);
|
||||
assert(memcmp(proof, fake_proof, 40) == 0);
|
||||
}
|
||||
|
||||
static void test_trailer_no_trailer(void)
|
||||
{
|
||||
uint8_t buf[20] = {0};
|
||||
uint32_t al; const uint8_t *p; size_t pl;
|
||||
int rc = uc2_ots_trailer_parse(buf, sizeof buf, &al, &p, &pl);
|
||||
assert(rc == 1); /* no trailer */
|
||||
}
|
||||
|
||||
static void test_trailer_corrupt_proof_len_mismatch(void)
|
||||
{
|
||||
/* Build a valid trailer, then mutate the front proof_len so it
|
||||
* disagrees with the back duplicate. */
|
||||
uint8_t buf[200];
|
||||
memset(buf, 0, sizeof buf);
|
||||
int n = uc2_ots_trailer_build(50, (uint8_t *)"\x01\x02\x03\x04", 4,
|
||||
buf + 50, sizeof buf - 50);
|
||||
assert(n > 0);
|
||||
size_t total = 50 + (size_t)n;
|
||||
/* Mutate front proof_len at offset 50+8+4+4 = 66 */
|
||||
buf[66] = 0xff;
|
||||
|
||||
uint32_t al; const uint8_t *p; size_t pl;
|
||||
int rc = uc2_ots_trailer_parse(buf, total, &al, &p, &pl);
|
||||
assert(rc == UC2_OTS_ERR_NONCANONICAL);
|
||||
}
|
||||
|
||||
static void test_trailer_truncated(void)
|
||||
{
|
||||
/* Build a valid trailer, then chop the file in the middle of the
|
||||
* proof. Reverse-scan won't see back magic; should report no trailer. */
|
||||
uint8_t buf[200];
|
||||
memset(buf, 0, sizeof buf);
|
||||
int n = uc2_ots_trailer_build(50, (uint8_t *)"AAAAAA", 6,
|
||||
buf + 50, sizeof buf - 50);
|
||||
assert(n > 0);
|
||||
size_t total = 50 + (size_t)n - 5; /* chop last 5 bytes */
|
||||
uint32_t al; const uint8_t *p; size_t pl;
|
||||
int rc = uc2_ots_trailer_parse(buf, total, &al, &p, &pl);
|
||||
assert(rc == 1); /* back magic absent */
|
||||
}
|
||||
|
||||
static void test_trailer_back_magic_collision(void)
|
||||
{
|
||||
/* If the file happens to end with the back magic but the recorded
|
||||
* proof_len is too large for the file size, trailer_parse must
|
||||
* report a hard error, not silently accept. */
|
||||
uint8_t buf[16];
|
||||
memset(buf, 0, sizeof buf);
|
||||
/* Last 8 bytes = back magic; preceding 4 bytes = bogus proof_len. */
|
||||
memcpy(buf + 8, UC2_OTS_TRAILER_MAGIC, UC2_OTS_TRAILER_MAGIC_LEN);
|
||||
buf[4] = 0xff; buf[5] = 0xff; buf[6] = 0xff; buf[7] = 0x00; /* huge proof_len */
|
||||
|
||||
uint32_t al; const uint8_t *p; size_t pl;
|
||||
int rc = uc2_ots_trailer_parse(buf, sizeof buf, &al, &p, &pl);
|
||||
assert(rc == UC2_OTS_ERR_TOO_LARGE || rc == UC2_OTS_ERR_TRUNCATED);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("OTS tests:\n");
|
||||
TEST(test_varint_roundtrip);
|
||||
TEST(test_varint_truncated);
|
||||
TEST(test_varint_noncanonical);
|
||||
TEST(test_varint_overflow_64bit);
|
||||
TEST(test_varint_max_64bit);
|
||||
TEST(test_file_envelope_roundtrip);
|
||||
TEST(test_file_bad_magic);
|
||||
TEST(test_walk_append_then_attest);
|
||||
TEST(test_walk_two_siblings);
|
||||
TEST(test_walk_sha256_op);
|
||||
TEST(test_walk_unsupported_op);
|
||||
TEST(test_walk_truncated);
|
||||
TEST(test_walk_trailing_garbage);
|
||||
TEST(test_attest_name);
|
||||
TEST(test_trailer_roundtrip);
|
||||
TEST(test_trailer_no_trailer);
|
||||
TEST(test_trailer_corrupt_proof_len_mismatch);
|
||||
TEST(test_trailer_truncated);
|
||||
TEST(test_trailer_back_magic_collision);
|
||||
printf("%d/%d tests passed\n", tests_passed, tests_run);
|
||||
return tests_passed == tests_run ? 0 : 1;
|
||||
}
|
||||
112
tests/src/test_sha256.c
Normal file
112
tests/src/test_sha256.c
Normal file
@@ -0,0 +1,112 @@
|
||||
/* Tests for SHA-256 (FIPS 180-4 vectors). */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <uc2/uc2_sha256.h>
|
||||
|
||||
static int tests_run = 0, tests_passed = 0;
|
||||
#define TEST(name) do { tests_run++; printf(" %s: ", #name); name(); tests_passed++; printf("OK\n"); } while (0)
|
||||
|
||||
static void hex(const uint8_t *h, int n, char *out)
|
||||
{
|
||||
for (int i = 0; i < n; i++) sprintf(out + i*2, "%02x", h[i]);
|
||||
out[n*2] = 0;
|
||||
}
|
||||
|
||||
static int hex_eq(const uint8_t *h, int n, const char *want)
|
||||
{
|
||||
char got[65];
|
||||
hex(h, n, got);
|
||||
return strcmp(got, want) == 0;
|
||||
}
|
||||
|
||||
static void test_empty(void)
|
||||
{
|
||||
uint8_t h[32];
|
||||
uc2_sha256_hash("", 0, h);
|
||||
assert(hex_eq(h, 32,
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"));
|
||||
}
|
||||
|
||||
static void test_abc(void)
|
||||
{
|
||||
uint8_t h[32];
|
||||
uc2_sha256_hash("abc", 3, h);
|
||||
assert(hex_eq(h, 32,
|
||||
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"));
|
||||
}
|
||||
|
||||
static void test_56_byte(void)
|
||||
{
|
||||
const char *m = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
|
||||
uint8_t h[32];
|
||||
uc2_sha256_hash(m, strlen(m), h);
|
||||
assert(hex_eq(h, 32,
|
||||
"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"));
|
||||
}
|
||||
|
||||
static void test_million_a(void)
|
||||
{
|
||||
struct uc2_sha256 ctx;
|
||||
uc2_sha256_init(&ctx);
|
||||
uint8_t buf[1000];
|
||||
memset(buf, 'a', sizeof buf);
|
||||
for (int i = 0; i < 1000; i++)
|
||||
uc2_sha256_update(&ctx, buf, sizeof buf);
|
||||
uint8_t h[32];
|
||||
uc2_sha256_final(&ctx, h);
|
||||
assert(hex_eq(h, 32,
|
||||
"cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"));
|
||||
}
|
||||
|
||||
static void test_incremental(void)
|
||||
{
|
||||
const char *m = "The quick brown fox jumps over the lazy dog";
|
||||
size_t len = strlen(m);
|
||||
|
||||
uint8_t oneshot[32];
|
||||
uc2_sha256_hash(m, len, oneshot);
|
||||
|
||||
struct uc2_sha256 ctx;
|
||||
uc2_sha256_init(&ctx);
|
||||
for (size_t i = 0; i < len; i++)
|
||||
uc2_sha256_update(&ctx, m + i, 1);
|
||||
uint8_t piecemeal[32];
|
||||
uc2_sha256_final(&ctx, piecemeal);
|
||||
|
||||
assert(memcmp(oneshot, piecemeal, 32) == 0);
|
||||
}
|
||||
|
||||
static void test_block_boundaries(void)
|
||||
{
|
||||
uint8_t buf[200];
|
||||
for (size_t i = 0; i < sizeof buf; i++) buf[i] = (uint8_t)(i & 0xFF);
|
||||
|
||||
uint8_t oneshot[32];
|
||||
uc2_sha256_hash(buf, sizeof buf, oneshot);
|
||||
|
||||
for (size_t split = 1; split < sizeof buf; split++) {
|
||||
struct uc2_sha256 ctx;
|
||||
uc2_sha256_init(&ctx);
|
||||
uc2_sha256_update(&ctx, buf, split);
|
||||
uc2_sha256_update(&ctx, buf + split, sizeof buf - split);
|
||||
uint8_t h[32];
|
||||
uc2_sha256_final(&ctx, h);
|
||||
assert(memcmp(oneshot, h, 32) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("SHA-256 tests:\n");
|
||||
TEST(test_empty);
|
||||
TEST(test_abc);
|
||||
TEST(test_56_byte);
|
||||
TEST(test_million_a);
|
||||
TEST(test_incremental);
|
||||
TEST(test_block_boundaries);
|
||||
printf("%d/%d tests passed\n", tests_passed, tests_run);
|
||||
return tests_passed == tests_run ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user