Skip to content

fix(security): issuer binding + fail-closed verification (SOC-32, SOC-35)#5

Open
chongkan wants to merge 2 commits into
mainfrom
fix/soc-32-35-vc-sdk-issuer-binding
Open

fix(security): issuer binding + fail-closed verification (SOC-32, SOC-35)#5
chongkan wants to merge 2 commits into
mainfrom
fix/soc-32-35-vc-sdk-issuer-binding

Conversation

@chongkan

Copy link
Copy Markdown
Member

Summary

Security release for @attestto/vc-sdk (v0.2.0 → 0.3.0). Verification was fail-open and not bound to the issuer, so an unsigned/unresolvable credential could report valid: true (SOC-35), and a credential could claim a trusted issuer while signed by an attacker's unrelated key (SOC-32). Both are now closed, fail-closed by default. Closes SOC-32, SOC-35.

SOC-32 — issuer binding

checkProofs now returns allValid && issuerBoundValid: every proof must verify and at least one valid proof must be bound to credential.issuer — either the proof's verificationMethod DID equals the issuer, or the optional VerifierConfig.verifyIssuerBinding(issuer, verificationMethod) hook authorizes it (DID-document assertionMethod delegation; fails closed if the hook throws). Multi-party co-signers from other DIDs remain supported (they must also verify). Blocks the "claim a trusted issuer, sign with your own key" forgery.

SOC-35 — fail-closed verification

Missing proof, missing resolver, unresolved key, or bad signature now push errors (not warnings), so valid can never be true for an unverifiable credential. Adds VerificationResult.signatureVerified and VerifyOptions.requireSignature (default true).

Breaking change (consumers)

valid: true now requires a verified signature bound to the issuer:

  • Callers relying on the fail-open path (proof present, no resolver → valid) must supply a resolver or set requireSignature: false.
  • Delegated signing keys: if verificationMethod's DID differs from issuer (e.g. issuer did:web:x signing with a separate assertionMethod key), you MUST provide verifyIssuerBinding or the default direct-binding check rejects it. cr-vc-sdk (SOC-16/19) and attestto-app (SOC-10) must wire this hook if they use delegated keys.

Verification

  • 155/155 tests pass (+7 security, incl. a genuine forgery — valid attacker signature with mismatched issuer → rejected; multi-party co-signing still works).
  • tsc --noEmit clean, tsup build green. CHANGELOG updated (Keep a Changelog / SemVer).

Note

Also bumps to 0.3.0 and adds @vitest/coverage-v8 dev dep (+ lockfile, kept consistent) so pnpm test:coverage works — the security change itself is src/verifier.ts + src/types.ts + the new tests/verifier-security.spec.ts.

Attestto Open added 2 commits July 19, 2026 11:22
…OC-32, SOC-35)

Verification was fail-open and unbound to the issuer: an unsigned or
unresolvable credential could report valid:true (SOC-35), and a credential could
claim a trusted issuer while being signed by an attacker's unrelated key
(SOC-32). Both closed, fail-closed by default.

SOC-32 — issuer binding (src/verifier.ts checkProofs):
- Requires at least one valid proof bound to credential.issuer: the proof's
  verificationMethod DID equals the issuer, or the new optional
  VerifierConfig.verifyIssuerBinding(issuer, verificationMethod) hook authorizes
  it (assertionMethod delegation; fails closed if the hook throws). Every proof
  must still verify; multi-party co-signers from other DIDs remain supported.
  checkProofs now returns allValid && issuerBoundValid.

SOC-35 — fail-closed (src/verifier.ts verify):
- Missing proof / missing resolver / unresolved key / bad signature now push
  errors, not warnings. New VerificationResult.signatureVerified and
  VerifyOptions.requireSignature (default true; opt-out only for explicit
  structural-only checks).

BREAKING: valid:true now requires a verified signature bound to the issuer.
Consumers using delegated signing keys (verificationMethod DID != issuer DID)
must supply verifyIssuerBinding, or the default direct-binding check rejects
them. Downstream cr-vc-sdk (SOC-16/19) and attestto-app (SOC-10) must wire this.

Also: bump to 0.3.0, add @vitest/coverage-v8 dev dep (+ lockfile) so
'pnpm test:coverage' works (kept package.json and pnpm-lock consistent).

Tests: 155 pass (+7 security, incl. a genuine forgery — valid attacker signature
with mismatched issuer -> rejected). typecheck clean, build green.
The workflow ran 'npm ci', but this repo has only pnpm-lock.yaml (no
package-lock.json), so install always failed. Switch to pnpm/action-setup +
pnpm install --frozen-lockfile. Lint marked non-blocking (eslint not installed
yet — tracked SOC-60); Stub guard + Test + Build gate PRs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant