lockpad_auth/
key.rs

1use crate::error::{Error, Result};
2use axum::extract::FromRef;
3use base64::Engine;
4use jsonwebtoken::{
5    jwk::{AlgorithmParameters, RSAKeyParameters},
6    Algorithm, DecodingKey,
7};
8use rsa::{
9    pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey},
10    PublicKeyParts, RsaPublicKey,
11};
12
13/// The binary representation of an Ed25519 public key
14/// This is used to verify JWT claims
15#[derive(Clone)]
16pub struct PublicKey {
17    raw: Vec<u8>,
18    key: DecodingKey,
19    rsa_key: rsa::RsaPublicKey,
20}
21
22impl PublicKey {
23    /// Create a new public key from a binary DER representation
24    pub fn new(raw: Vec<u8>) -> Result<Self> {
25        let contents = raw.clone();
26        let str = std::str::from_utf8(&contents)?;
27        Ok(Self {
28            key: DecodingKey::from_rsa_pem(&raw)?,
29            raw,
30            rsa_key: rsa::RsaPublicKey::from_pkcs1_pem(str)?,
31        })
32    }
33
34    pub fn parse_from_jwks(jwks_str: &str) -> Result<Vec<Self>> {
35        let jwks: jsonwebtoken::jwk::JwkSet = serde_json::from_str(jwks_str)?;
36
37        jwks.keys.into_iter().map(Self::try_from).collect()
38    }
39
40    pub fn parse_from_pem(pem_str: &str) -> Result<Self> {
41        let rsa_key = rsa::RsaPublicKey::from_pkcs1_pem(pem_str)?;
42        Self::try_from(rsa_key)
43    }
44}
45
46impl TryFrom<rsa::RsaPublicKey> for PublicKey {
47    type Error = Error;
48
49    fn try_from(key: RsaPublicKey) -> Result<Self> {
50        let data = key.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF)?;
51        let data = data.as_bytes().to_vec();
52        Self::new(data)
53    }
54}
55
56impl TryFrom<jsonwebtoken::jwk::Jwk> for PublicKey {
57    type Error = Error;
58
59    fn try_from(jwk: jsonwebtoken::jwk::Jwk) -> Result<Self> {
60        match jwk.common.algorithm {
61            Some(Algorithm::RS256) => {
62                let rsa_params = match jwk.algorithm {
63                    AlgorithmParameters::RSA(params) => params,
64                    _ => unimplemented!(),
65                };
66
67                let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
68
69                let n_bytes = engine.decode(rsa_params.n.as_bytes())?;
70                let e_bytes = engine.decode(rsa_params.e.as_bytes())?;
71
72                let n = rsa::BigUint::from_bytes_be(&n_bytes);
73                let e = rsa::BigUint::from_bytes_be(&e_bytes);
74
75                let rsa_key = rsa::RsaPublicKey::new(n, e)?;
76                Self::try_from(rsa_key)
77            }
78            _ => unimplemented!(),
79        }
80    }
81}
82
83impl From<PublicKey> for jsonwebtoken::jwk::Jwk {
84    fn from(key: PublicKey) -> Self {
85        let common = jsonwebtoken::jwk::CommonParameters {
86            public_key_use: Some(jsonwebtoken::jwk::PublicKeyUse::Signature),
87            key_id: Some("1".to_string()),
88            algorithm: Some(Algorithm::RS256),
89            ..Default::default()
90        };
91
92        let n = key.rsa_key.n().to_bytes_be();
93        let e = key.rsa_key.e().to_bytes_be();
94
95        let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
96        let n_base64 = engine.encode(n);
97        let e_base64 = engine.encode(e);
98
99        let algorithm = AlgorithmParameters::RSA(RSAKeyParameters {
100            key_type: jsonwebtoken::jwk::RSAKeyType::RSA,
101            n: n_base64,
102            e: e_base64,
103        });
104
105        jsonwebtoken::jwk::Jwk { common, algorithm }
106    }
107}
108
109impl AsRef<[u8]> for PublicKey {
110    fn as_ref(&self) -> &[u8] {
111        &self.raw
112    }
113}
114
115impl FromRef<PublicKey> for DecodingKey {
116    fn from_ref(key: &PublicKey) -> Self {
117        key.key.clone()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::{error::Result, Claims};
125
126    const RSA_PUBLIC_KEY: &str = "-----BEGIN RSA PUBLIC KEY-----
127MIIBCgKCAQEAwmNDtlawWXevdB5fDT4gkmBl/al/Ij9KcyAZc78O2m3Cnb9nfcSl
128MuiChPP3RsehaoyXGl8dCV39gI0bEViAX5/OF5x39mFJdhe00+bUe4X7mKgItA6L
129N10PeIhJR59zis8URSzVDm7OsA2HLbrRxM5k0kapNBmFN1+Us2XnNn0V2/QR9ite
130Qgkpe4I5/adAwj+fQo+dY13j5N11GP7jhnEmw7MiNjo0Kv9h1ZJJHDlb9edSmA6d
131g6jOxhlE4kQAJYObCF2TKhBcO+szl/zFVm9E55yiVYjWKk0R4uVXp9natMsQcbOL
132OAc5t3RZmw5L0Nikhso62g9oefgwWOIPJwIDAQAB
133-----END RSA PUBLIC KEY-----";
134
135    const JWK_JSON: &str = r#"{"use":"sig","alg":"RS256","kid":"1","kty":"RSA","n":"wmNDtlawWXevdB5fDT4gkmBl_al_Ij9KcyAZc78O2m3Cnb9nfcSlMuiChPP3RsehaoyXGl8dCV39gI0bEViAX5_OF5x39mFJdhe00-bUe4X7mKgItA6LN10PeIhJR59zis8URSzVDm7OsA2HLbrRxM5k0kapNBmFN1-Us2XnNn0V2_QR9iteQgkpe4I5_adAwj-fQo-dY13j5N11GP7jhnEmw7MiNjo0Kv9h1ZJJHDlb9edSmA6dg6jOxhlE4kQAJYObCF2TKhBcO-szl_zFVm9E55yiVYjWKk0R4uVXp9natMsQcbOLOAc5t3RZmw5L0Nikhso62g9oefgwWOIPJw","e":"AQAB"}"#;
136
137    const JWKS_JSON: &str = r#"{"keys":[{"use":"sig","alg":"RS256","kid":"1","kty":"RSA","n":"wmNDtlawWXevdB5fDT4gkmBl_al_Ij9KcyAZc78O2m3Cnb9nfcSlMuiChPP3RsehaoyXGl8dCV39gI0bEViAX5_OF5x39mFJdhe00-bUe4X7mKgItA6LN10PeIhJR59zis8URSzVDm7OsA2HLbrRxM5k0kapNBmFN1-Us2XnNn0V2_QR9iteQgkpe4I5_adAwj-fQo-dY13j5N11GP7jhnEmw7MiNjo0Kv9h1ZJJHDlb9edSmA6dg6jOxhlE4kQAJYObCF2TKhBcO-szl_zFVm9E55yiVYjWKk0R4uVXp9natMsQcbOLOAc5t3RZmw5L0Nikhso62g9oefgwWOIPJw","e":"AQAB"}]}"#;
138
139    const JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwMUdWRjRCQ00zMlRUTTE5WUJGQjJaTkU0UiIsImV4cCI6MTY3OTUzNTg5MSwiaWF0IjoxNjc4OTMxMDkxfQ.Uug8lF11bjBUYzEnNr-HIgwlkckVxtUh5RQyEAinmUzB4nZiMQEb9cTx9LsU0mLTLz7lHOdo49CmjyNqEnn-KDX9p0dn97AZISHihIjEtfBbbmh3DqsLBN_QzKyB_h4gLGT5Fx33tgYkx8Sbpk8GmIlidUsjrHqENPOoI3dZssdgi4z0TcZ7cSJgpnNy-fb63oHy0_Gmfu5viNGi8V4ydGR7hBFt6-SBr9aOF2Y6FeePmxKDdIz4PuOa0gQCGbiIdX1agx2SXnWAUYdnW4IlEUo_Yv-N-Rbf_YhAG0uENdcL6p-NfkMhey0stb9UMj8KVU6KKrP6TRn_TfO9jiF7qQ";
140
141    /// Test that a jwks can be converted to a public key
142    /// and that the public key can be used to verify a token
143    #[tokio::test]
144    async fn jwk_to_key() -> Result<()> {
145        let jwk: jsonwebtoken::jwk::Jwk = serde_json::from_str(JWK_JSON)?;
146        let key = PublicKey::try_from(jwk)?;
147
148        // don't validate exp
149        let mut validation = jsonwebtoken::Validation::new(Algorithm::RS256);
150        validation.validate_exp = false;
151
152        let key: jsonwebtoken::DecodingKey = axum::extract::FromRef::from_ref(&key);
153        Claims::decode_validation(JWT, &key, &validation).await?;
154
155        Ok(())
156    }
157
158    /// Test that a jwks can be converted to a public key
159    /// and that the public key can be used to verify a token
160    #[tokio::test]
161    async fn jwks_to_key() -> Result<()> {
162        let key_set = PublicKey::parse_from_jwks(JWKS_JSON)?;
163        let key = key_set.first().unwrap();
164
165        // don't validate exp
166        let mut validation = jsonwebtoken::Validation::new(Algorithm::RS256);
167        validation.validate_exp = false;
168
169        let key: jsonwebtoken::DecodingKey = axum::extract::FromRef::from_ref(key);
170        Claims::decode_validation(JWT, &key, &validation).await?;
171
172        Ok(())
173    }
174
175    /// Test that an RSA PEM can be converted to a public key
176    /// and that the public key can be used to verify a token
177    #[tokio::test]
178    async fn pem_to_key() -> Result<()> {
179        let key = PublicKey::parse_from_pem(RSA_PUBLIC_KEY)?;
180
181        let mut validation = jsonwebtoken::Validation::new(Algorithm::RS256);
182        validation.validate_exp = false;
183
184        let key: jsonwebtoken::DecodingKey = axum::extract::FromRef::from_ref(&key);
185        Claims::decode_validation(JWT, &key, &validation).await?;
186
187        Ok(())
188    }
189}