Skip to main content

oauth_resource_server/
algorithms.rs

1//! Which JWS algorithms may ever verify a token, and which ones each key may.
2//!
3//! Two independent gates: the configured allowlist ([`parse_algorithm`] refuses
4//! HMAC and `none` outright, and [`Algorithm`] has no variant for either, so no
5//! config can turn them on), and the key a token names, whose own type bounds
6//! the algorithms it can verify ([`key_algorithms`]). A token's `alg` must pass
7//! both, which is what stops an attacker-chosen header from steering an RSA key
8//! into an ECDSA verification, or any key into HMAC.
9
10use std::fmt;
11
12use jsonwebtoken::jwk::{AlgorithmParameters, EllipticCurve, KeyAlgorithm};
13
14/// Default [`crate::OAuthConfig::algorithms`]: every asymmetric JWS algorithm
15/// this crate can verify. Deliberately wide — which algorithm a token may use is
16/// ALSO constrained by the key it names, so accepting ES256 here cannot make an
17/// RSA key verify an ES256 signature. HS256/384/512 and `none` are not merely
18/// absent: [`parse_algorithm`] refuses them, so no config can turn them on.
19/// ES512 is absent because the underlying `ring` cannot verify P-521.
20pub const DEFAULT_ALGORITHMS: &[&str] = &[
21    "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "EdDSA",
22];
23
24/// A JWS signature algorithm this crate can verify an access token with.
25///
26/// Crate-owned rather than a re-export of the JWT library's type, so replacing
27/// that library is not a breaking change here. It has no variant for HMAC
28/// (`HS256`/`HS384`/`HS512`) or for `none`: a resource server must never verify
29/// with a shared secret, and a [`crate::ResolvedOAuthConfig`] whose
30/// `algorithms` names one cannot be written down.
31///
32/// `Display` (and [`Algorithm::as_str`]) give the JWS name; `Debug` prints the
33/// same name. `#[non_exhaustive]`: an algorithm may be added in a minor
34/// release.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36#[non_exhaustive]
37#[allow(clippy::upper_case_acronyms)]
38pub enum Algorithm {
39    /// RSASSA-PKCS1-v1_5 with SHA-256.
40    RS256,
41    /// RSASSA-PKCS1-v1_5 with SHA-384.
42    RS384,
43    /// RSASSA-PKCS1-v1_5 with SHA-512.
44    RS512,
45    /// RSASSA-PSS with SHA-256.
46    PS256,
47    /// RSASSA-PSS with SHA-384.
48    PS384,
49    /// RSASSA-PSS with SHA-512.
50    PS512,
51    /// ECDSA on P-256 with SHA-256.
52    ES256,
53    /// ECDSA on P-384 with SHA-384.
54    ES384,
55    /// EdDSA (Ed25519).
56    EdDSA,
57}
58
59impl Algorithm {
60    /// The JWS `alg` name, e.g. `"RS256"` or `"EdDSA"`.
61    pub fn as_str(self) -> &'static str {
62        match self {
63            Algorithm::RS256 => "RS256",
64            Algorithm::RS384 => "RS384",
65            Algorithm::RS512 => "RS512",
66            Algorithm::PS256 => "PS256",
67            Algorithm::PS384 => "PS384",
68            Algorithm::PS512 => "PS512",
69            Algorithm::ES256 => "ES256",
70            Algorithm::ES384 => "ES384",
71            Algorithm::EdDSA => "EdDSA",
72        }
73    }
74
75    /// The JWT library's equivalent, for verification.
76    pub(crate) fn to_jwt(self) -> jsonwebtoken::Algorithm {
77        match self {
78            Algorithm::RS256 => jsonwebtoken::Algorithm::RS256,
79            Algorithm::RS384 => jsonwebtoken::Algorithm::RS384,
80            Algorithm::RS512 => jsonwebtoken::Algorithm::RS512,
81            Algorithm::PS256 => jsonwebtoken::Algorithm::PS256,
82            Algorithm::PS384 => jsonwebtoken::Algorithm::PS384,
83            Algorithm::PS512 => jsonwebtoken::Algorithm::PS512,
84            Algorithm::ES256 => jsonwebtoken::Algorithm::ES256,
85            Algorithm::ES384 => jsonwebtoken::Algorithm::ES384,
86            Algorithm::EdDSA => jsonwebtoken::Algorithm::EdDSA,
87        }
88    }
89
90    /// The crate's equivalent of a JWT library algorithm; `None` for HMAC, which
91    /// this crate never verifies.
92    pub(crate) fn from_jwt(alg: jsonwebtoken::Algorithm) -> Option<Self> {
93        Some(match alg {
94            jsonwebtoken::Algorithm::RS256 => Algorithm::RS256,
95            jsonwebtoken::Algorithm::RS384 => Algorithm::RS384,
96            jsonwebtoken::Algorithm::RS512 => Algorithm::RS512,
97            jsonwebtoken::Algorithm::PS256 => Algorithm::PS256,
98            jsonwebtoken::Algorithm::PS384 => Algorithm::PS384,
99            jsonwebtoken::Algorithm::PS512 => Algorithm::PS512,
100            jsonwebtoken::Algorithm::ES256 => Algorithm::ES256,
101            jsonwebtoken::Algorithm::ES384 => Algorithm::ES384,
102            jsonwebtoken::Algorithm::EdDSA => Algorithm::EdDSA,
103            _ => return None,
104        })
105    }
106}
107
108impl fmt::Display for Algorithm {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.write_str(self.as_str())
111    }
112}
113
114/// Why [`parse_algorithm`] refused a name.
115///
116/// `Display` spells out the reason, quoting the refused value; it is what
117/// [`crate::OAuthConfig::resolve`] puts in its problem list.
118#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
119#[non_exhaustive]
120pub enum AlgorithmError {
121    /// `none`, in any case: an unsigned token.
122    #[error("\"{name}\" — an unsigned token is never acceptable")]
123    #[non_exhaustive]
124    Unsigned {
125        /// The refused value, trimmed.
126        name: String,
127    },
128    /// `HS256`, `HS384` or `HS512`: verification with a shared secret.
129    #[error(
130        "\"{name}\" — HMAC algorithms verify with a shared secret, which a resource \
131         server must never hold, and accepting one alongside a public key set is the \
132         classic key-confusion attack (a token signed with the PUBLIC key as the HMAC \
133         secret)"
134    )]
135    #[non_exhaustive]
136    Hmac {
137        /// The refused value, trimmed.
138        name: String,
139    },
140    /// Anything else that is not a JWS algorithm this crate can verify
141    /// (including `ES512`, which the underlying `ring` cannot, and a name in
142    /// the wrong case).
143    #[error("\"{name}\" — not a JWS algorithm this server can verify (supported: {supported})")]
144    #[non_exhaustive]
145    Unsupported {
146        /// The refused value, trimmed.
147        name: String,
148        /// [`DEFAULT_ALGORITHMS`], comma-separated.
149        supported: String,
150    },
151}
152
153/// Parse one [`crate::OAuthConfig::algorithms`] entry, refusing everything that
154/// must never be accepted with the reason spelled out (the error lands in a
155/// startup failure). [`crate::OAuthConfig::resolve`] calls it for every entry;
156/// it is public for applications that validate an algorithm list themselves.
157///
158/// Names are JWS names, matched case-sensitively after trimming.
159///
160/// # Errors
161///
162/// [`AlgorithmError::Unsigned`] for `none` (in any case),
163/// [`AlgorithmError::Hmac`] for `HS256`/`HS384`/`HS512`, and
164/// [`AlgorithmError::Unsupported`] for anything else that is not a JWS
165/// algorithm this crate can verify (including `ES512`, which the underlying
166/// `ring` cannot).
167///
168/// # Examples
169///
170/// ```
171/// use oauth_resource_server::{Algorithm, AlgorithmError, parse_algorithm};
172///
173/// assert_eq!(parse_algorithm("ES256"), Ok(Algorithm::ES256));
174/// let err = parse_algorithm("HS256").unwrap_err();
175/// assert!(matches!(err, AlgorithmError::Hmac { .. }));
176/// assert!(err.to_string().contains("key-confusion"));
177/// assert!(matches!(parse_algorithm("none"), Err(AlgorithmError::Unsigned { .. })));
178/// ```
179pub fn parse_algorithm(name: &str) -> Result<Algorithm, AlgorithmError> {
180    let name = name.trim();
181    if name.eq_ignore_ascii_case("none") {
182        return Err(AlgorithmError::Unsigned {
183            name: name.to_string(),
184        });
185    }
186    match name.parse::<jsonwebtoken::Algorithm>() {
187        Ok(
188            jsonwebtoken::Algorithm::HS256
189            | jsonwebtoken::Algorithm::HS384
190            | jsonwebtoken::Algorithm::HS512,
191        ) => Err(AlgorithmError::Hmac {
192            name: name.to_string(),
193        }),
194        Ok(alg) => Algorithm::from_jwt(alg).ok_or_else(|| unsupported(name)),
195        Err(_) => Err(unsupported(name)),
196    }
197}
198
199fn unsupported(name: &str) -> AlgorithmError {
200    AlgorithmError::Unsupported {
201        name: name.to_string(),
202        supported: DEFAULT_ALGORITHMS.join(", "),
203    }
204}
205
206/// The signature algorithms a key of this type can produce. `None` for a key that
207/// can never verify an access token here: symmetric (`oct`) keys above all — a
208/// shared secret has no business in a public key set, and honouring one would
209/// re-open the HMAC confusion that [`parse_algorithm`] closes — plus curves `ring`
210/// cannot verify (P-521) and non-signature curves (X25519).
211pub(crate) fn key_algorithms(params: &AlgorithmParameters) -> Option<Vec<Algorithm>> {
212    use Algorithm::*;
213    match params {
214        AlgorithmParameters::RSA(_) => Some(vec![RS256, RS384, RS512, PS256, PS384, PS512]),
215        AlgorithmParameters::EllipticCurve(p) => match p.curve {
216            EllipticCurve::P256 => Some(vec![ES256]),
217            EllipticCurve::P384 => Some(vec![ES384]),
218            _ => None,
219        },
220        AlgorithmParameters::OctetKeyPair(p) => match p.curve {
221            EllipticCurve::Ed25519 => Some(vec![EdDSA]),
222            _ => None,
223        },
224        AlgorithmParameters::OctetKey(_) => None,
225    }
226}
227
228/// A JWK `alg` as a JWS signature algorithm; `None` for HMAC and for key-management
229/// (encryption) algorithms, neither of which may verify an access token.
230pub(crate) fn signing_algorithm(alg: &KeyAlgorithm) -> Option<Algorithm> {
231    Some(match alg {
232        KeyAlgorithm::RS256 => Algorithm::RS256,
233        KeyAlgorithm::RS384 => Algorithm::RS384,
234        KeyAlgorithm::RS512 => Algorithm::RS512,
235        KeyAlgorithm::PS256 => Algorithm::PS256,
236        KeyAlgorithm::PS384 => Algorithm::PS384,
237        KeyAlgorithm::PS512 => Algorithm::PS512,
238        KeyAlgorithm::ES256 => Algorithm::ES256,
239        KeyAlgorithm::ES384 => Algorithm::ES384,
240        KeyAlgorithm::EdDSA => Algorithm::EdDSA,
241        _ => return None,
242    })
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn hmac_and_none_can_never_be_configured() {
251        for bad in [
252            "HS256", "HS384", "HS512", "none", "None", "ES512", "rs256", "",
253        ] {
254            assert!(parse_algorithm(bad).is_err(), "{bad:?} must be refused");
255        }
256        assert!(
257            parse_algorithm("HS256")
258                .unwrap_err()
259                .to_string()
260                .contains("key-confusion")
261        );
262        for good in DEFAULT_ALGORITHMS {
263            let alg = parse_algorithm(good).expect("every default parses");
264            assert_eq!(alg.as_str(), *good);
265            assert_eq!(alg.to_string(), *good);
266            assert_eq!(format!("{alg:?}"), *good);
267            assert_eq!(Algorithm::from_jwt(alg.to_jwt()), Some(alg));
268        }
269    }
270
271    #[test]
272    fn refusals_are_typed_and_keep_their_text() {
273        assert_eq!(
274            parse_algorithm(" none "),
275            Err(AlgorithmError::Unsigned {
276                name: "none".into()
277            })
278        );
279        assert_eq!(
280            parse_algorithm("none").unwrap_err().to_string(),
281            "\"none\" — an unsigned token is never acceptable"
282        );
283        assert!(matches!(
284            parse_algorithm("HS384"),
285            Err(AlgorithmError::Hmac { .. })
286        ));
287        let err = parse_algorithm("ES512").unwrap_err();
288        assert!(matches!(err, AlgorithmError::Unsupported { .. }));
289        assert_eq!(
290            err.to_string(),
291            "\"ES512\" — not a JWS algorithm this server can verify (supported: RS256, RS384, \
292             RS512, PS256, PS384, PS512, ES256, ES384, EdDSA)"
293        );
294        // A std error, so `?` into `Box<dyn Error>` works.
295        let _: Box<dyn std::error::Error + Send + Sync> = Box::new(err);
296    }
297
298    #[test]
299    fn hmac_has_no_crate_algorithm() {
300        for hmac in [
301            jsonwebtoken::Algorithm::HS256,
302            jsonwebtoken::Algorithm::HS384,
303            jsonwebtoken::Algorithm::HS512,
304        ] {
305            assert_eq!(Algorithm::from_jwt(hmac), None);
306        }
307    }
308}