cadrumo.adapters.persistence.storage.sql.secure_object_crypto module¶
Cryptographic hash helpers for SQL secure object revision metadata.
- derive_revision_id(*, namespace, object_key, schema_version, written_at, payload_hash, ciphertext_hash, previous_revision_id, previous_payload_hash)[source]¶
Derive a deterministic secure-object revision id from row metadata.
- verify_revision_self_consistency(revision_id, *, namespace, object_key, schema_version, written_at, previous_revision_id, payload_hash, ciphertext_hash, previous_payload_hash)[source]¶
Return whether a stored
revision_idrecomputes from its lineage columns.The revision id is a content address over exactly the eight inputs
derive_revision_id()mixes: namespace, object-key digest, schema version,written_at,payload_hash,ciphertext_hash, and the previous revision/payload-hash links. Recomputing it from the stored columns and comparing to the storedrevision_idis a read-time integrity gate: a tamper of any column in THAT tuple — includingpayload_hash— that does not also recomputerevision_idis detected and can be failed closed.The coverage stops at that tuple, and the stamping write is wider than it. The repository’s write funnel also stamps
revision_ancestor_ids,revision_written_at,write_provenance,source_event_id, andconflict_policy, none of which is a derivation input; a row whose stored value for any of them is edited recomputes the samerevision_idand is admitted here.revision_ancestor_idsis the sharp case, because it is the ancestry chain itself: only its head, the directprevious_revision_idlink, is mixed, so this gate pins the parent edge and not the chain behind it.revision_written_atis a second copy of the already-mixedwritten_at; this gate reads the latter and never the former. What the remaining columns are worth protecting FROM is a question about their consumers, which this module cannot see and does not assert. Bringing any of them under the digest would restamp every storedrevision_id, which is a persisted-format decision rather than a local fix, so the honest statement of today’s guarantee is the eight-input tuple named above.The check is purely metadata-internal (it never touches the encrypted payload bytes), so it does not interfere with the AEAD payload authentication or with corruption probes that re-encrypt a mutated payload without restamping the metadata. It is not redundant with that authentication either: the payload associated data binds namespace, object-key digest, and schema version, so
written_at, the two content hashes, and both previous-revision links are covered here and nowhere else.A row carrying no
revision_idis REFUSED, not tolerated. Every write stamps revision metadata inside the same transaction that inserts the ciphertext, so a committed row without one is corruption now rather than an older shape this application ever wrote; the pre-release compatibility regime carries no read path for shapes nothing released produced. Refusing here is also what keeps this gate the single authority on the question:SecureObjectRecordtypesrevision_idas an exactly-64-character digest, so a row this function admitted with no revision id could only ever fail again — untyped, and past the gate — while the record was being built. A row whoserevision_idis present but whose required hash inputs are absent is likewise inconsistent.Returning
Truenarrowsrevision_idtostrfor the caller, which is precisely the guarantee the record contract needs. The narrowing is aTypeGuardrather than aTypeIsbecauseFalsecarries no type claim at all: a genuinestrwhose lineage fails to recompute is refused too.