gose/jose/jws
JSON Web Signature (JWS) - RFC 7515
Digital signatures 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
import gose/jose/jws
let key = gose.generate_hmac_key(gose.HmacSha256)
let payload = <<"hello world":utf8>>
// Create and sign a JWS
let assert Ok(signed) = jws.new(gose.Mac(gose.Hmac(gose.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(gose.Mac(gose.Hmac(gose.HmacSha256)), keys: [key])
let assert Ok(Nil) = jws.verify(verifier, parsed)
Phantom Types
Jws(state, origin) carries two phantom parameters. The state is
Unsigned before sign and Signed after, so only signed instances
can be serialized or verified. The origin is Built for JWS values
produced by new and sign, and Parsed for values from
parse_compact or parse_json. This prevents calling
decode_unprotected_header on a builder-created JWS (which has no
raw JSON to decode from).
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 viagose.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(
gose.DigitalSignature(gose.RsaPkcs1(gose.RsaPkcs1Sha256)),
keys: rsa_keys,
)
let assert Ok(ec_verifier) =
jws.verifier(
gose.DigitalSignature(gose.Ecdsa(gose.EcdsaP256)),
keys: ec_keys,
)
let assert Ok(parsed) = jws.parse_compact(token)
let result = case jws.verify(rs_verifier, parsed) {
Ok(Nil) -> Ok(Nil)
_ -> 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
parse_json accepts only a single signature. For multi-signer
messages, use gose/jose/jws_multi.
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 cty(jws: Jws(state, origin)) -> Result(String, Nil)
Get the content type (cty) from a JWS header.
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.
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.
pub fn has_unencoded_payload(jws: Jws(state, origin)) -> Bool
Check if the JWS uses an unencoded payload (b64=false per RFC 7797).
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.
pub fn is_detached(jws: Jws(state, origin)) -> Bool
Check if the JWS has a detached payload.
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
pub fn new(alg: gose.SigningAlg) -> Jws(Unsigned, Built)
Create a new unsigned JWS with the specified signing algorithm. The payload
is provided at sign time via sign.
Example
let assert Ok(signed) = jws.new(gose.Mac(gose.Hmac(gose.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.
pub fn parse_json(
json_str: String,
) -> Result(Jws(Signed, Parsed), gose.GoseError)
Parse a JWS from JSON format (supports both General and Flattened).
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.
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.
Example
let assert Ok(signed) =
jws.new(gose.Mac(gose.Hmac(gose.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.
Example
let assert Ok(signed) =
jws.new(gose.Mac(gose.Hmac(gose.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: gose.Key(String),
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
pub fn typ(jws: Jws(state, origin)) -> Result(String, Nil)
Get the type (typ) from a JWS header.
pub fn verifier(
alg: gose.SigningAlg,
keys keys: List(gose.Key(String)),
) -> 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
pub fn verify(
verifier: Verifier,
jws: Jws(Signed, origin),
) -> Result(Nil, 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.
Example
let assert Ok(v) =
jws.verifier(gose.Mac(gose.Hmac(gose.HmacSha256)), [key])
let assert Ok(parsed) = jws.parse_compact(token)
let assert Ok(Nil) = jws.verify(v, parsed)
pub fn verify_detached(
verifier: Verifier,
jws jws: Jws(Signed, origin),
payload payload: BitArray,
) -> Result(Nil, gose.GoseError)
Verify a JWS with a detached payload using the verifier.
Use this when the payload was not included in the serialized JWS.
Example
let assert Ok(v) =
jws.verifier(gose.Mac(gose.Hmac(gose.HmacSha256)), [key])
let assert Ok(parsed) = jws.parse_compact(detached_token)
let assert Ok(Nil) = 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.
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.
pub fn with_header(
jws: Jws(Unsigned, Built),
name name: String,
value 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.
pub fn with_kid(
jws: Jws(Unsigned, Built),
kid: String,
) -> Jws(Unsigned, Built)
Set the key ID (kid) header parameter.
pub fn with_typ(
jws: Jws(Unsigned, Built),
typ: String,
) -> Jws(Unsigned, Built)
Set the type (typ) header parameter (e.g., “JWT”).
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.
pub fn with_unprotected(
jws: Jws(Unsigned, Built),
name name: String,
value 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.