{-# LANGUAGE OverloadedStrings #-} -- | 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: -- -- * Core data types (HashAlgorithm, SDJWT, SDJWTPayload, etc.) -- * Serialization functions ('serializeSDJWT', 'deserializeSDJWT') -- * Issuance functions ('createSDJWT', 'createSDJWTWithDecoys') -- * Helper functions ('addHolderKeyToClaims') -- -- == 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 module SDJWT.Issuer ( -- * Core Types module SDJWT.Internal.Types -- * Serialization , module SDJWT.Internal.Serialization -- * Creating SD-JWTs -- | Functions for creating SD-JWTs from claims sets. , createSDJWT , createSDJWTWithDecoys -- * Helper Functions -- | Convenience functions for common operations. , addHolderKeyToClaims ) where import SDJWT.Internal.Types import SDJWT.Internal.Serialization import SDJWT.Internal.Issuance ( createSDJWT , createSDJWTWithDecoys , addHolderKeyToClaims )