# `Jwk`

A public key a token is checked against (RFC 7517, RFC 7518 and RFC 8037), one of three kinds by its `kty`: `OKP`, an Ed25519 key, `crv` `Ed25519` and `x`; `RSA`, `n` and `e`, its modulus 2,048 to 4,096 bits; or `EC`, a P-256 key, `crv` `P-256`, `x` and `y`, a point on the curve; each in unpadded Base64url, used with one algorithm, `EdDSA`, `RS256` or `ES256`, with an optional key id and certificates (`x5c`, each standard Base64 DER); `use_` is sent as `use`, and `key_ops` holds the operations it may be used for, a key without `verify` checking no token. Never a private key or a shared secret; members it doesn't hold are ignored.

[Security](types.md#security); not ordered; since Jadpo 0.1.0.

## Make one

Build one from its fields, `Jwk { kty: ... }`.

Its parts keep these rules; a value that breaks one fails with the rule's name:

- `parts_of_its_kty`: `an `OKP` key has `crv` `Ed25519` and `x`, an `RSA` key `n` and `e`, and an `EC` key `crv` `P-256`, `x` and `y`, and none another's`
- `algorithm_of_its_kty`: `alg is EdDSA for an `OKP` key, RS256 for an `RSA` key and ES256 for an `EC` key`
- `a_usable_key`: `an `RSA` key's modulus is 2,048 to 4,096 bits and odd, its exponent odd and from 3 to 2^32 - 1; an `EC` key's point is on P-256`

## Fields

| Field | Type |
|---|---|
| `kty` | `Text` |
| `crv` | `Text?` |
| `x` | `Text?` |
| `y` | `Text?` |
| `n` | `Text?` |
| `e` | `Text?` |
| `kid` | `Text?` |
| `alg` | `Text?` |
| `use_` | `Text?` |
| `key_ops` | `Set<Text>?` |
| `x5c` | `List<Text>?` |

## Text, JSON and storage

It has no single text form, so `text.from` and `text.parse` don't take it. JSON sends an object of its parts.

| JSON |
|---|
| `{"kty":"OKP","crv":"Ed25519","x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo","kid":"2026-10","alg":"EdDSA","use":"sig"}` |
| `{"kty":"OKP","crv":"Ed25519","x":"PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw"}` |
| `{"kty":"OKP","crv":"Ed25519","x":"PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw","key_ops":["verify"]}` |
| `{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM","kid":"1","use":"sig"}` |
| `{"kty":"RSA","n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw","e":"AQAB","kid":"2011-04-29","alg":"RS256"}` |
| `{"kty":"EC","crv":"P-256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0","alg":"ES256"}` |
| `{"kty":"RSA","n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ","e":"AQAB"}` |

It is stored in 11 columns, each named for the field, `_` and its suffix. This is `{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM","kid":"1","use":"sig"}`:

| Column | Type | Holds |
|---|---|---|
| `<field>_kty` | `text` | `'EC'` |
| `<field>_crv` | `text` | `'P-256'` |
| `<field>_x` | `text` | `'MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4'` |
| `<field>_y` | `text` | `'4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM'` |
| `<field>_n` | `text` | `NULL::text` |
| `<field>_e` | `text` | `NULL::text` |
| `<field>_kid` | `text` | `'1'` |
| `<field>_alg` | `text` | `NULL::text` |
| `<field>_use` | `text` | `'sig'` |
| `<field>_key_ops` | `text[]` | `NULL::text[]` |
| `<field>_x5c` | `text[]` | `NULL::text[]` |

## Shown to people

`text.format(value, locale:)` shows it to people in their locale.

With `locale: Locale.en_gb`:

| Value | Shown |
|---|---|
| `{"kty":"OKP","crv":"Ed25519","x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo","kid":"2026-10","alg":"EdDSA","use":"sig"}` | `2026-10` |
| `{"kty":"OKP","crv":"Ed25519","x":"PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw"}` | `PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw` |
| `{"kty":"OKP","crv":"Ed25519","x":"PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw","key_ops":["verify"]}` | `PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw` |
| `{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM","kid":"1","use":"sig"}` | `1` |
| `{"kty":"RSA","n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw","e":"AQAB","kid":"2011-04-29","alg":"RS256"}` | `2011-04-29` |
| `{"kty":"EC","crv":"P-256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0","alg":"ES256"}` | `f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU` |
| `{"kty":"RSA","n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ","e":"AQAB"}` | `ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ` |

## Operations

- `crypto.verify_jwt(token: Jwt, keys: List<Jwk>, issuer: Text?, audience: Text?, as: Type?) -> JwtClaims`: The registered claims of `token` once it holds: signed by one of `keys` with that key's own algorithm (Ed25519, RS256 for an RSA key, ES256 for a P-256 key), never the one the token names, so `none` and every other algorithm fail `invalid_signature`; not expired, not used before its `nbf` and not issued later than now, against the clock with 30 seconds' leeway (`expired`, `not_yet_valid`); issued by `issuer` when one is given (`wrong_issuer`); and for `audience`, or for no audience when none is given (`wrong_audience`). A token that isn't one, or has no `exp`, is `malformed`. With `as:`, an object type of the application's, it gives instead the token's claims the type names, read as `json.read` reads it, a registered one as `JwtClaims` holds it, and fails `mismatch` when they aren't the type. It reads the clock, so it is called only in actions.

Like every standard type, it also has the contract's own operations: `text.format`, `json.read`, `json.write` and `==`. It isn't ordered, so `<` and sorting don't take it. [Every standard type meets one contract](../topics/every-standard-type-meets-one-contract.md) says how they work.
