packages feed

sd-jwt-0.1.0.0: README.md

# SD-JWT: Selective Disclosure for JSON Web Tokens

[![CI](https://github.com/yaronf/sd-jwt/actions/workflows/ci.yml/badge.svg)](https://github.com/yaronf/sd-jwt/actions/workflows/ci.yml)
[![Hackage](https://img.shields.io/hackage/v/sd-jwt)](https://hackage.haskell.org/package/sd-jwt)
[![Hackage deps](https://img.shields.io/hackage-deps/v/sd-jwt)](https://hackage.haskell.org/package/sd-jwt)

[![Haddocks](https://img.shields.io/badge/docs-haddock-blue)](https://hackage.haskell.org/package/sd-jwt/docs)
[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue)](LICENSE)

Haskell implementation of [RFC 9901](https://www.rfc-editor.org/rfc/rfc9901.html): Selective Disclosure for JSON Web Tokens (SD-JWT).

## Overview

SD-JWT enables selective disclosure of individual elements of a JSON data structure used as the payload of a JSON Web Signature (JWS). The primary use case is the selective disclosure of JSON Web Token (JWT) claims.

## Features

- ✅ SD-JWT issuance (issuer side)
- ✅ SD-JWT presentation (holder side)
- ✅ SD-JWT verification (verifier side)
- ✅ Key Binding support (SD-JWT+KB)
- ✅ Nested and recursive disclosures
- ✅ Multiple hash algorithms (SHA-256, SHA-384, SHA-512)
- ✅ Multiple signing algorithms: PS256 (RSA-PSS, default), RS256 (deprecated), ES256 (EC P-256), EdDSA (Ed25519)

## Status

✅ **Stable** - This implementation is feature-complete and ready for use.

The library implements RFC 9901 with comprehensive test coverage (224 tests). See [internal-docs/IMPLEMENTATION_PLAN.md](internal-docs/IMPLEMENTATION_PLAN.md) for implementation details.

## Installation

```bash
stack build
# or
cabal build
```

## Examples

A complete end-to-end example demonstrating the full SD-JWT flow (issuer → holder → verifier) is available:

```bash
stack exec sd-jwt-example
# or
stack runghc examples/EndToEndExample.hs
```

This example shows:
- Issuer creating an SD-JWT with selective disclosure
- Holder selecting which claims to disclose and creating a presentation
- Verifier verifying the presentation and extracting claims

## Usage

### Recommended: Use Persona-Specific Modules

The library provides three persona-specific modules for different use cases:

#### For Issuers (Creating SD-JWTs)

⚠️ **Security Warning**: 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, default for RSA keys) or Ed25519 (EdDSA) keys instead.

**Note**: RS256 (RSA-PKCS#1 v1.5) is deprecated per [draft-ietf-jose-deprecate-none-rsa15](https://datatracker.ietf.org/doc/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.

```haskell
import SDJWT.Issuer
import qualified Data.Map.Strict as Map
import qualified Data.Aeson as Aeson
import qualified Data.Text as T

-- Create claims
let claims = Map.fromList
      [ ("sub", Aeson.String "user_123")
      , ("given_name", Aeson.String "John")
      , ("family_name", Aeson.String "Doe")
      ]

-- Load issuer's private key (can be Text or jose JWK object)
-- Example Text format: "{\"kty\":\"RSA\",\"n\":\"...\",\"e\":\"AQAB\",\"d\":\"...\"}"
issuerPrivateKeyJWK <- loadPrivateKeyJWK  -- Your function to load the key (returns Text or JWK.JWK)

-- Create SD-JWT with selective disclosure
-- PS256 (RSA-PSS) is used by default for RSA keys
-- createSDJWT signature: mbTyp mbKid hashAlg key claimNames claims
result <- createSDJWT (Just "sd-jwt") Nothing SHA256 issuerPrivateKeyJWK ["given_name", "family_name"] claims
case result of
  Right sdjwt -> do
    let serialized = serializeSDJWT sdjwt
    -- Send serialized SD-JWT to holder
  Left err -> putStrLn $ "Error creating SD-JWT: " ++ show err
```

#### For Holders (Creating Presentations)

```haskell
import SDJWT.Holder
import qualified Data.Text as T
import Data.Int (Int64)

-- Deserialize SD-JWT received from issuer
case deserializeSDJWT sdjwtText of
  Right sdjwt -> do
    -- Select which disclosures to include in the presentation
    -- The holder chooses which claims to reveal (e.g., only "given_name", not "family_name")
    case selectDisclosuresByNames sdjwt ["given_name"] of
      Right presentation -> do
        -- The presentation now contains:
        -- - presentationJWT: The issuer-signed JWT (with digests for all claims)
        -- - selectedDisclosures: Only the disclosures for "given_name"
        -- Optionally add key binding (SD-JWT+KB) for proof of possession
        holderPrivateKeyJWK <- loadPrivateKeyJWK  -- Your function to load holder's private key (Text or jose JWK)
        let audience = "verifier.example.com"
        let nonce = "random-nonce-12345"
        let issuedAt = 1683000000 :: Int64
        result <- addKeyBindingToPresentation SHA256 holderPrivateKeyJWK audience nonce issuedAt presentation (Aeson.object [])
        case result of
          Right presentationWithKB -> do
            -- Serialize the presentation: JWT~disclosure1~disclosure2~...~KB-JWT
            -- This includes both the issuer-signed JWT and the selected disclosures
            let serialized = serializePresentation presentationWithKB
            -- Send serialized presentation to verifier
            -- The verifier will verify the signature and reconstruct claims from the selected disclosures
          Left err -> putStrLn $ "Error adding key binding: " ++ show err
      Left err -> putStrLn $ "Error selecting disclosures: " ++ show err
  Left err -> putStrLn $ "Error deserializing SD-JWT: " ++ show err
```

#### For Verifiers (Verifying SD-JWTs)

```haskell
import SDJWT.Verifier
import qualified Data.Text as T

-- Deserialize presentation received from holder
case deserializePresentation presentationText of
  Right presentation -> do
    -- Load issuer's public key (can be Text or jose JWK object)
    issuerPublicKeyJWK <- loadPublicKeyJWK  -- Your function to load issuer's public key (Text or jose JWK)
    
    -- Verify the SD-JWT (optionally require specific typ header)
    -- Pass Nothing to allow any typ, or Just "sd-jwt" to require specific typ
    result <- verifySDJWT issuerPublicKeyJWK presentation Nothing
    case result of
      Right processedPayload -> do
        -- Extract claims
        let claims = processedClaims processedPayload
        -- Use verified claims
      Left err -> putStrLn $ "Verification failed: " ++ show err
  Left err -> putStrLn $ "Error deserializing presentation: " ++ show err
```

### Advanced Usage

For library developers or advanced use cases requiring low-level access,
import specific Internal modules as needed:

```haskell
import SDJWT.Internal.Types
import SDJWT.Internal.Serialization
import SDJWT.Internal.Issuance
-- etc.
```

### Nested Structures

The library supports nested structures using JSON Pointer syntax (RFC 6901), including both object properties and array elements:

```haskell
let claims = Map.fromList
      [ ("address", Aeson.Object $ KeyMap.fromList
          [ (Key.fromText "street_address", Aeson.String "123 Main St")
          , (Key.fromText "locality", Aeson.String "City")
          , (Key.fromText "country", Aeson.String "US")
          ])
      , ("nationalities", Aeson.Array $ V.fromList
          [ Aeson.String "US"
          , Aeson.String "CA"
          , Aeson.String "UK"
          ])
      ]

-- Structured SD-JWT (Section 6.2): parent stays, sub-claims get _sd array
result <- buildSDJWTPayload SHA256 ["address/street_address", "address/locality"] claims

-- Recursive Disclosures (Section 6.3): parent is selectively disclosable
result <- buildSDJWTPayload SHA256 ["address", "address/street_address", "address/locality"] claims

-- Array elements: mark elements at indices 0 and 2 as selectively disclosable
result <- buildSDJWTPayload SHA256 ["nationalities/0", "nationalities/2"] claims

-- Mixed object and array paths
result <- buildSDJWTPayload SHA256 ["address/street_address", "nationalities/1"] claims

-- Nested arrays: mark element at index 0 of the array at index 0
result <- buildSDJWTPayload SHA256 ["nested_array/0/0", "nested_array/1/1"] claims
```

#### JSON Pointer Escaping

Keys containing forward slashes or tildes must be escaped using JSON Pointer syntax (RFC 6901):

- `~1` = literal `/` (forward slash)
- `~0` = literal `~` (tilde)

**Important**: When creating claims Maps, use the actual (unescaped) JSON keys. When passing claim names to `buildSDJWTPayload`, use escaped forms for keys containing special characters.

Examples:
- Map key: `"contact/email"`, path: `["contact~1email"]` → marks literal key `"contact/email"` (not nested)
- Map key: `"user~name"`, path: `["user~0name"]` → marks literal key `"user~name"` (not nested)
- Map key: `"address"` (with nested `"email"`), path: `["address/email"]` → marks `email` within `address` object (nested path)

**Why escaping is necessary**: Without escaping, there would be ambiguity between:
- A literal key named `"address/email"` 
- The `email` key nested within an `address` object

JSON Pointer escaping resolves this ambiguity. See [RFC 6901](https://www.rfc-editor.org/rfc/rfc6901.html) for the complete specification.

## Supported Algorithms

### Signing Algorithms

- **PS256 (RSA-PSS)** - Default for RSA keys, recommended for security
- **RS256 (RSA-PKCS#1 v1.5)** - Deprecated per [draft-ietf-jose-deprecate-none-rsa15](https://datatracker.ietf.org/doc/draft-ietf-jose-deprecate-none-rsa15/), but still supported for backward compatibility
- **ES256 (EC P-256)** - Elliptic Curve, may be vulnerable to timing attacks during signing
- **EdDSA (Ed25519)** - Recommended for high-security applications

**Note**: RSA keys default to PS256. To use RS256, include `"alg": "RS256"` in your JWK.

### Hash Algorithms

- **SHA-256** - Default algorithm
- **SHA-384**
- **SHA-512**

## Key Format

Keys can be provided in two formats:

1. **Text (JSON string)** - Most convenient, no need to import `jose`:
   ```haskell
   let claims = Map.fromList [("claim", Aeson.String "value")]
   let issuerKey :: T.Text = "{\"kty\":\"RSA\",\"n\":\"...\",\"e\":\"AQAB\",\"d\":\"...\"}"
   -- createSDJWT takes: mbTyp mbKid hashAlg key claimNames claims
   result <- createSDJWT Nothing Nothing SHA256 issuerKey ["claim"] claims
   -- Or with typ header (recommended):
   result <- createSDJWT (Just "sd-jwt") Nothing SHA256 issuerKey ["claim"] claims
   ```

2. **jose JWK object** - If you're already working with the `jose` library:
   ```haskell
   import Crypto.JOSE.JWK as JWK
   let claims = Map.fromList [("claim", Aeson.String "value")]
   jwk <- loadJWK  -- Your function that returns JWK.JWK
   -- createSDJWT takes: mbTyp mbKid hashAlg key claimNames claims
   result <- createSDJWT Nothing Nothing SHA256 jwk ["claim"] claims
   -- Or with typ header (recommended):
   result <- createSDJWT (Just "sd-jwt") Nothing SHA256 jwk ["claim"] claims
   ```

The library automatically handles both formats through the `JWKLike` type class. Users who don't import `jose` can use Text strings directly, while users already working with `jose` can pass JWK objects without serialization overhead.

**JWK JSON Format Example:**
```json
{
  "kty": "RSA",
  "n": "base64url-encoded-modulus",
  "e": "AQAB",
  "d": "base64url-encoded-private-exponent"
}
```

For public keys, omit the `d` field. See [RFC 7517](https://www.rfc-editor.org/rfc/rfc7517.html) for JWK format specification.

## Documentation

- [RFC 9901](https://www.rfc-editor.org/rfc/rfc9901.html) - The SD-JWT specification
- [RFC 7517](https://www.rfc-editor.org/rfc/rfc7517.html) - JSON Web Key (JWK) format
- [RFC 7519](https://www.rfc-editor.org/rfc/rfc7519.html) - JSON Web Token (JWT)
- [RFC 8725](https://www.rfc-editor.org/rfc/rfc8725.html) - JSON Web Token Best Current Practices
- [internal-docs/IMPLEMENTATION_PLAN.md](internal-docs/IMPLEMENTATION_PLAN.md) - Implementation plan
- [internal-docs/TEST_PLAN.md](internal-docs/TEST_PLAN.md) - Test coverage documentation

## License

BSD-3-Clause