# `Attesto.SdJwt`
[🔗](https://github.com/XukuLLC/attesto/blob/v2.0.1/lib/attesto/sd_jwt.ex#L1)

Selective Disclosure for JWTs (SD-JWT), draft-ietf-oauth-selective-disclosure-jwt.

An SD-JWT lets an issuer sign a set of claims where individual claims are
*selectively disclosable*: the signed JWT carries only the SHA-256 digest of
each hidden claim (in the `_sd` array, or as an array element `{"...": digest}`),
and the cleartext claim travels separately as a **Disclosure**. The holder
chooses which Disclosures to release when presenting the credential, so a
verifier learns only the disclosed subset while the issuer's single signature
still covers all of them.

The wire form is a `~`-separated list:

    <Issuer-signed JWT>~<Disclosure 1>~...~<Disclosure N>~<optional Key Binding JWT>

A **Disclosure** is `base64url(UTF-8(JSON(array)))` with no padding, where the
array is `[salt, claim_name, claim_value]` for an object property or
`[salt, value]` for an array element. Its **digest** is
`base64url(SHA-256(ASCII(disclosure)))` — computed over the exact disclosure
string, so interop does not depend on how any party serialises JSON.

This module is the base SD-JWT mechanism. `Attesto.SdJwtVc` layers the
IETF SD-JWT VC profile (`vct`, `cnf`, `vc+sd-jwt` typing) on top.

## What is implemented

  * `issue/2` — sign an SD-JWT making chosen **top-level** claims selectively
    disclosable. (Nested/recursive and array-element issuance is a planned
    extension; the wire format it would produce is already a superset this
    module can verify.)
  * `verify/3` — the full recursive processing model from the spec: verify the
    issuer signature, then resolve `_sd` digests and array `{"...": digest}`
    elements at every depth, rejecting a presentation whose Disclosures do not
    all reference a digest (spec §7.3) or whose digests collide.
  * `verify_key_binding/3` — verify a holder Key Binding JWT over a verified
    presentation (nonce/aud/`sd_hash`), for the OID4VP verifier path.

Like the rest of attesto core this module is conn-free and fail-closed.

# `key_binding_input`

```elixir
@type key_binding_input() :: %{
  :key_binding_jwt =&gt; String.t() | nil,
  :issuer_jwt =&gt; String.t(),
  :disclosures =&gt; [String.t()],
  optional(term()) =&gt; term()
}
```

# `verified`

```elixir
@type verified() :: %{
  claims: map(),
  key_binding_jwt: String.t() | nil,
  issuer_jwt: String.t(),
  disclosures: [String.t()]
}
```

A parsed, verified SD-JWT presentation.

# `verify_error`

```elixir
@type verify_error() ::
  :malformed
  | :invalid_signature
  | :unsupported_alg
  | :invalid_typ
  | :invalid_disclosure
  | :unused_disclosure
  | :duplicate_digest
  | :unsupported_sd_alg
  | :reserved_claim_name
```

# `array_disclosure`

```elixir
@spec array_disclosure(String.t(), term()) :: String.t()
```

The Disclosure string for an array element (`[salt, value]`).

# `digest`

```elixir
@spec digest(String.t(), String.t()) :: String.t()
```

The digest of a Disclosure under `sd_alg` (default `sha-256`):
`base64url(SHA-256(ASCII(disclosure)))`.

# `generate_salt`

```elixir
@spec generate_salt() :: String.t()
```

A fresh salt for a Disclosure: 128-bit CSPRNG value, base64url, no padding.

# `issue`

```elixir
@spec issue(
  map(),
  keyword()
) :: String.t()
```

Issue an SD-JWT, making the chosen top-level claims selectively disclosable.

`claims` is the full claim set. Options:

  * `:disclosable` - the list of top-level claim names to hide behind `_sd`
    digests (each becomes a Disclosure). Every other claim is signed in the
    clear. Defaults to `[]` (a plain, fully-visible JWT with an empty `_sd`).
  * exactly one of `:pem` (legacy extractable private key) or `:keystore`
    (an `Attesto.Keystore` that may also implement `Attesto.Signer`).
  * `:alg` - the JWS algorithm; inferred from the key when omitted.
  * `:typ` - the JOSE `typ` header (e.g. `"vc+sd-jwt"`); omitted when nil.
  * `:kid` - the JOSE `kid` header; omitted when nil.
  * `:sd_alg` - the hashing algorithm name for `_sd_alg`. Default
    `sha-256`.

Returns the combined **Issuance** string (`<JWT>~<D1>~...~<DN>~`, trailing
separator, no Key Binding JWT).

# `object_disclosure`

```elixir
@spec object_disclosure(String.t(), String.t(), term()) :: String.t()
```

The Disclosure string for an object property (`[salt, name, value]`).

`name` must be a string; `value` is any JSON-encodable term. Returns the
base64url(no-pad) of the UTF-8 JSON array.

# `verify`

```elixir
@spec verify(String.t(), map() | [map()] | list(), keyword()) ::
  {:ok, verified()} | {:error, verify_error()}
```

Verify an SD-JWT presentation and reconstruct the disclosed claims.

`combined` is the `~`-separated presentation. `jwks` is the issuer's JWK Set
(`%{"keys" => [...]}`, a single JWK map, or a list) used to verify the
Issuer-signed JWT. Options:

  * `:accepted_algs` - JWS algorithms accepted for the issuer signature.
    Defaults to `Attesto.SigningAlg.fapi_algs/0`.

Returns `{:ok, %{claims:, key_binding_jwt:, issuer_jwt:}}` where `claims` is
the payload with every `_sd`/array digest resolved from the presented
Disclosures (and the `_sd`/`_sd_alg` machinery removed). The Key Binding JWT,
if present, is returned UNVERIFIED (see `verify_key_binding/3`) - reconstructing
claims and checking holder binding are separate steps. Rejects a presentation
in which any Disclosure is unused (spec §7.3) or any digest is duplicated.

# `verify_key_binding`

```elixir
@spec verify_key_binding(key_binding_input(), map(), keyword()) ::
  :ok | {:error, atom()}
```

Verify a holder Key Binding JWT (`kb+jwt`) over an already-verified
presentation.

`verified` is the map `verify/3` returned; `holder_jwk` is the key the issuer
bound the credential to (the `cnf` key). Options:

  * `:nonce` - the expected `nonce` (REQUIRED).
  * `:audience` - the expected `aud` (REQUIRED) - this verifier's identifier.
  * `:now` / `:max_age_seconds` - freshness bounds on `iat` (default 300s).

Verifies the KB-JWT signature under `holder_jwk`, its `typ` (`kb+jwt`), the
`nonce`/`aud`, and that `sd_hash` equals `base64url(SHA-256(<Issuer JWT and
presented Disclosures, `~`-joined, trailing `~`>))` (spec §4.3) - so the
holder signed over exactly the presentation the verifier reconstructed claims
from.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
