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.

222 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) => match usable.iter().find(|key| key["kid"].as_str() == Some(kid)) {
88 Some(key) => Some(*key),
89 // Keys published without ids: the only one, if there is one.
90 None if usable.len() == 1 && usable[0]["kid"].is_null() => Some(usable[0]),
91 None => None,
92 },
93 None if usable.len() == 1 => usable.into_iter().next(),
94 None => None,
95 };
96 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())
97}
98
99/// Verifies `token`'s signature with a key of `jwks` and returns its claims.
100/// Only the signature: what the claims must say is checked after.
101pub fn verify(token: &str, jwks: &Value) -> Result<Value, String> {
102 let mut parts = token.trim().split('.');
103 let (Some(header), Some(payload), Some(signature), None) = (parts.next(), parts.next(), parts.next(), parts.next()) else {
104 return Err(UNREADABLE.to_owned());
105 };
106 let head: Value = serde_json::from_slice(&decode(header)?).map_err(|_| UNREADABLE.to_owned())?;
107 let name = head["alg"].as_str().unwrap_or("none");
108 let alg = Alg::parse(name)?;
109 if head.get("crit").is_some() || head.get("jku").is_some() || head.get("x5u").is_some() || head.get("jwk").is_some() {
110 // Keys named in the token itself are never trusted.
111 return Err("The ID token names its own key, which g1t does not accept.".to_owned());
112 }
113 let key = key_for(jwks, head["kid"].as_str(), alg, name)?;
114 let signed = format!("{header}.{payload}");
115 let signature = decode(signature)?;
116 let verified = match alg {
117 Alg::Rs(bits) | Alg::Ps(bits) => {
118 let (Some(n), Some(e)) = (key["n"].as_str(), key["e"].as_str()) else {
119 return Err(BAD_SIGNATURE.to_owned());
120 };
121 let modulus = decode(n)?;
122 // Under 2048 bits is not a key to trust.
123 if modulus.iter().skip_while(|b| **b == 0).count() < 256 {
124 return Err("The identity provider signs with an RSA key shorter than 2048 bits, which g1t does not accept.".to_owned());
125 }
126 let public = RsaPublicKey::new(BigUint::from_bytes_be(&modulus), BigUint::from_bytes_be(&decode(e)?)).map_err(|_| BAD_SIGNATURE.to_owned())?;
127 let hashed = bits.digest(signed.as_bytes());
128 match (alg, bits) {
129 (Alg::Rs(_), Bits::B256) => public.verify(Pkcs1v15Sign::new::<Sha256>(), &hashed, &signature),
130 (Alg::Rs(_), Bits::B384) => public.verify(Pkcs1v15Sign::new::<Sha384>(), &hashed, &signature),
131 (Alg::Rs(_), Bits::B512) => public.verify(Pkcs1v15Sign::new::<Sha512>(), &hashed, &signature),
132 (_, Bits::B256) => public.verify(Pss::new::<Sha256>(), &hashed, &signature),
133 (_, Bits::B384) => public.verify(Pss::new::<Sha384>(), &hashed, &signature),
134 (_, Bits::B512) => public.verify(Pss::new::<Sha512>(), &hashed, &signature),
135 }
136 .is_ok()
137 }
138 Alg::Es256 => {
139 use p256::ecdsa::signature::Verifier;
140 use p256::ecdsa::{Signature, VerifyingKey};
141 if key["crv"].as_str() != Some("P-256") {
142 return Err(BAD_SIGNATURE.to_owned());
143 }
144 let (Some(x), Some(y)) = (key["x"].as_str(), key["y"].as_str()) else {
145 return Err(BAD_SIGNATURE.to_owned());
146 };
147 let mut point = vec![0x04];
148 point.extend(decode(x)?);
149 point.extend(decode(y)?);
150 match (VerifyingKey::from_sec1_bytes(&point), Signature::from_slice(&signature)) {
151 (Ok(public), Ok(signature)) => public.verify(signed.as_bytes(), &signature).is_ok(),
152 _ => false,
153 }
154 }
155 };
156 if !verified {
157 return Err(BAD_SIGNATURE.to_owned());
158 }
159 serde_json::from_slice(&decode(payload)?).map_err(|_| UNREADABLE.to_owned())
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use serde_json::json;
166
167 fn fixtures() -> Value {
168 serde_json::from_str(include_str!("jwt_fixtures.json")).unwrap()
169 }
170
171 fn token(name: &str) -> String {
172 fixtures()["tokens"][name].as_str().unwrap().to_owned()
173 }
174
175 #[test]
176 fn tokens_signed_by_the_issuer_verify() {
177 let jwks = fixtures()["jwks"].clone();
178 for name in ["rs256", "ps256", "es256", "rs256_other_key"] {
179 let claims = verify(&token(name), &jwks).unwrap_or_else(|e| panic!("{name}: {e}"));
180 assert_eq!(claims["sub"], "u1", "{name}");
181 }
182 }
183
184 #[test]
185 fn a_changed_token_does_not() {
186 let jwks = fixtures()["jwks"].clone();
187 for name in ["rs256", "ps256", "es256"] {
188 let token = token(name);
189 let mut parts: Vec<String> = token.split('.').map(str::to_owned).collect();
190 let mut claims: Value = serde_json::from_slice(&decode(&parts[1]).unwrap()).unwrap();
191 claims["sub"] = json!("eve");
192 parts[1] = URL_SAFE_NO_PAD.encode(claims.to_string());
193 assert_eq!(verify(&parts.join("."), &jwks).unwrap_err(), BAD_SIGNATURE, "{name}");
194 }
195 }
196
197 #[test]
198 fn a_key_the_issuer_did_not_publish_is_not_found() {
199 let without = fixtures()["jwks_without_rsa2"].clone();
200 assert!(verify(&token("rs256_other_key"), &without).unwrap_err().contains("none for this ID token"));
201 }
202
203 #[test]
204 fn unsigned_and_shared_secret_tokens_are_refused() {
205 let jwks = fixtures()["jwks"].clone();
206 let unsigned = format!("{}.{}.", URL_SAFE_NO_PAD.encode(r#"{"alg":"none"}"#), URL_SAFE_NO_PAD.encode(r#"{"sub":"eve"}"#));
207 assert!(verify(&unsigned, &jwks).unwrap_err().contains("\"none\""));
208 let hmac = format!("{}.{}.c2ln", URL_SAFE_NO_PAD.encode(r#"{"alg":"HS256","kid":"rsa1"}"#), URL_SAFE_NO_PAD.encode(r#"{"sub":"eve"}"#));
209 assert!(verify(&hmac, &jwks).unwrap_err().contains("HS256"));
210 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"}"#));
211 assert!(verify(&own_key, &jwks).unwrap_err().contains("names its own key"));
212 }
213
214 #[test]
215 fn an_es256_token_is_not_checked_with_an_rsa_key() {
216 // The EC token's header renamed to an RSA key: the types differ.
217 let token = token("es256");
218 let mut parts: Vec<String> = token.split('.').map(str::to_owned).collect();
219 parts[0] = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256","kid":"rsa1"}"#);
220 assert!(verify(&parts.join("."), &fixtures()["jwks"]).is_err());
221 }
222}