ump_spec.integrity
module ump-spec
Record integrity (spec §18): canonical JSON, RFC 9162 Merkle tree, proofs, Ed25519 checkpoints.
- Canonical JSON: RFC 8785 (JCS): members sorted by UTF-16 code units, ECMAScript number form, no
whitespace, UTF-8; null members omitted, instants as UTC
Zwith milliseconds (spec §4, §18); non-finite numbers and duplicate member names rejected. - Artifact content hash (spec §6):
sha256:<hex>of the canonical bytes of the body without the governance members (GOVERNANCE_MEMBERS). - Log entry (§18):
{record_id, kind, content_hash, key_version, signed_at, subject_ref?}wherecontent_hashissha256:over the canonical bytes of the complete stored record (log_entry). - Leaf hash (RFC 9162 §2.1.1):
SHA-256(0x00 || leaf_input)withleaf_input = domain_tag || 0x0A || canonical_json(log_entry)(domain separation, §18). Verification is two-step: the record hashes to the entry’scontent_hash, and the entry hashes to the proven leaf. - Node hash:
SHA-256(0x01 || left || right). - Inclusion and consistency proofs follow RFC 9162 §2.1.3 and §2.1.4 exactly.
- Checkpoints: C2SP tlog-checkpoint body (origin, decimal size, base64 root; no time line) signed as a C2SP
signed note with Ed25519. The checkpoint’s time is the genTime of an RFC 3161 token held beside it
(
timestamp_checkpoint), whose imprint is SHA-256 over the note as first published by the log. The POC holds keys locally (tsa: poc-standin); production uses Cloud KMS plus a real TSA. - Approval statement (§18):
approval_statement(approver as an HMAC-SHA-256commitment, artifact id, revision, content hash,gate_result_set_hash, acknowledged codes, decision, time, key version), signed as a compact EdDSA JWS over its RFC 8785 bytes (sign_jws,verify_jws).
Classes
Section titled “Classes”MerkleLog
Section titled “MerkleLog”class
class MerkleLogIn-memory append-only log (leaf hashes in hex). Core persists leaves in SQLite.
Fields
| Name | Type | Default | Description |
|---|---|---|---|
leaves | list[str] | list(leaves or []) | |
entries | list[dict[str, Any] | None] | [None] * len(self.leaves) | The log entry of each leaf appended with append_entry (None for raw leaves). |
Methods and properties (8)
init constructor
MerkleLog(self, leaves: Sequence[str] | None = None) -> Nonesize property
size: intappend_record method
def append_record(self, domain_tag: str, record: Any) -> tuple[int, str]Raw leaf over record itself (a teaching primitive). UMP records go through append_entry.
append_entry method
def append_entry( self, domain_tag: str, record: Any, *, record_id: str, kind: str, key_version: str, signed_at: datetime | str, subject_ref: str | None = None,) -> tuple[int, str, dict[str, Any]]Spec §18: seal record as a log entry; returns (index, leaf hash, entry).
append_hash method
def append_hash(self, h: str) -> introot method
def root(self, size: int | None = None) -> strinclusion_proof method
def inclusion_proof(self, index: int, size: int | None = None) -> InclusionProofconsistency_proof method
def consistency_proof( self, first_size: int, second_size: int | None = None,) -> ConsistencyProofSource: packages/ump-spec/src/ump_spec/integrity.py, line 393
Functions
Section titled “Functions”canonical_json
Section titled “canonical_json”function
def canonical_json(obj: Any) -> strRFC 8785 (JCS) text, spec §18: members sorted by UTF-16 code units, ECMAScript number form, no
whitespace, null members omitted, instants as UTC Z with milliseconds; non-finite numbers and
duplicate member names are refused (ValueError). Encode as UTF-8 for the canonical bytes.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 158
sha256_hex
Section titled “sha256_hex”function
def sha256_hex(data: bytes | str) -> strSource: packages/ump-spec/src/ump_spec/integrity.py, line 165
content_hash
Section titled “content_hash”function
def content_hash(obj: Any) -> strsha256:<hex> of the canonical bytes of obj (all of it; see artifact_content_hash).
Source: packages/ump-spec/src/ump_spec/integrity.py, line 171
artifact_content_hash
Section titled “artifact_content_hash”function
def artifact_content_hash(body: Mapping[str, Any] | BaseModel) -> strSpec §6 content address of a JSON artifact: sha256:<hex> of the canonical bytes of its body,
excluding GOVERNANCE_MEMBERS, so a state change or approval never changes the content identity.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 181
leaf_input
Section titled “leaf_input”function
def leaf_input(domain_tag: str, record: Any) -> bytesSource: packages/ump-spec/src/ump_spec/integrity.py, line 197
leaf_hash_bytes
Section titled “leaf_hash_bytes”function
def leaf_hash_bytes(data: bytes) -> bytesSource: packages/ump-spec/src/ump_spec/integrity.py, line 201
leaf_hash
Section titled “leaf_hash”function
def leaf_hash(domain_tag: str, record: Any) -> strHex leaf hash of a record under a domain tag.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 205
log_entry
Section titled “log_entry”function
def log_entry( record: Any, *, record_id: str, kind: str, key_version: str, signed_at: datetime | str, subject_ref: str | None = None,) -> dict[str, Any]Spec §18 log entry for record as a JSON object ready to canonicalise. content_hash covers the
complete stored record (governance members included); signed_at is written per the time profile.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 214
entry_problems
Section titled “entry_problems”function
def entry_problems( domain_tag: str, record: Any, entry: Any, leaf_hash_hex: str,) -> list[str]Two-step verification (spec §18), as plain problems (empty when it verifies): the entry carries the spec
members, the record hashes to the entry’s content_hash, and the entry under domain_tag hashes to
leaf_hash_hex. Never raises for bad input; an uncanonicalisable value is a problem.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 225
node_hash
Section titled “node_hash”function
def node_hash(left: bytes, right: bytes) -> bytesSource: packages/ump-spec/src/ump_spec/integrity.py, line 250
merkle_root
Section titled “merkle_root”function
def merkle_root(leaf_hashes: Sequence[str]) -> strRoot (hex) over leaf hashes (hex).
Source: packages/ump-spec/src/ump_spec/integrity.py, line 279
inclusion_proof
Section titled “inclusion_proof”function
def inclusion_proof( leaf_hashes: Sequence[str], index: int, tree_size: int | None = None,) -> InclusionProofSource: packages/ump-spec/src/ump_spec/integrity.py, line 294
verify_inclusion
Section titled “verify_inclusion”function
def verify_inclusion( leaf_hash_hex: str, index: int, tree_size: int, audit_path: Sequence[str], root_hex: str,) -> boolRFC 9162 §2.1.3.2.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 303
verify_inclusion_proof
Section titled “verify_inclusion_proof”function
def verify_inclusion_proof(proof: InclusionProof, root_hex: str | None = None) -> boolSource: packages/ump-spec/src/ump_spec/integrity.py, line 327
consistency_proof
Section titled “consistency_proof”function
def consistency_proof( leaf_hashes: Sequence[str], first_size: int, second_size: int | None = None,) -> ConsistencyProofSource: packages/ump-spec/src/ump_spec/integrity.py, line 342
verify_consistency
Section titled “verify_consistency”function
def verify_consistency( first_size: int, second_size: int, first_root: str, second_root: str, proof: Sequence[str],) -> boolRFC 9162 §2.1.4.2.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 353
verify_consistency_proof
Section titled “verify_consistency_proof”function
def verify_consistency_proof(p: ConsistencyProof) -> boolSource: packages/ump-spec/src/ump_spec/integrity.py, line 389
verify_record_inclusion
Section titled “verify_record_inclusion”function
def verify_record_inclusion( domain_tag: str, record: Any, proof: InclusionProof, root_hex: str | None = None, *, entry: Mapping[str, Any] | None,) -> boolTwo-step check of a sealed record (spec §18, G-INT-03): the record hashes to entry.content_hash, the
entry hashes to the proven leaf, and the audit path folds to the root. Without an entry it is False.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 438
generate_keypair
Section titled “generate_keypair”function
def generate_keypair() -> tuple[str, str]Return (private_key_b64, public_key_b64) raw 32-byte Ed25519 keys.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 450
public_key_of
Section titled “public_key_of”function
def public_key_of(private_b64: str) -> strSource: packages/ump-spec/src/ump_spec/integrity.py, line 460
function
def sign(private_b64: str, message: bytes | str) -> strSource: packages/ump-spec/src/ump_spec/integrity.py, line 467
verify
Section titled “verify”function
def verify(public_b64: str, message: bytes | str, signature_b64: str) -> boolSource: packages/ump-spec/src/ump_spec/integrity.py, line 474
key_id
Section titled “key_id”function
def key_id(name: str, public_b64: str) -> strC2SP signed-note key hash: first 4 bytes of SHA-256(name || 0x0A || 0x01 || pubkey), hex.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 485
checkpoint_body
Section titled “checkpoint_body”function
def checkpoint_body(cp: Checkpoint) -> strC2SP tlog-checkpoint body: origin, decimal tree size and base64 root hash, each on a line. No time line: the time is the genTime of the RFC 3161 token held beside the checkpoint (spec §18).
Source: packages/ump-spec/src/ump_spec/integrity.py, line 490
sign_checkpoint
Section titled “sign_checkpoint”function
def sign_checkpoint(cp: Checkpoint, name: str, private_b64: str) -> CheckpointSource: packages/ump-spec/src/ump_spec/integrity.py, line 497
verify_checkpoint
Section titled “verify_checkpoint”function
def verify_checkpoint(cp: Checkpoint, name: str, public_b64: str) -> boolSource: packages/ump-spec/src/ump_spec/integrity.py, line 505
signed_note_text
Section titled “signed_note_text”function
def signed_note_text(cp: Checkpoint) -> strC2SP signed note: the body, one blank line, then one signature line per signer (U+2014, name, base64 of key hash and signature).
Source: packages/ump-spec/src/ump_spec/integrity.py, line 519
first_published_note
Section titled “first_published_note”function
def first_published_note(cp: Checkpoint) -> bytesThe note bytes as first published by the log: body, blank line and the log’s own signature lines (signer name equal to the origin), before any cosignature. The RFC 3161 imprint covers these bytes (spec §18).
Source: packages/ump-spec/src/ump_spec/integrity.py, line 525
timestamp_checkpoint
Section titled “timestamp_checkpoint”function
def timestamp_checkpoint( cp: Checkpoint, name: str, private_b64: str, *, gen_time: datetime, nonce: str,) -> CheckpointStamp a log-signed checkpoint with an RFC 3161 stand-in token held beside it (tsa: poc-standin):
imprint SHA-256 over first_published_note, a fresh nonce, certReq true. The checkpoint’s timestamp
becomes the token’s genTime (spec §18).
Source: packages/ump-spec/src/ump_spec/integrity.py, line 536
verify_checkpoint_timestamp
Section titled “verify_checkpoint_timestamp”function
def verify_checkpoint_timestamp(cp: Checkpoint, public_b64: str) -> boolThe token held beside cp verifies under the TSA key, its imprint matches the note as first published,
and the checkpoint’s time is the token’s genTime.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 551
commitment
Section titled “commitment”function
def commitment(key: bytes, value: str) -> strSpec §18 Erasure: the commitment that replaces a personal field before sealing,
hmac-sha256:<hex> of HMAC-SHA-256(k_subject, value). The per-subject key and the plaintext stay outside
the log; destroying the key leaves the commitment unopenable while every hash still verifies.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 579
is_commitment
Section titled “is_commitment”function
def is_commitment(value: Any) -> boolSource: packages/ump-spec/src/ump_spec/integrity.py, line 588
gate_result_set_hash
Section titled “gate_result_set_hash”function
def gate_result_set_hash(results: Sequence[Mapping[str, Any] | BaseModel]) -> strsha256: over the canonical bytes of the gate result set the approver saw, as a set: each result is
canonicalised whole and the list is sorted by those bytes, so storage order never changes the hash.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 593
approval_statement
Section titled “approval_statement”function
def approval_statement( *, approver_commitment: str, artifact_id: str, revision: int, content_hash: str, gate_result_set_hash: str, acknowledged_codes: Sequence[str], decision: str, time: datetime | str, key_version: str, auth_event_id: str | None = None,) -> dict[str, Any]Spec §18 approval statement as a JSON object ready to sign with sign_jws. The approver appears only as
a commitment (commitment); acknowledged fallback codes are a sorted set; time follows the time profile.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 600
sign_jws
Section titled “sign_jws”function
def sign_jws(private_b64: str, payload: Any, *, kid: str) -> strJWS compact serialisation (RFC 7515) with alg: EdDSA (RFC 8037, Ed25519) whose payload is the RFC 8785
bytes of payload (spec §18). kid names the signing key version. Stand-in for KMS asymmetricSign.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 631
jws_payload
Section titled “jws_payload”function
def jws_payload(token: str) -> AnyThe decoded payload of a compact JWS, without verifying it (None when malformed).
Source: packages/ump-spec/src/ump_spec/integrity.py, line 640
verify_jws
Section titled “verify_jws”function
def verify_jws(public_b64: str, token: str) -> AnyThe payload of a compact EdDSA JWS when the signature verifies under public_b64 and the payload bytes are
exactly its RFC 8785 form (spec §18 signs the canonical bytes); None otherwise. Never raises.
Source: packages/ump-spec/src/ump_spec/integrity.py, line 648
Constants
Section titled “Constants”| Name | Type | Value | Description |
|---|---|---|---|
DOMAIN_TAGS | tuple[str, ...] | ('ump/decision/v1', 'ump/gate/v1', 'ump/run/v1', 'ump/approval/v1', 'ump/vint... | |
GOVERNANCE_MEMBERS | tuple[str, ...] | ('id', 'revision', 'state', 'owner', 'submitted_by', 'approved_by', 'gate_res... | Spec §6: members of an artifact body that the artifact content hash leaves out. |
LOG_ENTRY_MEMBERS | tuple[str, ...] | ('record_id', 'kind', 'content_hash', 'key_version', 'signed_at') | Spec §18: the members every log entry carries (subject_ref is added only for a record about a person). |
EMPTY_ROOT | hashlib.sha256(b'').hexdigest() | ||
APPROVAL_TAG | 'ump/approval/v1' | ||
KEY_MANIFEST_TAG | 'ump/key-manifest/v1' | Spec §18 Log keys: the key manifest leaf (provisional tag). | |
TOMBSTONE_TAG | 'ump/tombstone/v1' | Spec §18 Erasure: the tombstone leaf naming the affected record ids (provisional tag). | |
COMMITMENT_PREFIX | 'hmac-sha256:' | ||
APPROVAL_STATEMENT_MEMBERS | tuple[str, ...] | ('approver_commitment', 'artifact_id', 'revision', 'content_hash', 'gate_resu... | Spec §18 Approval statement members. auth_event_id appears only when a step-up authentication event was given (required at the Full materiality tier by the spec; the POC has no step-up, see core’s gap table). |
APPROVAL_DECISIONS | tuple[str, ...] | ('approve', 'reject') |