| Safe Haskell | None |
|---|---|
| Language | Haskell2010 |
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
xis an array →["x/22"]refers to array element at index 22 - If
xis 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:
~1represents a literal forward slash/~0represents 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"]→ marksemailwithinaddressobject as selectively disclosable
Nested Structure Patterns
The module supports two patterns for nested structures:
- Structured SD-JWT (Section 6.2): Parent object stays in payload with
_sdarray containing digests for sub-claims. - Recursive Disclosures (Section 6.3): Parent is selectively disclosable, and its
disclosure contains an
_sdarray 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:
- Build the SD-JWT payload using buildSDJWTPayload
- Generate decoy digests using addDecoyDigest
- Manually add them to the
_sdarray in the payload - 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
- createSDJWT :: JWKLike jwk => Maybe Text -> Maybe Text -> HashAlgorithm -> jwk -> [Text] -> Object -> IO (Either SDJWTError SDJWT)
- createSDJWTWithDecoys :: JWKLike jwk => Maybe Text -> Maybe Text -> HashAlgorithm -> jwk -> [Text] -> Object -> Int -> IO (Either SDJWTError SDJWT)
- addDecoyDigest :: HashAlgorithm -> IO Digest
- buildSDJWTPayload :: HashAlgorithm -> [Text] -> Object -> IO (Either SDJWTError (SDJWTPayload, [EncodedDisclosure]))
- addHolderKeyToClaims :: Text -> Object -> Object
Public API
Arguments
| :: JWKLike jwk | |
| => Maybe Text | Optional typ header value (RFC 9901 Section 9.11 recommends explicit typing). If |
| -> Maybe Text | Optional kid header value (Key ID for key management). If |
| -> 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 |
| -> 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 |
| -> Maybe Text | Optional kid header value (Key ID for key management). If |
| -> 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 |
| -> 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.
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])) |
Arguments
| :: Text | Holder's public key as a JWK JSON string |
| -> Object | Original claims object |
| -> Object | Claims object with |
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