Building the GitHubContributorCredential: SD-JWT Design and Hedera DID Verification
This week's goals were:
- Define the complete
GitHubContributorCredentialschema, including contributor DID, GitHub username, GitHub numeric account ID, verified GPG fingerprint, issuer DID, issuance time, expiry, and credential status reference. - Design the SD-JWT disclosure policy and determine which claims are always disclosed versus selectively disclosed.
- Create example credential payloads and schema fixtures that can be reused throughout implementation and testing.
- Validate the credential model with unit tests covering valid and invalid credential subjects.
- Create and resolve a
did:hederaDID on Hedera testnet using the operator credentials agreed during Week 1. - Capture proof of successful DID creation and resolution through HashScan and resolver outputs.
- Implement deterministic verification-method selection so signing operations never depend on array ordering such as
verificationMethod[0]. - Add tests covering DID resolution and verification-method selection behavior.
Here's how the week went, day by day.
1. Defining the GitHubContributorCredential schema
GitHubContributorCredential holds every field our system needs to identify a contributor. To keep it usable by the Heka backend service, we split the fields into two categories: standard VC metadata (the outer wrapper) and the credential subject (the actual identity claims).
Standard VC metadata
-
Issuer DID — the
did:hederaidentifier of the Heka platform issuing the credential. - Issuance time — the ISO 8601 timestamp of when the proof was generated.
- Expiry — when the credential needs to be renewed (e.g., six months or a year from issuance).
- Credential status reference — a pointer to the revocation registry on the Hedera network, so Heka can revoke the credential if a contributor's keys are compromised.
Credential subject (the contributor claims)
-
Contributor DID — the developer's personal
did:hederaidentifier. -
GitHub username — the human-readable handle (e.g.,
darshit2308). - GitHub numeric account ID — the immutable integer ID GitHub assigns to every account. This matters because a user can change their handle, but the numeric ID never changes.
- Verified GPG fingerprint — the cryptographic fingerprint of the key used to sign commits, proving the holder controls the private key registered against their GitHub account.
Putting it all together, here's the schema draft:
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://w3id.org/security/suites/ed25519-2020/v1"
],
"id": "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5",
"type": ["VerifiableCredential", "GitHubContributorCredential"],
"issuer": "did:hedera:testnet:z6MkhaXgBZDvotDkL5257faiztiuC2ZXOS",
"issuanceDate": "2026-06-28T00:00:00Z",
"expirationDate": "2027-06-28T00:00:00Z",
"credentialStatus": {
"id": "https://heka.network/status/123",
"type": "CredentialStatusList2020"
},
"credentialSubject": {
"id": "did:hedera:testnet:z6Mkq...",
"githubUsername": "darshit2308",
"githubAccountId": 4115704,
"gpgFingerprint": "3AA5C34371567BD2"
}
}
A quick walkthrough for anyone new to verifiable credentials:
-
@contexttells any parser "read this using W3C Verifiable Credentials rules" — it gives the document its vocabulary. -
idis just a unique identifier for the credential document itself. -
typedeclares what kind of credential this is. - Everything else maps to the eight fields described above.
Note for later:
CredentialStatusList2020isn't an officially standardized status type — the current W3C recommendation is the Bitstring Status List spec (BitstringStatusListEntry). Worth revisiting before this goes to production, even ifCredentialStatusList2020stays as an internal placeholder for now.
2. Designing the SD-JWT disclosure policy
Quick primer on SD-JWT (Selective Disclosure JWT) for anyone unfamiliar: it lets a credential holder reveal only the claims required to verify something, without exposing everything else. A simple analogy — a bouncer checking you're over 18 doesn't need your phone number or home address, just proof of age.
So we split GitHubContributorCredential into two buckets: fields that are always disclosed (mandatory for the system to function) and fields that are selectively disclosed (hidden by default, to protect the developer's privacy).
Always disclosed
- Issuer DID — Heka needs to know who issued the credential.
- Issuance & expiry dates — Heka needs to verify the time limits.
- Credential status — Heka must be able to check whether it's been revoked.
-
Contributor DID (
sub) — the developer's public identifier on the Hedera network.
Selectively disclosed
- GitHub username — a developer may want to prove they're an authorized LFDT contributor without permanently linking their Hedera DID to a public GitHub handle anyone could scrape.
- GitHub numeric account ID — kept hidden unless the backend specifically needs to map the DID to an exact GitHub account.
- Verified GPG fingerprint — only revealed when a repository explicitly needs it to verify a signed commit.
3. Example payloads and schema fixtures
Since we won't reveal every field by default, the SD-JWT payload replaces the hidden fields with an _sd (selective disclosure) array of hashes. A first pass looked like this:
{
"iss": "did:hedera:testnet:z6MkhaXgBZDvotDkL5257faiztiuC2ZXOS",
"iat": 1782604800,
"exp": 1814140800,
"sub": "did:hedera:testnet:z6Mkq...",
"vct": "GitHubContributorCredential",
"_sd": [
"JzYx0... (hash of githubUsername)",
"Qp2Lm... (hash of githubAccountId)",
"Xy9Rq... (hash of gpgFingerprint)"
],
"_sd_alg": "sha-256"
}
Public (always-disclosed) fields: iss (issuer), iat (issuance time), exp (expiry), sub (contributor DID), and vct (the credential type — since it's not in _sd, it's visible by default too).
Hidden fields: username, account ID, GPG fingerprint.
_sd_alg declares exactly which hash function was used, so the verifier knows how to check the disclosures later.
The verifier can't just brute-force the hidden fields from the hashes, since that would defeat the point — anyone could disclose everything by guessing. So each hidden claim is paired with a random salt before hashing: the disclosure is really hash(salt + claim_name + claim_value), and only the holder can produce the matching salt+value pair when asked to disclose it.
Where this landed
The finished schema fixture (JSON Schema for the credential subject):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "GitHubContributorCredentialSubject",
"description": "The exact rules for Heka GitHub Contributor claims",
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^did:hedera:.*",
"description": "The developer's Hedera DID"
},
"githubUsername": {
"type": "string",
"minLength": 1
},
"githubAccountId": {
"type": "integer",
"minimum": 1
},
"gpgFingerprint": {
"type": "string",
"pattern": "^[0-9A-F]{16,40}$",
"description": "Uppercase HEX fingerprint"
}
},
"required": ["id", "githubUsername", "githubAccountId", "gpgFingerprint"],
"additionalProperties": false
}
Note: the
gpgFingerprintpattern allows 16–40 hex characters. A full GPG fingerprint is conventionally 40 hex characters (SHA-1, 160-bit); a 16-character value is really a GPG key ID, not a fingerprint. Worth tightening this to exactly 40 characters unless we genuinely want to accept short key IDs too.
And the final credential payload fixture, now including credentialStatus so it matches the "always disclosed" policy from Section 2:
{
"iss": "did:hedera:testnet:z6MkhaXgBZDvotDkL5257faiztiuC2ZXOS",
"iat": 1782604800,
"exp": 1814140800,
"sub": "did:hedera:testnet:z6MkqExampleDeveloperDID",
"vct": "GitHubContributorCredential",
"credentialStatus": {
"id": "https://heka.network/status/123",
"type": "CredentialStatusList2020"
},
"_sd": [
"JzYx0aB9... (hash of 'githubUsername': 'darshit2308')",
"Qp2LmC4... (hash of 'githubAccountId': 4115704)",
"Xy9RqZ1... (hash of 'gpgFingerprint': '3AA5C34371567BD2')"
],
"_sd_alg": "sha-256"
}
These live in the repo as:
-
heka-identity-service/src/credentials/fixtures/github-contributor-credential.schema.json— the JSON Schema defining what a validGitHubContributorCredentialsubject looks like, the same way a government ID schema defines which fields (name, date of birth, etc.) must be present. -
heka-identity-service/src/credentials/fixtures/mock-sd-jwt-payload.json— a mockGitHubContributorCredentialfor a fictional contributor, used across tests.
4. Validating the model with Jest
This was my first time writing Jest tests, so a quick summary of what I learned.
We use AJV (Another JSON Schema Validator) to check whether a piece of JSON data conforms to a given JSON Schema — for example, confirming githubAccountId is actually an integer, as the schema requires.
The tests live in heka-identity-service/src/credentials/credential-subject.spec.ts. The .spec.ts suffix is Jest's convention for a specification (test) file, and its job is to verify the schema behaves correctly against both valid and invalid inputs.
A small helper does the heavy lifting of turning a fixture file into a usable object:
const loadFixture = (fileName: string) => {
return JSON.parse(readFileSync(join(__dirname, "fixtures", fileName), "utf8"));
};
From there, the test suite exercises as many valid and invalid permutations of the credential subject as we could think of — missing fields, wrong types, malformed DIDs, malformed fingerprints — to make sure AJV rejects exactly what it should and accepts exactly what it should.
5. did:hedera DID creation and deterministic verification-method selection
Using the operator credentials agreed on in Week 1, we created and resolved a did:hedera DID on Hedera testnet, with proof of creation and resolution captured via HashScan and the resolver output.
We also implemented deterministic verification-method selection, so signing operations always pick a specific, well-defined verification method rather than relying on array position (e.g., verificationMethod[0]), which isn't guaranteed to be stable. Tests now cover both DID resolution and this selection logic.
The code-level detail for both of these — along with the full schema, fixtures, and Jest suite above — is in the PR linked below; it's a better reference than a paraphrase here.
PR for reference: hiero-ledger/heka-identity-platform#179
Top comments (0)