Skip to main content

ullun/
jwt_keys.rs

1//! RS256 and ES256 JSON Web Tokens and JWKS (feature `jwt-asymmetric`)
2//!
3//! Use asymmetric tokens when the party that verifies them must not be able to
4//! mint them: an identity provider signs, your API only holds public keys.
5//! [`KeySet::from_jwks`] reads a provider's JWKS document, [`SigningKey`]
6//! signs tokens yourself. As with HS256 only the algorithm of the key decides
7//! how a token is verified, never the `alg` header: `none`, `HS256` and a
8//! mismatching algorithm are rejected, and `exp` / `nbf` are checked.
9//!
10//! ```
11//! use ullun::jwt_keys::{KeySet, SigningKey};
12//! use ullun::jwt::Validation;
13//!
14//! // ES256 key pair (PKCS#8); in production load it from a secret store
15//! let rng = ring::rand::SystemRandom::new();
16//! let pkcs8 = ring::signature::EcdsaKeyPair::generate_pkcs8(
17//!     &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
18//!     &rng,
19//! )
20//! .unwrap();
21//! let signing = SigningKey::es256_pkcs8(pkcs8.as_ref()).unwrap().with_kid("k1");
22//! let token = signing
23//!     .encode(&serde_json::json!({ "sub": "ann", "exp": ullun::jwt::expires_in(60) }))
24//!     .unwrap();
25//!
26//! let keys = KeySet::new().with(signing.verifying_key());
27//! let claims: serde_json::Value = keys.decode(&token, &Validation::default()).unwrap();
28//! assert_eq!(claims["sub"], "ann");
29//! ```
30//!
31//! As middleware: `JwtAuth::with_keys(keys)`.
32
33use crate::error::{Error, Result};
34use crate::jwt::{check_claims, now, Validation};
35use base64::engine::general_purpose::URL_SAFE_NO_PAD;
36use base64::Engine;
37use ring::rand::SystemRandom;
38use ring::signature::{
39    EcdsaKeyPair, KeyPair, RsaKeyPair, UnparsedPublicKey, ECDSA_P256_SHA256_FIXED,
40    ECDSA_P256_SHA256_FIXED_SIGNING, RSA_PKCS1_2048_8192_SHA256, RSA_PKCS1_SHA256,
41};
42use serde::de::DeserializeOwned;
43use serde::Serialize;
44use serde_json::Value;
45use std::sync::Arc;
46
47/// Supported asymmetric algorithms
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum Algorithm {
50    /// RSASSA-PKCS1-v1_5 with SHA-256 (2048 to 8192 bit keys)
51    RS256,
52    /// ECDSA over P-256 with SHA-256
53    ES256,
54}
55
56impl Algorithm {
57    fn name(self) -> &'static str {
58        match self {
59            Algorithm::RS256 => "RS256",
60            Algorithm::ES256 => "ES256",
61        }
62    }
63}
64
65fn invalid() -> Error {
66    Error::unauthorized("Invalid token")
67}
68
69fn bad_key(what: &str) -> Error {
70    Error::internal(format!("Invalid JWT key: {what}"))
71}
72
73/// A public key that verifies tokens
74#[derive(Debug, Clone)]
75pub struct VerifyingKey {
76    algorithm: Algorithm,
77    kid: Option<String>,
78    /// RS256: DER `RSAPublicKey`; ES256: uncompressed SEC1 point
79    public: Vec<u8>,
80}
81
82impl VerifyingKey {
83    /// RS256 key from a DER `RSAPublicKey` (PKCS#1)
84    pub fn rs256_der(der: &[u8]) -> Self {
85        Self {
86            algorithm: Algorithm::RS256,
87            kid: None,
88            public: der.to_vec(),
89        }
90    }
91
92    /// ES256 key from an uncompressed P-256 point (`0x04 || x || y`, 65 bytes)
93    pub fn es256_point(point: &[u8]) -> Result<Self> {
94        if point.len() != 65 || point[0] != 4 {
95            return Err(bad_key("expected an uncompressed P-256 point"));
96        }
97        Ok(Self {
98            algorithm: Algorithm::ES256,
99            kid: None,
100            public: point.to_vec(),
101        })
102    }
103
104    /// Key from a JSON Web Key (`kty` `RSA` or `EC` with `crv` `P-256`)
105    pub fn from_jwk(jwk: &Value) -> Result<Self> {
106        let field = |name: &str| -> Result<Vec<u8>> {
107            let v = jwk
108                .get(name)
109                .and_then(Value::as_str)
110                .ok_or_else(|| bad_key(&format!("JWK is missing `{name}`")))?;
111            URL_SAFE_NO_PAD
112                .decode(v)
113                .map_err(|_| bad_key(&format!("JWK `{name}` is not base64url")))
114        };
115        let mut key = match jwk.get("kty").and_then(Value::as_str) {
116            Some("RSA") => {
117                let (n, e) = (field("n")?, field("e")?);
118                let body = [der_integer(&n), der_integer(&e)].concat();
119                Self::rs256_der(&der_wrap(0x30, &body))
120            }
121            Some("EC") => {
122                if jwk.get("crv").and_then(Value::as_str) != Some("P-256") {
123                    return Err(bad_key("only the P-256 curve is supported"));
124                }
125                let (x, y) = (field("x")?, field("y")?);
126                if x.len() != 32 || y.len() != 32 {
127                    return Err(bad_key("JWK coordinates must be 32 bytes"));
128                }
129                Self::es256_point(&[&[4][..], &x, &y].concat())?
130            }
131            _ => return Err(bad_key("unsupported JWK `kty`")),
132        };
133        key.kid = jwk.get("kid").and_then(Value::as_str).map(String::from);
134        Ok(key)
135    }
136
137    /// Set the key id matched against the token's `kid` header
138    pub fn with_kid(mut self, kid: impl Into<String>) -> Self {
139        self.kid = Some(kid.into());
140        self
141    }
142
143    pub fn algorithm(&self) -> Algorithm {
144        self.algorithm
145    }
146
147    pub fn kid(&self) -> Option<&str> {
148        self.kid.as_deref()
149    }
150
151    fn verify(&self, message: &[u8], signature: &[u8]) -> bool {
152        let alg: &dyn ring::signature::VerificationAlgorithm = match self.algorithm {
153            Algorithm::RS256 => &RSA_PKCS1_2048_8192_SHA256,
154            Algorithm::ES256 => &ECDSA_P256_SHA256_FIXED,
155        };
156        UnparsedPublicKey::new(alg, &self.public)
157            .verify(message, signature)
158            .is_ok()
159    }
160}
161
162/// DER length octets
163fn der_len(len: usize) -> Vec<u8> {
164    match len {
165        0..=127 => vec![len as u8],
166        128..=255 => vec![0x81, len as u8],
167        _ => vec![0x82, (len >> 8) as u8, len as u8],
168    }
169}
170
171fn der_wrap(tag: u8, body: &[u8]) -> Vec<u8> {
172    [&[tag][..], &der_len(body.len()), body].concat()
173}
174
175/// Unsigned big-endian integer as a DER INTEGER
176fn der_integer(bytes: &[u8]) -> Vec<u8> {
177    let start = bytes.iter().position(|&b| b != 0).unwrap_or(bytes.len());
178    let mut body = bytes[start..].to_vec();
179    if body.first().is_none_or(|b| b & 0x80 != 0) {
180        body.insert(0, 0);
181    }
182    der_wrap(0x02, &body)
183}
184
185/// A private key that signs tokens
186pub struct SigningKey {
187    kind: Kind,
188    kid: Option<String>,
189    rng: SystemRandom,
190}
191
192enum Kind {
193    Rsa(RsaKeyPair),
194    Ec(EcdsaKeyPair),
195}
196
197impl std::fmt::Debug for SigningKey {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        f.debug_struct("SigningKey")
200            .field("algorithm", &self.algorithm())
201            .field("kid", &self.kid)
202            .finish_non_exhaustive()
203    }
204}
205
206impl SigningKey {
207    /// RS256 key from PKCS#8 DER
208    pub fn rs256_pkcs8(der: &[u8]) -> Result<Self> {
209        let pair = RsaKeyPair::from_pkcs8(der).map_err(|_| bad_key("not an RSA PKCS#8 key"))?;
210        Ok(Self::new(Kind::Rsa(pair)))
211    }
212
213    /// ES256 key from PKCS#8 DER
214    pub fn es256_pkcs8(der: &[u8]) -> Result<Self> {
215        let pair =
216            EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, der, &SystemRandom::new())
217                .map_err(|_| bad_key("not a P-256 PKCS#8 key"))?;
218        Ok(Self::new(Kind::Ec(pair)))
219    }
220
221    fn new(kind: Kind) -> Self {
222        Self {
223            kind,
224            kid: None,
225            rng: SystemRandom::new(),
226        }
227    }
228
229    /// Put `kid` in the header of issued tokens
230    pub fn with_kid(mut self, kid: impl Into<String>) -> Self {
231        self.kid = Some(kid.into());
232        self
233    }
234
235    pub fn algorithm(&self) -> Algorithm {
236        match self.kind {
237            Kind::Rsa(_) => Algorithm::RS256,
238            Kind::Ec(_) => Algorithm::ES256,
239        }
240    }
241
242    /// The matching public key, to publish or to verify with
243    pub fn verifying_key(&self) -> VerifyingKey {
244        let (algorithm, public) = match &self.kind {
245            Kind::Rsa(p) => (Algorithm::RS256, p.public_key().as_ref().to_vec()),
246            Kind::Ec(p) => (Algorithm::ES256, p.public_key().as_ref().to_vec()),
247        };
248        VerifyingKey {
249            algorithm,
250            kid: self.kid.clone(),
251            public,
252        }
253    }
254
255    /// Sign `claims` as a token
256    pub fn encode<T: Serialize>(&self, claims: &T) -> Result<String> {
257        let mut header = serde_json::json!({ "alg": self.algorithm().name(), "typ": "JWT" });
258        if let Some(kid) = &self.kid {
259            header["kid"] = Value::String(kid.clone());
260        }
261        let payload = serde_json::to_vec(claims)
262            .map_err(|e| Error::internal(format!("JWT claims: {}", e)))?;
263        let input = format!(
264            "{}.{}",
265            URL_SAFE_NO_PAD.encode(header.to_string()),
266            URL_SAFE_NO_PAD.encode(payload)
267        );
268        let signature = match &self.kind {
269            Kind::Rsa(pair) => {
270                let mut sig = vec![0u8; pair.public().modulus_len()];
271                pair.sign(&RSA_PKCS1_SHA256, &self.rng, input.as_bytes(), &mut sig)
272                    .map_err(|_| Error::internal("JWT signing failed"))?;
273                sig
274            }
275            Kind::Ec(pair) => pair
276                .sign(&self.rng, input.as_bytes())
277                .map_err(|_| Error::internal("JWT signing failed"))?
278                .as_ref()
279                .to_vec(),
280        };
281        Ok(format!("{}.{}", input, URL_SAFE_NO_PAD.encode(signature)))
282    }
283}
284
285/// Public keys a token may be verified with (a JWKS)
286#[derive(Debug, Clone, Default)]
287pub struct KeySet {
288    keys: Arc<Vec<VerifyingKey>>,
289}
290
291impl KeySet {
292    pub fn new() -> Self {
293        Self::default()
294    }
295
296    /// Add a key
297    pub fn with(mut self, key: VerifyingKey) -> Self {
298        Arc::make_mut(&mut self.keys).push(key);
299        self
300    }
301
302    /// Parse a JWKS document (`{"keys": [...]}`). Keys of an unsupported type
303    /// (other curves, `oct`) are skipped; it is an error if none remain.
304    pub fn from_jwks(json: &str) -> Result<Self> {
305        let doc: Value = serde_json::from_str(json).map_err(|_| bad_key("JWKS is not JSON"))?;
306        let list = doc
307            .get("keys")
308            .and_then(Value::as_array)
309            .ok_or_else(|| bad_key("JWKS has no `keys` array"))?;
310        let mut set = Self::new();
311        for jwk in list {
312            if let Ok(key) = VerifyingKey::from_jwk(jwk) {
313                set = set.with(key);
314            }
315        }
316        if set.keys.is_empty() {
317            return Err(bad_key("JWKS contains no supported keys"));
318        }
319        Ok(set)
320    }
321
322    pub fn len(&self) -> usize {
323        self.keys.len()
324    }
325
326    pub fn is_empty(&self) -> bool {
327        self.keys.is_empty()
328    }
329
330    /// Verify signature, `exp` and `nbf` (and `validation`) and return the claims
331    pub fn decode<T: DeserializeOwned>(&self, token: &str, validation: &Validation) -> Result<T> {
332        let mut parts = token.split('.');
333        let (Some(h), Some(p), Some(s), None) =
334            (parts.next(), parts.next(), parts.next(), parts.next())
335        else {
336            return Err(invalid());
337        };
338        let header: Value =
339            serde_json::from_slice(&URL_SAFE_NO_PAD.decode(h).map_err(|_| invalid())?)
340                .map_err(|_| invalid())?;
341        let algorithm = match header.get("alg").and_then(Value::as_str) {
342            Some("RS256") => Algorithm::RS256,
343            Some("ES256") => Algorithm::ES256,
344            _ => return Err(invalid()),
345        };
346        let kid = header.get("kid").and_then(Value::as_str);
347        let signature = URL_SAFE_NO_PAD.decode(s).map_err(|_| invalid())?;
348        let message = format!("{h}.{p}");
349
350        let verified = self
351            .keys
352            .iter()
353            .filter(|k| k.algorithm == algorithm)
354            // a token naming a key must hit that key; keys without a kid match any token
355            .filter(|k| match (kid, k.kid.as_deref()) {
356                (Some(t), Some(k)) => t == k,
357                _ => true,
358            })
359            .any(|k| k.verify(message.as_bytes(), &signature));
360        if !verified {
361            return Err(invalid());
362        }
363
364        let claims: Value =
365            serde_json::from_slice(&URL_SAFE_NO_PAD.decode(p).map_err(|_| invalid())?)
366                .map_err(|_| invalid())?;
367        check_claims(&claims, validation, now())?;
368        serde_json::from_value(claims).map_err(|_| invalid())
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use serde_json::json;
376
377    const RSA_PKCS8: &[u8] = include_bytes!("../tests/fixtures/rsa_pkcs8.der");
378    const RSA_PUBLIC: &[u8] = include_bytes!("../tests/fixtures/rsa_public.der");
379
380    fn ec() -> SigningKey {
381        let pkcs8 =
382            EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &SystemRandom::new())
383                .unwrap();
384        SigningKey::es256_pkcs8(pkcs8.as_ref()).unwrap()
385    }
386
387    fn rsa() -> SigningKey {
388        SigningKey::rs256_pkcs8(RSA_PKCS8).unwrap()
389    }
390
391    fn claims() -> Value {
392        json!({ "sub": "ann", "exp": now() + 60 })
393    }
394
395    fn v() -> Validation {
396        Validation::default()
397    }
398
399    #[test]
400    fn es256_and_rs256_roundtrip() {
401        for key in [ec(), rsa()] {
402            let token = key.encode(&claims()).unwrap();
403            let set = KeySet::new().with(key.verifying_key());
404            let got: Value = set.decode(&token, &v()).unwrap();
405            assert_eq!(got["sub"], "ann", "{:?}", key.algorithm());
406        }
407    }
408
409    #[test]
410    fn rsa_public_der_fixture_matches_the_private_key() {
411        assert_eq!(rsa().verifying_key().public, RSA_PUBLIC);
412    }
413
414    #[test]
415    fn rejects_tampering_wrong_key_and_malformed() {
416        let key = ec();
417        let token = key.encode(&claims()).unwrap();
418        let set = KeySet::new().with(key.verifying_key());
419
420        let other = KeySet::new().with(ec().verifying_key());
421        assert!(other.decode::<Value>(&token, &v()).is_err());
422
423        let mut parts: Vec<String> = token.split('.').map(String::from).collect();
424        parts[1] = URL_SAFE_NO_PAD.encode(r#"{"sub":"root"}"#);
425        assert!(set.decode::<Value>(&parts.join("."), &v()).is_err());
426
427        for bad in ["", "a", "a.b", "a.b.c", "a.b.c.d", "!!!.!!!.!!!"] {
428            assert_eq!(
429                set.decode::<Value>(bad, &v()).unwrap_err().status_code(),
430                401,
431                "{bad}"
432            );
433        }
434    }
435
436    #[test]
437    fn algorithm_confusion_is_rejected() {
438        // an RS256 token must not verify against an ES256 key and vice versa
439        let (e, r) = (ec(), rsa());
440        let rs_token = r.encode(&claims()).unwrap();
441        let es_token = e.encode(&claims()).unwrap();
442        let only_ec = KeySet::new().with(e.verifying_key());
443        let only_rsa = KeySet::new().with(r.verifying_key());
444        assert!(only_ec.decode::<Value>(&rs_token, &v()).is_err());
445        assert!(only_rsa.decode::<Value>(&es_token, &v()).is_err());
446
447        // `none` and HS256 headers never reach a verifier
448        let payload = URL_SAFE_NO_PAD.encode(r#"{"sub":"root"}"#);
449        for alg in ["none", "HS256"] {
450            let header = URL_SAFE_NO_PAD.encode(format!(r#"{{"alg":"{alg}"}}"#));
451            let token = format!("{header}.{payload}.");
452            assert!(only_ec.decode::<Value>(&token, &v()).is_err(), "{alg}");
453        }
454    }
455
456    #[test]
457    fn expired_tokens_and_claim_checks_apply() {
458        let key = ec();
459        let set = KeySet::new().with(key.verifying_key());
460        let old = key.encode(&json!({ "exp": now() - 10 })).unwrap();
461        assert_eq!(
462            set.decode::<Value>(&old, &v()).unwrap_err().message(),
463            "Token expired"
464        );
465        let strict = Validation {
466            issuer: Some("me".into()),
467            ..Default::default()
468        };
469        let token = key.encode(&json!({ "iss": "you" })).unwrap();
470        assert!(set.decode::<Value>(&token, &strict).is_err());
471        let token = key.encode(&json!({ "iss": "me" })).unwrap();
472        assert!(set.decode::<Value>(&token, &strict).is_ok());
473    }
474
475    #[test]
476    fn kid_selects_the_key() {
477        let (a, b) = (ec().with_kid("a"), ec().with_kid("b"));
478        let set = KeySet::new()
479            .with(a.verifying_key())
480            .with(b.verifying_key());
481        assert!(set
482            .decode::<Value>(&a.encode(&claims()).unwrap(), &v())
483            .is_ok());
484        assert!(set
485            .decode::<Value>(&b.encode(&claims()).unwrap(), &v())
486            .is_ok());
487
488        // a token claiming kid "a" but signed by b's key must fail
489        let forged = ec().with_kid("a").encode(&claims()).unwrap();
490        assert!(set.decode::<Value>(&forged, &v()).is_err());
491        assert_eq!(a.verifying_key().kid(), Some("a"));
492    }
493
494    #[test]
495    fn jwks_with_rsa_and_ec_keys() {
496        // build the JWKS for our own keys
497        let rsa_key = rsa();
498        let ec_key = ec().with_kid("ec1");
499        let pub_ec = ec_key.verifying_key().public;
500        let (n, e) = rsa_components(RSA_PUBLIC);
501        let jwks = json!({ "keys": [
502            { "kty": "oct", "k": "AAAA" },
503            { "kty": "EC", "crv": "P-384", "x": "AA", "y": "AA" },
504            { "kty": "RSA", "kid": "rsa1", "n": URL_SAFE_NO_PAD.encode(&n), "e": URL_SAFE_NO_PAD.encode(&e) },
505            { "kty": "EC", "crv": "P-256", "kid": "ec1",
506              "x": URL_SAFE_NO_PAD.encode(&pub_ec[1..33]),
507              "y": URL_SAFE_NO_PAD.encode(&pub_ec[33..]) },
508        ]});
509        let set = KeySet::from_jwks(&jwks.to_string()).unwrap();
510        assert_eq!(set.len(), 2);
511        assert!(!set.is_empty());
512
513        let rs_token = rsa_key.with_kid("rsa1").encode(&claims()).unwrap();
514        assert!(set.decode::<Value>(&rs_token, &v()).is_ok());
515        assert!(set
516            .decode::<Value>(&ec_key.encode(&claims()).unwrap(), &v())
517            .is_ok());
518    }
519
520    #[test]
521    fn bad_jwks_and_keys_are_refused() {
522        assert!(KeySet::from_jwks("not json").is_err());
523        assert!(KeySet::from_jwks("{}").is_err());
524        assert!(KeySet::from_jwks(r#"{"keys":[]}"#).is_err());
525        assert!(KeySet::from_jwks(r#"{"keys":[{"kty":"oct","k":"AA"}]}"#).is_err());
526        assert!(VerifyingKey::from_jwk(&json!({ "kty": "RSA", "n": "AA" })).is_err());
527        assert!(VerifyingKey::from_jwk(
528            &json!({ "kty": "EC", "crv": "P-256", "x": "AA", "y": "AA" })
529        )
530        .is_err());
531        assert!(VerifyingKey::es256_point(&[4; 10]).is_err());
532        assert!(SigningKey::rs256_pkcs8(b"junk").is_err());
533        assert!(SigningKey::es256_pkcs8(b"junk").is_err());
534        assert!(KeySet::new().is_empty());
535    }
536
537    #[test]
538    fn der_integers_are_minimal_and_positive() {
539        assert_eq!(der_integer(&[0, 0, 1]), [2, 1, 1]);
540        assert_eq!(der_integer(&[0x80]), [2, 2, 0, 0x80]);
541        assert_eq!(der_integer(&[]), [2, 1, 0]);
542        assert_eq!(der_len(300), [0x82, 1, 44]);
543        assert_eq!(der_len(200), [0x81, 200]);
544    }
545
546    /// (n, e) of a DER RSAPublicKey, to build test JWKs
547    fn rsa_components(der: &[u8]) -> (Vec<u8>, Vec<u8>) {
548        fn read(buf: &[u8]) -> (u8, &[u8], &[u8]) {
549            let (tag, rest) = (buf[0], &buf[1..]);
550            let (len, rest) = match rest[0] {
551                n if n < 0x80 => (n as usize, &rest[1..]),
552                0x81 => (rest[1] as usize, &rest[2..]),
553                _ => (((rest[1] as usize) << 8) | rest[2] as usize, &rest[3..]),
554            };
555            (tag, &rest[..len], &rest[len..])
556        }
557        let (_, body, _) = read(der);
558        let (_, n, rest) = read(body);
559        let (_, e, _) = read(rest);
560        let trim = |b: &[u8]| {
561            b.iter()
562                .skip_while(|&&x| x == 0)
563                .copied()
564                .collect::<Vec<_>>()
565        };
566        (trim(n), trim(e))
567    }
568
569    #[test]
570    fn der_encoding_roundtrips_the_rsa_fixture() {
571        let (n, e) = rsa_components(RSA_PUBLIC);
572        let jwk =
573            json!({ "kty": "RSA", "n": URL_SAFE_NO_PAD.encode(n), "e": URL_SAFE_NO_PAD.encode(e) });
574        assert_eq!(VerifyingKey::from_jwk(&jwk).unwrap().public, RSA_PUBLIC);
575    }
576}