gose

Package VersionHex Docs

A Gleam implementation of JOSE (JSON Object Signing and Encryption) and COSE (CBOR Object Signing and Encryption) standards:

JOSE:

COSE:

Project Goals

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

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

Documentation

Full API documentation is available at hexdocs.pm/gose.