Skip to main content

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.

PropertyValue
Media typeapplication/json
File namedisclosure-<first 8 hex characters of the credential id>.json
Format id"format": "certichain-disclosure"
File version1
Maximum size32,768 characters when parsed; the verify panel refuses files over 64 KiB before reading
Produced byDisclosureService.createDisclosure, serialised by serializeDisclosure (two-space JSON with a trailing newline)
Parsed byparseDisclosure 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.").

FieldTypeMeaning and validation
formatstringMust be certichain-disclosure.
versionnumberMust be 1; another value gives "This disclosure file was made by another version of the app."
domainobjectThe EIP-712 domain both signatures were made under.
domain.namestringCertichain (checked against the app's domain when verifying).
domain.versionstring1.
domain.chainIdstringDecimal chain id, 1 to 18 digits.
domain.verifyingContractstringAddress of the CredentialRegistry of the credential's contract set.
credentialIdstring32-byte hex credential id.
issuerstringThe issuer address as the package states it (the verdict uses the issuer the chain holds).
attributesRootstring32-byte root of the attribute tree.
attributesVersionnumberInteger from 0 to 2^32 - 1; the verifier accepts only 1.
attestationstringThe issuer's EIP-712 signature over AttributeSet { credentialId, attributesRoot, version }, hex, 1 to 2048 bytes.
challengestringThe verifier's 32-byte challenge.
fieldsarray1 to 8 shown fields, in slot order, each slot once.
fields[].indexnumberSlot, 0 to 7.
fields[].namestringMust equal the slot's name: 0 holderName, 1 studentId, 2 programme, 3 award, 4 awardedOn.
fields[].valuestringShown value, 1 to 500 characters.
fields[].saltstringThe slot's 32-byte salt.
fields[].siblingsarrayExactly three 32-byte hashes, from the leaf's sibling up to the root's child.
holderobjectThe holder's signature.
holder.addressstringThe holder wallet address.
holder.signaturestringEIP-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​

  1. parseDisclosure: size, JSON, format, version, exact key sets, types and lengths.
  2. analyseDisclosure: for each field, the leaf keccak256(abi.encode("certichain.attr.v1", index, name, value, salt)) is hashed up the three siblings and must reach attributesRoot; 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.
  3. 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 equals HolderRegistry.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.ts
  • src/services/domain/disclosure-service.ts
  • src/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