This document is the technical specification of the proof package issued by TEL050JP proof storage. It states the rules identified by tel050jp-communication-proof-v1 precisely enough for a third party to implement a verifier of their own. The ideas behind them are explained on the how it works page; other versions are listed on the versions page.
0. Identification
| Protocol identifier | tel050jp-communication-proof-v1 |
|---|---|
| Package format identifier | tel050jp-communication-proof-package-v1 (§10) |
| Status | frozen: the rules of this version do not change (§14) |
| Frozen on | 2026-08-30 |
| Successor | none (current) |
| Where the identifier appears | the schema field of communication.json, the protocol field of proof.json, LP(schema) in the leaf and in every signing input, the PROTOCOL constant of verify.html |
1. Scope
Defined here: the structure and canonicalization of the communication record manifest, the communication record hash and the leaf, the Merkle tree and audit paths, the form of the message recorded on the Symbol blockchain, the issuer signatures (over proof.json and over certificate.pdf), the structure of the proof package proof.json, history.json, and the verification procedure with its result codes.
Not defined here: the encryption used in the archive, the purchase and extension workflow, the appearance of certificate.pdf, and how a Symbol node is contacted. These may change without affecting compatibility with this version.
2. Notation
- SHA-256(x)
- The FIPS 180-4 hash function. 32-byte output.
- a ‖ b
- Concatenation of byte strings.
- LP(s)
- The UTF-8 encoding of s, preceded by its length in bytes as an unsigned 16-bit big-endian integer. An s longer than 65,535 bytes is rejected.
- UINT64_BE(n) / UINT32_BE(n)
- Unsigned 64 / 32-bit big-endian integers.
- hex
- All hexadecimal is lowercase. Hashes are 64 digits, Ed25519 public keys 64 digits, Ed25519 signatures 128 digits.
- timestamp
- RFC 3339 in UTC with second precision only:
YYYY-MM-DDTHH:MM:SSZ. epoch(t) is its Unix time in seconds. - UUID
- 8-4-4-4-12 lowercase hex. The third group starts with 1 to 8, the fourth with 8, 9, a or b.
- 0xNN
- A one-byte constant (domain separation byte).
3. The communication record manifest (communication.json)
A JSON object describing one communication. It carries exactly the following fields; an unknown field and a missing one are both rejected.
| Field | Type | Constraint |
|---|---|---|
schema | string | fixed to tel050jp-communication-proof-v1 |
proof_id | string | UUID |
tenant_id | string | not blank |
object_uid | string | not blank; call:, voice_message: or fax: followed by the communication id |
kind | string | call / voice_message / fax |
direction | string | inbound / outbound |
started_at | string | timestamp |
ended_at | string or null | timestamp, not before started_at |
duration_seconds | integer | 0 or more |
disposition | string | not blank |
source_party | string or null | — |
destination_party | string or null | — |
artifact | object | see below |
retention | object | see below |
previous_proof_leaf | string or null | 64 hex digits; null when extension_sequence is 1, required when it is 2 or more |
Field of artifact | Type | Constraint |
|---|---|---|
sha256 | string | 64 hex digits; SHA-256 of the original file's bytes |
archive_ciphertext_sha256 | string | 64 hex digits; SHA-256 of the encrypted object in the archive |
size_bytes | integer | 0 or more |
media_type | string | lowercase type/subtype |
archive_version_id | string | not blank |
Field of retention | Type | Constraint |
|---|---|---|
extended_from | string | timestamp |
retain_until | string | timestamp, after extended_from |
extension_days | integer | 1 or more |
extension_sequence | integer | 1 or more; which proof this is for the same communication |
previous_proof_leaf must be null exactly when extension_sequence is 1, and must be the leaf (§6) of the previous version whenever it is 2 or more. A manifest that breaks this rule is rejected, so that a gap in the history cannot be hidden.
4. Canonicalization
- Normalize every string and every key to Unicode NFC.
- Sort JSON keys by their UTF-8 bytes, ascending (keys are ASCII only).
- No whitespace (
{"a":1,"b":2}). - Emit UTF-8 and do not escape non-ASCII characters.
- Do not escape
/. - Numbers are safe integers only (integers below 253). Floating point numbers and numbers written as strings are forbidden.
null,""and an absent field are three different things.- Reject control characters (U+0000 to U+001F, U+007F to U+009F) in every string.
The bytes of the canonicalized manifest are called canonical_manifest. The communication.json bundled in a proof package is exactly these bytes. Rule 8 exists because a control character is invisible on a certificate and is escaped differently by every JSON encoder, which would be the one remaining way for the same manifest to hash differently in different implementations.
5. Rejection conditions
Every implementation rejects a manifest that meets any of the following.
- An unknown field, a missing field, or a
schemamismatch (including an attempt to pass the manifest off as an older version). - A
kindordirectionoutside the enumerations. - Uppercase hex, or hex that is not 64 digits.
- A timestamp that is not RFC 3339 UTC at second precision (
+09:00, milliseconds, a date that does not exist). ended_at < started_at, orretain_until <= extended_from.extension_sequence == 1disagreeing withprevious_proof_leaf == null.- A non-integer number, or a number sent as a string.
- A string containing a control character, or one that is not valid UTF-8.
6. Hashes and the leaf
communication_record_hash = SHA-256(canonical_manifest) artifact_hash = SHA-256(the exact bytes of the original file) leaf = SHA-256( 0x00 ‖ LP(schema) ‖ LP(tenant_id) ‖ LP(object_uid) ‖ communication_record_hash[32] ‖ artifact_hash[32] ‖ UINT64_BE(epoch(retain_until)) ‖ UINT32_BE(extension_sequence) ‖ previous_proof_leaf[32] (32 zero bytes when null) )
The strings passed to LP are the NFC-normalized values, identical to the strings inside canonical_manifest. In this version schema is always tel050jp-communication-proof-v1. The leaf can be recomputed without the manifest, from the seven values proof.json carries: tenant_id, object_uid, communication_record_hash, artifact.sha256, retention.retain_until, retention.extension_sequence and retention.previous_proof_leaf.
7. The Merkle tree and audit paths
- Leaves in a batch are ordered by
proof_id, ascending by bytes. A duplicateproof_idand an empty batch are rejected. - Parent node = SHA-256(0x01 ‖ left[32] ‖ right[32]).
- Odd rule: when a level has an odd number of nodes, the last one is promoted to the next level unchanged, not duplicated. Duplicating the last node would let two different sets of leaves share one root.
- The root of a one-leaf batch is that leaf.
- An audit path is an array of
{"sibling": hex64, "position": "left" | "right"}. A promoted level contributes no entry.positionis the side of the sibling:leftgives SHA-256(0x01 ‖ sibling ‖ current),rightgives SHA-256(0x01 ‖ current ‖ sibling).
Example: for leaves A, B, C in that order, the first level is [H(A‖B), C] and the root is H(H(A‖B) ‖ C). The audit path of A is [{B, right}, {C, right}]; the audit path of C is [{H(A‖B), left}].
8. The message recorded on Symbol
tel050jp:proof:v1 kind=communication root=sha256:<root> batch=<batch_id>
Fixed length of 156 characters. Field order is fixed, fields are separated by exactly one space, there is no trailing whitespace, hex is lowercase, and no field may be added. A parser rejects anything that does not match the following expression.
^tel050jp:proof:v1 kind=communication root=sha256:[0-9a-f]{64} batch=<UUID>$
The message is placed in the message field of a transfer transaction sent from the TEL050JP Symbol signing account to a fixed recipient address. A proof package is issued only once the height of the block containing that transaction is at or below the finalized height.
9. Issuer signatures
The signature scheme is Ed25519 (RFC 8032). The signing key is the TEL050JP manifest signing key, which is a different key from the one that signs Symbol transactions.
The signature over proof.json
signing_input = 0x02 ‖ LP("tel050jp-communication-proof-v1")
‖ canonical_json(proof.json without its signature field)
canonical_json follows the rules of §4 (NFC, sorted keys, no whitespace, UTF-8, unescaped slashes). The signature is stored in the signature object of proof.json.
Field of signature | Value |
|---|---|
algorithm | ed25519 |
public_key | 64 hex digits; the Ed25519 public key that signed |
value | 128 hex digits; the signature |
The first 34 bytes of the signing input are always 02 001f 74656c3035306a702d636f6d6d756e69636174696f6e2d70726f6f662d7631 (0x02, the length 31, the identifier in UTF-8).
The signature over certificate.pdf
signing_input = 0x03 ‖ LP("tel050jp-communication-proof-v1") ‖ SHA-256(the bytes of certificate.pdf)[32]
certificate.pdf is rendered from proof.json when the package is downloaded, so the signature over proof.json does not cover it. Instead the package carries certificate.sig.json, a signature by the same key over the input above.
| Field of certificate.sig.json | Value |
|---|---|
algorithm | ed25519 |
signs | certificate.pdf |
sha256 | 64 hex digits; SHA-256 of the PDF at signing time |
public_key | 64 hex digits |
value | 128 hex digits |
Domain separation bytes
| Byte | Use |
|---|---|
0x00 | Merkle leaf (§6) |
0x01 | Merkle inner node (§7) |
0x02 | signature over proof.json |
0x03 | signature over certificate.pdf |
10. The proof package proof.json (tel050jp-communication-proof-package-v1)
The document the customer keeps and the verifier reads. It does not contain the manifest itself, only the values needed to recompute the leaf.
| Field | Type | Content |
|---|---|---|
schema | string | fixed to tel050jp-communication-proof-package-v1 |
protocol | string | fixed to tel050jp-communication-proof-v1 |
proof_id | string | UUID, as in the manifest |
issued_at | string | timestamp; when the package was issued |
tenant_id | string | as in the manifest |
object_uid | string | as in the manifest |
kind | string | as in the manifest |
artifact.sha256 | string | 64 hex digits; SHA-256 of the original file |
artifact.size_bytes | integer | size of the original file |
artifact.media_type | string | media type of the original file |
communication_record_hash | string | 64 hex digits (§6) |
retention.retain_until | string | timestamp |
retention.extension_sequence | integer | 1 or more |
retention.previous_proof_leaf | string or null | 64 hex digits |
merkle.leaf | string | 64 hex digits; the leaf of this proof |
merkle.audit_path | array | the audit path of §7 |
merkle.root | string | 64 hex digits; the root of the batch |
merkle.batch_id | string | UUID |
symbol.network | string | the Symbol network written to |
symbol.transaction_hash | string | hash of the recording transaction |
symbol.block_height | integer | height of the block containing it |
symbol.finalized_height | integer | finalized height at issue time |
symbol.signer_public_key | string | public key of the recording Symbol account |
symbol.recipient_address | string | the transfer's recipient address |
symbol.message | string | the message of §8 |
signature | object | the signature of §9 |
proof.json (and its symbol object) must not carry a node_url field. A proof may not name the node that verifies it.
Contents of the ZIP: proof.json, history.json (§11), certificate.pdf, certificate.sig.json (§9) and verify.html (the offline verifier). The variant with the communication record adds communication.json, which is exactly the canonical_manifest of §4; the variant with the original adds original.<extension>.
11. history.json
An array of the proof.json documents for the same object_uid, ordered by retention.extension_sequence ascending. Each entry's retention.previous_proof_leaf equals the merkle.leaf of the entry before it, and only the first entry has null. A history that skips a sequence number or breaks the chain is rejected.
12. Verification procedure and result codes
A verifier takes proof.json, its own allowlist (the trusted Ed25519 public keys, the Symbol signer public key, the recipient address and the network), and optionally the original file, communication.json, history.json, and certificate.pdf with certificate.sig.json. The allowlist is fixed inside the verifier; it is never taken from the proof.
schemaandprotocolmatch the known identifiers exactly. Otherwise the result isunknown_schemaand no further check runs.- There is no
node_url. signature.algorithmised25519,public_keyis on the allowlist, and the signature verifies over the input of §9.artifact.sha256is 64 hex digits and, when the original file is given, equals its SHA-256.- The leaf recomputed from the seven values of proof.json by the formula of §6 equals
merkle.leaf. - Folding the leaf up
merkle.audit_pathyieldsmerkle.root. symbol.messageequals the string built frommerkle.rootandmerkle.batch_idin the form of §8.symbol.signer_public_key,recipient_addressandnetworkmatch the allowlist.symbol.block_height > 0andsymbol.finalized_height >= symbol.block_height.- (Optional) A trusted node, asked about
transaction_hash, reports the same signer, recipient, message and block height. - When communication.json is given, the SHA-256 of its bytes equals
communication_record_hash. - When history.json is given, the chain of §11 holds.
- When certificate.pdf and certificate.sig.json are given, the SHA-256 of the PDF equals
sha256and the signature verifies over the input of §9.
| Result code | Meaning |
|---|---|
unknown_schema | the identifiers do not match a known version |
proof_names_its_own_node | the proof names a node to verify it with |
missing_signature | no signature, or a scheme other than ed25519 |
untrusted_signing_key | the signing key is not on the allowlist |
bad_signature | the signature does not verify |
bad_artifact_hash | artifact.sha256 is malformed |
artifact_does_not_match | the original file's SHA-256 differs |
leaf_does_not_match_its_fields | the recomputed leaf differs from merkle.leaf |
audit_path_does_not_reach_the_root | folding the audit path does not yield the root |
malformed_audit_path | the audit path is malformed |
anchor_message_does_not_carry_this_root | the recorded message does not express this root and batch id |
anchor_is_not_the_allowlisted_one | signer, recipient or network differ from the allowlist |
anchor_is_not_finalized | the block height is above the finalized height |
chain_disagrees_with_the_proof | the node's answer differs from the proof |
history_skips_a_version | a sequence number is missing from the history |
history_chain_is_broken | consecutive history entries are not linked by their leaves |
13. Test vectors
An excerpt of the test vectors shared by the three implementations of this version (PHP, TypeScript and Python). An independent implementation should reproduce these values first.
Manifest (call_first_extension)
{
"schema": "tel050jp-communication-proof-v1",
"proof_id": "3f2a1c64-6f5f-4d2e-9c1a-8b0d5e7a4411",
"tenant_id": "tel050jp",
"object_uid": "call:01JCX9K8T4V2QW3M5N7P8R9S0T",
"kind": "call",
"direction": "inbound",
"started_at": "2026-08-30T04:15:07Z",
"ended_at": "2026-08-30T04:18:22Z",
"duration_seconds": 195,
"disposition": "answered",
"source_party": "+815012345678",
"destination_party": "+815058109483",
"artifact": {
"sha256": "397c4d496b5be24cde8027f261de3adf418f1178fed7638e91d02cc90ccf0102",
"archive_ciphertext_sha256": "7e2573be8f2b70bb144640f08ed1db7744841bcee81c43b6d7ce0801151ef844",
"size_bytes": 320044,
"media_type": "audio/wav",
"archive_version_id": "3sL4kqtJlcpXroDTDmJ.rmSpXd3dIbrHY"
},
"retention": {
"extended_from": "2026-08-30T05:00:00Z",
"retain_until": "2027-08-30T05:00:00Z",
"extension_days": 365,
"extension_sequence": 1
},
"previous_proof_leaf": null
}
canonical_manifest:
{"artifact":{"archive_ciphertext_sha256":"7e2573be8f2b70bb144640f08ed1db7744841bcee81c43b6d7ce0801151ef844","archive_version_id":"3sL4kqtJlcpXroDTDmJ.rmSpXd3dIbrHY","media_type":"audio/wav","sha256":"397c4d496b5be24cde8027f261de3adf418f1178fed7638e91d02cc90ccf0102","size_bytes":320044},"destination_party":"+815058109483","direction":"inbound","disposition":"answered","duration_seconds":195,"ended_at":"2026-08-30T04:18:22Z","kind":"call","object_uid":"call:01JCX9K8T4V2QW3M5N7P8R9S0T","previous_proof_leaf":null,"proof_id":"3f2a1c64-6f5f-4d2e-9c1a-8b0d5e7a4411","retention":{"extended_from":"2026-08-30T05:00:00Z","extension_days":365,"extension_sequence":1,"retain_until":"2027-08-30T05:00:00Z"},"schema":"tel050jp-communication-proof-v1","source_party":"+815012345678","started_at":"2026-08-30T04:15:07Z","tenant_id":"tel050jp"}
| communication_record_hash | 6143dd3478a04f2b4fa108c4cef10dcf6f1ac2bda78965eaeb21e09475afa271 |
|---|---|
| leaf | 7fb734be444f24687d183f3118d6c9154645a0f4bc90ca393eb1666f698213e5 |
A batch of three
| batch_id | 7b2f0c18-9d3e-4a51-b6c7-2e8f4a0d1b39 |
|---|---|
| proof_id order (ascending) | 0a1b2c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d 3f2a1c64-6f5f-4d2e-9c1a-8b0d5e7a4411 c7d8e9f0-1a2b-4c3d-8e4f-5a6b7c8d9e0f |
| leaves (same order) | ede0abdbb444f6efd417b579ace33fd9bc51c941b2ff14f85e26d6f65070c7b3 7fb734be444f24687d183f3118d6c9154645a0f4bc90ca393eb1666f698213e5 8f78067d1eca3637c498647b4d991a61be214a8ab27d8a45c6ffc5b0eddd57a5 |
| root | 4a702235fe4922a434f675e424d971fa486ec6c97114e2034b212e64480e804c |
| Symbol message | tel050jp:proof:v1 kind=communication root=sha256:4a702235fe4922a434f675e424d971fa486ec6c97114e2034b212e64480e804c batch=7b2f0c18-9d3e-4a51-b6c7-2e8f4a0d1b39 |
In a one-leaf batch the root is the leaf itself and the audit path is an empty array.
14. Versioning
- This version is frozen. When a rule has to change, a new identifier (for example
tel050jp-communication-proof-v2) is defined and added to the versions page. - Proofs issued under this version remain verifiable under the rules of this version.
- Verifiers compare identifiers for exact equality and refuse an unknown version, as well as a proof that tries to pass as an older one.
- Any change that alters the test vectors of §13 requires a new version.
15. Normative references
- National Institute of Standards and Technology. Secure Hash Standard (SHS). FIPS PUB 180-4, 2015.
- S. Josefsson and I. Liusvaara. Edwards-Curve Digital Signature Algorithm (EdDSA). RFC 8032, IETF, 2017.
- A. Rundgren, B. Jordan, and S. Erdtman. JSON Canonicalization Scheme (JCS). RFC 8785, IETF, 2020. The canonicalization here has the same intent as JCS but is stricter: numbers are limited to safe integers and control characters are rejected.
- G. Klyne and C. Newman. Date and Time on the Internet: Timestamps. RFC 3339, IETF, 2002.
- B. Laurie, A. Langley, and E. Kasper. Certificate Transparency. RFC 6962, IETF, 2013. The precedent for separating leaves from inner nodes.
- NEM Group. Symbol Technical Reference. 2021. https://docs.symbol.dev/
This page is a technical specification. It says nothing about legal effect or about how a proof is weighed as evidence.