Solidus
Create Your DID

The Digest Your Wallet Sends When You Share Less Than Everything

The unlinkability implementation described here is unaudited.

The primitives underneath are well-scrutinised; our assembly of them is not.

What a disclosure digest is

A selectively-disclosable credential does not contain your fields in the clear. The signed payload carries an array, conventionally _sd, of digests, one per hideable field.

When you disclose a field, your wallet sends the disclosure: a short encoded string containing the field. The verifier hashes exactly that string and looks for the result in the _sd array. A match means the issuer signed that field, even though the issuer's signature covered only the digest.

The elegance is that the issuer signs once, and you choose afterwards how much of what it signed to reveal. The signature does not need to be re-made for each combination.

Check it with any SHA-256 tool

You do not need our library, and we would rather you did not use it for this:

# from a compact SD-JWT (issue one with @solidus-network/sdk's issueSdJwtVc):
#   1. take one disclosure segment — the base64url text between the '~' separators
#   2. SHA-256 that exact string, as bytes
#   3. base64url-encode the hash
#   4. find it in the decoded JWS payload's _sd array
# It matches exactly.

Any generic hash tool reproduces this. If a vendor's selective disclosure can only be verified with that vendor's SDK, that is worth asking about.

No crypto-agility, and it is in the published code

Our hasher supports exactly one algorithm and throws on anything else. From the package you can install:

export function sdJwtHasher(data, alg) {
  if (alg !== 'sha-256') { throw new Error(`unsupported SD-JWT hash algorithm: ${alg}`) }
  ...
}

Its own comment says: "Phase 1 only supports sha-256."

So every credential we issue carries sha-256 by construction, not by a configuration choice a caller can override. That is not a hidden default you could change with a flag, the alternative path does not exist and calling for one is an error.

Is that bad? Today, mostly no: SHA-256 is the specification's default and the algorithm the format emits without explicit configuration, so we are aligned with everyone else. It becomes bad the day that stops being true, and the honest framing is that we have no migration path, not that we chose a good default. Crypto-agility is a property you build before you need it.

The part we cannot fix, and would rather you heard from us

An undisclosed field's digest still travels with every presentation.

When you withhold a field, its value stays hidden: the digest does not reveal it. But the digest itself is in the signed payload, and it goes out every single time you present that credential.

For a credential you reuse, the set of digests is stable and distinctive. Two verifiers who compare notes can match on it. They do not learn what you withheld. They learn that the same credential appeared at both of them, which is often the fact you were trying not to reveal.

This is a property of the construction, not something our implementation introduces or could remove. Any conforming implementation of this format has it.

The argument is good; what is unverified is our implementation, not the shape of the idea.

The complementary defence is a different identifier per verifier, so there is no account to join the observations onto. That helps and does not close the gap: the digest set is still the same digest set.

Practical reading

  • A credential presented once has no correlation problem here. A credential presented at many verifiers over years does.
  • Withholding a field is not the same as it not existing. Its digest still tells a verifier that some hidden field is there.
  • If unlinkability across verifiers is your requirement, this format does not give it to you, at any implementation quality. The BBS+ implementation referenced here is unaudited.

Keep reading

The Digest Your Wallet Sends When You Share Less Than Everything · Solidus — Solidus Identity