tscrypto

package
v0.12.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package tscrypto implements the Truestamp-specific cryptographic primitives used by proofs: SHA-256 with one-byte domain-separation prefixes (see kb/cryptography/byte-prefix-registry.md in truestamp-v2 for the prefix registry) and Ed25519 signature verification.

Index

Constants

View Source
const (
	PrefixMerkleLeaf      = 0x00
	PrefixMerkleInternal  = 0x01
	PrefixItemClaims      = 0x11
	PrefixItemMetadata    = 0x12
	PrefixItemHash        = 0x13
	PrefixEntropy         = 0x21
	PrefixEntropyMetadata = 0x22
	PrefixObservationHash = 0x23
	PrefixBlockMetadata   = 0x33
	PrefixBlockHash       = 0x32
	PrefixCommitmentData  = 0x34
	PrefixCommitmentHash  = 0x35
	PrefixKeyID           = 0x51
	PrefixProofHash       = 0x61
)

Domain separation prefix bytes per kb/cryptography/byte-prefix-registry.md (in truestamp-v2).

The block carries the Merkle, Items, Entropy, Blockchain, Key-management and Proofs prefixes a reader has to be able to check a proof preimage against, including the producer-side ones a verifier never recomputes (0x12, 0x22, 0x33, 0x34, 0x35) so the frozen numbering stays legible — a constant cannot be called with the wrong arguments, unlike the producer-side hash builders that used to sit alongside it. The registry's remaining prefixes are deliberately omitted as out of scope for proof verification: 0x31 (genesis block constant), 0x41-0x45 (random tools) and 0x53 (pre-rotation commitment).

Variables

View Source
var ErrNotLowercaseHex = errors.New("not lowercase hex (E.4)")

ErrNotLowercaseHex is the sentinel every E.4 encoding refusal wraps, so a caller can tell "this field is not lowercase hex" from any other decode failure and tag its step with E.23's `invalid_hex_encoding` identifier. Appendix E.4 makes carrying that identifier a MUST, and the report layer cannot infer it from an error string without matching prose.

Functions

func BuildCompactProofPayload

func BuildCompactProofPayload(version byte, typeCode uint16, keyIDHex string, timestampMs uint64, subjectHashHex, blockHashHex string, epochRootHexes []string) ([]byte, error)

BuildCompactProofPayload builds the compact proof signature payload and computes SHA256(0x61 || payload). The returned 32-byte hash is what the Ed25519 signature covers.

Byte layout (big-endian throughout):

offset  size  field
0       1     v  (version, uint8)
1       2     t  (type code, uint16 BE)
3       4     kid (4 bytes, hex-decoded)
7       8     ts_ms (timestamp in ms since Unix epoch, uint64 BE)
15      32    subject_hash
47      32    block_hash
79      2     N (epoch root count, uint16 BE)
81      32*N  epoch_roots (concatenated, in cx order)

keyIDHex MUST be the key id DERIVED from the bundle's `pk` — that is, ComputeKeyID(pk) — and MUST NOT be read from `b.kid` or `s.kid`. Appendix E.9 of the whitepaper is explicit about the split: this slot and the E.17 keyring cross-check take the derived value, while the E.10 subject-hash and E.14 block-hash preimages take the bundle's stored `s.kid` / `b.kid` verbatim, because those composites were hashed at creation time with the then-current key and are frozen into the Merkle tree. Under legitimate key rotation the stored kids differ from the derived one, and feeding a stored kid in here makes every rotated proof fail signature verification.

For block-like subjects (t ∈ {10, 11} — plain block and beacon), subject_hash == block_hash — the same 32 bytes appear in both slots. The `t` byte in the payload domain-separates block (t=10) and beacon (t=11) signatures for the same underlying block.

func BytesEqual added in v0.12.0

func BytesEqual(a, b []byte) bool

BytesEqual reports whether two byte slices are equal, in constant time.

func BytesToHex

func BytesToHex(b []byte) string

BytesToHex encodes bytes to lowercase hex string.

func ComputeBlockHash

func ComputeBlockHash(id, prevHashHex, merkleRootHex, metadataHashHex, signingKeyIDHex string) (string, error)

ComputeBlockHash computes the length-prefixed block hash with domain prefix 0x32. Field order: id, previous_block_hash, merkle_root, metadata_hash, signing_key_id

Decode failures name the wire key; see ComputeObservationHash.

func ComputeEntropyHash

func ComputeEntropyHash(jcsBytes []byte) string

ComputeEntropyHash computes SHA256(0x21 || JCS(entropy_data)).

func ComputeItemHash

func ComputeItemHash(id, claimsHashHex, metadataHashHex, signingKeyIDHex string) (string, error)

ComputeItemHash computes the length-prefixed item hash with domain prefix 0x13. Field order: id, claims_hash, metadata_hash, signing_key_id

Decode failures name the wire key; see ComputeObservationHash.

func ComputeKeyID

func ComputeKeyID(pubkey []byte) string

ComputeKeyID derives key_id from public key: truncate4(SHA256(0x51 || pubkey)).

func ComputeObservationHash

func ComputeObservationHash(id, entropyHashHex, metadataHashHex, signingKeyIDHex string) (string, error)

ComputeObservationHash computes the length-prefixed observation hash with domain prefix 0x23. Field order: id, entropy_hash, metadata_hash, signing_key_id This mirrors ComputeItemHash but uses prefix 0x23 for entropy observations.

Decode failures name the WIRE key (`s.mh`, `s.kid`), not the parameter, because E.4 requires an encoding failure to name the offending field and only the wire key is actionable: `metadata_hash` alone does not say whether the subject's or the block's is at fault, and the two produce identically worded rows under different groups. The derived inputs keep their descriptive names — no wire field carries them.

func DecodeCompactMerkleProof

func DecodeCompactMerkleProof(base64urlProof string) ([]string, error)

DecodeCompactMerkleProof decodes a compact base64url-encoded Merkle proof into the standard ["l:hex", "r:hex", ...] format.

Binary format:

  • Byte 0: depth (number of proof steps, 0-64)
  • Next ceil(depth/8) bytes: direction bitfield (little-endian) bit=0 means left sibling ("l:"), bit=1 means right sibling ("r:")
  • Remaining: depth * 32 bytes of raw sibling hashes

func DecodePublicKey

func DecodePublicKey(b64 string) (ed25519.PublicKey, error)

DecodePublicKey decodes a base64-encoded Ed25519 public key.

func DomainHash

func DomainHash(prefix byte, data []byte) []byte

DomainHash computes SHA256(prefix_byte || data).

func ExtractULIDTimestamp

func ExtractULIDTimestamp(id string) (time.Time, error)

ExtractULIDTimestamp extracts the millisecond timestamp from a ULID string.

func ExtractUUIDv7Timestamp

func ExtractUUIDv7Timestamp(id string) (time.Time, error)

ExtractUUIDv7Timestamp extracts the millisecond timestamp from a UUIDv7 string.

func FormatBlockTime

func FormatBlockTime(blockID string) string

FormatBlockTime extracts and formats the timestamp from a UUIDv7 block ID. Returns "unknown" if extraction fails.

func FormatItemTime

func FormatItemTime(itemID string) string

FormatItemTime extracts and formats the timestamp from a ULID item ID. Returns "unknown" if extraction fails.

func HexEqual

func HexEqual(a, b string) bool

HexEqual reports whether two hex strings are equal, ignoring ASCII case in the range 'A'-'F'.

The comparison is constant-time in the CONTENTS of the operands: every byte is folded and accumulated, and only the length short-circuits (a length difference is not secret). Appendix E.4 of the whitepaper makes this a MUST for all hash and digest comparisons, restated for the E.7 hash comparison and the E.13 inclusion-proof root.

Nothing is hex-decoded, so non-hex and odd-length operands compare exactly as they always have, on hex TEXT rather than decoded bytes — which is what the reference verifier's secure_equal?/2 compares too.

The case fold is NOT shared with the reference verifier, and keeping it is a deliberate split rather than an oversight. secure_equal?/2 is a raw binary compare; the reference downcases only at its one caller-supplied operand, the expected hash. This function's live call sites are the two places where a case fold is required rather than merely tolerated:

  • E.7's expected-hash comparison, which the appendix instructs a verifier to normalize ("trim, downcase") before comparing against s.d.hash, and which the reference implements the same way;
  • the E.21 and E.18/E.19 comparisons against a value fetched from an outside service, where the remote party chooses the case. The NIST beacon API emits its outputValue in uppercase, so a case-sensitive compare there would grade a sound entropy proof as a value mismatch.

Bundle-carried hex no longer needs the fold, because ValidateLowercaseHex and HexToBytes reject a non-lowercase field before any comparison reaches it. The fold is therefore not what lets an uppercase b.mr or cx[].memo through; enforcement happens upstream, and by the time a bundle value gets here it is already known to be lowercase.

func HexToBytes

func HexToBytes(h string) ([]byte, error)

HexToBytes decodes a lowercase-hex string to bytes. Returns empty slice for empty input.

This is the decoder every preimage builder in this package runs its hex inputs through, and E.4's lowercase rule is enforced here rather than at parse time on purpose. E.6's hard-rejection table is exhaustive and carries no row for hex case, so aborting the whole run would invent a rejection the appendix does not authorize; blanking the field instead would make the report say a present field is absent. Failing the decode leaves it a graded step failure that names the field, which is what a bundle carrying an unusable value already gets.

func ValidateClaimsHash

func ValidateClaimsHash(hash, hashType string) error

ValidateClaimsHash checks that a claimed hash has both the length AND the lowercase-hex character set required by its named hash type.

Appendix E.11 of the whitepaper: "check that the hex length equals twice the algorithm's output size and that the character set is lowercase hex, for one of the twelve registered algorithms". Callers MUST render a non-nil error as a warn and MUST NOT fail on it — every E.11 soft check is advisory.

The scan is a byte loop, not a rune loop: a multi-byte UTF-8 hash trips the length check first rather than the charset check, but either way the caller warns, which is all E.11 constrains.

func ValidateLowercaseHex added in v0.12.0

func ValidateLowercaseHex(s string) error

ValidateLowercaseHex reports whether s carries the encoding Appendix E.4 mandates for every hash field: "Encodings: hashes are lowercase hex". It returns nil for a conforming value and, for a non-conforming one, an error naming the first offending byte and its offset so a report can say which character is wrong rather than only that the field "is invalid".

Empty is accepted. An absent field is not an encoding defect, and the steps that need one already grade its absence on their own terms (E.10's and E.14's "no usable value" arms); reporting "" here would claim the bundle carries a malformed value where it carries none.

Uppercase is called out separately from a non-hex byte because the two are different mistakes with different fixes, and because uppercase is the one that silently verified before this check existed: Go's hex.DecodeString is case-insensitive, so `b.kid` = "F2C39DF9" decoded to the same four bytes as "f2c39df9" and every derivation downstream agreed. The reference verifier's Base.decode16lower!/1 does not, which made uppercase an interoperability break as well as a malleability one — the same wire bundle verified here and aborted there.

func VerifyEd25519

func VerifyEd25519(hashBytes []byte, signatureB64 string, pubkey ed25519.PublicKey) (bool, error)

VerifyEd25519 verifies an Ed25519 signature over a hash.

func VerifyMerkleProof

func VerifyMerkleProof(itemHashHex string, proof []string, expectedRootHex string) (bool, error)

VerifyMerkleProof walks an RFC 6962 Merkle proof from leaf to root. Proof elements are in "l:hex" or "r:hex" format. Returns true if the computed root matches expectedRootHex.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL