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 ComputeBlockMetadataHash(jcsBytes []byte) string
- func ComputeClaimsHash(jcsBytes []byte) string
- func ComputeEntropyHash(jcsBytes []byte) string
- func ComputeEntropyMetadataHash(jcsBytes []byte) string
- func ComputeItemHash(id, claimsHashHex, metadataHashHex, signingKeyIDHex string) (string, error)
- func ComputeItemMetadataHash(jcsBytes []byte) string
- 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 SecureEqual(a, b string) bool
- 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)
- func WalkMerkleProof(leafHashHex string, proof []string) (string, 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 verifier recomputes a bundle's hashes under. Since the published format carries every metadata map rather than its digest, the metadata prefixes (0x12, 0x22, 0x33) are computed by a verifier too. The commitment-record prefixes (0x34, 0x35) are kept so the frozen numbering stays legible; a verifier never recomputes them. 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 `public_key`, that is ComputeKeyID(public_key), and MUST NOT be read from any `signing_key_id` the bundle carries. Appendix E.9 of the whitepaper is explicit about the split: this slot and the E.17 key binding take the derived value, while the E.10 subject-hash and E.14 block-hash preimages take the bundle's stored `signing_key_id` values 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 ids differ from the derived one, and feeding a stored id in here makes every rotated proof fail signature verification.
For block-like subjects (block and beacon), subject_hash == block_hash: the same 32 bytes appear in both slots. The type code in the payload domain-separates block (10) and beacon (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
The metadata hash is DERIVED by the caller from the carried metadata map (SHA-256(0x33 || JCS(metadata)), see [BlockMetadataHash]); a bundle never carries it. The same procedure hashes the top-level block, every block_path entry, the head block carried as the block witness, and the signing key event's block (Appendix E.14).
Decode failures name the field that refused; see ComputeObservationHash.
func ComputeBlockMetadataHash ¶ added in v0.13.0
ComputeBlockMetadataHash computes SHA256(0x33 || JCS(block.metadata)) for any block map (E.14).
func ComputeClaimsHash ¶ added in v0.13.0
ComputeClaimsHash computes SHA256(0x11 || JCS(subject.claims)).
func ComputeEntropyHash ¶
ComputeEntropyHash computes SHA256(0x21 || JCS(payload)). The same derivation hashes an entropy subject's `subject.entropy` and every entropy witness payload carried under `subject.witnesses` (E.17a).
func ComputeEntropyMetadataHash ¶
ComputeEntropyMetadataHash computes SHA256(0x22 || JCS(subject.metadata)) for an entropy subject (E.10). The carried map is always empty, but the hash is derived from it rather than substituted, so a non-empty map is caught.
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 field that refused; see ComputeObservationHash.
func ComputeItemMetadataHash ¶ added in v0.13.0
ComputeItemMetadataHash computes SHA256(0x12 || JCS(subject.metadata)) for an item: the derived timing fingerprint over the witness map the bundle carries (E.10).
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 field that refused (`signing_key_id`, `metadata_hash`), and the caller adds which map it belongs to: the same procedure runs over the subject and over every block map, and only the caller knows which one it is grading.
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 merkle_root or epoch_merkle_root 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 SecureEqual ¶ added in v0.13.0
SecureEqual reports whether two strings are byte-for-byte equal, in constant time for equal-length inputs. It is the comparison Appendix E.4 requires for every hash and digest a verifier derives against one the bundle carries: exact, with no case folding, so a mis-cased wire value never compares equal to a derived one (the E.4 sweep names the encoding defect; this refuses to paper over it).
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 a signing_key_id of "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.
func VerifyMerkleProof ¶
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.
func WalkMerkleProof ¶ added in v0.13.0
WalkMerkleProof walks a decoded proof from a leaf value up to the root it implies (Appendix E.13): the leaf is hashed under 0x00, then each sibling is folded in on the side its direction names under 0x01. Nothing here trusts the proof; the caller compares the derived root against the one the bundle claims. Proof elements are in "l:hex" / "r:hex" form as returned by DecodeCompactMerkleProof.
Types ¶
This section is empty.