DEV Community

Darshit Khandelwal
Darshit Khandelwal

Posted on

Week 4 - LFX Mentorship

A quick note before I start: I was running slightly behind schedule, so I combined Weeks 3 and 4 into one push. The good news is that what came out the other side is more complete than either week would have been in isolation — the two pieces fit together naturally, and building them back-to-back made the design tighter.

Here's everything that happened.


The Problem These Two Weeks Solve

Before Week 3, we had a schema on paper and a DID on the testnet. But nothing was actually checking anything. No one was being verified. No PR was being gated.

The gap was this: how do you take "this person claims to be darshit2308 on GitHub" and turn it into something a machine can trust cryptographically?

That's what Week 3 answered. And Week 4 answered the follow-up question: once you've verified the GPG key, how do you bind that proof permanently to a real identity — one that ties together their GitHub account, their GPG key, and their Heka wallet — so the system doesn't have to re-verify every single time?

Let's go through both.


Week 3: The Cryptographic Foundation

How GPG Challenge-Response Works (and Why We're Doing It This Way)

The core idea is simple but elegant. Your GitHub profile can optionally have GPG public keys attached to it. Anyone can see them. What only you can do is sign something with the matching private key — which never leaves your machine.

So the verification flow goes like this:

  1. You ask Heka for a challenge — a random one-time string called a nonce
  2. Heka generates the nonce, ties it to your GitHub username, and starts a 5-minute countdown
  3. You run one command locally: echo "<nonce>" | gpg --clearsign
  4. You send the signed output back to Heka
  5. Heka fetches your public GPG key directly from github.com/your-username.gpg and checks the math

If the signature is valid, it proves two things at once: you control the private key, and that private key is registered to your GitHub account. You can't fake either of those.

This is not a new idea — it's the same mechanism behind SSH authentication and code signing. We're applying it to contributor identity.

Building the Module

The GPG module lives inside heka-identity-service/src/gpg-challenge/ as a self-contained NestJS module. Three endpoints:

POST /request — Generate a challenge. Takes a GitHub username, returns a nonce. Under the hood, it uses crypto.randomBytes(32) — 256 bits of OS-level randomness — and stores it with a 5-minute expiry.

POST /verify — Verify a signed challenge. This is where the interesting work happens.

GET /status — Check if a GitHub username is verified.

The Security Detail I'm Most Proud Of: Burn Before Verify

The replay attack scenario goes like this: someone intercepts a valid signed payload in transit, and tries submitting it again after you've already verified. If we mark the nonce as consumed after verification succeeds, and the process crashes between those two steps, the attacker can replay the same payload and it'll pass again.

The fix is simple but requires thinking about failure modes:

challenge.consumed = true;
await this.challengeRepo.getEntityManager().flush(); // This happens FIRST
// Only after the DB write commits do we touch the network
Enter fullscreen mode Exit fullscreen mode

The nonce is marked consumed and flushed to the database before any network call or cryptographic work happens. Even if the server crashes mid-verification, the nonce is permanently dead. The contributor just needs to request a fresh one.

This is called "burn before verify" and it eliminates the replay window entirely.

Error Handling: Every Failure Path

One thing I wanted to get right early was making errors actually useful to contributors. There are 10 distinct failure scenarios in this module, and each one returns a specific HTTP status and a message that tells you what to do:

What went wrong Status
Challenge ID doesn't exist 404
Nonce already used (replay blocked) 400
Challenge expired 400
No GPG key on your GitHub profile 400
GitHub rate limiting us 429
GitHub unreachable 503
GPG key body is empty 400
Key format is unparseable 400
Signed message is malformed 400
Signature is mathematically wrong 200 { verified: false }

That last one is deliberate — a failed signature isn't a server error, it's a valid answer from the verification system. It returns 200 with verified: false rather than throwing an exception.

25 Tests, All Passing

  • 16 unit tests covering every failure path above, with the burn-before-verify ordering explicitly tested (the test verifies that flush() is called before the GitHub API mock fires)
  • 9 integration tests against a real PostgreSQL instance, covering the full HTTP stack including replay via double-submit and expired record insertion via ORM

The GitHub App: Making PRs Actually Respond

The GitHub App lives in a separate repository (GitHub integration code stays separate from heka-identity-service, as agreed in the architecture). It's built on Probot.

Why Probot? Two things I would have had to build myself are handled automatically:

Webhook signature validation: Every incoming request from GitHub is signed with HMAC-SHA256. Probot validates that signature before any application code runs. Invalid signature → 400, nothing fires.

GitHub App authentication: GitHub Apps authenticate using JWT tokens that expire every 10 minutes. Probot manages the entire JWT lifecycle. I never touch it.

The async acknowledgment problem: GitHub requires your webhook endpoint to respond with HTTP 200 within 10 seconds. But checking Heka, creating a check run, and doing DID resolution can take longer than that under real network conditions. Probot solves this cleanly — it returns 200 immediately, and fires application listeners asynchronously through its internal event emitter. The listeners are not in the response path.

Idempotency: GitHub can and does redeliver webhook events. Without idempotency, you'd end up with multiple check runs for the same commit cluttering the PR view. Before creating a new check run, the app looks up whether one already exists for the exact commit SHA and check name. If a non-queued run exists, it exits early. The key is using the SHA (not the PR number) — a new push to the same PR produces a new SHA and correctly triggers a new check, while a retried webhook delivery hits the guard and does nothing.

End-to-end proof: The full flow was validated live on the Heka-Webhook-Fixture repository. A PR opened, Heka's identity check fired, and the check run appeared in the GitHub UI. That closed the loop on Week 3.


Week 4: Binding the Proof to a Real Identity

Week 3 answered "can you prove you own this GPG key?" Week 4 answered the follow-up question: "okay, but now what do we actually do with that proof?"

The answer is a ContributorBinding — a record that ties together three things permanently:

  1. The GitHub OAuth account (with its numeric account ID)
  2. The verified GPG fingerprint
  3. The Heka wallet

Why all three? Because each one alone is insufficient:

  • GitHub account alone is spoofable
  • GPG key alone doesn't prove which GitHub account you are
  • Wallet alone is just a key pair with no human identity attached

The binding is what makes the proof durable. Once created, the system can verify you on future PRs without requiring a fresh GPG signature every single time.

What Changed in the Database

The GpgChallenge entity was upgraded to track not just githubUsername, but also:

  • githubAccountId — the immutable integer ID from GitHub (users can change their handle, but this never changes, which is how we handle username renames without losing their verification status)
  • walletId — the Heka wallet of the authenticated user who requested the challenge

Audit Logging: Every State Transition is Recorded

The ContributorOnboardingModule is now wired directly into the GPG verification flow. Every meaningful event gets recorded:

  • recordChallengeRequested — fires when a challenge is generated, against the contributor's wallet profile
  • recordProofAccepted — fires when verification succeeds
  • recordProofRejected — fires when verification fails

This matters for maintainers. If something goes wrong with a contributor's verification status, there's a full audit trail of what happened and when.

The Wallet Enforcement Check

This one is a subtle but important security detail. When a contributor submits their GPG signature for verification, we now check that the walletId submitting the request matches the walletId that originally requested the challenge.

Why does this matter? Without it, someone could theoretically observe that wallet A requested a challenge for username X, then try to claim that verification from wallet B. The wallet enforcement check closes that gap.

The Status Endpoint Rework

The /status endpoint in Week 3 checked the GPG challenges table directly — if a challenge was verified, you're good. That's fine for a prototype, but it has a flaw: it only tells you the GPG proof succeeded. It doesn't tell you whether the full binding (GPG key + GitHub account + Heka wallet) was actually completed.

In Week 4 the endpoint was refactored to check for a finalized ContributorBinding record instead. This means:

  • verified: true now means the GPG key, GitHub account, and Heka wallet are all tied together and confirmed
  • There's no state where partial verification gets treated as complete
  • The GitHub App can trust the status endpoint as a real signal, not just an intermediate step

How the Two Weeks Connect

The progression makes more sense when you see it as one arc:

Week 3 built the cryptographic primitive: "this person controls this GPG key."

Week 4 turned that primitive into a durable identity claim: "this GPG key, this GitHub account, and this Heka wallet all belong to the same person."

Without Week 3, you have no cryptographic proof. Without Week 4, that proof evaporates after each verification and doesn't attach to anything persistent. Together, they're the foundation that Weeks 5 and beyond (OID4VCI issuance, the Web Wallet, PR-gating) are built on top of.


What's Next

Week 5 is OID4VCI issuer metadata and SD-JWT VC issuance — taking the GithubContributorCredential schema from Week 2 and wiring it into Heka's OpenID4VC issuance flow so contributors actually receive a signed credential into their wallet.

The schema design, the DID infrastructure, the GPG verification, and the identity binding are all in place. Next week is where it starts issuing real credentials.

See you then.


This is part of my LFDT Mentorship 2026 blog series. Each week I write about what I built, what I learned, and what surprised me. If you're working on decentralized identity or open source supply chain security, I'd love to hear from you.

Top comments (0)