From 5c01fec996a252544aba2d53deaae15a3365aeff Mon Sep 17 00:00:00 2001 From: Eremey Valetov Date: Sun, 3 May 2026 12:15:30 -0400 Subject: [PATCH] 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. --- ROADMAP.md | 22 +- cli/src/main.c | 436 ++++++++++++++++++++++++++++++- lib/CMakeLists.txt | 2 +- lib/include/uc2/uc2_ots.h | 166 ++++++++++++ lib/include/uc2/uc2_sha256.h | 27 ++ lib/src/uc2_ots.c | 341 ++++++++++++++++++++++++ lib/src/uc2_sha256.c | 131 ++++++++++ tests/CMakeLists.txt | 28 ++ tests/scripts/cross_check_ots.py | 119 +++++++++ tests/src/test_ots.c | 338 ++++++++++++++++++++++++ tests/src/test_sha256.c | 112 ++++++++ 11 files changed, 1717 insertions(+), 5 deletions(-) create mode 100644 lib/include/uc2/uc2_ots.h create mode 100644 lib/include/uc2/uc2_sha256.h create mode 100644 lib/src/uc2_ots.c create mode 100644 lib/src/uc2_sha256.c create mode 100644 tests/scripts/cross_check_ots.py create mode 100644 tests/src/test_ots.c create mode 100644 tests/src/test_sha256.c diff --git a/ROADMAP.md b/ROADMAP.md index 09ab985..afaf5b9 100644 --- a/ROADMAP.md +++ b/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 diff --git a/cli/src/main.c b/cli/src/main.c index 758dd84..786b282 100644 --- a/cli/src/main.c +++ b/cli/src/main.c @@ -35,6 +35,8 @@ void setprogname(const char *argv0); #include #include #include +#include +#include #include #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 +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 : ""); + 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" : ""); + 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 (separate argv entry; rejected if path starts with '-') + * --ots-attach= (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= 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 [-f] archive.uc2\n" + "uc2 --ots-extract archive.uc2 \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; diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 109dc2d..39b4ff8 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -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) diff --git a/lib/include/uc2/uc2_ots.h b/lib/include/uc2/uc2_ots.h new file mode 100644 index 0000000..af646ab --- /dev/null +++ b/lib/include/uc2/uc2_ots.h @@ -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 +#include + +/* 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 diff --git a/lib/include/uc2/uc2_sha256.h b/lib/include/uc2/uc2_sha256.h new file mode 100644 index 0000000..11decc7 --- /dev/null +++ b/lib/include/uc2/uc2_sha256.h @@ -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 +#include + +#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 diff --git a/lib/src/uc2_ots.c b/lib/src/uc2_ots.c new file mode 100644 index 0000000..105ebfa --- /dev/null +++ b/lib/src/uc2_ots.c @@ -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 + +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; +} diff --git a/lib/src/uc2_sha256.c b/lib/src/uc2_sha256.c new file mode 100644 index 0000000..1964682 --- /dev/null +++ b/lib/src/uc2_sha256.c @@ -0,0 +1,131 @@ +/* SHA-256 (FIPS 180-4). Reference textbook implementation. */ + +#include "uc2/uc2_sha256.h" +#include + +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); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 072ffe9..ae08797 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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 + $ + ${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 diff --git a/tests/scripts/cross_check_ots.py b/tests/scripts/cross_check_ots.py new file mode 100644 index 0000000..82d7427 --- /dev/null +++ b/tests/scripts/cross_check_ots.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Cross-check uc2 OTS output against the python-opentimestamps reference. + +Usage: cross_check_ots.py + +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 ", 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()) diff --git a/tests/src/test_ots.c b/tests/src/test_ots.c new file mode 100644 index 0000000..ab5a862 --- /dev/null +++ b/tests/src/test_ots.c @@ -0,0 +1,338 @@ +/* Tests for the OpenTimestamps integration. */ + +#include +#include +#include +#include +#include +#include + +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 . + * 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; +} diff --git a/tests/src/test_sha256.c b/tests/src/test_sha256.c new file mode 100644 index 0000000..765e95c --- /dev/null +++ b/tests/src/test_sha256.c @@ -0,0 +1,112 @@ +/* Tests for SHA-256 (FIPS 180-4 vectors). */ + +#include +#include +#include +#include +#include + +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; +}