Skip to content

Integrity Verification

PAM provides three levels of integrity verification: per-memory content hashes, file-level integrity blocks, and optional cryptographic signatures.

Every memory includes a content_hash field containing the SHA-256 hash of its normalized content.

  1. Take the content field value
  2. Normalize it per spec §6:
  • Trim leading/trailing whitespace
  • Convert to lowercase
  • Apply Unicode NFC normalization
  • Collapse consecutive whitespace to single spaces
  1. Compute SHA-256 hex digest of the UTF-8 encoded result
  2. Compare with the stored content_hash (format: sha256:<hex>)

Content: "User is a cloud infrastructure engineer"

After normalization: "user is a cloud infrastructure engineer"

Expected hash: sha256:e1bae3ec291c99eced01fc91b4152a0cef541fccf2034fc11b3f90f4e4d79b6e

import hashlib
import unicodedata
def verify_content_hash(content: str, expected_hash: str) -> bool:
text = content.strip().lower()
text = unicodedata.normalize("NFC", text)
text = " ".join(text.split())
computed = f"sha256:{hashlib.sha256(text.encode('utf-8')).hexdigest()}"
return computed == expected_hash

The optional integrity block verifies that the memories array has not been tampered with. The checksum covers only the memories array, not the entire document.

Per spec §15:

  1. Extract the memories array from the document
  2. Sort the memory objects by id field, ascending
  3. Canonicalize the sorted array using RFC 8785 (JSON Canonicalization Scheme)
  4. Compute SHA-256 of the canonical UTF-8 bytes
  5. Compare with integrity.checksum (format: sha256:<hex>)
  6. Verify that integrity.total_memories matches the length of the memories array
import hashlib
from jcs import canonicalize # pip install jcs (RFC 8785 compliant)
def verify_integrity(pam_data: dict) -> bool:
integrity = pam_data.get("integrity")
if not integrity:
return True # no integrity block, nothing to verify
memories = pam_data["memories"]
# check count
if integrity["total_memories"] != len(memories):
return False
# sort by id ascending
sorted_memories = sorted(memories, key=lambda m: m["id"])
# canonicalize with RFC 8785 (JCS)
# Note: json.dumps with sort_keys is NOT equivalent to RFC 8785.
canonical = canonicalize(sorted_memories)
computed = f"sha256:{hashlib.sha256(canonical).hexdigest()}"
return computed == integrity["checksum"]

PAM v1.0 supports optional cryptographic signatures for authenticity verification and tamper detection via the signature block (spec §18).

The signature is computed over a specific payload object, not the integrity checksum alone. The payload contains four fields, canonicalized with RFC 8785:

{
"checksum": "<integrity.checksum>",
"export_date": "<export_date>",
"export_id": "<export_id>",
"owner_id": "<owner.id>"
}

This prevents replay attacks — a valid checksum from one export cannot be reused in a different export with a different ID or date.

AlgorithmNotes
Ed25519Recommended. Fast, small keys, resistant to side-channel attacks
ES256ECDSA with P-256 curve
ES384ECDSA with P-384 curve
RS256RSA with SHA-256
RS384RSA with SHA-384
RS512RSA with SHA-512
  1. Extract the signature block
  2. Reconstruct the payload: {checksum, export_date, export_id, owner_id}
  3. Canonicalize the payload with RFC 8785
  4. Verify the signature using signature.algorithm, signature.public_key, and signature.value (Base64url-encoded per RFC 4648 §5)
  5. If owner.did is present, optionally resolve the DID document and verify that signature.public_key corresponds to a verification method in the document