Skip to content

Expiry and revocation no longer halt verification - #75

Merged
ottonomy merged 4 commits into
mainfrom
fix/expiry-and-status-no-halt
Oct 21, 2026
Merged

ottonomy merged 4 commits into
mainfrom
fix/expiry-and-status-no-halt

Conversation

@ottonomy

Copy link
Copy Markdown
Contributor

Closes #63. Closes #64.

Why

fatal decided two things at once: a failure made verified false, and it halted every later check. So:

What changed

  • halts?: boolean on VerificationCheck / CheckResult, defaulting to fatal. A fatal check with halts: false still makes verified false, but the checks after it run. Existing checks and caller suites behave as before, and their rows keep their shape.

  • New proof.validity-period check (fatal, halts: false), run after proof.signature. It judges validFrom/validUntil (v2) or issuanceDate/expirationDate (v1) against the verifier's TimeService, and reports CREDENTIAL_EXPIRED / CREDENTIAL_NOT_YET_VALID.

  • proof.signature reads a date-only rejection as a verified signature. The Data Integrity crypto service reports vc's date failure as a typed problem only when all of these hold:

    • it is a plain Error;
    • it is not a VerificationError;
    • it has no errors[];
    • its message matches vc's wording in full.

    So a date phrase quoted inside a wrapped proof error never counts. The CryptoService TSDoc states the contract.

  • status.bitstring stays fatal but sets halts: false. It now classifies an expired status list by problem type, not by message text.

  • status.list-issuer compares only lists whose proof verified (or was rejected only for its dates), so it now runs, and reports, after a revocation mark. Each list's proof verification is memoised per verification, beside the existing memo of loaded lists, so both checks share one verification per list.

  • Docs: README, docs/api/verification-results.md, docs/architecture.md, CHANGELOG. More than one suite can now carry fatalFailureAt; verified is false exactly when any does.

verified does not change for any input.

Checked locally

  • pnpm run build: clean.
  • pnpm run test (lint + vitest): 941 passed, up from 907 on main.
  • pnpm exec tsc -p tsconfig.dev.json: the same 6 errors as main.
  • The ordering guard. Offline tests with real signatures check that a credential that is tampered and expired (v1 and v2) still reports a signature failure, with proof.validity-period "Not run". If a future vc checked dates before the proof, these tests fail.
  • Removing each safeguard fails its test. Checked for the VerificationError name check, the errors[] check, the anchored match, proof.signature passing any rejection, status.bitstring's halts: false, and list-issuer trusting unverified lists.
  • New offline real-signature cases:
    • revoked + expired → both problems; list issuer and registry still run;
    • a mark on a foreign list → CREDENTIAL_REVOKED + STATUS_LIST_ISSUER_MISMATCH;
    • an expired foreign list → STATUS_LIST_EXPIRED + mismatch;
    • an unverified list → list-issuer skipped.
  • Not run: test:browser (Playwright).

For consumers

Expiry now appears on cryptographic.proof.validity-period, not on cryptographic.proof.signature. A UI should expect more than one suite with fatalFailureAt, and results for rows that used to be "Not run" after an expiry or revocation.

🤖 Generated with Claude Code

ottonomy and others added 4 commits October 20, 2026 15:52
`fatal` decided two things at once: a failure made `verified` false, and
it halted every later check for the subject. A check can now set
`halts: false` to keep the first and drop the second, so a fatal failure
that leaves later checks meaningful no longer hides their results.

`halts` defaults to `fatal`, so existing checks and caller-added suites
behave as before, and their rows keep their shape. A throwing `applies`
predicate still halts, since it leaves every check in its suite unknown.
More than one suite summary can now carry `fatalFailureAt`; `verified`
is false exactly when any does.

No built-in check sets `halts` yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… longer halts

An expired or not-yet-valid credential failed `proof.signature`, which
halts, so its revocation status, its issuer's registration and any
caller-added suites came back "Not run". The credential is authentic at
that point: @digitalcredentials/vc checks dates only after the proof
verifies.

- The Data Integrity crypto service reports vc's date failure as a
  typed CREDENTIAL_EXPIRED / CREDENTIAL_NOT_YET_VALID problem. It does
  so only for a plain Error, not a VerificationError and with no
  errors[], whose message matches vc's wording in full, so a date
  phrase inside a wrapped proof error never counts.
- `proof.signature` reads a rejection made only of those problems as a
  verified signature.
- New `proof.validity-period` check, after the signature, judges the
  dates against the verifier's clock. Fatal, so `verified` stays
  false, but `halts: false`, so later checks still run.
- `status.bitstring` tells an expired status list from a bad one by
  problem type rather than message text.

A tampered and expired credential is still reported as a bad signature;
offline tests with real signatures pin this, and fail if a future vc
checks dates before the proof.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`status.bitstring` halted the run when it failed, so a revoked or
suspended credential, or one whose status list could not be trusted,
never reported whether its issuer is registered or who signed the list.
A revocation read from a list signed by someone other than the issuer
therefore read as a plain revocation.

- `status.bitstring` stays fatal but sets `halts: false`.
- `status.list-issuer` compares only lists whose proof verified, or was
  rejected only for its validity dates. It no longer relied on running
  only after every list verified, which held only while the status check
  halted. A list whose proof did not verify is skipped, not compared.
- Each status list's proof verification is memoised per verification,
  beside the existing memo of loaded lists, so both checks share one
  verification per list, judged at one moment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README, the verification-results and architecture docs, and the
2.0.0 CHANGELOG entry said that every fatal failure halts later checks,
that at most one suite carries `fatalFailureAt`, and that expiry is part
of the signature check. Describe `halts`, `proof.validity-period`, the
two built-in fatal checks that do not halt, and what an expired or
revoked credential now reports, with an expired-and-revoked example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ottonomy
ottonomy added this pull request to stack #77 October 20, 2026 22:36
@ottonomy
ottonomy merged commit e86bb79 into main Oct 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant