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
- Variables
- func BuildCompactProofPayload(version byte, typeCode uint16, keyIDHex string, timestampMs uint64, ...) ([]byte, error)
- func BytesEqual(a, b []byte) bool
- func BytesToHex(b []byte) string
- func ComputeBlockHash(id, prevHashHex, merkleRootHex, metadataHashHex, signingKeyIDHex string) (string, error)
- func ComputeEntropyHash(jcsBytes []byte) string
- func ComputeItemHash(id, claimsHashHex, metadataHashHex, signingKeyIDHex string) (string, error)
- func ComputeKeyID(pubkey []byte) string
- func ComputeObservationHash(id, entropyHashHex, metadataHashHex, signingKeyIDHex string) (string, error)
- func DecodeCompactMerkleProof(base64urlProof string) ([]string, error)
- func DecodePublicKey(b64 string) (ed25519.PublicKey, error)
- func DomainHash(prefix byte, data []byte) []byte
- func ExtractULIDTimestamp(id string) (time.Time, error)
- func ExtractUUIDv7Timestamp(id string) (time.Time, error)
- func FormatBlockTime(blockID string) string
- func FormatItemTime(itemID string) string
- func HexEqual(a, b string) bool
- func HexToBytes(h string) ([]byte, error)
- func ValidateClaimsHash(hash, hashType string) error
- func ValidateLowercaseHex(s string) error
- func VerifyEd25519(hashBytes []byte, signatureB64 string, pubkey ed25519.PublicKey) (bool, error)
- func VerifyMerkleProof(itemHashHex string, proof []string, expectedRootHex string) (bool, error)
Constants ¶
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 ¶
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
BytesEqual reports whether two byte slices are equal, in constant time.
func BytesToHex ¶
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 ¶
ComputeEntropyHash computes SHA256(0x21 || JCS(entropy_data)).
func ComputeItemHash ¶
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 ¶
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 ¶
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 ¶
DecodePublicKey decodes a base64-encoded Ed25519 public key.
func DomainHash ¶
DomainHash computes SHA256(prefix_byte || data).
func ExtractULIDTimestamp ¶
ExtractULIDTimestamp extracts the millisecond timestamp from a ULID string.
func ExtractUUIDv7Timestamp ¶
ExtractUUIDv7Timestamp extracts the millisecond timestamp from a UUIDv7 string.
func FormatBlockTime ¶
FormatBlockTime extracts and formats the timestamp from a UUIDv7 block ID. Returns "unknown" if extraction fails.
func FormatItemTime ¶
FormatItemTime extracts and formats the timestamp from a ULID item ID. Returns "unknown" if extraction fails.
func HexEqual ¶
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 ¶
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 ¶
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
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 ¶
VerifyEd25519 verifies an Ed25519 signature over a hash.
Types ¶
This section is empty.