Verifiable credential file
Certichain exports a credential package in two standard shapes. Both carry the same Certichain proof: the issuer's EIP-712 signature over the on-chain credential, with the signed values next to it. The flow (who exports, who checks, what the screens say) is in Verifiable credentials and Open Badges.
| Property | VC 2.0 file | Open Badges 3.0 file |
|---|---|---|
| File name | credential-<first 8 hex characters of the id>.vc.json | badge-<first 8 hex characters of the id>.ob.json |
| Media type | application/json | application/json |
type | ["VerifiableCredential", "CertichainCredential"] | ["VerifiableCredential", "OpenBadgeCredential"] |
| Produced by | exportVc in src/lib/vc/export.ts | exportOpenBadge in src/lib/vc/openbadges/export.ts, or exportStandardOpenBadge in src/lib/vc/openbadges/standard/export.ts |
| Parsed by | parseVc in src/lib/vc/parse.ts | parseOpenBadge in src/lib/vc/openbadges/parse.ts |
| Maximum size | 65,536 characters (MAX_VC_CHARS) | Same |
parseCredentialFile in src/lib/vc/detect.ts tells the two apart: a type array that contains OpenBadgeCredential is read as a badge, anything else as a VC. Upload panels refuse files over 64 KiB before reading them.
Both parsers accept exactly the keys listed below and refuse any other key, any other context and any other type. Free text (holder name, student id, programme, award) is 1 to 500 characters; the optional issuer name is 1 to 200.
VC 2.0 document
| Field | Type | Required | Meaning and validation |
|---|---|---|---|
@context | array | yes | Exactly two entries: https://www.w3.org/ns/credentials/v2 and the embedded Certichain context object. The embedded object is compared canonically with CERTICHAIN_CONTEXT; any edit is refused. |
id | string | yes | urn:certichain:eip155:<chainId>:<CredentialRegistry address>:<credentialId>. The id must be the EIP-712 digest of the signed fields. |
type | array | yes | ["VerifiableCredential", "CertichainCredential"], in that order. |
issuer | object | yes | id: did:pkh:eip155:<chainId>:<issuer address>. Optional name, not signed, advisory only. |
validFrom | string | yes | issuedAt as YYYY-MM-DDTHH:MM:SSZ. Only this spelling is accepted. |
validUntil | string | no | validUntil in the same form. Absent means no expiry (signed value 0). |
credentialSubject | object | yes | See below. |
credentialStatus | object or array | yes | The chain status entry alone, or an array of exactly three: the chain entry, then a revocation and a suspension list entry. |
proof | object | yes | The Certichain proof. See below. |
credentialSubject
| Field | Type | Required | Meaning |
|---|---|---|---|
recordType | number | yes | Record type id, 0 to 2^32 - 1. Must equal the signed recordType. |
id | string | no | did:pkh of the holder wallet. Added only when the holder ticks the opt-in. Checked against HolderRegistry.holderOf on chain. |
holderName | string | no | Shown name. Proven by metadataHash. |
studentId | string | no | Shown student id. Proven by metadataHash and, with subjectSalt, by subjectCommitment. |
programme | string | no | Shown programme. Proven by metadataHash. |
award | string | no | Shown award. Proven by metadataHash. |
If holderName, studentId and metadataSalt are all absent, the details are "unproven" (shown as Credential verified, details not proven). If only some are present, it is a mismatch.
credentialStatus entries
| Entry | Fields |
|---|---|
| Chain entry | type CertichainChainStatus, chainId (decimal string), registry (address), credentialId (32 bytes). Must name the signing domain's chain and registry and the recomputed id. |
| List entry | id = <statusListCredential>#<statusListIndex>, type BitstringStatusListEntry, statusPurpose revocation or suspension, statusListIndex (decimal string below 131,072), statusListCredential (a list address under /api/status-lists/<chainId>/<registry>/<issuer>/<purpose>/<list>). |
The list entries are not covered by the issuer signature. The app recomputes them from the chain and refuses entries that point anywhere else. See Status lists.
proof (cryptosuite certichain-eip712-2026)
| Field | Type | Required | Meaning and validation |
|---|---|---|---|
type | string | yes | DataIntegrityProof. |
cryptosuite | string | yes | certichain-eip712-2026. |
proofPurpose | string | yes | assertionMethod. |
created | string | yes | Equal to validFrom (the signed issuedAt). |
verificationMethod | string | yes | <issuer did:pkh>#blockchainAccountId. |
proofValue | string | yes | f followed by the lowercase hex of the signature (multibase base16). For an externally owned account the only accepted spelling is 65 bytes ending in 1b or 1c; any other value is left to the chain's ERC-1271 check. |
signingDomain | object | yes | name Certichain, version 1, chainId (decimal string), verifyingContract (the CredentialRegistry). Name and version are pinned. |
signedFields | object | yes | The eight values the issuer signed (below). |
metadataSalt | string | no | 32-byte salt that opens metadataHash. |
subjectSalt | string | no | 32-byte salt that opens subjectCommitment. |
signedFields:
| Field | Type | Range |
|---|---|---|
issuer | address | |
subjectCommitment | bytes32 | |
documentHash | bytes32 | SHA-256 of the issued document |
metadataHash | bytes32 | |
recordType | number | 0 to 2^32 - 1 |
issuedAt | number | Unix seconds |
validUntil | number | Unix seconds, 0 for no expiry |
nonce | string | Decimal, up to 2^256 - 1 |
Example VC
This is the shared golden vector vc/test-vectors/golden-vc.json (a test credential for "Asha Rai", S-1001, issued by a Hardhat test account). The embedded context is shortened here; the real file carries the whole object.
{
"@context": [
"https://www.w3.org/ns/credentials/v2",
{ "@version": 1.1, "@protected": true, "CertichainCredential": "urn:certichain:vocab:CertichainCredential", "...": "..." }
],
"id": "urn:certichain:eip155:11155111:0x475Bb8555d160c85f0175df117576193984C8083:0x6c4ee9b64594000fcff7304678b1db3fbee9a5bb3c56436d8156c44d521c618f",
"type": ["VerifiableCredential", "CertichainCredential"],
"issuer": { "id": "did:pkh:eip155:11155111:0x70997970C51812dc3A010C7d01b50e0d17dc79C8" },
"validFrom": "2027-01-15T08:00:00Z",
"credentialSubject": {
"recordType": 4,
"holderName": "Asha Rai",
"studentId": "S-1001",
"programme": "Computer Science",
"award": "BSc"
},
"credentialStatus": {
"type": "CertichainChainStatus",
"chainId": "11155111",
"registry": "0x475Bb8555d160c85f0175df117576193984C8083",
"credentialId": "0x6c4ee9b64594000fcff7304678b1db3fbee9a5bb3c56436d8156c44d521c618f"
},
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "certichain-eip712-2026",
"proofPurpose": "assertionMethod",
"created": "2027-01-15T08:00:00Z",
"verificationMethod": "did:pkh:eip155:11155111:0x70997970C51812dc3A010C7d01b50e0d17dc79C8#blockchainAccountId",
"proofValue": "f56a1fae66d584b7470d4db1f4e74db59e7b1566c75b3cb0d4801505a173a9dae6e183967eb06543cc1c9056b25a6cc5b0c56743ead537d90f657e0898beff0c71c",
"signingDomain": { "name": "Certichain", "version": "1", "chainId": "11155111", "verifyingContract": "0x475Bb8555d160c85f0175df117576193984C8083" },
"signedFields": {
"issuer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"subjectCommitment": "0x0de72e2aa4fde8c5058a57074f6a80d78d9d6f95b172f36e3e60522ad51639e6",
"documentHash": "0xba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
"metadataHash": "0xad52dfcdc40c7e615de64764601fcb8dcc869d5dcf9201e88117aff776610e28",
"recordType": 4,
"issuedAt": 1800000000,
"validUntil": 0,
"nonce": "7"
},
"metadataSalt": "0xa1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"subjectSalt": "0xb2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2"
}
}
Open Badges 3.0 document
A badge has the same id, validFrom, validUntil and Certichain proof as the VC of the same package. These members differ:
| Field | Type | Required | Meaning |
|---|---|---|---|
@context | array | yes | VC 2.0 context, https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json, and the embedded Certichain context (with an extra keyBinding term when the badge has the standard proof). |
type | array | yes | ["VerifiableCredential", "OpenBadgeCredential"]. |
name | string | yes | The award, else the programme, else the record type name. |
issuer | object | yes | id (did:pkh, or the derived did:key with the standard proof), type ["Profile"], optional name, and with the standard proof one otherIdentifier entry { type: "IdentifierEntry", identifier: <did:pkh>, identifierType: "ext:didPkh" }. |
credentialSubject | object | yes | type ["AchievementSubject"], optional id (did:pkh), optional identifier (two IdentityObjects with hashed: false: identityType name and ext:studentId, the value in identityHash), achievement, recordType, optional programme and award. |
credentialSubject.achievement | object | yes | id urn:certichain:achievement:<chainId>:<issuer>:<recordType>:<8 hex>, type ["Achievement"], achievementType, name, description ("<type name> recorded on the Certichain registry."), criteria.narrative (fixed text), optional fieldOfStudy (the programme). |
credentialStatus | object | yes | One entry: the chain entry, or the revocation BitstringStatusListEntry when lists are on. |
evidence | array | no | One Evidence with id <site>/verify?id=<credentialId>, name "Certichain anchor", description "Checks this credential against the Certichain registry." |
proof | object or array | yes | The Certichain proof alone, or [standardProof, certichainProofWithKeyBinding]. |
Every badge claim that is not signed must be derivable from signed values or be fixed text. The checker recomputes the name, achievement, achievement type, identifiers and evidence and reports any difference by name (identifier, achievementType, name, achievement, evidence, title, issuerKey).
Standard proof and key binding
With the standard proof, proof[0] is an eddsa-rdfc-2022 Data Integrity proof made by the issuer's derived Ed25519 key. Its verificationMethod is <did:key>#<multibase key>. proof[1] is the Certichain proof plus:
| Field | Type | Meaning |
|---|---|---|
keyBinding.keyVersion | number | Key version (1 in the app). |
keyBinding.publicKey | string | 32-byte Ed25519 public key, hex. Must be the key in the issuer did:key. |
keyBinding.attestation | string | The issuer wallet's EIP-712 signature over OpenBadgeKeyBinding { registry, issuer, publicKey, keyVersion } (domain Certichain keys, version 1, chain id). |
issuerKey fails unless the otherIdentifier names the registered did:pkh, the verification method matches the did:key, the public key matches, and the attestation recovers to the issuer for this registry and chain.
Versioning
- There is no
versionmember. The format is pinned by the exact context object, the types and the cryptosuite name. A change to the embedded context changes its canonical form, which a test pins (src/lib/vc/golden.test.ts), and older parsers would refuse it. - The proof never depends on JSON-LD processing, so the embedded context could later move to a URL without changing any signature.
- The context and schema files are kept in the repository under
vc/contexts/(certichain-v1.jsonldwith its SHA-256,credentials-v2.json,ob-v3p0-context-3.0.3.json) andvc/schemas/(certichain-credential.schema.json,ob_v3p0_achievementcredential_schema.json).
How it is checked
- Parse (
parseVcorparseOpenBadge): size, JSON, exact keys, pinned context and types, formats of every field. - Offline (
verifyViewOffline, plusverifyOpenBadgeOfflineandcheckOpenBadgefor badges): consistency of claims with the signed fields, id recomputation, details against the hashes, signature recovery, expiry, badge claims, key binding, standard proof. - On chain (
VcService): same chain and registry, credential found, anchor equals signed fields, issuer registered now,isValid, ERC-1271 when needed, holder binding when a subject id is present. - Status lists (
StatusListChecker), only when the file has list entries and the page has lists configured.
Source files
src/lib/vc/constants.ts,src/lib/vc/types.ts,src/lib/vc/fields.ts,src/lib/vc/parse.ts,src/lib/vc/export.ts,src/lib/vc/verify.ts,src/lib/vc/detect.tssrc/lib/vc/status/entry.ts,src/lib/vc/status/location.tssrc/lib/vc/openbadges/types.ts,src/lib/vc/openbadges/constants.ts,src/lib/vc/openbadges/export.ts,src/lib/vc/openbadges/parse.ts,src/lib/vc/openbadges/verify.tssrc/lib/vc/openbadges/standard/key.ts,src/lib/vc/openbadges/standard/export.ts,src/lib/vc/openbadges/standard/eddsa-rdfc-2022.tsvc/cryptosuite-certichain-eip712-2026.md,vc/contexts/,vc/schemas/,vc/test-vectors/golden-vc.json
Tests
src/lib/vc/parse.test.ts,src/lib/vc/export.test.ts,src/lib/vc/verify.test.ts,src/lib/vc/detect.test.ts,src/lib/vc/golden.test.ts,src/lib/vc/schema.test.tssrc/lib/vc/openbadges/parse.test.ts,export.test.ts,verify.test.ts,schema.test.tssrc/lib/vc/openbadges/standard/key.test.ts,export.test.ts,verify.test.ts,verify.errors.test.ts,eddsa-rdfc-2022.test.tsinterop/(Digital Bazaar verifier checks)