sd-jwt-0.1.0.0: Selective Disclosure for JSON Web Tokens (RFC 9901)
Safe HaskellNone
LanguageHaskell2010

SDJWT.Internal.Issuance

Contents

Description

SD-JWT issuance: Creating SD-JWTs from claims sets.

This module provides functions for creating SD-JWTs on the issuer side. It handles marking claims as selectively disclosable, creating disclosures, computing digests, and building the final signed JWT.

Nested Structures

This module supports nested structures (RFC 9901 Sections 6.2 and 6.3) using JSON Pointer syntax (RFC 6901) for specifying nested claim paths.

JSON Pointer Syntax

Nested paths use forward slash (/) as a separator. Paths can refer to both object properties and array elements:

-- Object properties
["address/street_address", "address/locality"]

This marks street_address and locality within the address object as selectively disclosable.

-- Array elements
["nationalities/0", "nationalities/2"]

This marks elements at indices 0 and 2 in the nationalities array as selectively disclosable.

-- Mixed object and array paths
["address/street_address", "nationalities/1"]

Ambiguity Resolution

Paths with numeric segments (e.g., ["x/22"]) are ambiguous: they could refer to an array element at index 22, or an object property with key "22". The library resolves this ambiguity by checking the actual claim type at runtime:

  • If x is an array → ["x/22"] refers to array element at index 22
  • If x is an object → ["x/22"] refers to object property "22"

This follows JSON Pointer semantics (RFC 6901) where the path alone doesn't determine the type.

Escaping Special Characters

JSON Pointer provides escaping for keys containing special characters:

  • ~1 represents a literal forward slash /
  • ~0 represents a literal tilde ~

Examples:

  • ["contact~1email"] → marks the literal key "contact/email" as selectively disclosable
  • ["user~0name"] → marks the literal key "user~name" as selectively disclosable
  • ["address/email"] → marks email within address object as selectively disclosable

Nested Structure Patterns

The module supports two patterns for nested structures:

  1. Structured SD-JWT (Section 6.2): Parent object stays in payload with _sd array containing digests for sub-claims.
  2. Recursive Disclosures (Section 6.3): Parent is selectively disclosable, and its disclosure contains an _sd array with digests for sub-claims.

The pattern is automatically detected based on whether the parent claim is also in the selective claims list.

Examples

Structured SD-JWT (Section 6.2):

buildSDJWTPayload SHA256 ["address/street_address", "address/locality"] claims

This creates a payload where address object contains an _sd array.

Recursive Disclosures (Section 6.3):

buildSDJWTPayload SHA256 ["address", "address/street_address", "address/locality"] claims

This creates a payload where address digest is in top-level _sd, and the address disclosure contains an _sd array with sub-claim digests.

Array Elements:

buildSDJWTPayload SHA256 ["nationalities/0", "nationalities/2"] claims

This marks array elements at indices 0 and 2 as selectively disclosable.

Nested Arrays:

buildSDJWTPayload SHA256 ["nested_array/0/0", "nested_array/0/1", "nested_array/1/0"] claims

This marks nested array elements. The path ["nested_array/0/0"] refers to element at index 0 of the array at index 0 of nested_array.

Mixed Object and Array Paths:

buildSDJWTPayload SHA256 ["address/street_address", "nationalities/1"] claims

This marks both an object property and an array element as selectively disclosable.

Decoy Digests

Decoy digests are optional random digests added to _sd arrays to obscure the actual number of selectively disclosable claims. This is useful for privacy-preserving applications where you want to hide how many claims are selectively disclosable.

To use decoy digests:

  1. Build the SD-JWT payload using buildSDJWTPayload
  2. Generate decoy digests using addDecoyDigest
  3. Manually add them to the _sd array in the payload
  4. Sign the modified payload

Example:

-- Build the initial payload
(payload, disclosures) <- buildSDJWTPayload SHA256 ["given_name", "email"] claims

-- Generate decoy digests
decoy1 <- addDecoyDigest SHA256
decoy2 <- addDecoyDigest SHA256

-- Add decoy digests to the _sd array
case payloadValue payload of
  Aeson.Object obj -> do
    case KeyMap.lookup "_sd" obj of
      Just (Aeson.Array sdArray) -> do
        let decoyDigests = [Aeson.String (unDigest decoy1), Aeson.String (unDigest decoy2)]
        let updatedSDArray = sdArray <> V.fromList decoyDigests
        let updatedObj = KeyMap.insert "_sd" (Aeson.Array updatedSDArray) obj
        -- Sign the updated payload...
      _ -> -- Handle error
  _ -> -- Handle error

During verification, decoy digests that don't match any disclosure are automatically ignored, so they don't affect verification.

Synopsis

Public API

createSDJWT Source #

Arguments

:: JWKLike jwk 
=> Maybe Text

Optional typ header value (RFC 9901 Section 9.11 recommends explicit typing). If Nothing, no typ header is added. If Just "sd-jwt" or Just "example+sd-jwt", the typ header is included in the JWT header.

-> Maybe Text

Optional kid header value (Key ID for key management). If Nothing, no kid header is added.

-> HashAlgorithm

Hash algorithm for digests

-> jwk

Issuer private key JWK (Text or jose JWK object)

-> [Text]

Claim names to mark as selectively disclosable

-> Object

Original claims object. May include standard JWT claims such as exp (expiration time), nbf (not before), iss (issuer), sub (subject), iat (issued at), etc. These standard claims will be validated during verification if present (see verifySDJWT).

-> IO (Either SDJWTError SDJWT) 

Create a complete SD-JWT (signed).

This function creates an SD-JWT and signs it using the issuer's key. Creates a complete SD-JWT with signed JWT using jose.

Returns the created SD-JWT or an error.

Standard JWT Claims

Standard JWT claims (RFC 7519) can be included in the claims map and will be preserved in the issuer-signed JWT payload. During verification, standard claims like exp and nbf are automatically validated if present. See RFC 9901 Section 4.1 for details.

Example

-- Create SD-JWT without typ header
result <- createSDJWT Nothing SHA256 issuerKey ["given_name", "family_name"] claims

-- Create SD-JWT with typ header
result <- createSDJWT (Just "sd-jwt") SHA256 issuerKey ["given_name", "family_name"] claims

-- Create SD-JWT with expiration time
let claimsWithExp = Map.insert "exp" (Aeson.Number (fromIntegral expirationTime)) claims
result <- createSDJWT (Just "sd-jwt") SHA256 issuerKey ["given_name"] claimsWithExp

createSDJWTWithDecoys Source #

Arguments

:: JWKLike jwk 
=> Maybe Text

Optional typ header value (e.g., Just "sd-jwt" or Just "example+sd-jwt"). If Nothing, no typ header is added.

-> Maybe Text

Optional kid header value (Key ID for key management). If Nothing, no kid header is added.

-> HashAlgorithm

Hash algorithm for digests

-> jwk

Issuer private key JWK (Text or jose JWK object)

-> [Text]

Claim names to mark as selectively disclosable

-> Object

Original claims object. May include standard JWT claims such as exp (expiration time), nbf (not before), iss (issuer), sub (subject), iat (issued at), etc. These standard claims will be validated during verification if present (see verifySDJWT).

-> Int

Number of decoy digests to add (must be >= 0)

-> IO (Either SDJWTError SDJWT) 

Create an SD-JWT with optional typ header and decoy digests.

This function is similar to createSDJWT but automatically adds a specified number of decoy digests to the _sd array to obscure the actual number of selectively disclosable claims.

Returns the created SD-JWT or an error.

Standard JWT Claims

Standard JWT claims (RFC 7519) can be included in the claims map and will be preserved in the issuer-signed JWT payload. During verification, standard claims like exp and nbf are automatically validated if present. See RFC 9901 Section 4.1 for details.

Example

-- Create SD-JWT with 5 decoy digests, no typ header
result <- createSDJWTWithDecoys Nothing SHA256 issuerKey ["given_name", "email"] claims 5

-- Create SD-JWT with 5 decoy digests and typ header
result <- createSDJWTWithDecoys (Just "sd-jwt") SHA256 issuerKey ["given_name", "email"] claims 5

addDecoyDigest :: HashAlgorithm -> IO Digest Source #

Generate a decoy digest.

Decoy digests are random digests that don't correspond to any disclosure. They are used to obscure the actual number of selectively disclosable claims.

According to RFC 9901 Section 4.2.5, decoy digests should be created by hashing over a cryptographically secure random number, then base64url encoding.

Advanced Use

Decoy digests are an advanced feature used to hide the number of selectively disclosable claims. They are optional and must be manually added to the _sd array if you want to obscure the actual number of selectively disclosable claims.

To use decoy digests, call this function to generate them and manually add them to the _sd array in your payload. This is useful for privacy-preserving applications where you want to hide how many claims are selectively disclosable.

buildSDJWTPayload Source #

Arguments

:: HashAlgorithm 
-> [Text]

Claim names to mark as selectively disclosable (supports JSON Pointer syntax for nested paths)

-> Object

Original claims object

-> IO (Either SDJWTError (SDJWTPayload, [EncodedDisclosure])) 

addHolderKeyToClaims Source #

Arguments

:: Text

Holder's public key as a JWK JSON string

-> Object

Original claims object

-> Object

Claims object with cnf claim added

Add holder's public key to claims as a cnf claim (RFC 7800).

This convenience function adds the holder's public key to the claims map in the format required by RFC 7800 for key confirmation:

{
  "cnf": {
    "jwk": "holderPublicKeyJWK"
  }
}

The cnf claim is used during key binding to prove that the holder possesses the corresponding private key.

Example

let holderPublicKeyJWK = "{"kty":"EC","crv":"P-256","x":"...","y":"..."}"
let claimsWithCnf = addHolderKeyToClaims holderPublicKeyJWK claims
result <- createSDJWT (Just "sd-jwt") SHA256 issuerKey ["given_name"] claimsWithCnf

See Also

  • RFC 7800: Proof-of-Possession Key Semantics for JSON Web Tokens (JWT)
  • RFC 9901 Section 4.3: Key Binding