Disclosure file
A disclosure file shows some fields of one credential to one verifier. The holder makes it on /student (panel Share some details only) and the verifier checks it on /verify (panel Check a disclosure). The flow is described in Selective disclosure.
| Property | Value |
|---|---|
| Media type | application/json |
| File name | disclosure-<first 8 hex characters of the credential id>.json |
| Format id | "format": "certichain-disclosure" |
| File version | 1 |
| Maximum size | 32,768 characters when parsed; the verify panel refuses files over 64 KiB before reading |
| Produced by | DisclosureService.createDisclosure, serialised by serializeDisclosure (two-space JSON with a trailing newline) |
| Parsed by | parseDisclosure in src/lib/disclosure/disclosure.ts |
Fields
The parser accepts exactly these keys at each level and refuses any extra key ("... has fields this app does not know.").
| Field | Type | Meaning and validation |
|---|---|---|
format | string | Must be certichain-disclosure. |
version | number | Must be 1; another value gives "This disclosure file was made by another version of the app." |
domain | object | The EIP-712 domain both signatures were made under. |
domain.name | string | Certichain (checked against the app's domain when verifying). |
domain.version | string | 1. |
domain.chainId | string | Decimal chain id, 1 to 18 digits. |
domain.verifyingContract | string | Address of the CredentialRegistry of the credential's contract set. |
credentialId | string | 32-byte hex credential id. |
issuer | string | The issuer address as the package states it (the verdict uses the issuer the chain holds). |
attributesRoot | string | 32-byte root of the attribute tree. |
attributesVersion | number | Integer from 0 to 2^32 - 1; the verifier accepts only 1. |
attestation | string | The issuer's EIP-712 signature over AttributeSet { credentialId, attributesRoot, version }, hex, 1 to 2048 bytes. |
challenge | string | The verifier's 32-byte challenge. |
fields | array | 1 to 8 shown fields, in slot order, each slot once. |
fields[].index | number | Slot, 0 to 7. |
fields[].name | string | Must equal the slot's name: 0 holderName, 1 studentId, 2 programme, 3 award, 4 awardedOn. |
fields[].value | string | Shown value, 1 to 500 characters. |
fields[].salt | string | The slot's 32-byte salt. |
fields[].siblings | array | Exactly three 32-byte hashes, from the leaf's sibling up to the root's child. |
holder | object | The holder's signature. |
holder.address | string | The holder wallet address. |
holder.signature | string | EIP-712 signature over DisclosureRequest { credentialId, attributesRoot, fieldMask, challenge }, hex, 1 to 2048 bytes. |
fieldMask is not stored in the file. The verifier recomputes it from the shown slots (bit i set for slot i) and checks the holder signature against it, so a shown field cannot be added or removed after signing.
Example
Values below come from the shared test vector src/lib/disclosure/vectors.json (salts 1 to 8, programme "Computer Science"). The addresses, ids and signatures are fake.
{
"format": "certichain-disclosure",
"version": 1,
"domain": {
"name": "Certichain",
"version": "1",
"chainId": "11155111",
"verifyingContract": "0x0000000000000000000000000000000000000c0d"
},
"credentialId": "0x1111111111111111111111111111111111111111111111111111111111111111",
"issuer": "0x00000000000000000000000000000000000015a5",
"attributesRoot": "0x7d1cd9875fa8a5e6c9b476c33565f01ab57465622a501bbe0ce99e0dcac48062",
"attributesVersion": 1,
"attestation": "0xaaaa...1b",
"challenge": "0x2222222222222222222222222222222222222222222222222222222222222222",
"fields": [
{
"index": 2,
"name": "programme",
"value": "Computer Science",
"salt": "0x0000000000000000000000000000000000000000000000000000000000000003",
"siblings": [
"0x22e7eaa67a8c76007e6b85abb4148e1cb4221b737ce8d31eecbf354f9244e343",
"0x4639e550933113af4a368b1585f3cc3177053d4367ca3e1e8029758027d5f3f3",
"0x341b8735bbd435303d3150f54d2fda1b993329d84dfdb505e69a79fa5ab254ca"
]
}
],
"holder": {
"address": "0x0000000000000000000000000000000000000b0b",
"signature": "0xbbbb...1c"
}
}
How it is checked
parseDisclosure: size, JSON,format,version, exact key sets, types and lengths.analyseDisclosure: for each field, the leafkeccak256(abi.encode("certichain.attr.v1", index, name, value, salt))is hashed up the three siblings and must reachattributesRoot; then the attestation signer and the holder signer are recovered. A signature that recovers to no account gives the zero address and is left to the chain's ERC-1271 check.DisclosureService.verify: domain name, version and attribute version; chain id and registry; credential existence and validity; issuer registration; attestation signer equals the issuer (or ERC-1271); holder signer equalsHolderRegistry.holderOf(issuer, subjectCommitment)(or ERC-1271); challenge equals the one on screen.
What a verifier learns
The shown values, the holder's wallet address, the issuer, the credential id and the root. Two disclosure files of the same credential share the id and the root, so they can be linked. Fields that are not shown, and their salts, are not in the file.
Source files
src/lib/disclosure/disclosure.ts(types,assembleDisclosure,serializeDisclosure,parseDisclosure,analyseDisclosure)src/lib/disclosure/attributes.ts,src/lib/disclosure/attestation.tssrc/services/domain/disclosure-service.tssrc/app/student/make-disclosure-file.ts
Tests
src/lib/disclosure/disclosure.test.ts(parser, field order, mask, signers),src/lib/disclosure/vectors.test.ts(shared tree vector)e2e/journeys/selective-disclosure.journey.ts