Skip to content

v3.8.0

Choose a tag to compare

@dmitrizagidulin dmitrizagidulin released this 12 Oct 20:15
· 8 commits to main since this release

Security

  • unsignedPresentation: true no 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 challenge is always enforced. The default crypto service used to
    pick the proof purpose from the VP's own proofPurpose, so a replayed
    assertionMethod VP verified even when the caller passed a challenge. A
    supplied challenge now selects AuthenticationProofPurpose, and an
    assertionMethod VP fails proof.signature.
  • The built-in HTTP service no longer fetches whatever a credential names. It
    allows https: only and refuses localhost, 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. Injected httpGetService and documentLoader
    implementations keep their own policy.
  • OIDF registry membership is now verified. Both entity statements used to be
    payload-decoded only, and any statement with a metadata object counted as
    registered. The trust anchor's entity configuration is now verified against
    its own jwks (iss === sub, unexpired exp), the federation fetch endpoint
    is read only from that verified payload, and the issuer's subordinate
    statement is verified against the anchor's keys (iss is the anchor entity
    id, sub is the issuer DID, unexpired exp). Both headers must carry
    typ: entity-statement+jwt and no crit. ES256 and EdDSA are supported;
    among alg-compatible keys, those matching kid are tried first, then all of
    them. A subordinate statement no longer needs a metadata object. 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 at exp. Response bodies
    are trimmed before decoding.
  • dcc-legacy lookups match own keys only, so __proto__ and constructor
    cannot match.
  • registryKeyHash is renamed registryCacheKey and returns the JSON encoding
    of the sorted [type, name, url] triples (plus sorted acceptedIssuers for
    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.bitstring and compares each verified list's issuer id (a string, or
    the object's id) with the credential's. Each differing list is one
    STATUS_LIST_ISSUER_MISMATCH problem naming both issuers; a missing issuer on
    either side counts as a mismatch. verified is 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 the verifyStatusListCredential hook already verified, so each list
    is still fetched once per verification. The list's issuer is the proven
    signer only when the crypto service binds the issuer to the signing key, as
    the default DataIntegrityCryptoService does; with a signature-only
    CryptoService it is the list's own claim. The check sits in the
    cryptographic phase with status.bitstring, whose run it reads, so a
    phases: ['trust'] pass does not report it. Its skips carry a code from the
    exported STATUS_LIST_ISSUER_SKIP_CODES (no-list-checked,
    list-not-verified).
  • A BitstringStatusListEntry whose type array includes other types is
    checked. Requires @interop/vc-bitstring-status-list 3.4.0, which matches the
    entry type by inclusion; earlier versions ignored such an entry.

Changed

  • BREAKING: proof.signature reports an authentic but out-of-date
    credential as CREDENTIAL_EXPIRED (validUntil / expirationDate passed) or
    CREDENTIAL_NOT_YET_VALID (validFrom / issuanceDate in the future)
    instead of INVALID_SIGNATURE. @interop/vc judges dates only after the
    proof verifies, so the date rejection proves the signature is good. Both are
    new ProblemTypes entries; the failure stays fatal. A tampered credential
    still reports INVALID_SIGNATURE. The default crypto service classifies the
    rejection itself, so proof.signature passes its problems through. The
    problem detail names 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 map INVALID_SIGNATURE on proof.signature
    to a legacy "bad signature" row (the freewallet and dcw compatibility shims)
    need a case for the two new URIs.

  • The verifier's timeService now decides credential and status list validity.
    The instant is resolved once per verify call as VerificationContext.now (now
    a required field); proof.signature, the status list hook and the opt-in
    validity.expiration all 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 as CryptoVerifyOptions.now, so a FakeTimeService set
    inside an expired credential's validity window verifies it. A custom
    CryptoService that judges dates should honour now and report an expired or
    not-yet-valid document as CREDENTIAL_EXPIRED / CREDENTIAL_NOT_YET_VALID;
    status.bitstring maps those onto STATUS_LIST_EXPIRED /
    STATUS_LIST_NOT_YET_VALID and no longer inspects problem text.

  • Validity dates are compared exactly against the verification instant. The
    default crypto service passes maxClockSkew: 0 to @interop/vc, which would
    otherwise allow 300 seconds of skew that validity.expiration does not, so a
    credential that expired moments ago no longer passes one check and fails the
    other.

  • BREAKING: EXPIRED_PROBLEM_TYPE is removed; expirationSuite reports
    ProblemTypes.CREDENTIAL_EXPIRED (the URI string is unchanged).

  • BREAKING: the Zod gate in front of verifyCredential /
    verifyPresentation is now minimal. A credential needs only @context,
    type, issuer (a string or an object with a string id) and
    credentialSubject; a presentation needs @context, type and, when
    present, holder. Everything else passes through, so spec-legal inputs the
    old gate rejected (an id-less or non-Bitstring credentialStatus, a
    language-map name, an issuer image that is only { id }, a proof with an
    object verificationMethod, an array credentialSchema.type) now reach the
    suites. The inferred VerifiableCredential / VerifiablePresentation types
    are passthrough objects: fields other than the gated ones are unknown.
    ProofSchema, JsonLdField, JsonLdFieldAllowEmpty, CredentialV1Schema,
    CredentialV2Schema and IssuerObjectSchema are removed.

  • A presentation's gate no longer validates embedded credentials. Each one is
    gated and verified by its own verifyCredential call, so a malformed or
    enveloped credential yields its own parsing.envelope failure in
    credentialResults instead 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.

  • coreSuite runs for presentations, ahead of proof; only core.vp-structure
    applies to a VP. A presentation result's summary gains a core entry.

  • BREAKING: CREDENTIAL_REVOKED_OR_SUSPENDED is replaced by
    CREDENTIAL_REVOKED and CREDENTIAL_SUSPENDED. status.bitstring reports
    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.bitstring checks only BitstringStatusListEntry entries with purpose
    revocation or suspension. Entries for other purposes (refresh,
    message) are no longer fetched or read as revoked; the success message and
    skip reason name them as Unchecked status purposes. Entry selection is done
    by @interop/vc-bitstring-status-list (now ^3.3.0) through its
    statusPurposes option, and the list URL in a STATUS_LIST_* detail comes
    from the library rather than the list's id.

  • STATUS_LIST_EXPIRED and STATUS_LIST_NOT_YET_VALID details 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 as skipped with code NOT_RUN_AFTER_FATAL (exported as
    RUN_SUITES_SKIP_CODES.notRunAfterFatal), haltedBy naming the failed check,
    and reason Not 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.subjectType is gone:
    halted checks are explicit rows, so the count no longer needs it. In the
    default pipeline core.proof-exists now halts before proof.signature, so an
    unsigned credential no longer also reports No Applicable Crypto Service.

  • registrySuite now runs before statusSuite in 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 new CHECK_ERROR problem type
    (Check "<id>" threw: <message>) and keeps the check's fatal flag. A
    throwing suite applies predicate becomes a <suite>.applies CHECK_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.canVerify is reported by dispatchProofVerification as
    threw.

  • OIDF lookups work in browsers and React Native: JWT decoding uses
    @scure/base instead of Buffer. @interop/ecdsa-multikey and @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
    its validUntil if sooner, measured on the verifier's timeService.
    ttlFromValidUntil takes nowMs and accepts a missing validUntil.

  • 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.

  • RegistryHandlerContext carries the verifier's timeService; custom handlers
    and tests that build the context must supply one. createRegistryLookup takes
    it as an optional fifth argument.

  • BREAKING: CryptoVerifyOptions.unsignedPresentation is removed. A
    CryptoService is only asked to verify a proof that exists. Custom
    implementations must drop the field.

  • BREAKING: RegistryLookupResult.matchingRegistries: string[] is replaced
    by matches: RegistryMatch[], each
    { registry: RegistryReference, entity?: RegistryEntity }, and
    uncheckedRegistries is now RegistryReference[] ({ name, type, url })
    instead of names. The registry.issuer outcome payload is the new
    RegistryCheckPayload (matches, uncheckedRegistries), present on success
    and on both lookup failures. CheckOutcome failures gain an optional
    payload. A registry's url is where it is read from: the list URL for
    dcc-legacy and vc-recognition, trustAnchorEC for oidf. The dcc-legacy
    handler fills entity (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: HandlerResult no longer carries registryName; the lookup
    names the registry from its configured entry. Custom registryHandlers
    handlers must drop the field.

  • When no configured registry could be checked, registry.issuer fails with
    REGISTRY_UNCHECKED only (detail
    Issuer registration could not be determined: ...) instead of a contradictory
    ISSUER_NOT_REGISTERED plus REGISTRY_UNCHECKED. When some registries
    answered and the issuer was not found, the ISSUER_NOT_REGISTERED detail
    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.signature with
    Presentation Not Signed, instead of No Applicable Crypto Service. With
    unsignedPresentation: true the check is skipped and verified stays true.

  • With no challenge supplied, an authentication VP proof is verified under a
    ControllerProofPurpose with the authentication term instead of the
    placeholder challenge meaningless. Only the signature and the key's
    authentication authorization are checked; the proof's own challenge and
    domain are not compared.

  • The default crypto service verifies only the VP's own proof, via @interop/vc
    verify with verifyCredentials: false. An embedded credential failure no
    longer reads as a VP Invalid Signature, and embedded credentials are not
    verified twice. Requires @interop/vc@11.1.0.

  • documentHasProof is shared by core.proof-exists, proof.signature and the
    default crypto service. A proof array counts when it holds at least one object
    entry.

  • A challenge, domain or proof purpose mismatch on a VP proof is reported as
    PROOF_PURPOSE_MISMATCH (titles Challenge Mismatch, Domain Mismatch,
    Proof Purpose Mismatch) instead of Invalid Signature.

  • verifyPresentation rejects a domain without a challenge, and an
    empty-string challenge, before any suite runs. Both produce a
    proof.signature failure with Challenge Required, whatever the input
    document.

  • The document loader reports a non-JSON body as response is not JSON instead
    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's cacheService under schema:<url> for 24 h, and their compiled
    AJV validators are kept per verifier and recompiled after the same 24 h, so
    verifiers that share a cacheService share 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 $id is 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
    credentialSchema URL 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-structure and core.vp-structure checks validate VC Data
    Model structure: type includes VerifiableCredential /
    VerifiablePresentation; credentialSubject is a non-empty object (or
    non-empty array of them) with a URL id if present; issuer is a URL or an
    object with a URL id; v1 issuanceDate (required) / expirationDate and v2
    validFrom / validUntil are whole-string dateTimes (format only, via
    @interop/vc dateRegex); credentialStatus, proof, termsOfUse and
    evidence entries are objects with a type; credentialStatus and
    evidence ids are URLs; credentialSchema entries have a type and a URL
    id; the first @context entry is a VC context. For a VP: id and holder
    are URLs if present, proof entries 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.
  • parseCredential rejects an EnvelopedVerifiableCredential (VC-JOSE-COSE)
    with
    Enveloped credentials (VC-JOSE-COSE) are not supported by this verifier.
    parsePresentation rejects an EnvelopedVerifiablePresentation the same way,
    with
    Enveloped presentations (VC-JOSE-COSE) are not supported by this verifier.
  • domain on verifyPresentation, VerificationContext and
    CryptoVerifyOptions, checked alongside challenge.
  • ProblemTypes.PROOF_PURPOSE_MISMATCH.
  • SIGNATURE_SKIP_CODES.unsignedPresentation, the code on the
    proof.signature skip for an accepted unsigned presentation.
  • BuiltinHttpGetService({ timeoutMs, maxBytes, allowInsecure }) and the
    exported BuiltinHttpGetServiceOptions type. Defaults are 10 s, 5 MiB and
    false. allowInsecure relaxes only the URL policy, for local development.
  • RegistryCheckPayload, RegistryMatch, RegistryReference, RegistryEntity
    and HandlerResult are exported from the main entry, so a custom
    registryHandlers handler can return an entity.
  • New fatal VERIFICATION_METHOD_ERROR problem type, reported by
    proof.signature when the proof's verification method resolved but its
    controller document does not list it, or does not authorize it for the proof
    purpose. detail carries 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); the verifyCredential / verifyPresentation option blocks
    now list recognizers, phases, verbose, timing and timeService.
  • Architecture doc: the "no Clock port" gap is restated as the cache layer
    bypassing the existing TimeService seam.
  • src/index.ts license header now matches LICENSE.md (MIT, 2025).