Skip to content

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 Z with 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?} where content_hash is sha256: over the canonical bytes of the complete stored record (log_entry).
  • Leaf hash (RFC 9162 §2.1.1): SHA-256(0x00 || leaf_input) with leaf_input = domain_tag || 0x0A || canonical_json(log_entry) (domain separation, §18). Verification is two-step: the record hashes to the entry’s content_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-256 commitment, 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).

class

class MerkleLog

In-memory append-only log (leaf hashes in hex). Core persists leaves in SQLite.

Fields

NameTypeDefaultDescription
leaveslist[str]list(leaves or [])
entrieslist[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) -> None

size property

size: int

append_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) -> int

root method

def root(self, size: int | None = None) -> str

inclusion_proof method

def inclusion_proof(self, index: int, size: int | None = None) -> InclusionProof

consistency_proof method

def consistency_proof(
self,
first_size: int,
second_size: int | None = None,
) -> ConsistencyProof

Source: packages/ump-spec/src/ump_spec/integrity.py, line 393

function

def canonical_json(obj: Any) -> str

RFC 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

function

def sha256_hex(data: bytes | str) -> str

Source: packages/ump-spec/src/ump_spec/integrity.py, line 165

function

def content_hash(obj: Any) -> str

sha256:<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

function

def artifact_content_hash(body: Mapping[str, Any] | BaseModel) -> str

Spec §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

function

def leaf_input(domain_tag: str, record: Any) -> bytes

Source: packages/ump-spec/src/ump_spec/integrity.py, line 197

function

def leaf_hash_bytes(data: bytes) -> bytes

Source: packages/ump-spec/src/ump_spec/integrity.py, line 201

function

def leaf_hash(domain_tag: str, record: Any) -> str

Hex leaf hash of a record under a domain tag.

Source: packages/ump-spec/src/ump_spec/integrity.py, line 205

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

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

function

def node_hash(left: bytes, right: bytes) -> bytes

Source: packages/ump-spec/src/ump_spec/integrity.py, line 250

function

def merkle_root(leaf_hashes: Sequence[str]) -> str

Root (hex) over leaf hashes (hex).

Source: packages/ump-spec/src/ump_spec/integrity.py, line 279

function

def inclusion_proof(
leaf_hashes: Sequence[str],
index: int,
tree_size: int | None = None,
) -> InclusionProof

Source: packages/ump-spec/src/ump_spec/integrity.py, line 294

function

def verify_inclusion(
leaf_hash_hex: str,
index: int,
tree_size: int,
audit_path: Sequence[str],
root_hex: str,
) -> bool

RFC 9162 §2.1.3.2.

Source: packages/ump-spec/src/ump_spec/integrity.py, line 303

function

def verify_inclusion_proof(proof: InclusionProof, root_hex: str | None = None) -> bool

Source: packages/ump-spec/src/ump_spec/integrity.py, line 327

function

def consistency_proof(
leaf_hashes: Sequence[str],
first_size: int,
second_size: int | None = None,
) -> ConsistencyProof

Source: packages/ump-spec/src/ump_spec/integrity.py, line 342

function

def verify_consistency(
first_size: int,
second_size: int,
first_root: str,
second_root: str,
proof: Sequence[str],
) -> bool

RFC 9162 §2.1.4.2.

Source: packages/ump-spec/src/ump_spec/integrity.py, line 353

function

def verify_consistency_proof(p: ConsistencyProof) -> bool

Source: packages/ump-spec/src/ump_spec/integrity.py, line 389

function

def verify_record_inclusion(
domain_tag: str,
record: Any,
proof: InclusionProof,
root_hex: str | None = None,
*,
entry: Mapping[str, Any] | None,
) -> bool

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

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

function

def public_key_of(private_b64: str) -> str

Source: packages/ump-spec/src/ump_spec/integrity.py, line 460

function

def sign(private_b64: str, message: bytes | str) -> str

Source: packages/ump-spec/src/ump_spec/integrity.py, line 467

function

def verify(public_b64: str, message: bytes | str, signature_b64: str) -> bool

Source: packages/ump-spec/src/ump_spec/integrity.py, line 474

function

def key_id(name: str, public_b64: str) -> str

C2SP 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

function

def checkpoint_body(cp: Checkpoint) -> str

C2SP 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

function

def sign_checkpoint(cp: Checkpoint, name: str, private_b64: str) -> Checkpoint

Source: packages/ump-spec/src/ump_spec/integrity.py, line 497

function

def verify_checkpoint(cp: Checkpoint, name: str, public_b64: str) -> bool

Source: packages/ump-spec/src/ump_spec/integrity.py, line 505

function

def signed_note_text(cp: Checkpoint) -> str

C2SP 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

function

def first_published_note(cp: Checkpoint) -> bytes

The 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

function

def timestamp_checkpoint(
cp: Checkpoint,
name: str,
private_b64: str,
*,
gen_time: datetime,
nonce: str,
) -> Checkpoint

Stamp 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

function

def verify_checkpoint_timestamp(cp: Checkpoint, public_b64: str) -> bool

The 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

function

def commitment(key: bytes, value: str) -> str

Spec §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

function

def is_commitment(value: Any) -> bool

Source: packages/ump-spec/src/ump_spec/integrity.py, line 588

function

def gate_result_set_hash(results: Sequence[Mapping[str, Any] | BaseModel]) -> str

sha256: 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

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

function

def sign_jws(private_b64: str, payload: Any, *, kid: str) -> str

JWS 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

function

def jws_payload(token: str) -> Any

The decoded payload of a compact JWS, without verifying it (None when malformed).

Source: packages/ump-spec/src/ump_spec/integrity.py, line 640

function

def verify_jws(public_b64: str, token: str) -> Any

The 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

NameTypeValueDescription
DOMAIN_TAGStuple[str, ...]('ump/decision/v1', 'ump/gate/v1', 'ump/run/v1', 'ump/approval/v1', 'ump/vint...
GOVERNANCE_MEMBERStuple[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_MEMBERStuple[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_ROOThashlib.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_MEMBERStuple[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_DECISIONStuple[str, ...]('approve', 'reject')