Repository navigation
v3.8.0
Security
unsignedPresentation: trueno longer skips a presentation proof that is
present. The flag was forwarded to@interop/vc, which then skipped the VP
proof, so a VP with a tampered holder signature verified. A proof that is
present is now always verified.- A supplied
challengeis always enforced. The default crypto service used to
pick the proof purpose from the VP's ownproofPurpose, so a replayed
assertionMethodVP verified even when the caller passed achallenge. A
suppliedchallengenow selectsAuthenticationProofPurpose, and an
assertionMethodVP failsproof.signature. - The built-in HTTP service no longer fetches whatever a credential names. It
allowshttps:only and refuseslocalhost, loopback, private, link-local,
multicast, reserved and unspecified addresses, including NAT64, 6to4 and
IPv4-compatible IPv6 forms and cloud metadata hosts. On Node the check runs on
the first URL and on every redirect hop (at most five). Browsers and React
Native hide redirect hops, so there only the first and final URLs are checked.
Each request has a 10 s deadline and a 5 MiB body cap. Errors have fixed text
and do not quote the response. InjectedhttpGetServiceanddocumentLoader
implementations keep their own policy. - OIDF registry membership is now verified. Both entity statements used to be
payload-decoded only, and any statement with ametadataobject counted as
registered. The trust anchor's entity configuration is now verified against
its ownjwks(iss === sub, unexpiredexp), the federation fetch endpoint
is read only from that verified payload, and the issuer's subordinate
statement is verified against the anchor's keys (issis the anchor entity
id,subis the issuer DID, unexpiredexp). Both headers must carry
typ: entity-statement+jwtand nocrit. ES256 and EdDSA are supported;
among alg-compatible keys, those matchingkidare tried first, then all of
them. A subordinate statement no longer needs ametadataobject. A statement
that does not verify reports the registry as unchecked. Only raw JWTs are
cached, re-verified on every hit, with a TTL capped atexp. Response bodies
are trimmed before decoding. - dcc-legacy lookups match own keys only, so
__proto__andconstructor
cannot match. registryKeyHashis renamedregistryCacheKeyand returns the JSON encoding
of the sorted[type, name, url]triples (plus sortedacceptedIssuersfor
vc-recognition) instead of a 32-bit djb2 hash, so two registry lists cannot
share a cache key.- A status list signed by someone other than the credential's issuer is now
reported. A new non-fatal check,status.list-issuer, runs after
status.bitstringand compares each verified list'sissuerid (a string, or
the object'sid) with the credential's. Each differing list is one
STATUS_LIST_ISSUER_MISMATCHproblem naming both issuers; a missing issuer on
either side counts as a mismatch.verifiedis unchanged, since some status
services sign every tenant's list with one DID of their own. Consumers that
need the binding enforced should treat the problem as decisive. The lists are
the ones theverifyStatusListCredentialhook already verified, so each list
is still fetched once per verification. The list'sissueris the proven
signer only when the crypto service binds the issuer to the signing key, as
the defaultDataIntegrityCryptoServicedoes; with a signature-only
CryptoServiceit is the list's own claim. The check sits in the
cryptographicphase withstatus.bitstring, whose run it reads, so a
phases: ['trust']pass does not report it. Its skips carry acodefrom the
exportedSTATUS_LIST_ISSUER_SKIP_CODES(no-list-checked,
list-not-verified). - A
BitstringStatusListEntrywhosetypearray includes other types is
checked. Requires@interop/vc-bitstring-status-list3.4.0, which matches the
entry type by inclusion; earlier versions ignored such an entry.
Changed
-
BREAKING:
proof.signaturereports an authentic but out-of-date
credential asCREDENTIAL_EXPIRED(validUntil/expirationDatepassed) or
CREDENTIAL_NOT_YET_VALID(validFrom/issuanceDatein the future)
instead ofINVALID_SIGNATURE.@interop/vcjudges dates only after the
proof verifies, so the date rejection proves the signature is good. Both are
newProblemTypesentries; the failure stays fatal. A tampered credential
still reportsINVALID_SIGNATURE. The default crypto service classifies the
rejection itself, soproof.signaturepasses its problems through. The
problemdetailnames the credential's date, the property it came from and
the verification instant
(Validity ended at <date> (validUntil). Verified at <instant>.), for VC 1.x
credentials too. Consumers that mapINVALID_SIGNATUREonproof.signature
to a legacy "bad signature" row (the freewallet and dcw compatibility shims)
need a case for the two new URIs. -
The verifier's
timeServicenow decides credential and status list validity.
The instant is resolved once per verify call asVerificationContext.now(now
a required field);proof.signature, the status list hook and the opt-in
validity.expirationall use it, so a credential and its status lists are
judged at one instant however long the list fetches take. The checks pass it
to the crypto service asCryptoVerifyOptions.now, so aFakeTimeServiceset
inside an expired credential's validity window verifies it. A custom
CryptoServicethat judges dates should honournowand report an expired or
not-yet-valid document asCREDENTIAL_EXPIRED/CREDENTIAL_NOT_YET_VALID;
status.bitstringmaps those ontoSTATUS_LIST_EXPIRED/
STATUS_LIST_NOT_YET_VALIDand no longer inspects problem text. -
Validity dates are compared exactly against the verification instant. The
default crypto service passesmaxClockSkew: 0to@interop/vc, which would
otherwise allow 300 seconds of skew thatvalidity.expirationdoes not, so a
credential that expired moments ago no longer passes one check and fails the
other. -
BREAKING:
EXPIRED_PROBLEM_TYPEis removed;expirationSuitereports
ProblemTypes.CREDENTIAL_EXPIRED(the URI string is unchanged). -
BREAKING: the Zod gate in front of
verifyCredential/
verifyPresentationis now minimal. A credential needs only@context,
type,issuer(a string or an object with a stringid) and
credentialSubject; a presentation needs@context,typeand, when
present,holder. Everything else passes through, so spec-legal inputs the
old gate rejected (an id-less or non-BitstringcredentialStatus, a
language-mapname, an issuerimagethat is only{ id }, a proof with an
objectverificationMethod, an arraycredentialSchema.type) now reach the
suites. The inferredVerifiableCredential/VerifiablePresentationtypes
are passthrough objects: fields other than the gated ones areunknown.
ProofSchema,JsonLdField,JsonLdFieldAllowEmpty,CredentialV1Schema,
CredentialV2SchemaandIssuerObjectSchemaare removed. -
A presentation's gate no longer validates embedded credentials. Each one is
gated and verified by its ownverifyCredentialcall, so a malformed or
enveloped credential yields its ownparsing.envelopefailure in
credentialResultsinstead of failing the whole presentation. -
Gate failures carry plain messages (for example
"issuer" is required and must be a string or an object with a string "id".)
instead of zod's "Invalid input", and the detail is no longer prefixed with a
path. -
coreSuiteruns for presentations, ahead ofproof; onlycore.vp-structure
applies to a VP. A presentation result'ssummarygains acoreentry. -
BREAKING:
CREDENTIAL_REVOKED_OR_SUSPENDEDis replaced by
CREDENTIAL_REVOKEDandCREDENTIAL_SUSPENDED.status.bitstringreports
one problem per set bit, naming the list URL and index in the detail, so a
consumer can tell a permanent revocation from a suspension without reading the
prose. -
status.bitstringchecks onlyBitstringStatusListEntryentries with purpose
revocationorsuspension. Entries for other purposes (refresh,
message) are no longer fetched or read as revoked; the success message and
skip reason name them asUnchecked status purposes. Entry selection is done
by@interop/vc-bitstring-status-list(now^3.3.0) through its
statusPurposesoption, and the list URL in aSTATUS_LIST_*detail comes
from the library rather than the list'sid. -
STATUS_LIST_EXPIREDandSTATUS_LIST_NOT_YET_VALIDdetails name the list
URL and carry the library's date message. -
A fatal failure halts every remaining check for the subject, in its own suite
and in all later suites, instead of only its own suite. Each halted check is
reported asskippedwith codeNOT_RUN_AFTER_FATAL(exported as
RUN_SUITES_SKIP_CODES.notRunAfterFatal),haltedBynaming the failed check,
and reasonNot run: <check-id> failed. Suite messages count these as "not
run after fatal"; a suite whose every check was halted reads
<suite> not run: <check-id> failed.FoldOptions.subjectTypeis gone:
halted checks are explicit rows, so the count no longer needs it. In the
default pipelinecore.proof-existsnow halts beforeproof.signature, so an
unsigned credential no longer also reportsNo Applicable Crypto Service. -
registrySuitenow runs beforestatusSuitein the default pipeline, so a
revoked credential or an unreachable status list still reports the issuer
registry lookup. -
A throwing check no longer rejects the verify call. A throwing
execute
becomes a failure carrying the newCHECK_ERRORproblem type
(Check "<id>" threw: <message>) and keeps the check'sfatalflag. A
throwing suiteappliespredicate becomes a<suite>.appliesCHECK_ERROR
failure, fatal when the suite has a fatal check that applies to the subject;
after a halt the suite's checks are listed as not run instead. A throwing
CryptoService.canVerifyis reported bydispatchProofVerificationas
threw. -
OIDF lookups work in browsers and React Native: JWT decoding uses
@scure/baseinstead ofBuffer.@interop/ecdsa-multikeyand@scure/base
are runtime dependencies. -
A registry lookup result with any unchecked registry is cached for 60 s
instead of 1 h. A recognition credential is cached for at most 1 h, or until
itsvalidUntilif sooner, measured on the verifier'stimeService.
ttlFromValidUntiltakesnowMsand accepts a missingvalidUntil. -
The OIDF lookup URL is built with
URL.searchParams, so a fetch endpoint that
already has a query keeps it; a non-URL endpoint yields unchecked. -
RegistryHandlerContextcarries the verifier'stimeService; custom handlers
and tests that build the context must supply one.createRegistryLookuptakes
it as an optional fifth argument. -
BREAKING:
CryptoVerifyOptions.unsignedPresentationis removed. A
CryptoServiceis only asked to verify a proof that exists. Custom
implementations must drop the field. -
BREAKING:
RegistryLookupResult.matchingRegistries: string[]is replaced
bymatches: RegistryMatch[], each
{ registry: RegistryReference, entity?: RegistryEntity }, and
uncheckedRegistriesis nowRegistryReference[]({ name, type, url })
instead of names. Theregistry.issueroutcomepayloadis the new
RegistryCheckPayload(matches,uncheckedRegistries), present on success
and on both lookup failures.CheckOutcomefailures gain an optional
payload. A registry'surlis where it is read from: the list URL for
dcc-legacyandvc-recognition,trustAnchorECforoidf. The dcc-legacy
handler fillsentity(name,url,raw) from the registry entry; OIDF
and vc-recognition matches carry no entity yet. The DID-level result cache key
is now versioned (reg-result:2:), so a persistent cache warmed by an older
release is not read as the new shape, and a lookup returns a copy of the
cached result rather than the cached object. -
BREAKING:
HandlerResultno longer carriesregistryName; the lookup
names the registry from its configured entry. CustomregistryHandlers
handlers must drop the field. -
When no configured registry could be checked,
registry.issuerfails with
REGISTRY_UNCHECKEDonly (detail
Issuer registration could not be determined: ...) instead of a contradictory
ISSUER_NOT_REGISTEREDplusREGISTRY_UNCHECKED. When some registries
answered and the issuer was not found, theISSUER_NOT_REGISTEREDdetail
reads
was not found in the N registries that could be checked; M could not be checked.;
with no unchecked registries the text is unchanged. -
A presentation with no proof fails
proof.signaturewith
Presentation Not Signed, instead ofNo Applicable Crypto Service. With
unsignedPresentation: truethe check isskippedandverifiedstays true. -
With no
challengesupplied, anauthenticationVP proof is verified under a
ControllerProofPurposewith theauthenticationterm instead of the
placeholder challengemeaningless. Only the signature and the key's
authenticationauthorization are checked; the proof's ownchallengeand
domainare not compared. -
The default crypto service verifies only the VP's own proof, via
@interop/vc
verifywithverifyCredentials: false. An embedded credential failure no
longer reads as a VPInvalid Signature, and embedded credentials are not
verified twice. Requires@interop/vc@11.1.0. -
documentHasProofis shared bycore.proof-exists,proof.signatureand the
default crypto service. A proof array counts when it holds at least one object
entry. -
A
challenge,domainor proof purpose mismatch on a VP proof is reported as
PROOF_PURPOSE_MISMATCH(titlesChallenge Mismatch,Domain Mismatch,
Proof Purpose Mismatch) instead ofInvalid Signature. -
verifyPresentationrejects adomainwithout achallenge, and an
empty-stringchallenge, before any suite runs. Both produce a
proof.signaturefailure withChallenge Required, whatever the input
document. -
The document loader reports a non-JSON body as
response is not JSONinstead
of quoting the parser message. -
The OBv3 schema check no longer fetches and compiles its schema on every
verification. The four published OB 3.0 schema documents are cached in the
verifier'scacheServiceunderschema:<url>for 24 h, and their compiled
AJV validators are kept per verifier and recompiled after the same 24 h, so
verifiers that share acacheServiceshare the fetch and a long-lived
verifier still picks up a republished schema. A document is cached, or trusted
from the cache, only when its$idis the requested URL, so a corrupt or
wrong-shaped entry in a shared cache is a miss. A failed fetch, including a
nested$ref, is retried on the next verification. Any other
credentialSchemaURL is still fetched and compiled on every verification
without being cached, so an issuer cannot grow the cache. The README
previously claimed the schema was cached after first use.
Added
- Fatal
core.vc-structureandcore.vp-structurechecks validate VC Data
Model structure:typeincludesVerifiableCredential/
VerifiablePresentation;credentialSubjectis a non-empty object (or
non-empty array of them) with a URLidif present;issueris a URL or an
object with a URLid; v1issuanceDate(required) /expirationDateand v2
validFrom/validUntilare whole-string dateTimes (format only, via
@interop/vcdateRegex);credentialStatus,proof,termsOfUseand
evidenceentries are objects with atype;credentialStatusand
evidenceids are URLs;credentialSchemaentries have atypeand a URL
id; the first@contextentry is a VC context. For a VP:idandholder
are URLs if present,proofentries are typed objects,verifiableCredential
entries are objects. Every violation is its own problem with a JSON Pointer
instance. ProblemTypes.MALFORMED_VALUE_ERROR(VC Data Model 2.0 section 7.1), emitted
by the two structure checks.parseCredentialrejects anEnvelopedVerifiableCredential(VC-JOSE-COSE)
with
Enveloped credentials (VC-JOSE-COSE) are not supported by this verifier.
parsePresentationrejects anEnvelopedVerifiablePresentationthe same way,
with
Enveloped presentations (VC-JOSE-COSE) are not supported by this verifier.domainonverifyPresentation,VerificationContextand
CryptoVerifyOptions, checked alongsidechallenge.ProblemTypes.PROOF_PURPOSE_MISMATCH.SIGNATURE_SKIP_CODES.unsignedPresentation, thecodeon the
proof.signatureskip for an accepted unsigned presentation.BuiltinHttpGetService({ timeoutMs, maxBytes, allowInsecure })and the
exportedBuiltinHttpGetServiceOptionstype. Defaults are 10 s, 5 MiB and
false.allowInsecurerelaxes only the URL policy, for local development.RegistryCheckPayload,RegistryMatch,RegistryReference,RegistryEntity
andHandlerResultare exported from the main entry, so a custom
registryHandlershandler can return anentity.- New fatal
VERIFICATION_METHOD_ERRORproblem type, reported by
proof.signaturewhen the proof's verification method resolved but its
controller document does not list it, or does not authorize it for the proof
purpose.detailcarries the library message. These used to read as
INVALID_SIGNATURE. The issuer-vs-controller case keeps
ISSUER_PROOF_MISMATCH. In a proof set, the other proofs' failures are
reported beside the fault. Same type and matching as the
digitalcredentials/verifier-core fork.
Documentation
- README: the Core suite row no longer claims to "resolve issuers" (that is the
registry suite); theverifyCredential/verifyPresentationoption blocks
now listrecognizers,phases,verbose,timingandtimeService. - Architecture doc: the "no
Clockport" gap is restated as the cache layer
bypassing the existingTimeServiceseam. src/index.tslicense header now matchesLICENSE.md(MIT, 2025).