Feature: Certificate Validation#295
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an optional, on-chain certificate-chain gate to Anchor HTTP services (and wires it into the Storage service) so authenticated callers can be required to present a published certificate chaining to a trusted issuer, with structured error reporting and storage client tests covering the new behavior.
Changes:
- Introduced
requireCertificateChainserver config and a resolved/cached requirement onKeetaNetAnchorHTTPServer. - Implemented certificate-chain verification utilities + a new structured user error (
KeetaAnchorCertificateRequiredError) for missing/untrusted chains. - Enforced the gate across Storage authenticated endpoints and added Storage client tests for 401/403 failure modes.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/storage/server.ts | Threads resolved certificate-chain requirement through auth helpers to enforce the gate on signed requests. |
| src/services/storage/common.ts | Exposes CertificateRequired in Storage Errors for client-side deserialization/matching. |
| src/services/storage/client.test.ts | Adds a harness to mint/publish cert chains and tests missing/untrusted rejection behavior. |
| src/lib/http-server/index.ts | Adds requireCertificateChain config, resolves it once, and provides a helper to enforce it. |
| src/lib/certificates.ts | Implements chain verification + new KeetaAnchorCertificateRequiredError + config resolution helpers. |
| src/lib/certificates.generated.ts | Adds typia assertion for the new error JSON payload shape. |
chpzcch
left a comment
There was a problem hiding this comment.
Review of PR 295 — Certificate Validation Gate
Issue 1 — server.ts bypasses the base class assertCertificateChain() method
The base class adds protected async assertCertificateChain(account: Account) precisely so subclasses don't need to import and wire the underlying function manually. But KeetaNetStorageAnchorHTTPServer imports assertAccountCertificateChain directly from certificates.ts and threads resolvedCertificateChainRequirement through verifyBodyAuth / verifyURLAuth as a parameter.
This creates two parallel paths for the same gate. The assertCertificateChain method on the base class is dead code for the storage service. Future subclasses will have to decide which pattern to follow.
Suggest: Either remove assertCertificateChain from the base class (if the parameter-threading pattern is the intended one), or refactor verifyBodyAuth / verifyURLAuth to be instance methods that use this.assertCertificateChain() so the base class method is actually used.
Issue 2 — Silent swallow of Certificate constructor errors in verifyAccountCertificateChain
try {
candidate = new Certificate(record.certificate.toPEM(), {
store: { root: rootSet, intermediate }
});
} catch {
continue; // ← any construction error silently treated as "cert doesn't count"
}A malformed or structurally invalid on-chain certificate silently falls through and does not contribute to chain verification. If an attacker could publish a cert that throws during construction but the account also has no valid cert, they'd still get 'untrusted' rather than leaking information — so this isn't a security regression.
However, the silent swallow makes debugging hard: an operator who publishes a cert with an unexpected format will see 401/403 with no signal about why their cert was rejected. Consider logging the error at debug level before continuing.
Issue 3 — Missing test: public route still serves when requireCertificateChain is set
The existing tests confirm that authenticated routes correctly enforce the cert gate (401 for missing, 403 for untrusted). But there's no test that GET /api/public/** continues to serve anonymous callers when requireCertificateChain is configured.
The route intentionally does not call assertAccountCertificateChain — the signer of a pre-signed public URL may have no cert published (e.g. a service account), and the fetch is done by a third party with no account at all. This is the correct design.
But without a test, the exemption is invisible. A future refactor that accidentally wires requireCertificateChain into the public route would have no regression safety net.
Suggest: Add a test case in client.test.ts that, with { useCertChain: true } configured, generates a pre-signed public URL (signed by the cert-holding account) and confirms an anonymous caller can fetch it successfully.
Issue 4 — implements Required<KeetaAnchorHTTPServerConfig> with an optional field
KeetaAnchorHTTPServerConfig.requireCertificateChain is CertificateChainConfig | undefined. The class declares readonly requireCertificateChain: CertificateChainConfig | undefined, which satisfies Required<> because Required<> only removes the ? modifier — it does not exclude undefined from the value type. So TypeScript is happy here.
This is worth noting because it can surprise maintainers: Required<T> does not mean "never undefined", it means "not optional". This is already the pattern used by logger (Logger | undefined). No change needed, but the JSDoc on the interface field makes this slightly confusing — consider a brief note that undefined is the off state.
No analytics files are touched. SonarCloud passed at 91.7% coverage on new code.
Code Review SummaryIssues reviewed: No linked issue found in the PR body. The PR summary describes the feature directly: optional on-chain certificate-chain gate for Anchor HTTP services. Threads addressed: 2 threads reviewed (both already resolved before this review)
New issues found: Must FixNone Consider Simplifying
Minor / Optional
|
|
…at/cert-validated-anchors



Summary
Adds an optional, on-chain certificate-chain gate to Anchor HTTP services.