Skip to main content

uselesskey_core_jwk_shape/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Typed JWK and JWKS helpers for uselesskey test fixtures.
4//!
5//! Provides structured JWK types ([`RsaPublicJwk`], [`EcPublicJwk`], [`OkpPublicJwk`], etc.)
6//! and [`Jwks`] for serializing collections of JWK values.
7//!
8//! # Examples
9//!
10//! Build an Ed25519 public JWK and serialize it to JSON:
11//!
12//! ```
13//! use uselesskey_core_jwk_shape::{OkpPublicJwk, PublicJwk, Jwks, AnyJwk};
14//!
15//! let jwk = OkpPublicJwk {
16//!     kty: "OKP",
17//!     use_: "sig",
18//!     alg: "EdDSA",
19//!     crv: "Ed25519",
20//!     kid: "my-key-1".to_string(),
21//!     x: "dGVzdC1wdWJsaWMta2V5".to_string(),
22//! };
23//!
24//! // Wrap in the enum and convert to a JSON value
25//! let public = PublicJwk::Okp(jwk);
26//! let value = public.to_value();
27//! assert_eq!(value["kty"], "OKP");
28//! assert_eq!(value["kid"], "my-key-1");
29//!
30//! // Collect into a JWKS
31//! let jwks = Jwks { keys: vec![AnyJwk::Public(public)] };
32//! let json = jwks.to_value();
33//! assert_eq!(json["keys"].as_array().unwrap().len(), 1);
34//! ```
35
36use serde::Serialize;
37use serde_json::Value;
38use std::fmt;
39
40/// A JSON Web Key Set containing zero or more JWK entries.
41#[derive(Clone, Serialize)]
42pub struct Jwks {
43    /// The `"keys"` array of the JWKS.
44    pub keys: Vec<AnyJwk>,
45}
46
47impl Jwks {
48    /// Serialize to a [`serde_json::Value`].
49    pub fn to_value(&self) -> Value {
50        serde_json::to_value(self).expect("serialize JWKS")
51    }
52}
53
54impl fmt::Display for Jwks {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        let s = serde_json::to_string(self).expect("serialize JWKS");
57        f.write_str(&s)
58    }
59}
60
61/// RSA public key in JWK format (contains `n` and `e`).
62#[derive(Clone, Serialize)]
63pub struct RsaPublicJwk {
64    pub kty: &'static str,
65    #[serde(rename = "use")]
66    pub use_: &'static str,
67    pub alg: &'static str,
68    pub kid: String,
69    pub n: String,
70    pub e: String,
71}
72
73impl RsaPublicJwk {
74    /// Return the key identifier.
75    pub fn kid(&self) -> &str {
76        &self.kid
77    }
78}
79
80/// RSA private key in JWK format (includes CRT parameters `p`, `q`, `dp`, `dq`, `qi`).
81#[derive(Clone, Serialize)]
82pub struct RsaPrivateJwk {
83    pub kty: &'static str,
84    #[serde(rename = "use")]
85    pub use_: &'static str,
86    pub alg: &'static str,
87    pub kid: String,
88    pub n: String,
89    pub e: String,
90    pub d: String,
91    pub p: String,
92    pub q: String,
93    pub dp: String,
94    pub dq: String,
95    #[serde(rename = "qi")]
96    pub qi: String,
97}
98
99impl RsaPrivateJwk {
100    /// Return the key identifier.
101    pub fn kid(&self) -> &str {
102        &self.kid
103    }
104}
105
106impl fmt::Debug for RsaPrivateJwk {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        f.debug_struct("RsaPrivateJwk")
109            .field("kid", &self.kid)
110            .field("alg", &self.alg)
111            .finish_non_exhaustive()
112    }
113}
114
115/// Elliptic-curve public key in JWK format (P-256 / P-384).
116#[derive(Clone, Serialize)]
117pub struct EcPublicJwk {
118    pub kty: &'static str,
119    #[serde(rename = "use")]
120    pub use_: &'static str,
121    pub alg: &'static str,
122    pub crv: &'static str,
123    pub kid: String,
124    pub x: String,
125    pub y: String,
126}
127
128impl EcPublicJwk {
129    /// Return the key identifier.
130    pub fn kid(&self) -> &str {
131        &self.kid
132    }
133}
134
135/// Elliptic-curve private key in JWK format (P-256 / P-384, includes `d`).
136#[derive(Clone, Serialize)]
137pub struct EcPrivateJwk {
138    pub kty: &'static str,
139    #[serde(rename = "use")]
140    pub use_: &'static str,
141    pub alg: &'static str,
142    pub crv: &'static str,
143    pub kid: String,
144    pub x: String,
145    pub y: String,
146    pub d: String,
147}
148
149impl EcPrivateJwk {
150    /// Return the key identifier.
151    pub fn kid(&self) -> &str {
152        &self.kid
153    }
154}
155
156impl fmt::Debug for EcPrivateJwk {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        f.debug_struct("EcPrivateJwk")
159            .field("kid", &self.kid)
160            .field("alg", &self.alg)
161            .field("crv", &self.crv)
162            .finish_non_exhaustive()
163    }
164}
165
166/// OKP (Octet Key Pair) public key in JWK format (Ed25519).
167#[derive(Clone, Serialize)]
168pub struct OkpPublicJwk {
169    pub kty: &'static str,
170    #[serde(rename = "use")]
171    pub use_: &'static str,
172    pub alg: &'static str,
173    pub crv: &'static str,
174    pub kid: String,
175    pub x: String,
176}
177
178impl OkpPublicJwk {
179    /// Return the key identifier.
180    pub fn kid(&self) -> &str {
181        &self.kid
182    }
183}
184
185/// OKP (Octet Key Pair) private key in JWK format (Ed25519, includes `d`).
186#[derive(Clone, Serialize)]
187pub struct OkpPrivateJwk {
188    pub kty: &'static str,
189    #[serde(rename = "use")]
190    pub use_: &'static str,
191    pub alg: &'static str,
192    pub crv: &'static str,
193    pub kid: String,
194    pub x: String,
195    pub d: String,
196}
197
198impl OkpPrivateJwk {
199    /// Return the key identifier.
200    pub fn kid(&self) -> &str {
201        &self.kid
202    }
203}
204
205impl fmt::Debug for OkpPrivateJwk {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        f.debug_struct("OkpPrivateJwk")
208            .field("kid", &self.kid)
209            .field("alg", &self.alg)
210            .field("crv", &self.crv)
211            .finish_non_exhaustive()
212    }
213}
214
215/// Symmetric (octet) key in JWK format (HMAC `HS256`/`HS384`/`HS512`).
216#[derive(Clone, Serialize)]
217pub struct OctJwk {
218    pub kty: &'static str,
219    #[serde(rename = "use")]
220    pub use_: &'static str,
221    pub alg: &'static str,
222    pub kid: String,
223    pub k: String,
224}
225
226impl OctJwk {
227    /// Return the key identifier.
228    pub fn kid(&self) -> &str {
229        &self.kid
230    }
231}
232
233impl fmt::Debug for OctJwk {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        f.debug_struct("OctJwk")
236            .field("kid", &self.kid)
237            .field("alg", &self.alg)
238            .finish_non_exhaustive()
239    }
240}
241
242/// A public JWK of any supported key type.
243#[derive(Clone, Serialize)]
244#[serde(untagged)]
245pub enum PublicJwk {
246    /// RSA public key.
247    Rsa(RsaPublicJwk),
248    /// Elliptic-curve public key.
249    Ec(EcPublicJwk),
250    /// OKP (Ed25519) public key.
251    Okp(OkpPublicJwk),
252}
253
254impl PublicJwk {
255    /// Return the key identifier.
256    pub fn kid(&self) -> &str {
257        match self {
258            PublicJwk::Rsa(jwk) => jwk.kid(),
259            PublicJwk::Ec(jwk) => jwk.kid(),
260            PublicJwk::Okp(jwk) => jwk.kid(),
261        }
262    }
263
264    /// Serialize to a [`serde_json::Value`].
265    pub fn to_value(&self) -> Value {
266        serde_json::to_value(self).expect("serialize JWK")
267    }
268}
269
270impl fmt::Display for PublicJwk {
271    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272        let s = serde_json::to_string(self).expect("serialize JWK");
273        f.write_str(&s)
274    }
275}
276
277/// A private (or symmetric) JWK of any supported key type.
278#[derive(Clone, Serialize)]
279#[serde(untagged)]
280pub enum PrivateJwk {
281    /// RSA private key.
282    Rsa(RsaPrivateJwk),
283    /// Elliptic-curve private key.
284    Ec(EcPrivateJwk),
285    /// OKP (Ed25519) private key.
286    Okp(OkpPrivateJwk),
287    /// Symmetric (HMAC) key.
288    Oct(OctJwk),
289}
290
291impl PrivateJwk {
292    /// Return the key identifier.
293    pub fn kid(&self) -> &str {
294        match self {
295            PrivateJwk::Rsa(jwk) => jwk.kid(),
296            PrivateJwk::Ec(jwk) => jwk.kid(),
297            PrivateJwk::Okp(jwk) => jwk.kid(),
298            PrivateJwk::Oct(jwk) => jwk.kid(),
299        }
300    }
301
302    /// Serialize to a [`serde_json::Value`].
303    pub fn to_value(&self) -> Value {
304        serde_json::to_value(self).expect("serialize JWK")
305    }
306}
307
308impl fmt::Display for PrivateJwk {
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        let s = serde_json::to_string(self).expect("serialize JWK");
311        f.write_str(&s)
312    }
313}
314
315impl fmt::Debug for PrivateJwk {
316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317        match self {
318            PrivateJwk::Rsa(jwk) => jwk.fmt(f),
319            PrivateJwk::Ec(jwk) => jwk.fmt(f),
320            PrivateJwk::Okp(jwk) => jwk.fmt(f),
321            PrivateJwk::Oct(jwk) => jwk.fmt(f),
322        }
323    }
324}
325
326/// Either a public or private JWK.
327#[derive(Clone, Serialize)]
328#[serde(untagged)]
329pub enum AnyJwk {
330    /// A public-only JWK.
331    Public(PublicJwk),
332    /// A private (or symmetric) JWK.
333    Private(PrivateJwk),
334}
335
336impl AnyJwk {
337    /// Return the key identifier.
338    pub fn kid(&self) -> &str {
339        match self {
340            AnyJwk::Public(jwk) => jwk.kid(),
341            AnyJwk::Private(jwk) => jwk.kid(),
342        }
343    }
344
345    /// Serialize to a [`serde_json::Value`].
346    pub fn to_value(&self) -> Value {
347        serde_json::to_value(self).expect("serialize JWK")
348    }
349}
350
351impl fmt::Display for AnyJwk {
352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353        let s = serde_json::to_string(self).expect("serialize JWK");
354        f.write_str(&s)
355    }
356}
357
358impl From<PublicJwk> for AnyJwk {
359    fn from(value: PublicJwk) -> Self {
360        AnyJwk::Public(value)
361    }
362}
363
364impl From<PrivateJwk> for AnyJwk {
365    fn from(value: PrivateJwk) -> Self {
366        AnyJwk::Private(value)
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    fn sample_rsa_public(kid: &str, n: &str) -> PublicJwk {
375        PublicJwk::Rsa(RsaPublicJwk {
376            kty: "RSA",
377            use_: "sig",
378            alg: "RS256",
379            kid: kid.to_string(),
380            n: n.to_string(),
381            e: "AQAB".to_string(),
382        })
383    }
384
385    fn sample_oct_private(kid: &str, k: &str) -> PrivateJwk {
386        PrivateJwk::Oct(OctJwk {
387            kty: "oct",
388            use_: "sig",
389            alg: "HS256",
390            kid: kid.to_string(),
391            k: k.to_string(),
392        })
393    }
394
395    fn sample_rsa_private(kid: &str, d: &str) -> RsaPrivateJwk {
396        RsaPrivateJwk {
397            kty: "RSA",
398            use_: "sig",
399            alg: "RS256",
400            kid: kid.to_string(),
401            n: "n".to_string(),
402            e: "e".to_string(),
403            d: d.to_string(),
404            p: "p".to_string(),
405            q: "q".to_string(),
406            dp: "dp".to_string(),
407            dq: "dq".to_string(),
408            qi: "qi".to_string(),
409        }
410    }
411
412    fn sample_ec_private(kid: &str, d: &str) -> EcPrivateJwk {
413        EcPrivateJwk {
414            kty: "EC",
415            use_: "sig",
416            alg: "ES256",
417            crv: "P-256",
418            kid: kid.to_string(),
419            x: "x".to_string(),
420            y: "y".to_string(),
421            d: d.to_string(),
422        }
423    }
424
425    fn sample_okp_public(kid: &str, x: &str) -> OkpPublicJwk {
426        OkpPublicJwk {
427            kty: "OKP",
428            use_: "sig",
429            alg: "EdDSA",
430            crv: "Ed25519",
431            kid: kid.to_string(),
432            x: x.to_string(),
433        }
434    }
435
436    fn sample_okp_private(kid: &str, d: &str) -> OkpPrivateJwk {
437        OkpPrivateJwk {
438            kty: "OKP",
439            use_: "sig",
440            alg: "EdDSA",
441            crv: "Ed25519",
442            kid: kid.to_string(),
443            x: "x".to_string(),
444            d: d.to_string(),
445        }
446    }
447
448    #[test]
449    fn display_outputs_json() {
450        let jwk = sample_rsa_public("kid-1", "n1");
451        let json = jwk.to_string();
452        let v: Value = serde_json::from_str(&json).expect("valid JSON");
453        assert_eq!(v["kty"], "RSA");
454
455        let private = sample_oct_private("kid-2", "secret");
456        let json = private.to_string();
457        let v: Value = serde_json::from_str(&json).expect("valid JSON");
458        assert_eq!(v["kty"], "oct");
459    }
460
461    #[test]
462    fn debug_omits_private_material() {
463        let secret = "super-secret-value";
464        let jwk = sample_oct_private("kid-3", secret);
465        let dbg = format!("{:?}", jwk);
466        assert!(dbg.contains("OctJwk"));
467        assert!(!dbg.contains(secret));
468    }
469
470    #[test]
471    fn any_jwk_from_conversions_work() {
472        let pub_jwk = sample_rsa_public("kid-4", "n4");
473        let any_pub = AnyJwk::from(pub_jwk.clone());
474        assert_eq!(any_pub.kid(), pub_jwk.kid());
475
476        let priv_jwk = sample_oct_private("kid-5", "k5");
477        let any_priv = AnyJwk::from(priv_jwk.clone());
478        assert_eq!(any_priv.kid(), priv_jwk.kid());
479    }
480
481    #[test]
482    fn kid_helpers_return_expected_kid() {
483        let rsa = sample_rsa_private("kid-rsa", "d-rsa");
484        assert_eq!(rsa.kid(), "kid-rsa");
485
486        let ec = sample_ec_private("kid-ec", "d-ec");
487        assert_eq!(ec.kid(), "kid-ec");
488
489        let okp_pub = sample_okp_public("kid-okp", "x-okp");
490        assert_eq!(okp_pub.kid(), "kid-okp");
491
492        let okp_priv = sample_okp_private("kid-okp-priv", "d-okp");
493        assert_eq!(okp_priv.kid(), "kid-okp-priv");
494
495        let oct = OctJwk {
496            kty: "oct",
497            use_: "sig",
498            alg: "HS256",
499            kid: "kid-oct".to_string(),
500            k: "secret".to_string(),
501        };
502        assert_eq!(oct.kid(), "kid-oct");
503    }
504
505    #[test]
506    fn enum_kid_and_to_value_cover_all_variants() {
507        let okp_pub = PublicJwk::Okp(sample_okp_public("kid-okp", "x-okp"));
508        assert_eq!(okp_pub.kid(), "kid-okp");
509        assert_eq!(okp_pub.to_value()["kty"], "OKP");
510
511        let okp_priv = PrivateJwk::Okp(sample_okp_private("kid-okp-priv", "d-okp"));
512        assert_eq!(okp_priv.kid(), "kid-okp-priv");
513        assert_eq!(okp_priv.to_value()["kty"], "OKP");
514
515        let oct = PrivateJwk::Oct(OctJwk {
516            kty: "oct",
517            use_: "sig",
518            alg: "HS256",
519            kid: "kid-oct".to_string(),
520            k: "secret".to_string(),
521        });
522        assert_eq!(oct.kid(), "kid-oct");
523        assert_eq!(oct.to_value()["kty"], "oct");
524    }
525
526    #[test]
527    fn enum_kid_covers_all_variants() {
528        let rsa_pub = PublicJwk::Rsa(RsaPublicJwk {
529            kty: "RSA",
530            use_: "sig",
531            alg: "RS256",
532            kid: "kid-rsa".to_string(),
533            n: "n".to_string(),
534            e: "e".to_string(),
535        });
536        assert_eq!(rsa_pub.kid(), "kid-rsa");
537
538        let ec_pub = PublicJwk::Ec(EcPublicJwk {
539            kty: "EC",
540            use_: "sig",
541            alg: "ES256",
542            crv: "P-256",
543            kid: "kid-ec".to_string(),
544            x: "x".to_string(),
545            y: "y".to_string(),
546        });
547        assert_eq!(ec_pub.kid(), "kid-ec");
548
549        let okp_pub = PublicJwk::Okp(sample_okp_public("kid-okp", "x-okp"));
550        assert_eq!(okp_pub.kid(), "kid-okp");
551
552        let rsa_priv = PrivateJwk::Rsa(sample_rsa_private("kid-rsa-priv", "d"));
553        assert_eq!(rsa_priv.kid(), "kid-rsa-priv");
554
555        let ec_priv = PrivateJwk::Ec(sample_ec_private("kid-ec-priv", "d"));
556        assert_eq!(ec_priv.kid(), "kid-ec-priv");
557
558        let okp_priv = PrivateJwk::Okp(sample_okp_private("kid-okp-priv", "d"));
559        assert_eq!(okp_priv.kid(), "kid-okp-priv");
560
561        let oct = PrivateJwk::Oct(OctJwk {
562            kty: "oct",
563            use_: "sig",
564            alg: "HS256",
565            kid: "kid-oct".to_string(),
566            k: "secret".to_string(),
567        });
568        assert_eq!(oct.kid(), "kid-oct");
569    }
570
571    #[test]
572    fn any_jwk_to_value_round_trips() {
573        let pub_any = AnyJwk::from(sample_rsa_public("kid-a", "n"));
574        assert_eq!(pub_any.to_value()["kid"], "kid-a");
575
576        let priv_any = AnyJwk::from(sample_oct_private("kid-b", "secret"));
577        assert_eq!(priv_any.to_value()["kid"], "kid-b");
578    }
579
580    #[test]
581    fn any_jwk_display_round_trips() {
582        let pub_any = AnyJwk::from(sample_rsa_public("kid-a", "n"));
583        let json = pub_any.to_string();
584        let v: Value = serde_json::from_str(&json).expect("valid JSON");
585        assert_eq!(v["kid"], "kid-a");
586
587        let priv_any = AnyJwk::from(sample_oct_private("kid-b", "secret"));
588        let json = priv_any.to_string();
589        let v: Value = serde_json::from_str(&json).expect("valid JSON");
590        assert_eq!(v["kid"], "kid-b");
591    }
592
593    #[test]
594    fn private_jwk_enum_debug_uses_inner_formatters() {
595        let rsa = PrivateJwk::Rsa(sample_rsa_private("kid-rsa", "secret"));
596        let dbg = format!("{:?}", rsa);
597        assert!(dbg.contains("RsaPrivateJwk"));
598
599        let ec = PrivateJwk::Ec(sample_ec_private("kid-ec", "secret"));
600        let dbg = format!("{:?}", ec);
601        assert!(dbg.contains("EcPrivateJwk"));
602
603        let okp = PrivateJwk::Okp(sample_okp_private("kid-okp", "secret"));
604        let dbg = format!("{:?}", okp);
605        assert!(dbg.contains("OkpPrivateJwk"));
606
607        let oct = PrivateJwk::Oct(OctJwk {
608            kty: "oct",
609            use_: "sig",
610            alg: "HS256",
611            kid: "kid-oct".to_string(),
612            k: "secret".to_string(),
613        });
614        let dbg = format!("{:?}", oct);
615        assert!(dbg.contains("OctJwk"));
616    }
617
618    #[test]
619    fn private_jwk_debug_omits_private_material() {
620        let secret = "super-secret";
621
622        let rsa = sample_rsa_private("kid-rsa", secret);
623        let dbg = format!("{:?}", rsa);
624        assert!(dbg.contains("RsaPrivateJwk"));
625        assert!(!dbg.contains(secret));
626
627        let ec = sample_ec_private("kid-ec", secret);
628        let dbg = format!("{:?}", ec);
629        assert!(dbg.contains("EcPrivateJwk"));
630        assert!(!dbg.contains(secret));
631
632        let okp = sample_okp_private("kid-okp", secret);
633        let dbg = format!("{:?}", okp);
634        assert!(dbg.contains("OkpPrivateJwk"));
635        assert!(!dbg.contains(secret));
636    }
637
638    proptest::proptest! {
639        #[test]
640        fn to_string_and_to_value_are_idempotent(
641            kid in "[a-zA-Z0-9._-]{1,24}",
642            n in "[A-Za-z0-9+/]{1,64}",
643            e in "[A-Za-z0-9+/]{1,64}",
644        ) {
645            let pub_jwk = PublicJwk::Rsa(RsaPublicJwk {
646                kty: "RSA",
647                use_: "sig",
648                alg: "RS256",
649                kid: kid.to_string(),
650                n: n.to_string(),
651                e: e.to_string(),
652            });
653
654            let pub_value = pub_jwk.to_value();
655            let pub_text = pub_jwk.to_string();
656            let pub_round_trip: Value = serde_json::from_str(&pub_text).expect("pub JWK should be JSON");
657
658            assert_eq!(pub_value["kid"], pub_round_trip["kid"]);
659            assert_eq!(pub_value["n"], pub_round_trip["n"]);
660
661            let private = PrivateJwk::Oct(OctJwk {
662                kty: "oct",
663                use_: "sig",
664                alg: "HS256",
665                kid: kid.to_string(),
666                k: n,
667            });
668
669            let private_value = private.to_value();
670            let private_text = private.to_string();
671            let private_round_trip: Value =
672                serde_json::from_str(&private_text).expect("private JWK should be JSON");
673
674            assert_eq!(private_value["kid"], private_round_trip["kid"]);
675            assert_eq!(private_value["k"], private_round_trip["k"]);
676        }
677    }
678}