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:
- State tracks signing progress:
Unsigned— created vianew, ready to signSigned— signed or parsed, can be serialized or verified
- Origin tracks how the JWS was obtained:
Built— created vianewandsignParsed— obtained fromparse_compactorparse_json
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:
- Verifier pinning:
verifier()requires the expected algorithm; tokens with different algorithms are rejected byverifyandverify_detached. - JWK
algmetadata: If a key hasalgset viajwk.with_alg, the JWS algorithm must match during signing and verification. - JWT verifier:
jwt.verifier()requires the expected algorithm upfront; tokens with different algorithms are rejected. - 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:
- Empty arrays are rejected
- Standard headers cannot appear in
crit b64(RFC 7797 unencoded payload) is the only supported extension- Unknown extensions are rejected
Key Metadata
JWK metadata (use, key_ops) is enforced during signing and verification.
Keys with incompatible metadata are rejected.
JSON Serialization Limitations
- Single signature only: General JSON Serialization rejects JWS with multiple signatures. Use separate JWS objects for multi-signature needs.
Types
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)
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
usefield (if set) isSigning - Each key’s
key_opsfield (if set) includesVerify
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- Adecode.Decoderfor 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- Adecode.Decoderfor 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=encare rejected - Keys with
key_opsthat don’t includesignare 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:
- If the JWS has a
kidheader, prioritize keys with matching kid - Try keys in order until one succeeds
- 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:
- Token’s
algheader matches the verifier’s expected algorithm - Signature is valid for one of the verifier’s keys
When multiple keys are configured, keys with matching kid are tried first.
Parameters
verifier- AVerifiercreated viaverifier()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- AVerifiercreated viaverifier()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).