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

SDJWT.Issuer

Description

Convenience module for SD-JWT issuers.

This module provides everything needed to create and issue SD-JWTs. It exports a focused API for the issuer role, excluding modules that issuers don't need (like Presentation and Verification).

Security Warning: EC Signing Timing Attack

⚠️ When using Elliptic Curve (EC) keys (ES256 algorithm), be aware that the underlying jose library's EC signing implementation may be vulnerable to timing attacks. This affects signing only, not verification.

For applications where timing attacks are a concern, consider using RSA-PSS (PS256) or Ed25519 (EdDSA) keys instead, which do not have this limitation.

Note: RS256 (RSA-PKCS#1 v1.5) is deprecated per draft-ietf-jose-deprecate-none-rsa15 due to padding oracle attack vulnerabilities. PS256 (RSA-PSS) is the recommended RSA algorithm and is used by default for RSA keys.

Usage

For issuers, import this module:

import SDJWT.Issuer

This gives you access to:

Creating SD-JWTs

The main function for creating SD-JWTs is createSDJWT:

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

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

-- Create SD-JWT with typ and kid headers
result <- createSDJWT (Just "sd-jwt") (Just "key-1") SHA256 issuerKey ["given_name"] claims

-- Create SD-JWT with array elements (using JSON Pointer syntax)
result <- createSDJWT Nothing Nothing SHA256 issuerKey ["nationalities/0", "nationalities/2"] claims

-- Create SD-JWT with mixed object and array paths
result <- createSDJWT Nothing Nothing SHA256 issuerKey ["address/street_address", "nationalities/1"] claims

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 (expiration time) and nbf (not before) are automatically validated if present. See RFC 9901 Section 4.1.

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

JWT Headers

Both typ and kid headers are supported natively through jose's API:

  • typ: Recommended by RFC 9901 Section 9.11 for explicit typing (e.g., "sd-jwt")
  • kid: Key ID for key management (useful when rotating keys)

Decoy Digests

To add decoy digests (to obscure the number of selectively disclosable claims), use createSDJWTWithDecoys:

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

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

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

For advanced use cases (e.g., adding decoys to nested _sd arrays or custom placement logic), import SDJWT.Internal.Issuance to access buildSDJWTPayload and other low-level functions.

Key Binding Support

To include the holder's public key in the SD-JWT (for key binding), use addHolderKeyToClaims to add the cnf claim:

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

Example

>>> :set -XOverloadedStrings
>>> import SDJWT.Issuer
>>> import qualified Data.Map.Strict as Map
>>> import qualified Data.Aeson as Aeson
>>> import qualified Data.Text as T
>>> let claims = Map.fromList [("sub", Aeson.String "user_123"), ("given_name", Aeson.String "John"), ("family_name", Aeson.String "Doe")]
>>> -- Note: In real usage, you would load your private key here
>>> -- issuerPrivateKeyJWK <- loadPrivateKeyJWK
>>> -- result <- createSDJWT Nothing SHA256 issuerPrivateKeyJWK ["given_name", "family_name"] claims
>>> -- case result of Right sdjwt -> serializeSDJWT sdjwt; Left err -> T.pack $ show err
Synopsis

Core Types

Serialization

Creating SD-JWTs

Functions for creating SD-JWTs from claims sets.

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

Helper Functions

Convenience functions for common operations.

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