Skip to main content

Credential package

The credential package is the portable proof a holder keeps. It carries everything needed to check a v0.3 credential offline (the fields the issuer signed and the EIP-712 signature), the salts that prove the holder's details against the on-chain commitments, and, for an encrypted document, the key that opens it.

A package is secret material

A version 2 package with a documentKey opens the stored document. Anyone who holds the file can read the document and see the holder's name and student ID. The issue page says "Keep this file private: it opens the document."

PropertyValue
File namecredential- followed by the first 8 hex characters of the credential id, .json (bulk import names them 0001-<externalId>.json inside a zip)
Current version2 (PACKAGE_VERSION); version 1 is still read
Produced bybuildPackage in src/lib/credential-package.ts, called by CredentialService.issue
Read byparsePackage in the same file
Size accepted by upload fields256 KiB (MAX_PACKAGE_BYTES in the student and dashboard panels)

Where it is produced and used​

Fields​

FieldTypeRequiredMeaningValidation on read
versionnumberyesPackage version2 or 1. A version 1 package may not carry documentKey.
domain.namestringyesEIP-712 domain name, "Certichain"Non-empty string
domain.versionstringyesEIP-712 domain version, "1"Non-empty string
domain.chainIdstringyesChain id as a decimal stringDecimal, at most 2^256 − 1
domain.verifyingContractstringyesAddress of the CredentialRegistry that anchored itAddress
credential.issuerstringyesIssuer wallet that signedAddress
credential.subjectCommitmentstringyesSalted commitment to the student id32-byte hex
credential.documentHashstringyesSHA-256 of the plaintext document32-byte hex
credential.metadataHashstringyesSalted hash of the display data32-byte hex
credential.recordTypenumberyesRecord type id (Record types)Integer up to 2^32 − 1
credential.issuedAtnumberyesUnix seconds when the issuer signedSafe integer
credential.validUntilnumberyesUnix seconds of expiry, 0 for none (the app always writes 0)Safe integer
credential.noncestringyesRandom 256-bit nonce as a decimal stringDecimal
signaturestringyesThe issuer's EIP-712 signature over credential64 or 65 byte hex signature
credentialIdstringyesThe EIP-712 digest of domain and credential, which is the on-chain id32-byte hex
cidstringnoIPFS CID of the stored document (container on v0.3)Kept when a non-empty string
documentKeystringnoBase64url of the 32-byte AES key that opens the containerExactly 32 bytes; version 2 only
displayobjectnoThe human-readable details (below)validateDisplay
subjectSaltstringnoSalt that proves subjectCommitment32-byte hex
metadataSaltstringnoSalt that proves metadataHash32-byte hex
attributesobjectnoSelective disclosure data: version, 8 salts, root, attestationExactly those keys (Disclosure file)

The signed part (domain, credential, signature, credentialId) has the same shape and is read by the same function (readSignedFields) as in the shared document file. A file with a type field is refused with a message that names what it looks like instead ("This is a shared document file, not a credential package."). Other unknown fields are dropped when the package is read.

display​

FieldRequiredRule
versionyes1
holderNameyesNon-empty, at most 500 characters, normalised to Unicode NFC
studentIdyesNon-empty, at most 500 characters, NFC
programmenoNon-empty, at most 500 characters, NFC
awardnoNon-empty, at most 500 characters, NFC
awardedOnnoA calendar date written YYYY-MM-DD (display only; the credential is dated by issuedAt)

How the hashes are built​

subjectCommitment = keccak256(abi.encode(
"certichain.subject.v1", issuer, normalise(studentId), subjectSalt))

normalise(studentId) = NFC, trim, upper case

metadataHash = keccak256(utf8( JCS({ ...display, salt: metadataSalt }) ))

credentialId = EIP-712 hash of credential under
domain { name: "Certichain", version: "1", chainId, verifyingContract }
  • subjectSalt is the issuer's stable student salt when the credential was issued with delivery ticked, so the commitment equals the student's enrolment commitment and the credential shows in their wallet. Otherwise it is fresh. See Enrolment.
  • metadataSalt is always fresh, so one package cannot be used to test guesses against another credential's metadata hash.
  • JCS is RFC 8785 canonical JSON. See Hashing and commitments.

Checks a reader can make​

CheckNeedsFunction
The fields still hash to credentialIdNothingverifyPackageOffline(pkg).digestMatches
The signature recovers to credential.issuerNothingverifyPackageOffline(pkg).signature is "valid"; "needs-contract-check" means the issuer may be an ERC-1271 contract wallet
The anchor on chain has the same fieldsRPCCredentialService.checkPackage (verify page) or verifyOnChain (script)
Name and student id match the commitmentsRPCcheckDisclosure inside checkPackage
The issuer is registeredRPCThe script's issuerRegistered line
Status (revoked, suspended, superseded)RPCOnly the chain answers this, never the package

The verify page panel Check holder details returns one of: "Holder details match the on-chain commitments", "Holder details do NOT match the on-chain commitments. The package may have been altered.", "This package does not match this credential on-chain.", "This package belongs to a different credential.", "This package does not include holder details, so only the document and signature can be checked." See Verification.

Versions​

VersionIntroduced withDifference
1v0.3 issuance before document encryptioncid points at a plaintext document; documentKey is not allowed
2Client-side encryptionMay carry documentKey. Applying a key update raises a version 1 package to 2.

A package with cid but no documentKey opens as legacy plaintext: the student page says "This document was stored unencrypted, before documents were protected." and offers Open the stored copy through the configured gateway. A package with no cid has no document ("This package does not point to a stored document."). Bulk import rows without a document still anchor a hash: the SHA-256 of zero bytes, 0xe3b0c442...b855.

Example​

Fake values throughout.

{
"version": 2,
"domain": {
"name": "Certichain",
"version": "1",
"chainId": "11155111",
"verifyingContract": "0x2222222222222222222222222222222222222222"
},
"credential": {
"issuer": "0x1111111111111111111111111111111111111111",
"subjectCommitment": "0x0000000000000000000000000000000000000000000000000000000000000a01",
"documentHash": "0x9f2c4d0000000000000000000000000000000000000000000000000000000001",
"metadataHash": "0x0000000000000000000000000000000000000000000000000000000000000b02",
"recordType": 1,
"issuedAt": 1792800000,
"validUntil": 0,
"nonce": "73829174628374619283746192837461928374619283746192837461928374"
},
"signature": "0x<65-byte signature>",
"credentialId": "0x0000000000000000000000000000000000000000000000000000000000000c03",
"cid": "bafybeiexampleexampleexampleexampleexampleexampleexampleexa",
"documentKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"display": {
"version": 1,
"holderName": "Asha Rai",
"studentId": "S-1001",
"programme": "BSc Computer Science",
"award": "First Class",
"awardedOn": "2026-07-15"
},
"subjectSalt": "0x0000000000000000000000000000000000000000000000000000000000000d04",
"metadataSalt": "0x0000000000000000000000000000000000000000000000000000000000000e05"
}

Other formats made from a package​

The package is the source for the verifiable credential and Open Badge exports, the disclosure file, the shared document file and capability link, and the phone wallet's compact codes. None of them carries the documentKey except the capability link, which carries the key on purpose.

Source files​

  • src/lib/credential-package.ts: CredentialPackage, buildPackage, parsePackage, readSignedFields, verifyPackageOffline
  • src/lib/eip712.ts: domain, CREDENTIAL_TYPES, credentialId
  • src/lib/commitments.ts: subjectCommitment, deriveStudentSalt, validateDisplay, metadataHash
  • src/lib/credential-disclosure.ts: commitment checks used by checkPackage
  • src/lib/doc-crypto/key-update.ts: applyKeyUpdate
  • src/services/domain/credential-service.ts: issue, checkPackage
  • blockchain/scripts/verify-credential.ts, blockchain/scripts/lib/verify-credential.ts: command line check

Tests​

  • src/lib/credential-package.test.ts, src/lib/eip712.test.ts, src/lib/commitments.test.ts, src/lib/credential-disclosure.test.ts, src/lib/doc-crypto/key-update.test.ts
  • src/services/domain/credential-service.test.ts
  • blockchain/test/v03/VerifyCredential.test.ts
  • E2E: e2e/journeys/issue-and-verify.journey.ts, e2e/journeys/encrypted-documents.journey.ts