Skip to content

fix(vc): fail closed when credential revocation cannot be verified - #135

Open
venables wants to merge 3 commits into
mainfrom
fix/revocation-check-fail-closed
Open

fix(vc): fail closed when credential revocation cannot be verified#135
venables wants to merge 3 commits into
mainfrom
fix/revocation-check-fail-closed

Conversation

@venables

@venables venables commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Changes

  • isRevoked fails closed: it throws when revocation status cannot be established, instead of returning false.
  • The status list credential is verified, not just parsed: proof, trusted issuer, id bound to the dereferenced URL, expiry, purpose, statusSize, and index bounds.
  • The status list fetch is bounded: http(s) only, no redirects, 5s timeout, streamed body cap, and a cap on the encoded list before it is decoded.
  • verifyParsedCredential checks trustedIssuers before the revocation fetch, so an untrusted issuer cannot choose a URL for the verifier to request.
  • A credential's issuer.id must match the DID that signed the JWT.
  • examples/issuer serves the status list credential unwrapped, and parses listId before it reaches the query or the credential id.
  • Status lists get a 24 hour expiry by default; maxStatusListAgeMs bounds lists from issuers that publish no expiry.

Problem

isRevoked treated every failure as "not revoked":

Condition Old result
Status list host down, DNS failure, timeout false — accepted
HTTP 404 / 502 false
Unsigned or substituted status list false — trusted on shape alone
statusListIndex: "abc" falseparseInt gave NaN
Index past the end of the list false

Anyone able to disrupt the status list endpoint could keep using a revoked credential.

The reference issuer made this worse. It served the credential inside this API's { ok, data } envelope, which is not a credential, so the body never parsed and every revocation check against it failed open. Revocation did not work at all for anyone following the example.

While fixing the status list issuer check, a second and more serious problem surfaced. did-jwt-vc builds the credential issuer as { id: iss, ...payload.issuer }, so an issuer object in the JWT payload replaces the id taken from iss. The signature binds iss only, and nothing compared the two. A proof of concept confirmed it: a JWT signed by did:web:attacker.example.com decoded with issuer.id of did:web:issuer.example.com. That defeated trustedIssuers, trustedReceiptIssuers, and the status list issuer binding.

Both defects date to the initial commit (2deaf04, 2025-05-19) and are present in every published version through 0.10.1.

Solution

Every branch in isRevoked now either returns a verified answer or throws. RevocationCheckError and UnsupportedCredentialStatusError each carry one fixed message, with the URL and the response in detail and cause, because API handlers return the message to the caller and it would otherwise report which hosts the verifier can reach.

parseJwtCredential rejects a credential whose issuer.id differs from the verified signer.

Status list replay is bounded by an expiry, because every earlier version of a list stays validly signed and can be served back to clear a later revocation.

Testing

pnpm run check passes: 29/29 tasks, 525 tests.

New coverage includes each attack path as an explicit test: unreachable list, HTTP error, envelope-wrapped body, unsigned list, invalid proof, untrusted issuer, substituted list, purpose mismatch, multi-bit list, NaN and out-of-range index, non-http scheme, oversized body and encoded list, replayed list, and the issuer spoof. verify-revoked-credential.test.ts covers the chain end to end, and examples/issuer/src/routes/status.test.ts asserts a verifier can actually read a revocation from the served list.

Notes

Deliberately out of scope, listed so they are not lost:

  • Presentations have the same spoofing weakness. verifyPresentation is re-exported from did-jwt-vc unchanged; normalizeJwtPresentationPayload takes holder from iss only when the payload has no holder, and it does not verify embedded credential proofs. Nothing in this repo calls it. Documented at the export; needs its own change.
  • StatusList2021Entry and statusPurpose: "suspension" now fail closed rather than being ignored. Supporting them needs type and dual-schema work.
  • An array-valued credentialStatus.type is still rejected: widening the type breaks assignability to did-jwt-vc's CredentialPayload (TS2345).
  • No DNS or private-address SSRF blocking. Checking the trusted issuer first removes the untrusted-attacker path.
  • maxStatusListAgeMs is off by default, so a third-party list with no expiry is not age-bounded unless the caller opts in. Changing that default is a policy call.

Two related fixes are on fix/verification-fail-closed-followups and depend on this branch: the isExpired fail-open for an unparseable expirationDate, and a revocation option passthrough for verifyPaymentReceipt.

Marked minor: isRevoked takes a required second argument, isRevocable accepts fewer shapes, and both throw where they returned false.

AI usage

Written with Claude Code (Claude Opus 5). It investigated the report, wrote the fixes and tests, and ran an eight-round multi-agent review loop (Claude, Codex, and two opencode models) that found the issuer-spoofing bug and a regression this change had introduced. All findings were verified against the code, and several were rejected as incorrect. Reviewed by the author before opening.

Summary by CodeRabbit

  • New Features

    • Added secure issuer–signer matching for JWT credentials.
    • Added fail-closed revocation checks with status-list authenticity, freshness, bounds, timeout, and issuer validation.
    • Added configurable revocation verification options and status-list expiration controls.
    • Added structural detection for credentials containing proofs.
  • Bug Fixes

    • Revocation failures now block verification instead of being treated as “not revoked.”
    • Untrusted issuers are rejected before revocation checks.
    • Status endpoint responses now return signed credentials directly with validated identifiers.
  • Documentation

    • Expanded revocation guidance, error behavior, and status endpoint examples.

venables and others added 3 commits August 3, 2026 20:20
did-jwt-vc builds the credential issuer as `{ id: iss, ...payload.issuer }`,
so an `issuer` object in the JWT payload replaces the `id` taken from `iss`.
The signature binds `iss` only, and nothing compared the two.

Anyone could sign a credential with their own key, name another DID as the
issuer, and pass every issuer check downstream: `trustedIssuers` in
`verifyParsedCredential`, `trustedReceiptIssuers` in `verifyPaymentReceipt`,
and the status list issuer check in `isRevoked`.

`parseJwtCredential` now rejects a credential whose `issuer.id` differs from
the verified signer. The presentation path has the same weakness through the
re-exported `verifyPresentation`; that is documented at the export and left
for a separate change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`isRevoked` returned `false` for every failure. A network error, DNS failure,
timeout, HTTP error, non-JSON body, or any body that did not match the
expected shape read as "not revoked", so `verifyParsedCredential` accepted a
revoked credential. Anyone able to disrupt the status list endpoint could use
a revoked credential indefinitely.

The fetched list was also trusted on shape alone: its proof was never
verified, its issuer never checked, and it was never bound to the URL the
credential named. `statusListIndex` went through `parseInt`, so a non-numeric
value became `NaN` and read as an unset bit. An index past the end of the list
read as unset too. The entry's `statusPurpose` was ignored, and an
unrecognized `credentialStatus` was skipped silently.

`isRevoked(credential, options)` now takes a resolver and throws
`RevocationCheckError`, or `UnsupportedCredentialStatusError` for a status it
does not implement. It verifies the list's proof, requires a trusted issuer
and a matching `id`, rejects an expired, pre-dated or multi-bit list, checks
the purpose, validates the index and its bounds, restricts the URL to http(s),
refuses redirects, and bounds the body and the decode.

`verifyParsedCredential` checks `trustedIssuers` before the revocation fetch,
so an untrusted issuer cannot make the verifier dereference a URL it chose.

Status lists carry a 24 hour expiry by default, because every earlier version
of a list stays validly signed and can be replayed to clear a later
revocation. Pass `null` to opt out, or bound age with `maxStatusListAgeMs`.

Both errors carry one fixed message, with the URL and the response in `detail`
and `cause`, because API handlers return the message to the caller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The status endpoint returned the credential inside this API's `{ ok, data }`
envelope. A verifier dereferences that URL expecting the credential itself, so
the body never parsed as one and every revocation check against this issuer
failed open. Revocation did not work at all.

The endpoint now returns the signed credential directly, as the W3C Bitstring
Status List specification requires. It also parses `listId` before the value
reaches either the query or the credential `id`, so `/status/01` cannot sign
caller-supplied text into the id.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The VC package now binds JWT credential issuers to verified signers and performs authenticated, fail-closed revocation checks. Credential verification validates trusted issuers first. The issuer example returns signed status-list credentials directly.

Changes

VC verification hardening

Layer / File(s) Summary
JWT issuer binding
packages/vc/src/verification/parse-jwt-credential.ts, packages/vc/src/verification/parse-jwt-credential.test.ts, .changeset/vc-issuer-signer-binding.md
parseJwtCredential rejects mismatched payload issuers with InvalidCredentialError. Tests cover attacker-signed credentials with a different declared issuer.
Authenticated fail-closed revocation
packages/vc/src/verification/is-revoked.ts, packages/vc/src/verification/errors.ts, packages/vc/src/schemas/*, packages/vc/src/revocation/*, packages/vc/src/verification/is-revoked.test.ts, packages/vc/README.md, .changeset/vc-revocation-fail-closed.md
isRevoked verifies signed status lists, validates bindings and limits, supports configurable options, and throws explicit errors when status is unsupported or indeterminate.
Credential verification integration
packages/vc/src/verification/verify-parsed-credential.ts, packages/vc/src/verification/verify-parsed-credential.test.ts, packages/vc/src/verification/verify-proof.test.ts, packages/vc/src/verification/verify-revoked-credential.test.ts, packages/vc/src/verification/is-verifiable.ts, packages/vc/src/index.ts, tools/api-utils/src/middleware/error-handler.ts, packages/vc/AGENTS.md
Trusted issuer validation runs before revocation checks. Revocation options use the shared resolver. New verification errors are logged while API responses remain sanitized.
Issuer status endpoint
examples/issuer/src/routes/status.ts, examples/issuer/src/routes/status.test.ts, examples/issuer/README.md
The endpoint validates nonnegative safe-integer identifiers and returns the signed status-list credential without an API envelope. Tests cover revoked, unrevoked, invalid, unknown, and zero-padded identifiers.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: credential revocation verification now fails closed when it cannot be verified.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/revocation-check-fail-closed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
packages/vc/src/verification/is-revoked.test.ts (1)

481-513: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for a future-dated status list.

assertStatusListIsFresh in packages/vc/src/verification/is-revoked.ts rejects an issuanceDate more than CLOCK_SKEW_MS ahead of the local clock, and it accepts one inside that window. No test covers either branch. The signedStatusList helper already accepts issuedAt, so both cases are cheap to add.

💚 Proposed tests for the clock-skew branch
it("throws when the status list is dated beyond the clock skew allowance", async () => {
  mockFetch.mockResolvedValueOnce(
    Response.json(
      await signedStatusList({
        revokedIndex: 5,
        issuedAt: new Date(Date.now() + 10 * 60 * 1000),
      }),
    ),
  )

  const error = await captureRevocationError(
    isRevoked(buildCredential(statusEntry()), { resolver }),
  )

  expect(error.detail).toMatch(/in the future/)
})

it("accepts a status list dated inside the clock skew allowance", async () => {
  mockFetch.mockResolvedValueOnce(
    Response.json(
      await signedStatusList({
        revokedIndex: 5,
        issuedAt: new Date(Date.now() + 60 * 1000),
      }),
    ),
  )

  await expect(
    isRevoked(buildCredential(statusEntry()), { resolver }),
  ).resolves.toBe(true)
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vc/src/verification/is-revoked.test.ts` around lines 481 - 513, Add
two tests alongside the existing freshness tests in is-revoked.test.ts: one
using signedStatusList with issuedAt beyond CLOCK_SKEW_MS that captures the
revocation error and asserts the detail mentions “in the future,” and another
using a timestamp within the skew allowance that verifies isRevoked resolves
successfully. Reuse the existing resolver, statusEntry, buildCredential, and
mockFetch setup.
packages/vc/src/verification/is-revoked.ts (2)

108-125: 🔒 Security & Privacy | 🔵 Trivial | ⚖️ Poor tradeoff

Consider a host policy for status list URLs.

toHttpUrl accepts any host on http: or https:. redirect: "error" stops a redirect to an internal host, but the first request still goes to whatever host the credential names. A credential from a trusted issuer can therefore point this process at 169.254.169.254, localhost, or an RFC1918 address.

The residual impact is bounded: verifyParsedCredential checks trustedIssuers before calling isRevoked, the response must carry a proof from a trusted issuer, and the error message is sanitized. The remaining signal is timing and reachability.

Two options are worth considering:

  • Add an optional allowedStatusListHosts (or a blocked-range predicate) to RevocationCheckOptions, mirroring the allowedHttpHosts policy that getDidResolver already uses in packages/did/src/did-resolvers/get-did-resolver.ts.
  • Deploy verifiers behind an egress proxy that denies link-local and private ranges.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vc/src/verification/is-revoked.ts` around lines 108 - 125, The
status-list URL validation in toHttpUrl permits requests to arbitrary HTTP(S)
hosts. Add an optional allowedStatusListHosts policy to RevocationCheckOptions,
consistent with the existing allowedHttpHosts behavior in getDidResolver, and
enforce it before returning the normalized URL; reject hosts outside the
configured policy, including private, loopback, and link-local targets when
applicable. Preserve current behavior when no policy is configured.

525-531: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document that bits.length is the status list bit count.
The range check uses statusListIndex as a bit position before statusSize scaling; state this requirement near if (index >= bits.length) so the length invariant is explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vc/src/verification/is-revoked.ts` around lines 525 - 531, Clarify
the comment immediately above the index boundary check in the status-list
verification flow that bits.length represents the total number of status-list
bits, before any statusSize scaling is applied. Keep the existing index
validation and undetermined error behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/vc/README.md`:
- Around line 118-121: Update the fetch-limits documentation in the README to
mention the RevocationCheckOptions.maxEncodedListBytes option, including its
default of 65536 bytes and that consumers can increase it when larger encoded
status lists are valid. Keep the existing statusListTimeoutMs and redirect
behavior documentation unchanged.

In `@packages/vc/src/verification/verify-proof.test.ts`:
- Around line 40-56: Update decodedIssuerId() to recognize a string-valued
issuer and return that DID before attempting to access issuer.id; preserve the
existing object-form handling and undefined result for invalid issuer values.

---

Nitpick comments:
In `@packages/vc/src/verification/is-revoked.test.ts`:
- Around line 481-513: Add two tests alongside the existing freshness tests in
is-revoked.test.ts: one using signedStatusList with issuedAt beyond
CLOCK_SKEW_MS that captures the revocation error and asserts the detail mentions
“in the future,” and another using a timestamp within the skew allowance that
verifies isRevoked resolves successfully. Reuse the existing resolver,
statusEntry, buildCredential, and mockFetch setup.

In `@packages/vc/src/verification/is-revoked.ts`:
- Around line 108-125: The status-list URL validation in toHttpUrl permits
requests to arbitrary HTTP(S) hosts. Add an optional allowedStatusListHosts
policy to RevocationCheckOptions, consistent with the existing allowedHttpHosts
behavior in getDidResolver, and enforce it before returning the normalized URL;
reject hosts outside the configured policy, including private, loopback, and
link-local targets when applicable. Preserve current behavior when no policy is
configured.
- Around line 525-531: Clarify the comment immediately above the index boundary
check in the status-list verification flow that bits.length represents the total
number of status-list bits, before any statusSize scaling is applied. Keep the
existing index validation and undetermined error behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8601c627-3fc2-46d3-998c-92e815295cb8

📥 Commits

Reviewing files that changed from the base of the PR and between 43a066c and 3cb4ee4.

📒 Files selected for processing (23)
  • .changeset/vc-issuer-signer-binding.md
  • .changeset/vc-revocation-fail-closed.md
  • examples/issuer/README.md
  • examples/issuer/src/routes/status.test.ts
  • examples/issuer/src/routes/status.ts
  • packages/vc/AGENTS.md
  • packages/vc/README.md
  • packages/vc/src/index.ts
  • packages/vc/src/revocation/status-list-credential.ts
  • packages/vc/src/revocation/types.ts
  • packages/vc/src/schemas/valibot.ts
  • packages/vc/src/schemas/zod.ts
  • packages/vc/src/verification/errors.ts
  • packages/vc/src/verification/is-revoked.test.ts
  • packages/vc/src/verification/is-revoked.ts
  • packages/vc/src/verification/is-verifiable.ts
  • packages/vc/src/verification/parse-jwt-credential.test.ts
  • packages/vc/src/verification/parse-jwt-credential.ts
  • packages/vc/src/verification/verify-parsed-credential.test.ts
  • packages/vc/src/verification/verify-parsed-credential.ts
  • packages/vc/src/verification/verify-proof.test.ts
  • packages/vc/src/verification/verify-revoked-credential.test.ts
  • tools/api-utils/src/middleware/error-handler.ts

Comment thread packages/vc/README.md
Comment on lines +118 to +121
Two limits on the fetch are worth knowing. `statusListTimeoutMs` bounds the
status list request only, not the DID resolution that verifies its proof. And
the request does not follow redirects, so serve the credential at the URL the
`statusListCredential` names.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document maxEncodedListBytes.

RevocationCheckOptions exposes maxEncodedListBytes, and the default of 65536 rejects a large dense status list with encodedList over the 65536 byte limit. The README does not mention the option, so a consumer that hits the limit has no documented way to raise it.

📝 Proposed documentation addition
-Two limits on the fetch are worth knowing. `statusListTimeoutMs` bounds the
+Three limits are worth knowing. `statusListTimeoutMs` bounds the
 status list request only, not the DID resolution that verifies its proof. And
 the request does not follow redirects, so serve the credential at the URL the
-`statusListCredential` names.
+`statusListCredential` names. `maxEncodedListBytes` caps the `encodedList`
+string at 65536 by default, because the decode inflates it with no output
+limit; raise it to consume a large list with many bits set. A fixed 5 MB cap on
+the whole response body applies first.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Two limits on the fetch are worth knowing. `statusListTimeoutMs` bounds the
status list request only, not the DID resolution that verifies its proof. And
the request does not follow redirects, so serve the credential at the URL the
`statusListCredential` names.
Three limits are worth knowing. `statusListTimeoutMs` bounds the
status list request only, not the DID resolution that verifies its proof. And
the request does not follow redirects, so serve the credential at the URL the
`statusListCredential` names. `maxEncodedListBytes` caps the `encodedList`
string at 65536 by default, because the decode inflates it with no output
limit; raise it to consume a large list with many bits set. A fixed 5 MB cap on
the whole response body applies first.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vc/README.md` around lines 118 - 121, Update the fetch-limits
documentation in the README to mention the
RevocationCheckOptions.maxEncodedListBytes option, including its default of
65536 bytes and that consumers can increase it when larger encoded status lists
are valid. Keep the existing statusListTimeoutMs and redirect behavior
documentation unchanged.

Comment on lines +40 to +56
/**
* Read `issuer.id` off a decoded-credential fixture, which may be any shape.
*/
function decodedIssuerId(credential: unknown): string | undefined {
if (typeof credential !== "object" || credential === null) {
return undefined
}

const { issuer } = credential as { issuer?: unknown }

if (typeof issuer !== "object" || issuer === null) {
return undefined
}

const { id } = issuer as { id?: unknown }

return typeof id === "string" ? id : undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return the signer DID for string-form issuers.

decodedIssuerId() returns undefined when a fixture uses issuer: "did:example:issuer". The mock then does not report the signer DID that Line 32 says it must report. Handle the W3C string form before reading issuer.id.

Proposed fix
   const { issuer } = credential as { issuer?: unknown }

+  if (typeof issuer === "string") {
+    return issuer
+  }
+
   if (typeof issuer !== "object" || issuer === null) {
     return undefined
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Read `issuer.id` off a decoded-credential fixture, which may be any shape.
*/
function decodedIssuerId(credential: unknown): string | undefined {
if (typeof credential !== "object" || credential === null) {
return undefined
}
const { issuer } = credential as { issuer?: unknown }
if (typeof issuer !== "object" || issuer === null) {
return undefined
}
const { id } = issuer as { id?: unknown }
return typeof id === "string" ? id : undefined
/**
* Read `issuer.id` off a decoded-credential fixture, which may be any shape.
*/
function decodedIssuerId(credential: unknown): string | undefined {
if (typeof credential !== "object" || credential === null) {
return undefined
}
const { issuer } = credential as { issuer?: unknown }
if (typeof issuer === "string") {
return issuer
}
if (typeof issuer !== "object" || issuer === null) {
return undefined
}
const { id } = issuer as { id?: unknown }
return typeof id === "string" ? id : undefined
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vc/src/verification/verify-proof.test.ts` around lines 40 - 56,
Update decodedIssuerId() to recognize a string-valued issuer and return that DID
before attempting to access issuer.id; preserve the existing object-form
handling and undefined result for invalid issuer values.

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