Skip to main content

uselesskey_jose_openid/
lib.rs

1#![forbid(unsafe_code)]
2
3//! JOSE/OpenID conversion traits for `uselesskey` key fixtures.
4//!
5//! This crate exposes narrowly-scoped helpers that convert fixture key/secret
6//! objects into `jsonwebtoken` key material used by JOSE-style integrations.
7
8use jsonwebtoken::{DecodingKey, EncodingKey};
9
10/// Conversion surface for fixtures in JOSE/OpenID-friendly key representations.
11pub trait JoseOpenIdKeyExt {
12    /// Convert this fixture into a JOSE encoding key.
13    fn encoding_key(&self) -> EncodingKey;
14
15    /// Convert this fixture into a JOSE decoding key.
16    fn decoding_key(&self) -> DecodingKey;
17}
18
19#[cfg(feature = "rsa")]
20impl JoseOpenIdKeyExt for uselesskey_rsa::RsaKeyPair {
21    fn encoding_key(&self) -> EncodingKey {
22        EncodingKey::from_rsa_pem(self.private_key_pkcs8_pem().as_bytes())
23            .expect("failed to create EncodingKey from RSA PKCS8")
24    }
25
26    fn decoding_key(&self) -> DecodingKey {
27        DecodingKey::from_rsa_pem(self.public_key_spki_pem().as_bytes())
28            .expect("failed to create DecodingKey from RSA SPKI")
29    }
30}
31
32#[cfg(feature = "ecdsa")]
33impl JoseOpenIdKeyExt for uselesskey_ecdsa::EcdsaKeyPair {
34    fn encoding_key(&self) -> EncodingKey {
35        EncodingKey::from_ec_pem(self.private_key_pkcs8_pem().as_bytes())
36            .expect("failed to create EncodingKey from ECDSA PKCS8")
37    }
38
39    fn decoding_key(&self) -> DecodingKey {
40        DecodingKey::from_ec_pem(self.public_key_spki_pem().as_bytes())
41            .expect("failed to create DecodingKey from ECDSA SPKI")
42    }
43}
44
45#[cfg(feature = "ed25519")]
46impl JoseOpenIdKeyExt for uselesskey_ed25519::Ed25519KeyPair {
47    fn encoding_key(&self) -> EncodingKey {
48        EncodingKey::from_ed_pem(self.private_key_pkcs8_pem().as_bytes())
49            .expect("failed to create EncodingKey from Ed25519 PKCS8")
50    }
51
52    fn decoding_key(&self) -> DecodingKey {
53        DecodingKey::from_ed_pem(self.public_key_spki_pem().as_bytes())
54            .expect("failed to create DecodingKey from Ed25519 SPKI")
55    }
56}
57
58#[cfg(feature = "hmac")]
59impl JoseOpenIdKeyExt for uselesskey_hmac::HmacSecret {
60    fn encoding_key(&self) -> EncodingKey {
61        EncodingKey::from_secret(self.secret_bytes())
62    }
63
64    fn decoding_key(&self) -> DecodingKey {
65        DecodingKey::from_secret(self.secret_bytes())
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use jsonwebtoken::{Algorithm, Header, Validation, decode, encode};
72    use serde::{Deserialize, Serialize};
73    use uselesskey_core::Factory;
74
75    use super::JoseOpenIdKeyExt;
76    use uselesskey_ecdsa::{EcdsaFactoryExt, EcdsaSpec};
77    use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
78
79    #[derive(Debug, Serialize, Deserialize, PartialEq)]
80    struct TestClaims {
81        sub: String,
82        scope: String,
83    }
84
85    fn relaxed_validation(algorithm: Algorithm) -> Validation {
86        let mut validation = Validation::new(algorithm);
87        validation.validate_exp = false;
88        validation.required_spec_claims = std::collections::HashSet::new();
89        validation
90    }
91
92    #[test]
93    fn rsa_sign_and_verify() {
94        let fx = Factory::random();
95        let keypair = fx.rsa("jwt-rsa", RsaSpec::rs256());
96
97        let claims = TestClaims {
98            sub: "alice".into(),
99            scope: "openid profile email".into(),
100        };
101
102        let token = encode(
103            &Header::new(Algorithm::RS256),
104            &claims,
105            &keypair.encoding_key(),
106        )
107        .expect("sign token with RS256 fixture");
108
109        let decoded = decode::<TestClaims>(
110            &token,
111            &keypair.decoding_key(),
112            &relaxed_validation(Algorithm::RS256),
113        )
114        .expect("decode token with RS256 fixture");
115
116        assert_eq!(decoded.claims, claims);
117    }
118
119    #[test]
120    fn cross_algorithm_fail_on_mismatch() {
121        let fx = Factory::random();
122        let rsa = fx.rsa("iss-a", RsaSpec::rs256());
123        let ecdsa = fx.ecdsa("iss-b", EcdsaSpec::es256());
124        let claims = TestClaims {
125            sub: "alice".into(),
126            scope: "openid".into(),
127        };
128
129        let token = encode(
130            &Header::new(Algorithm::ES256),
131            &claims,
132            &ecdsa.encoding_key(),
133        )
134        .expect("sign token with ES256 fixture");
135
136        let bad = decode::<TestClaims>(
137            &token,
138            &rsa.decoding_key(),
139            &Validation::new(Algorithm::RS256),
140        );
141        assert!(bad.is_err(), "cross-family verification should fail");
142    }
143
144    #[cfg(feature = "hmac")]
145    #[test]
146    fn hmac_roundtrip() {
147        use uselesskey_hmac::{HmacFactoryExt, HmacSpec};
148
149        let fx = Factory::random();
150        let secret = fx.hmac("secret", HmacSpec::hs256());
151
152        let claims = TestClaims {
153            sub: "service".into(),
154            scope: "read write".into(),
155        };
156
157        let token = encode(
158            &Header::new(Algorithm::HS256),
159            &claims,
160            &secret.encoding_key(),
161        )
162        .expect("sign token with HS256 fixture");
163
164        let decoded = decode::<TestClaims>(
165            &token,
166            &secret.decoding_key(),
167            &relaxed_validation(Algorithm::HS256),
168        )
169        .expect("decode token with HS256 fixture");
170
171        assert_eq!(decoded.claims, claims);
172
173        let _ = secret.encoding_key();
174        let _ = secret.decoding_key();
175    }
176}