{"id":49193566,"url":"https://github.com/threez/jose.cr","last_synced_at":"2026-04-23T08:30:42.394Z","repository":{"id":344909115,"uuid":"1183502612","full_name":"threez/jose.cr","owner":"threez","description":"JSON Object Signing and Encryption (JOSE) for Crystal — JWS (signing), JWE (encryption), JWT, and JWKS (key sets), in both compact and JSON serialization, backed by OpenSSL.","archived":false,"fork":false,"pushed_at":"2026-03-17T06:35:46.000Z","size":92,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-17T07:48:24.129Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://threez.github.io/jose.cr/","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/threez.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-16T17:09:59.000Z","updated_at":"2026-03-17T06:50:03.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/threez/jose.cr","commit_stats":null,"previous_names":["threez/jose.cr"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/threez/jose.cr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/threez%2Fjose.cr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/threez%2Fjose.cr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/threez%2Fjose.cr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/threez%2Fjose.cr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/threez","download_url":"https://codeload.github.com/threez/jose.cr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/threez%2Fjose.cr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32173022,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-23T02:19:40.750Z","status":"ssl_error","status_checked_at":"2026-04-23T02:17:55.737Z","response_time":53,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-23T08:30:41.528Z","updated_at":"2026-04-23T08:30:42.388Z","avatar_url":"https://github.com/threez.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# jose\n\n[![CI](https://github.com/threez/jose.cr/actions/workflows/ci.yml/badge.svg)](https://github.com/threez/jose.cr/actions/workflows/ci.yml)\n[![https://threez.github.io/jose.cr/](https://badgen.net/badge/api/documentation/green)](https://threez.github.io/jose.cr/)\n\nJSON Object Signing and Encryption (JOSE) for Crystal — JWS (signing), JWE\n(encryption), JWT, and JWKS (key sets), in both compact and JSON serialization,\nbacked by OpenSSL.\n\nImplements the following RFCs:\n\n- [RFC 7515](https://www.rfc-editor.org/rfc/rfc7515) — JSON Web Signature (JWS)\n- [RFC 7516](https://www.rfc-editor.org/rfc/rfc7516) — JSON Web Encryption (JWE)\n- [RFC 7517](https://www.rfc-editor.org/rfc/rfc7517) — JSON Web Key (JWK / JWKS)\n- [RFC 7518](https://www.rfc-editor.org/rfc/rfc7518) — JSON Web Algorithms (JWA)\n- [RFC 7519](https://www.rfc-editor.org/rfc/rfc7519) — JSON Web Token (JWT)\n- [RFC 7520](https://www.rfc-editor.org/rfc/rfc7520) — JOSE Cookbook (test vectors, fully covered)\n- [RFC 7797](https://www.rfc-editor.org/rfc/rfc7797) — JWS Unencoded Payload Option\n- [RFC 8725](https://www.rfc-editor.org/rfc/rfc8725) — JSON Web Token Best Current Practices\n\nHeavily inspired by [ruby-jose](https://github.com/potatosalad/ruby-jose) and\n[erlang-jose](https://hexdocs.pm/jose/).\n\n## Installation\n\n1. Add the dependency to your `shard.yml`:\n\n   ```yaml\n   dependencies:\n     jose:\n       github: threez/jose.cr\n   ```\n\n2. Run `shards install`\n\n## Usage\n\n### Sign \u0026 verify (HS256)\n\n```crystal\nrequire \"jose\"\n\n# Use a shared secret key with HMAC-SHA256.\njwk = JOSE::JWK.from_oct(\"symmetric key\".to_slice)\n\n# JWK as JSON.\njwk.to_binary\n# =\u003e \"{\\\"k\\\":\\\"c3ltbWV0cmljIGtleQ\\\",\\\"kty\\\":\\\"oct\\\"}\"\n\n# Sign a message.\nsigned = jwk.sign(\"test\")\nsigned.compact\n# =\u003e \"eyJhbGciOiJIUzI1NiJ9.dGVzdA.VlZz7pJCnos0k-WUL9O9RoT9N--2kHSakNIdOg-MIro\"\n\n# Verify with the same key.\nvalid, message = jwk.verify(signed)\n# =\u003e true, \"test\"\n```\n\n### Encrypt \u0026 decrypt (ECDH-ES, EC P-256)\n\n```crystal\nrequire \"jose\"\n\n# Alice generates a key pair and publishes the public half.\nalice_private = JOSE::JWK.generate_key_ec\nalice_public  = alice_private.to_public\n\n# Bob encrypts to Alice's public key.\ntoken = alice_public.block_encrypt(\"Secret for Alice\")\n\n# Alice decrypts with her private key.\nplaintext = alice_private.block_decrypt(token)\n# =\u003e \"Secret for Alice\"\n```\n\n### JWT (RFC 7519)\n\n```crystal\nrequire \"jose\"\n\n# Generate a shared HMAC key.\njwk = JOSE::JWK.generate_key_oct\n\n# Build a JWT and set registered claims with self-documenting aliases.\njwt = JOSE::JWT.new\njwt.subject    = \"alice\"\njwt.issuer     = \"example.com\"\njwt.expires_at = Time.utc + 1.hour\n\n# Sign — the \"typ\": \"JWT\" header is added automatically.\nsigned = JOSE::JWT.sign(jwk, jwt)\nvalid, decoded, header = JOSE::JWT.verify_strict(jwk, [\"HS256\"], signed,\n  iss: \"example.com\",\n  aud: \"api\")\nvalid               # =\u003e true\ndecoded[\"sub\"].as_s # =\u003e \"alice\"\nheader[\"typ\"].as_s  # =\u003e \"JWT\"\n```\n\n### JWKS (RFC 7517 §5)\n\n```crystal\nrequire \"jose\"\n\n# Build a key set from two keys with distinct kids.\nk1 = JOSE::JWK.generate_key_ec\nk1 = k1.with(kid: \"sig\")\nk2 = JOSE::JWK.generate_key_oct\nk2 = k2.with(kid: \"enc\")\njwks = JOSE::JWKS.new([k1, k2])\n\n# Publish only public key material (e.g. as /.well-known/jwks.json).\npublic_jwks = jwks.to_public\npublic_jwks.to_binary  # =\u003e {\"keys\":[...]}\n\n# Look up a key by kid during token verification.\nkey = jwks[\"sig\"]\n```\n\n### Load an external key via OpenSSL\n\n```bash\nopenssl ecparam -name prime256v1 -genkey -noout -out ec-p256.pem\n```\n\n```crystal\njwk = JOSE::JWK.from_pem(File.read(\"ec-p256.pem\"))\n```\n\n### Password-based encryption (PBES2)\n\n```crystal\nrequire \"jose\"\n\n# Encrypt using a plain-text password (no key material needed).\n# Default: PBES2-HS512+A256KW key-wrap + A256GCM content-encryption.\ntoken = JOSE::JWE.block_encrypt(\"correct horse battery staple\", \"secret message\")\n\n# Decrypt with the same password.\nplaintext = JOSE::JWE.block_decrypt(\"correct horse battery staple\", token)\n# =\u003e \"secret message\"\n```\n\n### JWS JSON Serialization (RFC 7515 §7.2)\n\n```crystal\nrequire \"jose\"\n\njwk = JOSE::JWK.generate_key_oct\n\n# Produce a flattened JWS JSON token.\njson_token = JOSE::JWS.sign_json(jwk, %({\"sub\":\"alice\"}))\n\n# Verify — works for both flattened and general (multi-signature) form.\nvalid, payload = JOSE::JWS.verify_json(jwk, json_token)\nvalid   # =\u003e true\npayload # =\u003e \"{\\\"sub\\\":\\\"alice\\\"}\"\n```\n\n### JWE JSON Serialization (RFC 7516 §7.2)\n\n```crystal\nrequire \"jose\"\n\njwk = JOSE::JWK.generate_key_oct(size: 16)\noverrides = {\"alg\" =\u003e JSON::Any.new(\"A128KW\"), \"enc\" =\u003e JSON::Any.new(\"A128GCM\")}\n\n# Encrypt to flattened JSON (optionally pass aad: Bytes for extra auth data).\njson_token = JOSE::JWE.json_encrypt(jwk, \"hello json\", overrides)\n\n# Decrypt — also handles the general form with multiple recipients.\nplaintext = JOSE::JWE.json_decrypt(jwk, json_token)\n# =\u003e \"hello json\"\n```\n\n### JWS Unencoded Payload (RFC 7797)\n\nWhen `b64: false` is set in the protected header the payload is transmitted\nwithout base64url-encoding. This is useful for webhook or streaming scenarios\nwhere the raw payload text is signed inline. The `crit: [\"b64\"]` entry is\ninjected automatically.\n\n\u003e **Compact serialization**: the raw payload must not contain `.` — use JSON\n\u003e serialization instead (e.g. for payloads like `$.02`).\n\n```crystal\nrequire \"jose\"\n\njwk = JOSE::JWK.generate_key_oct\n\n# ── Compact serialization (payload must not contain '.') ──────────────────────\noverrides = {\"b64\" =\u003e JSON::Any.new(false)}\nsigned = JOSE::JWS.sign(jwk, \"hello unencoded\", overrides)\n\n# The payload segment is the literal string, not base64url.\nsigned.peek_protected[\"b64\"].as_bool   # =\u003e false\nsigned.peek_protected[\"crit\"].as_a     # =\u003e [\"b64\"]\nsigned.peek_payload                    # =\u003e \"hello unencoded\"\n\nvalid, payload = JOSE::JWS.verify(jwk, signed)\nvalid   # =\u003e true\npayload # =\u003e \"hello unencoded\"\n\n# ── JSON serialization (supports any payload, including '.') ──────────────────\noverrides = {\"alg\" =\u003e JSON::Any.new(\"HS256\"), \"b64\" =\u003e JSON::Any.new(false)}\njson_token = JOSE::JWS.sign_json(jwk, \"$.02\", protected_overrides: overrides)\n\nvalid, payload = JOSE::JWS.verify_json(jwk, json_token)\nvalid   # =\u003e true\npayload # =\u003e \"$.02\"\n```\n\n### Detached JWS (RFC 7515 §7)\n\n```crystal\nrequire \"jose\"\n\njwk = JOSE::JWK.generate_key_oct\npayload = \"the payload travels out-of-band\"\n\ndetached_token = JOSE::JWS.sign_detached(jwk, payload)\n\n# Verify by supplying the payload separately.\nvalid, _ = JOSE::JWS.verify_detached(jwk, detached_token, payload)\nvalid # =\u003e true\n```\n\n### Supported algorithms\n\n#### JWS signing (alg)\n\n- `HS256`, `HS384`, `HS512` — HMAC (`oct`)\n- `ES256`, `ES384`, `ES512` — ECDSA (`EC`)\n- `RS256`, `RS384`, `RS512` — RSA PKCS#1 v1.5 (`RSA`)\n- `PS256`, `PS384`, `PS512` — RSA PSS (`RSA`)\n- `EdDSA` — Ed25519 (`OKP`)\n\n#### JWE key-wrap (alg)\n\n- `dir` — direct symmetric (`oct`)\n- `A128KW`, `A192KW`, `A256KW` — AES Key Wrap (`oct`)\n- `ECDH-ES`, `ECDH-ES+A128KW`, `ECDH-ES+A192KW`, `ECDH-ES+A256KW` — ECDH (`EC`)\n- `RSA-OAEP`, `RSA-OAEP-256`, `RSA1_5` — RSA (`RSA`)\n- `A128GCMKW`, `A192GCMKW`, `A256GCMKW` — AES-GCM Key Wrap (`oct`)\n- `PBES2-HS256+A128KW`, `PBES2-HS384+A192KW`, `PBES2-HS512+A256KW` — Password-based (`string`)\n\n#### JWE content-encryption (enc)\n\n- `A128GCM`, `A192GCM`, `A256GCM` — AES-GCM\n- `A128CBC-HS256`, `A192CBC-HS384`, `A256CBC-HS512` — AES-CBC + HMAC\n\n## Development\n\n```sh\nshards install\nmake spec          # run tests\nmake lint          # ameba static analysis\ncrystal tool format --check src/ spec/   # format check\n```\n\n## Contributing\n\n1. Fork it (\u003chttps://github.com/threez/jose.cr/fork\u003e)\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create a new Pull Request\n\n## Contributors\n\n- [Vincent Landgraf](https://github.com/threez) — creator and maintainer\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthreez%2Fjose.cr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthreez%2Fjose.cr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthreez%2Fjose.cr/lists"}