# gose
[](https://hex.pm/packages/gose)
[](https://hexdocs.pm/gose/)
A Gleam implementation of JOSE (JSON Object Signing and Encryption) and COSE (CBOR Object Signing and Encryption) standards:
**JOSE:**
- **JWS** ([RFC 7515](https://www.rfc-editor.org/rfc/rfc7515)) - JSON Web Signature
- **JWE** ([RFC 7516](https://www.rfc-editor.org/rfc/rfc7516)) - JSON Web Encryption
- **JWK** ([RFC 7517](https://www.rfc-editor.org/rfc/rfc7517)) - JSON Web Key (including JWK Sets)
- **JWA** ([RFC 7518](https://www.rfc-editor.org/rfc/rfc7518)) - JSON Web Algorithms
- **JWT** ([RFC 7519](https://www.rfc-editor.org/rfc/rfc7519)) - JSON Web Token
**COSE:**
- **COSE_Sign1** ([RFC 9052](https://www.rfc-editor.org/rfc/rfc9052)) - Single-signer signing
- **COSE_Sign** ([RFC 9052](https://www.rfc-editor.org/rfc/rfc9052)) - Multi-signer signing
- **COSE_Encrypt0** ([RFC 9052](https://www.rfc-editor.org/rfc/rfc9052)) - Symmetric encryption
- **COSE_Encrypt** ([RFC 9052](https://www.rfc-editor.org/rfc/rfc9052)) - Multi-recipient encryption
- **COSE_Mac0** ([RFC 9052](https://www.rfc-editor.org/rfc/rfc9052)) - Message authentication
- **COSE_Key** ([RFC 9052](https://www.rfc-editor.org/rfc/rfc9052)) - Key serialization
- **CWT** ([RFC 8392](https://www.rfc-editor.org/rfc/rfc8392)) - 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
```sh
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
```gleam
import gleam/dynamic/decode
import gleam/time/duration
import gleam/time/timestamp
import gose
import gose/jose/jwt
pub fn main() {
let signing_key = gose.generate_hmac_key(gose.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(gose.Mac(gose.Hmac(gose.HmacSha256)), claims:, key: signing_key)
let token = jwt.serialize(signed)
let assert Ok(verifier) =
jwt.verifier(gose.Mac(gose.Hmac(gose.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
```gleam
import gose
import gose/jose/jwe
pub fn main() {
let encryption_key = gose.generate_enc_key(gose.AesGcm(gose.Aes256))
let plaintext = <<"sensitive data":utf8>>
let assert Ok(encrypted) =
jwe.new_direct(gose.AesGcm(gose.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(gose.Direct, gose.AesGcm(gose.Aes256), keys: [encryption_key])
let assert Ok(decrypted) = jwe.decrypt(decryptor, parsed)
assert decrypted == <<"sensitive data":utf8>>
}
```
### COSE_Sign1
```gleam
import gose
import gose/cose/sign1
import kryptos/ec
pub fn main() {
let signing_key = gose.generate_ec(ec.P256)
let payload = <<"hello COSE":utf8>>
let assert Ok(signed) =
sign1.new(gose.Ecdsa(gose.EcdsaP256))
|> sign1.sign(signing_key, payload)
let data = sign1.serialize(signed)
let assert Ok(parsed) = sign1.parse(data)
let assert Ok(verifier) =
sign1.verifier(gose.Ecdsa(gose.EcdsaP256), keys: [signing_key])
let assert Ok(Nil) = sign1.verify(verifier, parsed)
assert sign1.payload(parsed) == Ok(payload)
}
```
### CWT
```gleam
import gleam/time/duration
import gleam/time/timestamp
import gose
import gose/cose/cwt
import kryptos/ec
pub fn main() {
let signing_key = gose.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: gose.Ecdsa(gose.EcdsaP256), key: signing_key)
let assert Ok(verifier) =
cwt.verifier(gose.Ecdsa(gose.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`](https://hexdocs.pm/gose/gose/jose/jwt.html#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](https://www.rfc-editor.org/rfc/rfc8725#section-3.6) notes.
- COSE_Mac (multiparty) not supported - only COSE_Mac0 (single-recipient) is implemented.
## Documentation
Full API documentation is available at [hexdocs.pm/gose](https://hexdocs.pm/gose/).