gose
A Gleam implementation of JOSE (JSON Object Signing and Encryption) and COSE (CBOR Object Signing and Encryption) standards:
JOSE:
- JWS (RFC 7515) - JSON Web Signature
- JWE (RFC 7516) - JSON Web Encryption
- JWK (RFC 7517) - JSON Web Key (including JWK Sets)
- JWA (RFC 7518) - JSON Web Algorithms
- JWT (RFC 7519) - JSON Web Token
COSE:
- COSE_Sign1 (RFC 9052) - Single-signer signing
- COSE_Sign (RFC 9052) - Multi-signer signing
- COSE_Encrypt0 (RFC 9052) - Symmetric encryption
- COSE_Encrypt (RFC 9052) - Multi-recipient encryption
- COSE_Mac0 (RFC 9052) - Message authentication
- COSE_Key (RFC 9052) - Key serialization
- CWT (RFC 8392) - CBOR Web Token
Project Goals
- Type-Safe by Design - types enforce correct API usage at compile time. Unsigned payloads (JWS, etc) can’t be serialized, unverified JWT/CWT claims can’t be trusted.
- Algorithm Pinning - require explicit algorithm declaration, preventing algorithm confusion attacks common in other libraries. It trades off verbosity for security.
- Invalid States Are Unconstructable - Keys and tokens are validated at construction time. If you have a
Key, it’s valid.
Should you use this?
My professional opinion as a long-time security engineering practitioner is that you should basically never use these algorithms in a greenfield system. This library was created for the purpose of integrating with existing systems that already use these standards (like ACME or Webauthn).
Installation
gleam add gose
Some examples below import kryptos directly for key generation; add it with gleam add kryptos if needed.
Platform support
- Erlang/OTP 27+
- Node.js 22+
Browser JavaScript is not supported.
Supported Algorithms
Signing (JWS, COSE_Sign1, and COSE_Sign)
| Family | Algorithms |
|---|---|
| HMAC | HS256, HS384, HS512 |
| RSA PKCS#1 v1.5 | RS256, RS384, RS512 |
| RSA-PSS | PS256, PS384, PS512 |
| ECDSA | ES256 (P-256), ES384 (P-384), ES512 (P-521), ES256K (secp256k1) |
| EdDSA | Ed25519, Ed448 |
MAC (COSE_Mac0)
| Family | Algorithms |
|---|---|
| HMAC | HS256, HS384, HS512 |
JWE Key Management
| Family | Algorithms |
|---|---|
| Direct | dir |
| AES Key Wrap | A128KW, A192KW, A256KW |
| AES-GCM Key Wrap | A128GCMKW, A192GCMKW, A256GCMKW |
| ChaCha20 Key Wrap | C20PKW, XC20PKW |
| RSA | RSA1_5, RSA-OAEP, RSA-OAEP-256 |
| ECDH-ES | ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW, ECDH-ES+C20PKW, ECDH-ES+XC20PKW |
| PBES2 | PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW |
Content Encryption (JWE and COSE_Encrypt0)
| Family | Algorithms |
|---|---|
| AES-GCM | A128GCM, A192GCM, A256GCM |
| AES-CBC + HMAC | A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 (JWE only) |
| ChaCha20 | C20P (ChaCha20-Poly1305), XC20P (XChaCha20-Poly1305) |
Quick Start
JWT
import gleam/dynamic/decode
import gleam/time/duration
import gleam/time/timestamp
import gose/algorithm
import gose/jose/jwt
import gose/key
pub fn main() {
let signing_key = key.generate_hmac_key(algorithm.HmacSha256)
let now = timestamp.system_time()
let claims =
jwt.claims()
|> jwt.with_subject("user123")
|> jwt.with_issuer("my-app")
|> jwt.with_expiration(timestamp.add(now, duration.hours(1)))
let assert Ok(signed) =
jwt.sign(algorithm.Mac(algorithm.Hmac(algorithm.HmacSha256)), claims:, key: signing_key)
let token = jwt.serialize(signed)
let assert Ok(verifier) =
jwt.verifier(algorithm.Mac(algorithm.Hmac(algorithm.HmacSha256)), keys: [signing_key], options: jwt.default_validation())
let assert Ok(verified) = jwt.verify_and_validate(verifier, token, now)
let decoder = decode.field("sub", decode.string, decode.success)
let assert Ok("user123") = jwt.decode(verified, using: decoder)
}
JWE
import gose/algorithm
import gose/jose/jwe
import gose/key
pub fn main() {
let encryption_key = key.generate_enc_key(algorithm.AesGcm(algorithm.Aes256))
let plaintext = <<"sensitive data":utf8>>
let assert Ok(encrypted) =
jwe.new_direct(algorithm.AesGcm(algorithm.Aes256))
|> jwe.encrypt(key: encryption_key, plaintext:)
let assert Ok(token) = jwe.serialize_compact(encrypted)
let assert Ok(parsed) = jwe.parse_compact(token)
let assert Ok(decryptor) = jwe.key_decryptor(algorithm.Direct, algorithm.AesGcm(algorithm.Aes256), keys: [encryption_key])
let assert Ok(decrypted) = jwe.decrypt(decryptor, parsed)
assert decrypted == <<"sensitive data":utf8>>
}
COSE_Sign1
import gose/algorithm
import gose/cose/sign1
import gose/key
import kryptos/ec
pub fn main() {
let signing_key = key.generate_ec(ec.P256)
let payload = <<"hello COSE":utf8>>
let assert Ok(signed) =
sign1.new(algorithm.Ecdsa(algorithm.EcdsaP256))
|> sign1.sign(signing_key, payload)
let data = sign1.serialize(signed)
let assert Ok(parsed) = sign1.parse(data)
let assert Ok(verifier) =
sign1.verifier(algorithm.Ecdsa(algorithm.EcdsaP256), keys: [signing_key])
let assert Ok(Nil) = sign1.verify(verifier, parsed)
assert sign1.payload(parsed) == Ok(payload)
}
CWT
import gleam/time/duration
import gleam/time/timestamp
import gose/algorithm
import gose/cose/cwt
import gose/key
import kryptos/ec
pub fn main() {
let signing_key = key.generate_ec(ec.P256)
let now = timestamp.system_time()
let claims =
cwt.new()
|> cwt.with_subject("user123")
|> cwt.with_issuer("my-app")
|> cwt.with_expiration(timestamp.add(now, duration.hours(1)))
let assert Ok(token) =
cwt.sign(claims, alg: algorithm.Ecdsa(algorithm.EcdsaP256), key: signing_key)
let assert Ok(verifier) =
cwt.verifier(algorithm.Ecdsa(algorithm.EcdsaP256), keys: [signing_key])
let assert Ok(verified) = cwt.verify_and_validate(verifier, token:, now:)
let verified_claims = cwt.verified_claims(verified)
let assert Ok(subject) = cwt.subject(verified_claims)
assert subject == "user123"
}
Error Handling
The library uses a two-tier error design:
GoseError used by JOSE primitives (JWS, JWE, JWK):
| Variant | When It Occurs |
|---|---|
ParseError | Invalid base64 encoding, malformed JSON, wrong token format |
CryptoError | Decryption failure, key derivation error |
InvalidState | Wrong key type for algorithm, missing required header, incompatible parameters |
VerificationFailed | Signature or MAC verification failed (intentionally opaque) |
JwtError used by JWT and encrypted JWT modules:
| Variant | When It Occurs |
|---|---|
TokenExpired | Token’s exp claim is in the past |
TokenNotYetValid | Token’s nbf claim is in the future |
IssuerMismatch | Token’s iss doesn’t match expected issuer |
AudienceMismatch | Token’s aud doesn’t match expected audience |
InvalidSignature | JWS signature verification failed |
DecryptionFailed | JWE decryption failed |
JoseError(GoseError) | Underlying JOSE operation failed (key validation, signing, etc.) |
| … | See JwtError type for all variants |
CwtError used by CWT and encrypted CWT modules:
| Variant | When It Occurs |
|---|---|
TokenExpired | Token’s exp claim is in the past |
TokenNotYetValid | Token’s nbf claim is in the future |
IssuerMismatch | Token’s iss doesn’t match expected issuer |
AudienceMismatch | Token’s aud doesn’t match expected audience |
MissingExpiration | Token lacks a required exp claim |
InvalidClaim | Claim value is invalid (empty audience list, etc.) |
InvalidSignature | COSE_Sign1 signature verification failed |
MalformedToken | CBOR structure or claim types are invalid |
DecryptionFailed | COSE decryption failed |
CoseError(GoseError) | Underlying COSE operation failed (key validation, signing, etc.) |
Limitations
- X.509 certificate parameters not supported - JWKs containing X.509 certificate chain parameters (
x5u,x5c,x5t,x5t#S256) are rejected with a parse error. Certificate-based key validation must be performed outside this library. - JWE compression (
zip) not supported - compressed JWEs are rejected. See JOSE vulnerability notes. - COSE_Mac (multiparty) not supported - only COSE_Mac0 (single-recipient) is implemented.
Documentation
Full API documentation is available at hexdocs.pm/gose.