Skip to main content

aauth_protocol/
jwk.rs

1//! Public JWKs: RFC 7638 thumbprints, algorithm determination by the `alg`
2//! member alone (draft-hardt-httpbis-signature-key §Algorithm Determination,
3//! RFC 9864), and signature verification for the algorithms AAuth uses.
4
5use base64::engine::general_purpose::URL_SAFE_NO_PAD;
6use base64::Engine;
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10use crate::error::{Error, Result};
11
12/// A public JWK. Private members are never read or written.
13#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
14pub struct Jwk {
15    /// Key type: `OKP`, `EC` or `RSA`.
16    pub kty: String,
17    /// Curve: `Ed25519` or `P-256`.
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub crv: Option<String>,
20    /// The public key (`OKP`), or its x coordinate (`EC`).
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub x: Option<String>,
23    /// An `EC` key's y coordinate.
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub y: Option<String>,
26    /// An `RSA` key's modulus.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub n: Option<String>,
29    /// An `RSA` key's exponent.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub e: Option<String>,
32    /// The fully-specified algorithm the key signs with. Required to verify.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub alg: Option<String>,
35    /// The key's identifier in its JWKS.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub kid: Option<String>,
38    /// `sig` for a signing key.
39    #[serde(default, rename = "use", skip_serializing_if = "Option::is_none")]
40    pub use_: Option<String>,
41}
42
43/// A JWK Set.
44#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
45pub struct Jwks {
46    /// The keys.
47    pub keys: Vec<Jwk>,
48}
49
50/// The fully-specified signature algorithms this crate signs and verifies.
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
52#[non_exhaustive]
53pub enum Algorithm {
54    /// EdDSA over Ed25519 (RFC 9864's `Ed25519`): what this crate signs with.
55    Ed25519,
56    /// ECDSA over P-256 with SHA-256.
57    Es256,
58}
59
60impl Algorithm {
61    /// Its JOSE name.
62    pub fn as_str(self) -> &'static str {
63        match self {
64            Algorithm::Ed25519 => "Ed25519",
65            Algorithm::Es256 => "ES256",
66        }
67    }
68
69    /// The algorithm a JOSE name names, among those supported.
70    pub fn from_name(name: &str) -> Option<Self> {
71        match name {
72            "Ed25519" => Some(Algorithm::Ed25519),
73            "ES256" => Some(Algorithm::Es256),
74            _ => None,
75        }
76    }
77}
78
79/// Why a key cannot be used, by its `Signature-Error` code.
80#[derive(Debug, Clone, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum KeyProblem {
83    /// `invalid_key`: malformed, symmetric, or inconsistent with its `alg`.
84    Invalid(String),
85    /// `unsupported_algorithm`: well-formed, but not one this crate verifies.
86    Unsupported(String),
87}
88
89impl Jwk {
90    /// RFC 7638: the required members, lexicographically ordered, no whitespace.
91    pub fn thumbprint(&self) -> Result<String> {
92        let member = |v: &Option<String>, name: &str| {
93            v.clone().ok_or_else(|| {
94                Error::Key(format!("{} key missing required member {name}", self.kty))
95            })
96        };
97        let canonical = match self.kty.as_str() {
98            "OKP" => serde_json::json!({
99                "crv": member(&self.crv, "crv")?, "kty": "OKP", "x": member(&self.x, "x")?,
100            }),
101            "EC" => serde_json::json!({
102                "crv": member(&self.crv, "crv")?, "kty": "EC",
103                "x": member(&self.x, "x")?, "y": member(&self.y, "y")?,
104            }),
105            "RSA" => serde_json::json!({
106                "e": member(&self.e, "e")?, "kty": "RSA", "n": member(&self.n, "n")?,
107            }),
108            other => return Err(Error::Key(format!("unsupported key type: {other}"))),
109        };
110        // serde_json's map is ordered by key, which is what RFC 7638 asks for.
111        Ok(URL_SAFE_NO_PAD.encode(Sha256::digest(canonical.to_string().as_bytes())))
112    }
113
114    /// The algorithm this key signs with, from its `alg` member, checked
115    /// against its key material.
116    pub fn algorithm(&self) -> Result<Algorithm, KeyProblem> {
117        if self.kty.is_empty() {
118            return Err(KeyProblem::Invalid(
119                "JWK missing required member: kty".into(),
120            ));
121        }
122        if self.kty == "oct" {
123            return Err(KeyProblem::Invalid(
124                "symmetric keys are not permitted".into(),
125            ));
126        }
127        let Some(alg) = self.alg.as_deref() else {
128            return Err(KeyProblem::Invalid(
129                "JWK missing required member: alg".into(),
130            ));
131        };
132        if alg == "EdDSA" || alg.starts_with("HS") || alg == "none" {
133            return Err(KeyProblem::Invalid(format!(
134                "alg {alg:?} is not fully specified or is symmetric"
135            )));
136        }
137        let algorithm = Algorithm::from_name(alg)
138            .ok_or_else(|| KeyProblem::Unsupported(format!("unsupported algorithm {alg:?}")))?;
139        let (kty, crv, members): (&str, &str, &[&Option<String>]) = match algorithm {
140            Algorithm::Ed25519 => ("OKP", "Ed25519", &[&self.x]),
141            Algorithm::Es256 => ("EC", "P-256", &[&self.x, &self.y]),
142        };
143        if self.kty != kty || self.crv.as_deref() != Some(crv) {
144            return Err(KeyProblem::Invalid(format!(
145                "JWK kty/crv are inconsistent with alg {alg}"
146            )));
147        }
148        if members.iter().any(|m| m.is_none()) {
149            return Err(KeyProblem::Invalid("JWK missing key material".into()));
150        }
151        Ok(algorithm)
152    }
153
154    /// Whether `signature` is this key's, under `algorithm`, over `message`.
155    /// ES256 signatures are the raw 64-byte `r || s` both JWS and RFC 9421 use.
156    pub fn verify(&self, algorithm: Algorithm, message: &[u8], signature: &[u8]) -> bool {
157        let decode = |v: &Option<String>| v.as_deref().and_then(|v| URL_SAFE_NO_PAD.decode(v).ok());
158        match algorithm {
159            Algorithm::Ed25519 => {
160                let Some(Ok(x)) = decode(&self.x).map(<[u8; 32]>::try_from) else {
161                    return false;
162                };
163                let Ok(key) = ed25519_dalek::VerifyingKey::from_bytes(&x) else {
164                    return false;
165                };
166                let Ok(signature) = ed25519_dalek::Signature::from_slice(signature) else {
167                    return false;
168                };
169                // Strict: a weak (small-order) key would verify a forged signature.
170                key.verify_strict(message, &signature).is_ok()
171            }
172            Algorithm::Es256 => {
173                use p256::ecdsa::signature::Verifier;
174                let (Some(x), Some(y)) = (decode(&self.x), decode(&self.y)) else {
175                    return false;
176                };
177                if x.len() != 32 || y.len() != 32 {
178                    return false;
179                }
180                let point = p256::EncodedPoint::from_affine_coordinates(
181                    x.as_slice().into(),
182                    y.as_slice().into(),
183                    false,
184                );
185                let Ok(key) = p256::ecdsa::VerifyingKey::from_encoded_point(&point) else {
186                    return false;
187                };
188                let Ok(signature) = p256::ecdsa::Signature::from_slice(signature) else {
189                    return false;
190                };
191                key.verify(message, &signature).is_ok()
192            }
193        }
194    }
195
196    /// The structural checks on a token's `cnf.jwk` (AAuth §Request-Context
197    /// Binding), before it is decoded.
198    pub fn check_confirmation_key(&self) -> Result<()> {
199        let required: &[(&str, &Option<String>)] = match self.kty.as_str() {
200            "" => {
201                return Err(Error::Key(
202                    "cnf.jwk is structurally incomplete: missing kty".into(),
203                ))
204            }
205            "oct" => return Err(Error::Key("cnf.jwk uses a symmetric key type".into())),
206            "OKP" => &[("crv", &self.crv), ("x", &self.x)],
207            "EC" => &[("crv", &self.crv), ("x", &self.x), ("y", &self.y)],
208            "RSA" => &[("n", &self.n), ("e", &self.e)],
209            other => {
210                return Err(Error::Key(format!(
211                    "cnf.jwk uses an unsupported key type: {other}"
212                )))
213            }
214        };
215        if let Some((name, _)) = required.iter().find(|(_, v)| v.is_none()) {
216            return Err(Error::Key(format!(
217                "cnf.jwk is structurally incomplete: missing {name}"
218            )));
219        }
220        match self.alg.as_deref() {
221            None | Some("") => Err(Error::Key(
222                "cnf.jwk: alg is REQUIRED and must be fully specified".into(),
223            )),
224            Some(alg @ ("none" | "EdDSA" | "HS256" | "HS384" | "HS512")) => {
225                Err(Error::Key(format!("cnf.jwk: alg {alg:?} MUST NOT be used")))
226            }
227            Some("Ed25519") if self.kty == "OKP" && self.crv.as_deref() != Some("Ed25519") => {
228                Err(Error::Key("cnf.jwk crv disagrees with alg".into()))
229            }
230            Some("ES256") if self.kty == "EC" && self.crv.as_deref() != Some("P-256") => {
231                Err(Error::Key("cnf.jwk crv disagrees with alg".into()))
232            }
233            Some(_) => Ok(()),
234        }
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn the_thumbprint_is_rfc_8037s() {
244        // RFC 8037 Appendix A.3.
245        let jwk = Jwk {
246            kty: "OKP".into(),
247            crv: Some("Ed25519".into()),
248            x: Some("11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo".into()),
249            ..Default::default()
250        };
251        assert_eq!(
252            jwk.thumbprint().unwrap(),
253            "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k"
254        );
255    }
256
257    #[test]
258    fn the_algorithm_comes_from_alg_and_must_agree_with_the_key() {
259        let mut jwk = Jwk {
260            kty: "OKP".into(),
261            crv: Some("Ed25519".into()),
262            x: Some("x".into()),
263            alg: Some("Ed25519".into()),
264            ..Default::default()
265        };
266        assert_eq!(jwk.algorithm(), Ok(Algorithm::Ed25519));
267        jwk.alg = Some("EdDSA".into());
268        assert!(matches!(jwk.algorithm(), Err(KeyProblem::Invalid(_))));
269        jwk.alg = None;
270        assert!(matches!(jwk.algorithm(), Err(KeyProblem::Invalid(_))));
271        jwk.alg = Some("ES256".into());
272        assert!(matches!(jwk.algorithm(), Err(KeyProblem::Invalid(_))));
273        jwk.alg = Some("Ed448".into());
274        assert!(matches!(jwk.algorithm(), Err(KeyProblem::Unsupported(_))));
275    }
276
277    #[test]
278    fn a_small_order_key_verifies_nothing() {
279        // The identity point, with an all-zero-R signature that a
280        // non-strict check accepts for any message.
281        let mut identity = [0u8; 32];
282        identity[0] = 1;
283        let jwk = Jwk {
284            kty: "OKP".into(),
285            crv: Some("Ed25519".into()),
286            x: Some(URL_SAFE_NO_PAD.encode(identity)),
287            alg: Some("Ed25519".into()),
288            ..Default::default()
289        };
290        let mut signature = [0u8; 64];
291        signature[0] = 1;
292        assert!(!jwk.verify(Algorithm::Ed25519, b"anything", &signature));
293    }
294}