jose
= jose :toc: left :toclevels: 2 :source-highlighter: rouge
JOSE (RFC 7515-7519) implementation in pure Crystal: JSON Web Signature (JWS), JSON Web Encryption (JWE), JSON Web Key (JWK). Elliptic curve and RSA.
French version: link:README.fr.adoc[README.fr.adoc].
== Why
jose exists primarily to support https://github.com/aloli-crystal/crystal-clevis-geli[crystal-clevis-geli], the Tang client for FreeBSD GELI volumes. Initial scope is intentionally minimal — only what the Tang protocol needs:
- JWK: thumbprints (RFC 7638), serialization, ECDH key types (P-256, P-384, P-521).
- JWS: ES256, ES384, ES512 (signature & verification).
- JWE: ECDH-ES key agreement, A256GCM content encryption.
Since v0.2, RSA is supported as well, for a second reason: OpenID Connect and WebAuthn both sign with RS256 by default, so an EC-only library cannot verify a token from a typical identity provider, nor an assertion from a Windows Hello authenticator.
- JWK:
RSAKey— generation, JWK parsing and serialization, thumbprints, PKCS#1 / PKCS#8 / SubjectPublicKeyInfo DER. - JWS: RS256, RS384, RS512 (signature & verification).
Still out of scope (may be added later if needed):
- RSA-OAEP and RSA-PSS (
PS256, …). - EdDSA (Ed25519).
- Full JWT validation (
exp,nbf,aud, …). - JWKS endpoints (key discovery over HTTP).
== Installation
Add to your shard.yml:
[source,yaml]
dependencies: jose: github: aloli-crystal/jose version: ~> 0.2
Then shards install.
== Usage
[source,crystal]
require "jose"
=== JWK, elliptic curve ============================================
key = Jose::JWK::ECKey.generate(Jose::JWK::Curve::P256) key.thumbprint_base64url # => "cn-I_WNMClehiVp51i_0VpOENW1upEerA8sEam5hn-s" key.public_key.to_json # => {"kty":"EC","crv":"P-256","x":"...","y":"..."}
Parse a JWK (e.g. from a Tang advertisement).
peer = Jose::JWK::ECKey.from_json(jwk_string)
=== JWS, elliptic curve ============================================
jws = Jose::JWS.sign("payload", Jose::JWS::Algorithm::ES256, key) payload = Jose::JWS.verify(jws, key.public_key)
=== JWE ============================================================
jwe = Jose::JWE.encrypt("secret", peer.public_key) plaintext = Jose::JWE.decrypt(jwe, peer)
=== RSA ============================================================
rsa = Jose::JWK::RSAKey.generate(2048) rsa.modulus_bits # => 2048 token = Jose::JWS.sign("payload", Jose::JWS::Algorithm::RS256, rsa) payload = Jose::JWS.verify(token, rsa.public_key)
Verifying a token from an identity provider: take the JWK it publishes
and check the token against it.
idp_key = Jose::JWK::RSAKey.from_json(published_jwk) claims = Jose::JWS.verify(id_token, idp_key)
An algorithm and a key type must agree: signing with RS256 and an ECKey, or ES256 and an RSAKey, raises rather than silently doing something surprising. Algorithm#family reports which key a given algorithm wants.
NOTE: A private RSAKey needs its CRT members (p, q, dp, dq, qi) to be usable for signing. RFC 7518 makes them optional in a JWK, but OpenSSL cannot build a key from n, e and d alone. #crt_complete? reports whether they are present; verification with a public key is unaffected.
== Development
[source,shell]
shards install crystal spec crystal tool format --check crystal run lib/ameba/bin/ameba.cr -- src spec
== Contributing
. Fork the repository. . Create a feature branch from production. . Run crystal tool format src/ spec/ before each commit. . Open a pull request against production.
== License
MIT — see link:LICENSE[LICENSE].
== References
- https://datatracker.ietf.org/doc/html/rfc7515[RFC 7515 — JSON Web Signature]
- https://datatracker.ietf.org/doc/html/rfc7516[RFC 7516 — JSON Web Encryption]
- https://datatracker.ietf.org/doc/html/rfc7517[RFC 7517 — JSON Web Key]
- https://datatracker.ietf.org/doc/html/rfc7518[RFC 7518 — JSON Web Algorithms]
- https://datatracker.ietf.org/doc/html/rfc7638[RFC 7638 — JWK Thumbprints]
- https://github.com/latchset/tang[Tang server (consumer of this lib via crystal-clevis-geli)]
jose
- 0
- 0
- 0
- 3
- 1
- 4 days ago
- April 27, 2026
MIT License
Sat, 26 Sep 2026 19:12:08 GMT