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:
Eremey Valetov
2026-05-03 12:15:30 -04:00
parent dae8a503e4
commit 5c01fec996
11 changed files with 1717 additions and 5 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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
View 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

View 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
View 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
View 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);
}

View File

@@ -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

View 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
View 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
View 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;
}