SolidusIdentity
Create Your DID

Issuing a Verifiable Credential: What the SDK Actually Signs

The unlinkability implementation described here is unaudited.

Leave out one optional parameter and what you have issued is a bearer credential, usable by whoever holds the file. That is not our characterisation; it is the wording in our own published type definitions.

What ends up inside the signature

Every issued credential carries:

  • iss, the issuer's identifier.
  • vct, the credential type, as an opaque string.
  • iat, when it was issued.
  • the subject's claims, each one salted and hashed so it can be revealed individually later.

And, only if the issuer asks for them:

  • sub, the holder's identifier.
  • nbf / exp, valid-from and valid-until.
  • cnf: a holder public key the credential is bound to.
  • status, a pointer into a revocation status list.

Signing is Ed25519 / EdDSA; disclosure hashing is SHA-256 and the code throws on any other hash algorithm rather than silently accepting one.

The default is the weakest configuration, so say it first

If the issuer does not pass a holder public key, there is no holder binding. The published documentation for that parameter reads: "Omit to issue a bearer credential (Phase 1 default)." And for the subject: "if absent, the SD-JWT is bearer."

A bearer credential is exactly what it sounds like. Anyone who obtains the file can present it. It does not matter that it was issued to you; it matters who has the bytes.

We are not going to describe this as "flexible." It is the weakest reachable configuration and it is what you get by leaving a parameter out, which is what people do. If you are issuing anything that matters, pass the holder's key. When you do, the credential carries a cnf claim and a verifier can require the holder to prove possession at presentation time.

What "selective disclosure" does and does not mean here

The credential you hand the holder contains every claim value. Our own module documentation is explicit: "Disclosure values stay attached to the compact form; selective revelation is a holder-side concern."

So issuance is not where anything is hidden. The selectivity happens later, when the holder chooses which disclosures to include in a presentation. Until then, the file is complete.

And the mechanism is salted hashes, not unlinkability. Two presentations of the same credential carry the same issuer signature, so a verifier who sees both can tell they came from the same credential. That is a property of this format and we are not going to obscure it: the longer version is on what the digest still reveals. The BBS+ implementation referenced here is unaudited.

The nested-claims footgun

By default, every top-level claim is disclosable and nothing nested is. Published wording: "nested objects are not auto-expanded", and the comment goes on to say you must pass explicit paths to selectively disclose them.

In practice that means an address object is one all-or-nothing unit. Reveal the address and you reveal every field inside it, street, city, postcode, whatever else you put there. If you wanted the holder to be able to show only a city, you had to say so at issuance, with explicit dotted paths, and you cannot fix it afterwards without reissuing.

This is the single most likely way to accidentally issue a credential that leaks more than intended, and it happens at the moment of issuance, silently, with no error.

Revocation is opt-in, and there is a further catch

A status-list reference is only present if the issuer supplies one. Omit it and the credential has no revocation pointer at all, nothing for a verifier to check, ever.

And even when supplied, the honest bound applies: the status-list endpoint on our stack is live, but no published list identifier is discoverable from outside, and a verifier that does not check is not stopped by anything.

The type is not verified against anything

vct is an opaque string and remote type metadata is switched off, the code sets loadTypeMetadataFormat: false, with a comment saying the issuer is authoritative for the type and that a metadata endpoint is future work.

So a verifier receiving a credential cannot confirm that the type means what the issuer says it means. The type is a label the issuer chose. That is a real interoperability limit, not a detail.

You can check all of this yourself

$ mkdir /tmp/check && cd /tmp/check && npm init -y
$ npm i @solidus-network/[email protected]
$ cat node_modules/@solidus-network/sdk/dist/sdjwt/types.d.ts

Clean directory, no monorepo, no local build: the way a stranger would do it. Done on 2026-07-31; the package installed and every quotation on this page came out of that file.

And two things wrong in our own published comments

While reading it, we found defects in what we ship, so we are naming them here rather than fixing them quietly:

  1. A comment points at .claude/plans/developer-tools/archieved/2026-05-09-sdjwt-vc-implementation.md: an internal file that is not in the published package and that no reader can open. A published comment that references something only we can see is a bug in the package.
  2. A comment calls SD-JWT VC "one of two W3C VC formats." SD-JWT VC is an IETF draft. It is not a W3C format, and we are precise about that distinction elsewhere on this site, so shipping the loose version in the package is our error.

Both are still present in the version quoted above.

Keep reading

Issuing a Verifiable Credential: What the SDK Actually Signs · Solidus — Solidus Identity