Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

212 linesCodeBlameRaw
1//! Verifying an OpenID Connect ID token's signature with the issuer's
2//! published keys (its JWKS, from `jwks_uri`): RSA (RS256/384/512 and
3//! PS256/384/512) and P-256 (ES256). Anything else, `none` and the HMAC
4//! algorithms above all, is refused: g1t never holds a key that would make
5//! a shared secret mean anything here.
6//!
7//! The key is the one the token's header names by `kid`, or the only
8//! signing key of the right type when it names none. A key marked for
9//! encryption, or for another algorithm, is never used.
10
11use base64::Engine;
12use base64::engine::general_purpose::URL_SAFE_NO_PAD;
13use rsa::pss::Pss;
14use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey};
15use serde_json::Value;
16use sha2::{Digest, Sha256, Sha384, Sha512};
17
18const UNREADABLE: &str = "The identity provider's ID token could not be read.";
19const BAD_SIGNATURE: &str = "The ID token's signature did not verify with the identity provider's published keys.";
20
21fn decode(part: &str) -> Result<Vec<u8>, String> {
22 URL_SAFE_NO_PAD.decode(part.trim_end_matches('=')).map_err(|_| UNREADABLE.to_owned())
23}
24
25/// The algorithms g1t accepts, and what each needs.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27enum Alg {
28 Rs(Bits),
29 Ps(Bits),
30 Es256,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34enum Bits {
35 B256,
36 B384,
37 B512,
38}
39
40impl Bits {
41 fn digest(self, data: &[u8]) -> Vec<u8> {
42 match self {
43 Bits::B256 => Sha256::digest(data).to_vec(),
44 Bits::B384 => Sha384::digest(data).to_vec(),
45 Bits::B512 => Sha512::digest(data).to_vec(),
46 }
47 }
48}
49
50impl Alg {
51 fn parse(name: &str) -> Result<Alg, String> {
52 Ok(match name {
53 "RS256" => Alg::Rs(Bits::B256),
54 "RS384" => Alg::Rs(Bits::B384),
55 "RS512" => Alg::Rs(Bits::B512),
56 "PS256" => Alg::Ps(Bits::B256),
57 "PS384" => Alg::Ps(Bits::B384),
58 "PS512" => Alg::Ps(Bits::B512),
59 "ES256" => Alg::Es256,
60 other => {
61 return Err(format!(
62 "The identity provider signs ID tokens with {other:?}, which g1t does not accept. Use RS256 (or PS256 or ES256)."
63 ));
64 }
65 })
66 }
67
68 fn kty(self) -> &'static str {
69 match self {
70 Alg::Rs(_) | Alg::Ps(_) => "RSA",
71 Alg::Es256 => "EC",
72 }
73 }
74}
75
76/// The key `kid` names, or the only one that fits.
77fn key_for<'a>(jwks: &'a Value, kid: Option<&str>, alg: Alg, name: &str) -> Result<&'a Value, String> {
78 let usable: Vec<&Value> = jwks["keys"]
79 .as_array()
80 .into_iter()
81 .flatten()
82 .filter(|key| key["kty"].as_str() == Some(alg.kty()))
83 .filter(|key| key["use"].as_str().is_none_or(|u| u == "sig"))
84 .filter(|key| key["alg"].as_str().is_none_or(|a| a == name))
85 .collect();
86 let found = match kid {
87 Some(kid) => usable.into_iter().find(|key| key["kid"].as_str() == Some(kid)),
88 None if usable.len() == 1 => usable.into_iter().next(),
89 None => None,
90 };
91 found.ok_or_else(|| "The identity provider's published keys have none for this ID token. If it just rotated its keys, try again.".to_owned())
92}
93
94/// Verifies `token`'s signature with a key of `jwks` and returns its claims.
95/// Only the signature: what the claims must say is checked after.
96pub fn verify(token: &str, jwks: &Value) -> Result<Value, String> {
97 let mut parts = token.trim().split('.');
98 let (Some(header), Some(payload), Some(signature), None) = (parts.next(), parts.next(), parts.next(), parts.next()) else {
99 return Err(UNREADABLE.to_owned());
100 };
101 let head: Value = serde_json::from_slice(&decode(header)?).map_err(|_| UNREADABLE.to_owned())?;
102 let name = head["alg"].as_str().unwrap_or("none");
103 let alg = Alg::parse(name)?;
104 if head.get("crit").is_some() || head.get("jku").is_some() || head.get("x5u").is_some() || head.get("jwk").is_some() {
105 // Keys named in the token itself are never trusted.
106 return Err("The ID token names its own key, which g1t does not accept.".to_owned());
107 }
108 let key = key_for(jwks, head["kid"].as_str(), alg, name)?;
109 let signed = format!("{header}.{payload}");
110 let signature = decode(signature)?;
111 let verified = match alg {
112 Alg::Rs(bits) | Alg::Ps(bits) => {
113 let (Some(n), Some(e)) = (key["n"].as_str(), key["e"].as_str()) else {
114 return Err(BAD_SIGNATURE.to_owned());
115 };
116 let public = RsaPublicKey::new(BigUint::from_bytes_be(&decode(n)?), BigUint::from_bytes_be(&decode(e)?)).map_err(|_| BAD_SIGNATURE.to_owned())?;
117 let hashed = bits.digest(signed.as_bytes());
118 match (alg, bits) {
119 (Alg::Rs(_), Bits::B256) => public.verify(Pkcs1v15Sign::new::<Sha256>(), &hashed, &signature),
120 (Alg::Rs(_), Bits::B384) => public.verify(Pkcs1v15Sign::new::<Sha384>(), &hashed, &signature),
121 (Alg::Rs(_), Bits::B512) => public.verify(Pkcs1v15Sign::new::<Sha512>(), &hashed, &signature),
122 (_, Bits::B256) => public.verify(Pss::new::<Sha256>(), &hashed, &signature),
123 (_, Bits::B384) => public.verify(Pss::new::<Sha384>(), &hashed, &signature),
124 (_, Bits::B512) => public.verify(Pss::new::<Sha512>(), &hashed, &signature),
125 }
126 .is_ok()
127 }
128 Alg::Es256 => {
129 use p256::ecdsa::signature::Verifier;
130 use p256::ecdsa::{Signature, VerifyingKey};
131 if key["crv"].as_str() != Some("P-256") {
132 return Err(BAD_SIGNATURE.to_owned());
133 }
134 let (Some(x), Some(y)) = (key["x"].as_str(), key["y"].as_str()) else {
135 return Err(BAD_SIGNATURE.to_owned());
136 };
137 let mut point = vec![0x04];
138 point.extend(decode(x)?);
139 point.extend(decode(y)?);
140 match (VerifyingKey::from_sec1_bytes(&point), Signature::from_slice(&signature)) {
141 (Ok(public), Ok(signature)) => public.verify(signed.as_bytes(), &signature).is_ok(),
142 _ => false,
143 }
144 }
145 };
146 if !verified {
147 return Err(BAD_SIGNATURE.to_owned());
148 }
149 serde_json::from_slice(&decode(payload)?).map_err(|_| UNREADABLE.to_owned())
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use serde_json::json;
156
157 fn fixtures() -> Value {
158 serde_json::from_str(include_str!("jwt_fixtures.json")).unwrap()
159 }
160
161 fn token(name: &str) -> String {
162 fixtures()["tokens"][name].as_str().unwrap().to_owned()
163 }
164
165 #[test]
166 fn tokens_signed_by_the_issuer_verify() {
167 let jwks = fixtures()["jwks"].clone();
168 for name in ["rs256", "ps256", "es256", "rs256_other_key"] {
169 let claims = verify(&token(name), &jwks).unwrap_or_else(|e| panic!("{name}: {e}"));
170 assert_eq!(claims["sub"], "u1", "{name}");
171 }
172 }
173
174 #[test]
175 fn a_changed_token_does_not() {
176 let jwks = fixtures()["jwks"].clone();
177 for name in ["rs256", "ps256", "es256"] {
178 let token = token(name);
179 let mut parts: Vec<String> = token.split('.').map(str::to_owned).collect();
180 let mut claims: Value = serde_json::from_slice(&decode(&parts[1]).unwrap()).unwrap();
181 claims["sub"] = json!("eve");
182 parts[1] = URL_SAFE_NO_PAD.encode(claims.to_string());
183 assert_eq!(verify(&parts.join("."), &jwks).unwrap_err(), BAD_SIGNATURE, "{name}");
184 }
185 }
186
187 #[test]
188 fn a_key_the_issuer_did_not_publish_is_not_found() {
189 let without = fixtures()["jwks_without_rsa2"].clone();
190 assert!(verify(&token("rs256_other_key"), &without).unwrap_err().contains("none for this ID token"));
191 }
192
193 #[test]
194 fn unsigned_and_shared_secret_tokens_are_refused() {
195 let jwks = fixtures()["jwks"].clone();
196 let unsigned = format!("{}.{}.", URL_SAFE_NO_PAD.encode(r#"{"alg":"none"}"#), URL_SAFE_NO_PAD.encode(r#"{"sub":"eve"}"#));
197 assert!(verify(&unsigned, &jwks).unwrap_err().contains("\"none\""));
198 let hmac = format!("{}.{}.c2ln", URL_SAFE_NO_PAD.encode(r#"{"alg":"HS256","kid":"rsa1"}"#), URL_SAFE_NO_PAD.encode(r#"{"sub":"eve"}"#));
199 assert!(verify(&hmac, &jwks).unwrap_err().contains("HS256"));
200 let own_key = format!("{}.{}.c2ln", URL_SAFE_NO_PAD.encode(r#"{"alg":"RS256","jku":"https://evil.test/keys"}"#), URL_SAFE_NO_PAD.encode(r#"{"sub":"eve"}"#));
201 assert!(verify(&own_key, &jwks).unwrap_err().contains("names its own key"));
202 }
203
204 #[test]
205 fn an_es256_token_is_not_checked_with_an_rsa_key() {
206 // The EC token's header renamed to an RSA key: the types differ.
207 let token = token("es256");
208 let mut parts: Vec<String> = token.split('.').map(str::to_owned).collect();
209 parts[0] = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256","kid":"rsa1"}"#);
210 assert!(verify(&parts.join("."), &fixtures()["jwks"]).is_err());
211 }
212}