gose/jws

JSON Web Signature (JWS) - RFC 7515

This module provides digital signature functionality using all algorithms from RFC 7518: HMAC (HS256/384/512), RSA (RS256/384/512, PS256/384/512), ECDSA (ES256/384/512), and EdDSA.

Example

import gose/jws
import gose/jwa
import gose/jwk

let key = jwk.generate_hmac_key(jwa.HmacSha256)
let payload = <<"hello world":utf8>>

// Create and sign a JWS
let assert Ok(signed) = jws.new(jwa.JwsHmac(jwa.HmacSha256))
  |> jws.sign(key, payload)

// Serialize to compact format
let assert Ok(token) = jws.serialize_compact(signed)

// Parse and verify using a Verifier
let assert Ok(parsed) = jws.parse_compact(token)
let assert Ok(verifier) = jws.verifier(jwa.JwsHmac(jwa.HmacSha256), [key])
let assert Ok(True) = jws.verify(verifier, parsed)

Phantom Types

Jws(state, origin) uses two phantom type parameters:

This prevents calling decode_unprotected_header on a builder-created JWS (which has no raw JSON to decode from) and ensures serialize_compact only accepts signed instances.

Algorithm Pinning

Each verifier is pinned to a single algorithm. This is a deliberate security design, not a limitation. Algorithm confusion attacks (e.g., CVE-2015-9235) exploit libraries that trust the alg header from the token itself, allowing an attacker to switch from an asymmetric algorithm to HMAC and sign with a public key. By requiring the caller to declare the expected algorithm upfront, gose ensures the token’s alg header is verified against the application’s intent, not the other way around. This follows RFC 8725 Section 3.1: the algorithm used for verification should be specified by the application, not taken from the message.

Algorithm pinning is enforced at multiple levels:

  1. Verifier pinning: verifier() requires the expected algorithm; tokens with different algorithms are rejected by verify and verify_detached.
  2. JWK alg metadata: If a key has alg set via jwk.with_alg, the JWS algorithm must match during signing and verification.
  3. JWT verifier: jwt.verifier() requires the expected algorithm upfront; tokens with different algorithms are rejected.
  4. Key type validation: The key type must match the algorithm (RSA for RS256, EC P-256 for ES256, etc.).

Multi-Algorithm Verification

When migrating between algorithms (e.g., RS256 to ES256) or consuming tokens from issuers that use different algorithms, create one verifier per algorithm and try each in sequence:

let assert Ok(rs_verifier) =
  jws.verifier(jwa.JwsRsaPkcs1(jwa.RsaPkcs1Sha256), keys: rsa_keys)
let assert Ok(ec_verifier) =
  jws.verifier(jwa.JwsEcdsa(jwa.EcdsaP256), keys: ec_keys)

let assert Ok(parsed) = jws.parse_compact(token)
let result = case jws.verify(rs_verifier, parsed) {
  Ok(True) -> Ok(True)
  _ -> jws.verify(ec_verifier, parsed)
}

This keeps each verifier’s algorithm policy explicit and auditable, rather than hiding multi-algorithm logic inside the library.

Custom Headers

Custom headers can be added via with_header when building a JWS. For parsed JWS, use decode_custom_headers with a custom decoder to extract header values. with_header rejects reserved names (alg, kid, typ, cty, crit, b64) to prevent conflicts with standard behavior.

Unprotected Headers

Unprotected headers can be added via with_unprotected (for JSON serialization) and accessed via decode_unprotected_header. When parsing JSON format, unprotected header names must not overlap with protected header names.

Security Warning: Unprotected headers are NOT integrity protected. They can be modified by an attacker without invalidating the signature. Only use for non-security-critical metadata.

Critical Header Support

The crit header is validated per RFC 7515:

Key Metadata

JWK metadata (use, key_ops) is enforced during signing and verification. Keys with incompatible metadata are rejected.

JSON Serialization Limitations

Types

Phantom type for JWS created via builder (new + sign).

pub type Built

A JSON Web Signature with phantom types for state and origin tracking.

The origin phantom type distinguishes between JWS created via builders (Built) and JWS obtained by parsing tokens (Parsed). This enables compile-time enforcement that decode_unprotected_header only works on parsed instances.

pub opaque type Jws(state, origin)

Phantom type for JWS obtained by parsing a token.

pub type Parsed

Phantom type for signed JWS.

pub type Signed

Phantom type for unsigned JWS.

pub type Unsigned

A JWS verifier that enforces algorithm pinning and validates key compatibility.

Create with verifier(). The verifier validates that:

  • All keys are compatible with the algorithm
  • Each key’s use field (if set) is Signing
  • Each key’s key_ops field (if set) includes Verify
pub opaque type Verifier

Values

pub fn alg(jws: Jws(state, origin)) -> jwa.JwsAlg

Get the algorithm (alg) from a JWS.

Parameters

  • jws - The JWS to read the algorithm from.

Returns

The JwsAlg signing algorithm.

pub fn cty(jws: Jws(state, origin)) -> Result(String, Nil)

Get the content type (cty) from a JWS header.

Parameters

  • jws - The JWS to read the content type from.

Returns

Ok(String) with the content type, or Error(Nil) if not set.

pub fn decode_custom_headers(
  jws: Jws(Signed, Parsed),
  decoder: decode.Decoder(a),
) -> Result(a, gose.GoseError)

Decode custom headers from a parsed JWS using a custom decoder.

This allows reading non-standard header fields that were present during parsing. For JWS built via new, you already know what headers you set.

Parameters

  • jws - A signed, parsed JWS containing header data to decode.
  • decoder - A decode.Decoder for extracting custom header fields.

Returns

Ok(a) with the decoded custom header value, or Error(ParseError) if no header data is available or decoding fails.

pub fn decode_unprotected_header(
  jws: Jws(Signed, Parsed),
  decoder: decode.Decoder(a),
) -> Result(a, gose.GoseError)

Decode the unprotected header using a custom decoder.

Security Warning: Unprotected headers are NOT integrity protected. They can be modified by an attacker without invalidating the signature. Only use for non-security-critical metadata.

This function only works on parsed JWS instances. When building a JWS, you already know what unprotected headers you set - use has_unprotected_header to check their presence.

Parameters

  • jws - A signed, parsed JWS that may contain unprotected headers.
  • decoder - A decode.Decoder for extracting unprotected header fields.

Returns

Ok(a) with the decoded unprotected header value, or Error(ParseError) if no unprotected headers are present or decoding fails.

pub fn has_unencoded_payload(jws: Jws(state, origin)) -> Bool

Check if the JWS uses an unencoded payload (b64=false per RFC 7797).

Parameters

  • jws - The JWS to check.

Returns

True if the JWS uses an unencoded payload, False otherwise.

pub fn has_unprotected_header(jws: Jws(Signed, origin)) -> Bool

Check if the JWS has unprotected headers.

Returns True if the JWS was parsed from JSON with unprotected headers, or if unprotected headers were added via with_unprotected.

Parameters

  • jws - The signed JWS to check.

Returns

True if unprotected headers are present, False otherwise.

pub fn is_detached(jws: Jws(state, origin)) -> Bool

Check if the JWS has a detached payload.

Parameters

  • jws - The JWS to check.

Returns

True if the JWS uses a detached payload, False otherwise.

pub fn kid(jws: Jws(state, origin)) -> Result(String, Nil)

Get the key ID (kid) from a JWS header.

Security Warning: The kid value comes from the token and is untrusted input. If you use it to look up keys (from a database, filesystem, or key store), you must sanitize it first to prevent injection attacks:

  • Use parameterized queries for database lookups
  • Validate the format matches your expected key ID pattern
  • Never use it directly in file paths or shell commands

Parameters

  • jws - The JWS to read the key ID from.

Returns

Ok(String) with the key ID, or Error(Nil) if not set.

pub fn new(alg: jwa.JwsAlg) -> Jws(Unsigned, Built)

Create a new unsigned JWS with the specified signing algorithm. The payload is provided at sign time via sign.

Parameters

  • alg - The JWS signing algorithm to use (e.g., jwa.JwsHmac(jwa.HmacSha256), jwa.JwsRsaPkcs1(jwa.RsaPkcs1Sha256), jwa.JwsEcdsa(jwa.EcdsaP256), jwa.JwsEddsa).

Returns

An unsigned Jws ready for signing.

Example

let assert Ok(signed) = jws.new(jwa.JwsHmac(jwa.HmacSha256))
  |> jws.sign(key, <<"hello":utf8>>)
pub fn parse_compact(
  token: String,
) -> Result(Jws(Signed, Parsed), gose.GoseError)

Parse a JWS from compact format.

Returns a signed JWS that can be verified with a Verifier. An empty payload segment (header..signature) is treated as a detached payload; use verify_detached to verify with the out-of-band payload.

Parameters

  • token - The compact serialization string (header.payload.signature).

Returns

Ok(Jws(Signed, Parsed)) with the parsed JWS ready for verification, or Error(ParseError) if the token is malformed or the header is invalid.

pub fn parse_json(
  json_str: String,
) -> Result(Jws(Signed, Parsed), gose.GoseError)

Parse a JWS from JSON format (supports both General and Flattened).

Parameters

  • json_str - A JSON string in either General or Flattened JWS Serialization format.

Returns

Ok(Jws(Signed, Parsed)) with the parsed JWS ready for verification, or Error(ParseError) if the JSON is malformed, the header is invalid, or the General format contains multiple signatures.

pub fn payload(jws: Jws(state, origin)) -> BitArray

Get the payload from a JWS.

Parameters

  • jws - The JWS to read the payload from.

Returns

The payload as a BitArray.

pub fn serialize_compact(
  jws: Jws(Signed, Built),
) -> Result(String, gose.GoseError)

Serialize a signed JWS to compact format.

Format: {base64url(header)}.{base64url(payload)}.{base64url(signature)}

For detached payloads: {base64url(header)}..{base64url(signature)}

For unencoded payloads (b64=false): {base64url(header)}.{payload}.{base64url(signature)}

Returns an error if the payload contains . characters when using b64=false, as this would create an invalid compact serialization (RFC 7797). Use JSON serialization instead for payloads containing periods.

Parameters

  • jws - The signed JWS to serialize.

Returns

Ok(String) with the compact serialization string, or Error(InvalidState) if unprotected headers are present (not supported in compact format) or the unencoded payload contains . characters.

Example

let assert Ok(token) = jws.serialize_compact(signed)
pub fn serialize_json_flattened(
  jws: Jws(Signed, Built),
) -> json.Json

Serialize a signed JWS to JSON Flattened format.

Format: {"payload":"...","protected":"...","signature":"..."}

For detached payloads, the payload field is omitted. If unprotected headers are present, includes the header field.

Parameters

  • jws - The signed JWS to serialize.

Returns

A json.Json value representing the JWS in JSON Flattened Serialization. Use json.to_string to convert to a JSON string.

Example

let assert Ok(signed) =
  jws.new(jwa.JwsHmac(jwa.HmacSha256))
  |> jws.sign(key, payload)
let json_str =
  jws.serialize_json_flattened(signed)
  |> json.to_string
pub fn serialize_json_general(
  jws: Jws(Signed, Built),
) -> json.Json

Serialize a signed JWS to JSON General format.

Format: {"payload":"...","signatures":[{"protected":"...","signature":"..."}]}

For detached payloads, the payload field is omitted. If unprotected headers are present, includes the header field in the signature entry.

Parameters

  • jws - The signed JWS to serialize.

Returns

A json.Json value representing the JWS in JSON General Serialization. Use json.to_string to convert to a JSON string.

Example

let assert Ok(signed) =
  jws.new(jwa.JwsHmac(jwa.HmacSha256))
  |> jws.sign(key, payload)
let json_str =
  jws.serialize_json_general(signed)
  |> json.to_string
pub fn sign(
  jws: Jws(Unsigned, Built),
  key key: jwk.Jwk,
  payload payload: BitArray,
) -> Result(Jws(Signed, Built), gose.GoseError)

Sign an unsigned JWS with the provided key.

JWK metadata (use, key_ops) is enforced when present:

  • Keys with use=enc are rejected
  • Keys with key_ops that don’t include sign are rejected

Parameters

  • jws - The unsigned JWS to sign.
  • key - The JWK to sign with. Must match the JWS algorithm.
  • payload - The payload bytes to sign.

Returns

Ok(Jws(Signed, Built)) with the signed JWS ready for serialization, Error(InvalidState) on key type mismatch, metadata incompatibility, or invalid UTF-8 in unencoded payload, or Error(CryptoError) if the signing operation fails.

pub fn typ(jws: Jws(state, origin)) -> Result(String, Nil)

Get the type (typ) from a JWS header.

Parameters

  • jws - The JWS to read the type from.

Returns

Ok(String) with the type, or Error(Nil) if not set.

pub fn verifier(
  alg: jwa.JwsAlg,
  keys keys: List(jwk.Jwk),
) -> Result(Verifier, gose.GoseError)

Create a verifier for JWS signature verification.

Accepts one or more keys for key rotation scenarios. The verifier pins the expected algorithm and will reject tokens with different algorithms.

Key selection during verification:

  1. If the JWS has a kid header, prioritize keys with matching kid
  2. Try keys in order until one succeeds
  3. Fail if no key verifies the signature

Parameters

  • alg - The expected JWS signing algorithm. Tokens with a different algorithm will be rejected during verification.
  • keys - One or more JWKs to try during verification. Supports key rotation by accepting multiple keys.

Returns

Ok(Verifier) with the configured verifier, or Error(InvalidState) if the key list is empty, any key type is incompatible with the algorithm, any key’s use field is set but not Signing, or any key’s key_ops field is set but doesn’t include Verify.

pub fn verify(
  verifier: Verifier,
  jws: Jws(Signed, origin),
) -> Result(Bool, gose.GoseError)

Verify a JWS signature using the verifier.

Checks:

  1. Token’s alg header matches the verifier’s expected algorithm
  2. Signature is valid for one of the verifier’s keys

When multiple keys are configured, keys with matching kid are tried first.

Parameters

  • verifier - A Verifier created via verifier() with pinned algorithm and keys.
  • jws - The signed JWS to verify.

Returns

Ok(True) if the signature is valid for one of the verifier’s keys, Ok(False) if no key produced a valid signature, or Error(GoseError) if the token’s algorithm doesn’t match the verifier’s expected algorithm.

Example

let assert Ok(v) =
  jws.verifier(jwa.JwsHmac(jwa.HmacSha256), [key])
let assert Ok(parsed) = jws.parse_compact(token)
let assert Ok(True) = jws.verify(v, parsed)
pub fn verify_detached(
  verifier: Verifier,
  jws: Jws(Signed, origin),
  payload: BitArray,
) -> Result(Bool, gose.GoseError)

Verify a JWS with a detached payload using the verifier.

Use this when the payload was not included in the serialized JWS.

Parameters

  • verifier - A Verifier created via verifier() with pinned algorithm and keys.
  • jws - The signed JWS with a detached payload to verify.
  • payload - The detached payload bytes to verify against.

Returns

Ok(True) if the signature is valid for one of the verifier’s keys, Ok(False) if no key produced a valid signature, or Error(GoseError) if the token’s algorithm doesn’t match the verifier’s expected algorithm.

Example

let assert Ok(v) =
  jws.verifier(jwa.JwsHmac(jwa.HmacSha256), [key])
let assert Ok(parsed) = jws.parse_compact(detached_token)
let assert Ok(True) = jws.verify_detached(v, parsed, payload)
pub fn with_cty(
  jws: Jws(Unsigned, Built),
  cty: String,
) -> Jws(Unsigned, Built)

Set the content type (cty) header parameter.

Parameters

  • jws - The unsigned JWS.
  • cty - The content type string (e.g. "JWT").

Returns

The Jws with the cty header set.

pub fn with_detached(
  jws: Jws(Unsigned, Built),
) -> Jws(Unsigned, Built)

Mark this JWS as using a detached payload.

The payload will not be included in the serialized output, but is still provided at sign time and used for signature computation.

Parameters

  • jws - The unsigned JWS to mark as detached.

Returns

The Jws configured for detached payload mode.

pub fn with_header(
  jws: Jws(Unsigned, Built),
  name: String,
  value: json.Json,
) -> Result(Jws(Unsigned, Built), gose.GoseError)

Add a custom protected header field.

Custom headers are sorted alphabetically by name and appear after standard fields (alg, kid, typ, cty). Returns an error if the name is a reserved header (alg, kid, typ, cty, crit, b64) to prevent security issues like algorithm confusion.

If the same header name is set multiple times, the last value wins.

Parameters

  • jws - The unsigned JWS to add the header to.
  • name - The header parameter name (must not be a reserved name).
  • value - The JSON value for the header parameter.

Returns

Ok(Jws(Unsigned, Built)) with the custom header added, or Error(InvalidState) if the header name is reserved.

pub fn with_kid(
  jws: Jws(Unsigned, Built),
  kid: String,
) -> Jws(Unsigned, Built)

Set the key ID (kid) header parameter.

Parameters

  • jws - The unsigned JWS.
  • kid - The key identifier string.

Returns

The Jws with the kid header set.

pub fn with_typ(
  jws: Jws(Unsigned, Built),
  typ: String,
) -> Jws(Unsigned, Built)

Set the type (typ) header parameter (e.g., “JWT”).

Parameters

  • jws - The unsigned JWS.
  • typ - The type string (e.g. "JWT").

Returns

The Jws with the typ header set.

pub fn with_unencoded(
  jws: Jws(Unsigned, Built),
) -> Jws(Unsigned, Built)

Mark this JWS as using an unencoded payload (RFC 7797, b64=false).

The payload will be included directly in the serialized output without base64 encoding. The header will include "crit":["b64"],"b64":false. The payload is still provided at sign time.

Parameters

  • jws - The unsigned JWS to mark as unencoded.

Returns

The Jws configured for unencoded payload mode.

pub fn with_unprotected(
  jws: Jws(Unsigned, Built),
  name: String,
  value: json.Json,
) -> Result(Jws(Unsigned, Built), gose.GoseError)

Add an unprotected header field (for JSON serialization only).

Security Warning: Unprotected headers are NOT integrity protected. They can be modified by an attacker without invalidating the signature. Only use for non-security-critical metadata.

Returns an error if the name is a protected-only header (crit, b64) which MUST be integrity protected per RFC 7515/7797.

Compact serialization will return an error if unprotected headers are present.

If the same header name is set multiple times, the last value wins.

Parameters

  • jws - The unsigned JWS to add the unprotected header to.
  • name - The header parameter name (must not be a protected-only header).
  • value - The JSON value for the header parameter.

Returns

Ok(Jws(Unsigned, Built)) with the unprotected header added, or Error(InvalidState) if the header name is protected-only (crit, b64).

Search Document