| Internet-Draft | CPP Core | January 2026 |
| Kamimura | Expires 2 August 2026 | [Page] |
The Content Provenance Profile (CPP) is an open specification for cryptographically verifiable media capture provenance. This document defines the core data model, hashing conventions, Merkle tree construction rules, RFC 3161 Time-Stamp Authority (TSA) anchoring protocol, and offline verification procedures for CPP.¶
CPP enables capture devices to produce tamper-evident provenance records that bind media content to external timestamps via trusted third parties. Unlike self-attestation models, CPP requires independent timestamp verification through RFC 3161 TSA services, providing externally verifiable proof of when media was captured.¶
This specification focuses on the interoperable core of CPP: the data structures, cryptographic operations, and verification algorithms necessary for independent third-party verification. Application-specific features such as depth analysis are defined as optional extensions.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 5 July 2026.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document.¶
Digital media authenticity faces several fundamental challenges:¶
CPP addresses these challenges through the following design principles:¶
This document specifies:¶
This document does NOT specify:¶
CPP defines its own Merkle tree construction that is NOT compatible with Certificate Transparency [RFC6962]. While inspired by similar principles, CPP uses different domain separation prefixes and padding rules optimized for media provenance use cases. Implementations MUST NOT assume RFC 6962 compatibility.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
Additionally, this document uses the following terms:¶
sha256:<64_hex_chars>.¶
sha256: prefix).¶
sha256:0000000000000000000000000000000000000000000000000000000000000000
(64 zeros).¶
| Threat | Mitigation |
|---|---|
| Timestamp forgery | RFC 3161 TSA provides independent timestamp |
| Evidence tampering | EventHash binds content; Merkle proof binds to anchor |
| Selective deletion | Completeness Invariant detects missing events |
| TSA token swapping | messageImprint must match AnchorDigest |
An Event is the fundamental unit of provenance in CPP. Events are signed records that capture discrete provenance actions.¶
| Type | Description |
|---|---|
| INGEST | Media captured from device sensor |
| SEAL | Collection sealed with Completeness Invariant |
| EXPORT | Proof shared externally |
| TOMBSTONE | Legitimate deletion record |
The following fields are REQUIRED for all events:¶
| Field | Type | Required | Description |
|---|---|---|---|
| EventID | string | REQUIRED | Unique identifier (UUID) |
| ChainID | string | REQUIRED | Identifier linking events in a sequence |
| PrevHash | string | REQUIRED | Hash of previous event in chain |
| Timestamp | string | REQUIRED | ISO 8601 timestamp with millisecond precision |
| EventType | string | REQUIRED | One of: INGEST, SEAL, EXPORT, TOMBSTONE |
| HashAlgo | string | REQUIRED | Always "SHA256" |
| SignAlgo | string | REQUIRED | "ES256" or "Ed25519" |
| EventHash | string | REQUIRED | SHA-256 hash of canonicalized event |
| Signature | string | REQUIRED | Raw Base64-encoded signature (no prefix) |
All Base64-encoded fields (Signature, TSA.Token, public_key) MUST conform to:¶
=) MUST be included when required¶
PROHIBITED:¶
INGEST events MUST include:¶
| Field | Type | Required | Description |
|---|---|---|---|
| Asset.AssetHash | string | REQUIRED | SHA-256 hash of media bytes |
| Asset.AssetType | string | REQUIRED | IMAGE or VIDEO |
| Asset.MimeType | string | REQUIRED | MIME type of asset |
| Asset.AssetID | string | OPTIONAL | Unique asset identifier |
| Asset.AssetName | string | OPTIONAL | Original filename |
| Asset.AssetSize | integer | OPTIONAL | File size in bytes |
SEAL events finalize a collection and commit the Completeness Invariant. A SEAL event MUST include:¶
| Field | Type | Required | Description |
|---|---|---|---|
| CollectionID | string | REQUIRED | Identifier for the sealed collection |
| EventCount | integer | REQUIRED | Number of events in collection (excluding SEAL) |
| CompletenessInvariant | object | REQUIRED | Completeness verification data |
| MerkleRoot | string | REQUIRED | Root hash of events in collection |
| Field | Type | Required | Description |
|---|---|---|---|
| ExpectedCount | integer | REQUIRED | Number of events that MUST be present |
| HashSum | string | REQUIRED | XOR of all EventHash values (sha256: format) |
| FirstTimestamp | string | REQUIRED | ISO 8601 timestamp of first event |
| LastTimestamp | string | REQUIRED | ISO 8601 timestamp of last event |
The HashSum is computed as:¶
HashSum = EventHash[0] XOR EventHash[1] XOR ... XOR EventHash[n-1]¶
Where XOR operates on the 32-byte binary values of each EventHash.¶
The recommended anchoring pattern for SEAL events:¶
This pattern ensures the Completeness Invariant itself is bound to an external timestamp. The SEAL event's EventHash covers all CI fields, so the TSA anchor proves the CI existed at GenTime.¶
Alternative Pattern (NOT RECOMMENDED): Including the SEAL event in the same Merkle tree it references creates a circular dependency and is prohibited.¶
TOMBSTONE events MUST additionally include:¶
| Field | Type | Required | Description |
|---|---|---|---|
| DeletedEventId | string | REQUIRED | EventID being invalidated |
| Reason | string | REQUIRED | Deletion reason code |
| DeletedAt | string | REQUIRED | ISO 8601 deletion timestamp |
Events form a hash chain through the PrevHash field:¶
Event 1: PrevHash = sha256:0000...0000 (genesis - 64 zeros) Event 2: PrevHash = EventHash(Event 1) Event 3: PrevHash = EventHash(Event 2)¶
Verification of chain integrity:¶
CPP defines its own binary Merkle tree construction optimized for media provenance. This construction uses domain separation prefixes to prevent attacks where leaf values could be confused with internal node values.¶
Important: CPP Merkle trees are NOT compatible with RFC 6962 (Certificate Transparency). Implementations MUST use the exact algorithms specified in this section.¶
CPP uses single-byte prefixes to separate domains:¶
| Domain | Prefix Byte | Description |
|---|---|---|
| Leaf | 0x00 | Applied to EventHash bytes |
| Internal | 0x01 | Applied to concatenated child hashes |
LeafHash = SHA256(0x00 || EventHash_bytes)¶
Where:¶
sha256: prefix)¶
Rationale: The 0x00 prefix ensures leaf hashes cannot collide with internal node hashes, preventing second preimage attacks on the tree structure.¶
InternalHash = SHA256(0x01 || Left_bytes || Right_bytes)¶
Where:¶
Step 1: Compute Leaf Hashes¶
For each event, compute LeafHash = SHA256(0x00 || EventHash_bytes).¶
Step 2: Determine Padding¶
PaddedSize is the smallest power of 2 >= TreeSize:¶
function computePaddedSize(treeSize):
if treeSize == 0:
return 0 // Invalid - TreeSize MUST be >= 1
paddedSize = 1
while paddedSize < treeSize:
paddedSize = paddedSize * 2
return paddedSize
¶
Step 3: Pad Leaf Array¶
If TreeSize < PaddedSize, duplicate the last leaf hash until the array length equals PaddedSize.¶
Step 4: Build Tree¶
function buildTree(paddedLeaves):
levels = [paddedLeaves]
current = paddedLeaves
while current.length > 1:
nextLevel = []
for i in range(0, current.length, 2):
left = current[i]
right = current[i + 1]
parent = SHA256(0x01 || left || right)
nextLevel.append(parent)
levels.append(nextLevel)
current = nextLevel
return levels // levels[0] = leaves, levels[-1] = [root]
¶
| Field | Type | Description |
|---|---|---|
| TreeSize | integer | Original leaf count (before padding), unsigned, MUST be >= 1 |
| LeafHashMethod | string | MUST be exactly SHA256(0x00||EventHash) (18 ASCII characters) |
| LeafHash | string | Computed LeafHash for this event with sha256: prefix |
| LeafIndex | integer | 0-based position in tree, range [0, TreeSize-1] |
| Proof | array | Sibling hashes from bottom to top, each with sha256: prefix |
| Root | string | MerkleRoot with sha256: prefix |
TreeSize Constraint: An empty Merkle tree (TreeSize = 0) is not permitted. Verifiers MUST reject proofs where TreeSize < 1.¶
| Field | Type | Description |
|---|---|---|
| AnchorID | string | Unique anchor identifier |
| AnchorType | string | MUST be "RFC3161" |
| AnchorDigest | string | MerkleRoot without prefix, 64 lowercase hex chars |
| AnchorDigestAlgorithm | string | MUST be "sha-256" |
| Merkle | object | Merkle proof structure |
| TSA | object | TSA response data |
Events MUST be canonicalized using [RFC8785] (JSON Canonicalization Scheme) before hashing.¶
The following fields MUST be excluded from canonicalization:¶
All other fields MUST be included. Field names in the canonical event object use PascalCase (e.g., EventID, ChainID, PrevHash).¶
function computeEventHash(event):
eventCopy = copy(event)
delete eventCopy.EventHash
delete eventCopy.Signature
canonical = JCS_canonicalize(eventCopy) // RFC 8785
hashBytes = SHA256(canonical)
return "sha256:" + lowercase_hex(hashBytes)
¶
The resulting EventHash is a 71-character string: the prefix "sha256:" followed by 64 lowercase hexadecimal characters.¶
function computeLeafHash(eventHash):
hexStr = eventHash.substring(7) // Remove "sha256:" prefix
eventHashBytes = hexDecode(hexStr) // 32 bytes
prefixedData = [0x00] + eventHashBytes // 33 bytes
leafHashBytes = SHA256(prefixedData)
return "sha256:" + lowercase_hex(leafHashBytes)
¶
The 0x00 prefix byte provides domain separation from internal nodes.¶
function computeInternalHash(left, right):
leftBytes = hexDecode(left.substring(7)) // Remove prefix, decode
rightBytes = hexDecode(right.substring(7))
prefixedData = [0x01] + leftBytes + rightBytes // 65 bytes
hashBytes = SHA256(prefixedData)
return "sha256:" + lowercase_hex(hashBytes)
¶
The 0x01 prefix byte distinguishes internal nodes from leaves.¶
AnchorDigest is the MerkleRoot value WITHOUT the sha256: prefix,
represented as 64 lowercase hexadecimal characters.¶
function computeAnchorDigest(merkleRoot):
return lowercase(merkleRoot.substring(7))
¶
PROHIBITED:¶
The messageImprint in TimeStampReq MUST contain:¶
TimeStampReq ::= SEQUENCE {
version INTEGER { v1(1) },
messageImprint MessageImprint,
reqPolicy OBJECT IDENTIFIER OPTIONAL,
nonce INTEGER OPTIONAL,
certReq BOOLEAN DEFAULT FALSE,
extensions [0] IMPLICIT Extensions OPTIONAL
}
MessageImprint ::= SEQUENCE {
hashAlgorithm AlgorithmIdentifier, -- SHA-256
hashedMessage OCTET STRING -- AnchorDigest (32 bytes)
}
¶
Producers SHOULD set certReq to TRUE to request the TSA's signing certificate be included in the response. This enables:¶
If certReq is FALSE and the TSA certificate is not included in the response, verifiers MUST attempt to obtain the certificate through other means (e.g., AIA extension, local cache) or return VALID_WARNING.¶
Upon receiving TimeStampResp, the producer:¶
When TreeSize equals 1, the following invariants MUST hold:¶
If any of these conditions fail, verification MUST return INVALID.¶
For TreeSize greater than 1:¶
| Code | Meaning |
|---|---|
| VALID | All checks passed, including TSA signature verification |
| VALID_WARNING | Cryptographic checks passed, but TSA certificate chain could not be fully validated |
| INVALID | Cryptographic verification failed |
| CHAIN_INTEGRITY_VIOLATION | Hash chain is broken |
| COMPLETENESS_VIOLATION | Completeness Invariant mismatch |
Note: VALID_WARNING indicates the proof is cryptographically sound but the TSA's identity could not be independently verified. Applications SHOULD display this distinction to users.¶
function verifyEvent(event, publicKey):
// Step 1: Recompute EventHash
computedHash = computeEventHash(event)
if computedHash != event.EventHash:
return INVALID("EventHash mismatch")
// Step 2: Verify signature
hashBytes = hexDecode(event.EventHash.substring(7))
sigBytes = base64Decode(event.Signature)
if not verifySignature(publicKey, hashBytes, sigBytes):
return INVALID("Signature verification failed")
return VALID
¶
function verifyMerkleProof(eventHash, leafIndex, proof,
expectedRoot, treeSize):
// Step 1: Validate inputs
if treeSize < 1:
return INVALID("TreeSize must be >= 1")
if leafIndex < 0 or leafIndex >= treeSize:
return INVALID("LeafIndex out of range")
paddedSize = computePaddedSize(treeSize)
maxProofLength = log2(paddedSize)
if proof.length > maxProofLength:
return INVALID("Proof too long")
// Step 2: Compute leaf hash with domain separation
currentHash = computeLeafHash(eventHash) // SHA256(0x00 || bytes)
// Step 3: Handle single-leaf case
if treeSize == 1:
if leafIndex != 0:
return INVALID("LeafIndex must be 0 for single-leaf")
if proof.length != 0:
return INVALID("Proof must be empty for single-leaf")
if lowercase(currentHash) != lowercase(expectedRoot):
return INVALID("Root != LeafHash for single-leaf")
return VALID
// Step 4: Traverse proof from bottom to top
index = leafIndex
for siblingHash in proof:
if index % 2 == 0:
// Current is left child
currentHash = computeInternalHash(currentHash, siblingHash)
else:
// Current is right child
currentHash = computeInternalHash(siblingHash, currentHash)
index = floor(index / 2)
// Step 5: Compare with expected root (case-insensitive)
if lowercase(currentHash) != lowercase(expectedRoot):
return INVALID("Computed root != expected root")
return VALID
¶
TSA verification ensures the timestamp token was legitimately issued by a Time-Stamp Authority and binds the correct digest.¶
function verifyTSAAnchor(eventHash, anchor):
// Step 1: Verify Merkle structure
merkle = anchor.Merkle
result = verifyMerkleProof(
eventHash,
merkle.LeafIndex,
merkle.Proof,
merkle.Root,
merkle.TreeSize
)
if result != VALID:
return result
// Step 2: Verify LeafHashMethod
if merkle.LeafHashMethod != "SHA256(0x00||EventHash)":
return INVALID("Unsupported LeafHashMethod")
// Step 3: Verify AnchorDigest == MerkleRoot
expectedDigest = lowercase(merkle.Root.substring(7))
if lowercase(anchor.AnchorDigest) != expectedDigest:
return INVALID("AnchorDigest != MerkleRoot")
// Step 4: Parse TSA Token (RFC 5652 ContentInfo)
tsaToken = base64Decode(anchor.TSA.Token)
contentInfo = parseContentInfo(tsaToken) // RFC 5652
signedData = parseSignedData(contentInfo.content)
tstInfo = parseTSTInfo(signedData.encapContentInfo.eContent)
// Step 5: Verify hash algorithm is SHA-256
if tstInfo.messageImprint.hashAlgorithm != SHA256_OID:
return INVALID("Unsupported TSA hash algorithm")
// Step 6: Verify messageImprint == AnchorDigest (MUST)
tstImprint = lowercase_hex(tstInfo.messageImprint.hashedMessage)
if tstImprint != lowercase(anchor.AnchorDigest):
return INVALID("TSA messageImprint != AnchorDigest")
// Step 7: Verify CMS signature over TSTInfo (MUST per RFC 5652)
signerInfo = signedData.signerInfos[0]
signatureValid = verifyCMSSignature(
signedData.encapContentInfo.eContent,
signerInfo.signature,
signerInfo.signatureAlgorithm,
extractSignerCert(signedData.certificates, signerInfo.sid)
)
if not signatureValid:
return INVALID("TSA signature verification failed")
// Step 8: Verify certificate chain (SHOULD)
certValid = verifyCertificateChain(
signedData.certificates,
signerInfo.sid,
trustAnchors
)
if certValid:
return VALID(genTime = tstInfo.genTime)
else:
return VALID_WARNING(
genTime = tstInfo.genTime,
warning = "TSA certificate chain could not be verified"
)
¶
Per [RFC5652], verifiers MUST:¶
Verifiers SHOULD:¶
GENESIS_PREV_HASH = "sha256:00000000000000000000000000000000" +
"00000000000000000000000000000000"
function verifyChainIntegrity(events):
if events.length == 0:
return VALID
// First event must have Genesis PrevHash (64 zeros)
if events[0].PrevHash != GENESIS_PREV_HASH:
return CHAIN_INTEGRITY_VIOLATION("Invalid genesis PrevHash")
for i in range(1, events.length):
expectedPrevHash = events[i-1].EventHash
if events[i].PrevHash != expectedPrevHash:
return CHAIN_INTEGRITY_VIOLATION(
"Break at event " + i +
": expected " + expectedPrevHash +
", found " + events[i].PrevHash)
return VALID
¶
The Completeness Invariant is verified against a SEAL event. The SEAL event MUST be anchored to a TSA to provide external timestamp binding for the entire collection.¶
function verifyCompleteness(events, sealEvent):
ci = sealEvent.CompletenessInvariant
// Step 1: Verify count matches
if events.length != ci.ExpectedCount:
return COMPLETENESS_VIOLATION(
"Count mismatch: expected " + ci.ExpectedCount +
", found " + events.length)
// Step 2: Compute XOR hash sum
computed = bytes(32) // Initialize to all zeros
for event in events:
eventHashBytes = hexDecode(event.EventHash.substring(7))
computed = XOR(computed, eventHashBytes)
// Step 3: Compare with sealed value
expectedHashSum = hexDecode(ci.HashSum.substring(7))
if computed != expectedHashSum:
return COMPLETENESS_VIOLATION(
"Hash sum mismatch - events may be missing or added")
// Step 4: Verify timestamp bounds
for event in events:
if event.Timestamp < ci.FirstTimestamp:
return COMPLETENESS_VIOLATION(
"Event timestamp before collection start")
if event.Timestamp > ci.LastTimestamp:
return COMPLETENESS_VIOLATION(
"Event timestamp after collection end")
return VALID
¶
| Attack | Detection |
|---|---|
| Delete event | Hash sum mismatch and/or count mismatch |
| Add fake event | Count mismatch and/or hash sum mismatch |
| Reorder events | Chain integrity violation (PrevHash mismatch) |
| Modify event | EventHash mismatch in chain |
Location collection SHOULD be disabled by default. When enabled, implementations SHOULD:¶
Implementations MUST NOT store raw biometric data (fingerprints, face images). Human presence verification, if implemented, SHOULD:¶
This specification mandates SHA-256 for all hash computations. Future versions MAY define additional algorithms via the HashAlgo field. Verifiers MUST reject unknown hash algorithms.¶
Implementations MUST support ES256 (ECDSA with P-256 and SHA-256) for mobile device compatibility. Ed25519 MAY be supported for non-mobile implementations.¶
Private keys SHOULD be stored in hardware security modules (Secure Enclave, StrongBox, TPM) where available.¶
Security of timestamp proofs depends on TSA trustworthiness. Implementations:¶
If certificate chain validation fails but CMS signature verification succeeds, the result SHOULD be VALID_WARNING rather than INVALID, as the timestamp binding is cryptographically sound even if the TSA's identity cannot be fully verified.¶
The 0x00/0x01 prefix bytes ensure:¶
This construction differs from Certificate Transparency [RFC6962] which uses a similar but incompatible scheme.¶
Device timestamps (Timestamp field) are self-attested and may be inaccurate. The authoritative timestamp is GenTime from the TSA response.¶
Implementations SHOULD warn users when device time differs significantly from TSA GenTime (e.g., more than 5 minutes).¶
The Completeness Invariant detects deletions within a sealed collection. It does NOT detect:¶
The Completeness Invariant uses XOR for omission detection, NOT for cryptographic commitment. Important limitations:¶
Threat model: The CI protects against accidental deletion or deletion by parties who cannot forge events (e.g., device owners deleting their own legitimately-captured evidence). It does NOT protect against adversaries who control event creation.¶
JSON canonicalization per [RFC8785] prevents ordering and whitespace attacks. However, implementations MUST ensure:¶
This document has no IANA actions.¶
VeraSnap is a consumer iOS application implementing CPP. It demonstrates:¶
Implementation validated that:¶
Deployment experience informed the explicit specification of:¶
The canonical event structure uses PascalCase field names. This is the structure that MUST be used for EventHash computation.¶
{
"EventID": "550e8400-e29b-41d4-a716-446655440001",
"ChainID": "urn:uuid:550e8400-e29b-41d4-a716-446655440000",
"PrevHash": "sha256:00000000000000000000000000000000000000000000000000000000000000",
"Timestamp": "2026-01-27T10:30:00.000Z",
"EventType": "INGEST",
"HashAlgo": "SHA256",
"SignAlgo": "ES256",
"Asset": {
"AssetID": "asset-001",
"AssetType": "IMAGE",
"AssetHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"AssetName": "IMG_0001.HEIC",
"MimeType": "image/heic"
},
"EventHash": "sha256:7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730",
"Signature": "MEUCIQDKsRwMv..."
}
¶
{
"EventID": "550e8400-e29b-41d4-a716-446655440010",
"ChainID": "urn:uuid:550e8400-e29b-41d4-a716-446655440000",
"PrevHash": "sha256:7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730",
"Timestamp": "2026-01-27T18:00:00.000Z",
"EventType": "SEAL",
"HashAlgo": "SHA256",
"SignAlgo": "ES256",
"CollectionID": "collection-2026-01-27",
"EventCount": 5,
"CompletenessInvariant": {
"ExpectedCount": 5,
"HashSum": "sha256:1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890",
"FirstTimestamp": "2026-01-27T10:30:00.000Z",
"LastTimestamp": "2026-01-27T17:45:00.000Z"
},
"MerkleRoot": "sha256:03938e2c8f758e6cae443d499b41c899c373eb0c0198bae61796a069f2b05904",
"EventHash": "sha256:abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab",
"Signature": "MEYCIQCx..."
}
¶
{
"Anchor": {
"AnchorID": "anchor-001",
"AnchorType": "RFC3161",
"AnchorDigest": "719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929",
"AnchorDigestAlgorithm": "sha-256",
"Merkle": {
"TreeSize": 1,
"LeafHashMethod": "SHA256(0x00||EventHash)",
"LeafHash": "sha256:719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929",
"LeafIndex": 0,
"Proof": [],
"Root": "sha256:719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929"
},
"TSA": {
"Token": "MIIEzAYJKoZIhvcNAQcCoIIEvTCCBLkCAQMx...",
"MessageImprint": {
"HashAlgorithm": "sha-256",
"HashedMessage": "719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929"
},
"GenTime": "2026-01-27T10:31:00.000Z",
"Service": "https://freetsa.org/tsr"
}
}
}
¶
The Evidence Pack is a distribution format. Field names use snake_case for compatibility with common JSON conventions in web APIs. This format is non-normative; implementations MAY use alternative formats.¶
{
"proof_version": "1.3",
"proof_type": "CPP_INGEST_PROOF",
"proof_id": "proof-001",
"event": {
"event_id": "550e8400-e29b-41d4-a716-446655440001",
"event_type": "INGEST",
"timestamp": "2026-01-27T10:30:00.000Z",
"asset_hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"asset_type": "IMAGE"
},
"event_hash": "sha256:7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730",
"signature": {
"algo": "ES256",
"value": "MEUCIQDKsRwMv..."
},
"public_key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...",
"timestamp_proof": {
"type": "RFC3161",
"anchor_digest": "719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929",
"digest_algorithm": "sha-256",
"merkle": {
"tree_size": 1,
"leaf_hash_method": "SHA256(0x00||EventHash)",
"leaf_index": 0,
"proof": [],
"root": "sha256:719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929"
},
"tsa": {
"token": "MIIEzAYJKoZIhvcNAQcCoIIEvTCCBLkCAQMx...",
"message_imprint": "719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929",
"gen_time": "2026-01-27T10:31:00.000Z",
"service": "https://freetsa.org/tsr"
}
}
}
¶
Note: When verifying an Evidence Pack, implementations MUST reconstruct the canonical event structure (PascalCase) from the evidence pack fields before computing EventHash.¶
All test vectors in this section use the domain-separated hash construction defined in this specification.¶
Input:¶
EventHash = "sha256:7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730"¶
Computation:¶
EventHash_bytes = 0x7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730
LeafHash = SHA256(0x00 || EventHash_bytes)
= SHA256(0x007d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730)
= sha256:719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929
For TreeSize=1:
Root = LeafHash = sha256:719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929
LeafIndex = 0
Proof = []
AnchorDigest = 719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929
¶
Expected Anchor:¶
{
"Merkle": {
"TreeSize": 1,
"LeafHashMethod": "SHA256(0x00||EventHash)",
"LeafHash": "sha256:719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929",
"LeafIndex": 0,
"Proof": [],
"Root": "sha256:719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929"
},
"AnchorDigest": "719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929"
}
¶
Verification:¶
verifyMerkleProof(
"sha256:7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730",
0, [],
"sha256:719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929",
1
) = VALID
¶
Input:¶
EventHash[0] = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" EventHash[1] = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"¶
Computation:¶
L0 = SHA256(0x00 || 0xaa...aa)
= sha256:e0bb82791bae3c50bd9c20fa4ccdcb8064a56e5c12bc69b07e6712ac9b4429e6
L1 = SHA256(0x00 || 0xbb...bb)
= sha256:4f16119d36ccd0da91102f57692d73934fd0ad2494280df88449accedbbfb7ea
Root = SHA256(0x01 || L0_bytes || L1_bytes)
= SHA256(0x01 || 0xe0bb82...e6 || 0x4f1611...ea)
= sha256:03938e2c8f758e6cae443d499b41c899c373eb0c0198bae61796a069f2b05904
TreeSize = 2
PaddedSize = 2 (no padding needed)
For index 0: Proof = ["sha256:4f16119d36ccd0da91102f57692d73934fd0ad2494280df88449accedbbfb7ea"]
For index 1: Proof = ["sha256:e0bb82791bae3c50bd9c20fa4ccdcb8064a56e5c12bc69b07e6712ac9b4429e6"]
¶
Verification of Index 0:¶
1. currentHash = SHA256(0x00 || EventHash[0]_bytes)
= sha256:e0bb82791bae3c50bd9c20fa4ccdcb8064a56e5c12bc69b07e6712ac9b4429e6
2. index = 0, which is EVEN -> current is LEFT child
3. siblingHash = sha256:4f16119d36ccd0da91102f57692d73934fd0ad2494280df88449accedbbfb7ea
4. currentHash = SHA256(0x01 || currentHash_bytes || siblingHash_bytes)
= sha256:03938e2c8f758e6cae443d499b41c899c373eb0c0198bae61796a069f2b05904
5. Compare with expected Root: MATCH
Result: VALID
¶
Input:¶
AnchorDigest = "719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929" TSA Token TSTInfo contains: messageImprint.hashAlgorithm = 2.16.840.1.101.3.4.2.1 (SHA-256) messageImprint.hashedMessage = 0x719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929¶
Verification:¶
1. Extract messageImprint.hashedMessage from TSTInfo hashedMessage = 0x719f871f...1e929 2. Convert to lowercase hex string tstImprint = "719f871f1018a17ebe199d4f0db27e3a4929f8ab3e46f5c0d30054f4b331e929" 3. Compare with AnchorDigest (case-insensitive) tstImprint == lowercase(AnchorDigest) ? "719f871f...1e929" == "719f871f...1e929" ? YES Result: VALID¶
The authors thank:¶