| ... | @@ -158,22 +158,33 @@ fn signed_claims( | ... | @@ -158,22 +158,33 @@ fn signed_claims( |
| 158 | nonce: &str, | 158 | nonce: &str, |
| 159 | require_nonce: bool, | 159 | require_nonce: bool, |
| 160 | ) -> Result<Value> { | 160 | ) -> Result<Value> { |
| 161 | let reject = || { | 161 | let reject = |reason: &str| { |
| | 162 | eprintln!( |
| | 163 | "guest token verification: {} {reason}", |
| | 164 | if require_nonce { |
| | 165 | "id_token" |
| | 166 | } else { |
| | 167 | "userinfo" |
| | 168 | } |
| | 169 | ); |
| 162 | Error::new( | 170 | Error::new( |
| 163 | 502, | 171 | 502, |
| 164 | "The provider couldn't verify your sign-in. Return to Shale and try again.", | 172 | "The provider couldn't verify your sign-in. Return to Shale and try again.", |
| 165 | ) | 173 | ) |
| 166 | }; | 174 | }; |
| 167 | if token.len() > 32768 { | 175 | if token.len() > 32768 { |
| 168 | return Err(reject()); | 176 | return Err(reject("size")); |
| 169 | } | 177 | } |
| 170 | let parts: Vec<_> = token.split('.').collect(); | 178 | let parts: Vec<_> = token.split('.').collect(); |
| 171 | if parts.len() != 3 { | 179 | if parts.len() != 3 { |
| 172 | return Err(reject()); | 180 | return Err(reject("format")); |
| 173 | } | 181 | } |
| 174 | let header: Value = | 182 | let header: Value = serde_json::from_slice( |
| 175 | serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[0]).map_err(|_| reject())?) | 183 | &URL_SAFE_NO_PAD |
| 176 | .map_err(|_| reject())?; | 184 | .decode(parts[0]) |
| | 185 | .map_err(|_| reject("header_encoding"))?, |
| | 186 | ) |
| | 187 | .map_err(|_| reject("header_json"))?; |
| 177 | if header | 188 | if header |
| 178 | .get("kid") | 189 | .get("kid") |
| 179 | .is_some_and(|id| id.as_str().is_none_or(str::is_empty)) | 190 | .is_some_and(|id| id.as_str().is_none_or(str::is_empty)) |
| ... | @@ -182,7 +193,7 @@ fn signed_claims( | ... | @@ -182,7 +193,7 @@ fn signed_claims( |
| 182 | || header.get("jku").is_some() | 193 | || header.get("jku").is_some() |
| 183 | || header.get("jwk").is_some() | 194 | || header.get("jwk").is_some() |
| 184 | { | 195 | { |
| 185 | return Err(reject()); | 196 | return Err(reject("header")); |
| 186 | } | 197 | } |
| 187 | let matching: Vec<_> = array(&keys["keys"]) | 198 | let matching: Vec<_> = array(&keys["keys"]) |
| 188 | .iter() | 199 | .iter() |
| ... | @@ -197,17 +208,17 @@ fn signed_claims( | ... | @@ -197,17 +208,17 @@ fn signed_claims( |
| 197 | }) | 208 | }) |
| 198 | .collect(); | 209 | .collect(); |
| 199 | if matching.len() != 1 { | 210 | if matching.len() != 1 { |
| 200 | return Err(reject()); | 211 | return Err(reject("key_selection")); |
| 201 | } | 212 | } |
| 202 | let key = matching[0]; | 213 | let key = matching[0]; |
| 203 | let x = URL_SAFE_NO_PAD | 214 | let x = URL_SAFE_NO_PAD |
| 204 | .decode(string(&key["x"])) | 215 | .decode(string(&key["x"])) |
| 205 | .map_err(|_| reject())?; | 216 | .map_err(|_| reject("key_x"))?; |
| 206 | let y = URL_SAFE_NO_PAD | 217 | let y = URL_SAFE_NO_PAD |
| 207 | .decode(string(&key["y"])) | 218 | .decode(string(&key["y"])) |
| 208 | .map_err(|_| reject())?; | 219 | .map_err(|_| reject("key_y"))?; |
| 209 | if x.len() != 32 || y.len() != 32 { | 220 | if x.len() != 32 || y.len() != 32 { |
| 210 | return Err(reject()); | 221 | return Err(reject("key_coordinates")); |
| 211 | } | 222 | } |
| 212 | let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?; | 223 | let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?; |
| 213 | let mut point = EcPoint::new(&group)?; | 224 | let mut point = EcPoint::new(&group)?; |
| ... | @@ -218,9 +229,11 @@ fn signed_claims( | ... | @@ -218,9 +229,11 @@ fn signed_claims( |
| 218 | let ec = EcKey::from_public_key(&group, &point)?; | 229 | let ec = EcKey::from_public_key(&group, &point)?; |
| 219 | ec.check_key()?; | 230 | ec.check_key()?; |
| 220 | let key = PKey::from_ec_key(ec)?; | 231 | let key = PKey::from_ec_key(ec)?; |
| 221 | let raw = URL_SAFE_NO_PAD.decode(parts[2]).map_err(|_| reject())?; | 232 | let raw = URL_SAFE_NO_PAD |
| | 233 | .decode(parts[2]) |
| | 234 | .map_err(|_| reject("signature_encoding"))?; |
| 222 | if raw.len() != 64 { | 235 | if raw.len() != 64 { |
| 223 | return Err(reject()); | 236 | return Err(reject("signature_length")); |
| 224 | } | 237 | } |
| 225 | let signature = EcdsaSig::from_private_components( | 238 | let signature = EcdsaSig::from_private_components( |
| 226 | BigNum::from_slice(&raw[..32])?, | 239 | BigNum::from_slice(&raw[..32])?, |
| ... | @@ -230,30 +243,55 @@ fn signed_claims( | ... | @@ -230,30 +243,55 @@ fn signed_claims( |
| 230 | let mut verify = Verifier::new(MessageDigest::sha256(), &key)?; | 243 | let mut verify = Verifier::new(MessageDigest::sha256(), &key)?; |
| 231 | verify.update(format!("{}.{}", parts[0], parts[1]).as_bytes())?; | 244 | verify.update(format!("{}.{}", parts[0], parts[1]).as_bytes())?; |
| 232 | if !verify.verify(&signature)? { | 245 | if !verify.verify(&signature)? { |
| 233 | return Err(reject()); | 246 | return Err(reject("signature")); |
| 234 | } | 247 | } |
| 235 | let claims: Value = | 248 | let claims: Value = serde_json::from_slice( |
| 236 | serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).map_err(|_| reject())?) | 249 | &URL_SAFE_NO_PAD |
| 237 | .map_err(|_| reject())?; | 250 | .decode(parts[1]) |
| | 251 | .map_err(|_| reject("claims_encoding"))?, |
| | 252 | ) |
| | 253 | .map_err(|_| reject("claims_json"))?; |
| 238 | let time = now() as i64; | 254 | let time = now() as i64; |
| 239 | let audience = claims["aud"].as_str().is_some_and(|a| a == client) | 255 | let audience = claims["aud"].as_str().is_some_and(|a| a == client) |
| 240 | || array(&claims["aud"]) | 256 | || array(&claims["aud"]) |
| 241 | .iter() | 257 | .iter() |
| 242 | .any(|a| a.as_str() == Some(client)); | 258 | .any(|a| a.as_str() == Some(client)); |
| 243 | if claims["iss"] != ASTHENO | 259 | for (reason, invalid) in [ |
| 244 | || !audience | 260 | ("issuer", claims["iss"] != ASTHENO), |
| 245 | || (claims["aud"].is_array() && array(&claims["aud"]).len() > 1 && claims["azp"] != client) | 261 | ("audience", !audience), |
| 246 | || claims.get("azp").is_some_and(|a| a != client) | 262 | ( |
| 247 | || string(&claims["sub"]).is_empty() | 263 | "authorized_party", |
| 248 | || string(&claims["sub"]).len() > 512 | 264 | (claims["aud"].is_array() |
| 249 | || claims["exp"].as_i64().is_none_or(|t| t <= time) | 265 | && array(&claims["aud"]).len() > 1 |
| 250 | || claims["iat"].as_i64().is_none_or(|t| t > time + 60) | 266 | && claims["azp"] != client) |
| 251 | || claims | 267 | || claims.get("azp").is_some_and(|a| a != client), |
| 252 | .get("nbf") | 268 | ), |
| 253 | .is_some_and(|t| t.as_i64().is_none_or(|n| n > time + 60)) | 269 | ( |
| 254 | || (require_nonce && claims["nonce"].as_str() != Some(nonce)) | 270 | "subject", |
| 255 | { | 271 | string(&claims["sub"]).is_empty() || string(&claims["sub"]).len() > 512, |
| 256 | return Err(reject()); | 272 | ), |
| | 273 | ( |
| | 274 | "expiration", |
| | 275 | claims["exp"].as_i64().is_none_or(|t| t <= time), |
| | 276 | ), |
| | 277 | ( |
| | 278 | "issued_at", |
| | 279 | claims["iat"].as_i64().is_none_or(|t| t > time + 60), |
| | 280 | ), |
| | 281 | ( |
| | 282 | "not_before", |
| | 283 | claims |
| | 284 | .get("nbf") |
| | 285 | .is_some_and(|t| t.as_i64().is_none_or(|n| n > time + 60)), |
| | 286 | ), |
| | 287 | ( |
| | 288 | "nonce", |
| | 289 | require_nonce && claims["nonce"].as_str() != Some(nonce), |
| | 290 | ), |
| | 291 | ] { |
| | 292 | if invalid { |
| | 293 | return Err(reject(reason)); |
| | 294 | } |
| 257 | } | 295 | } |
| 258 | Ok(claims) | 296 | Ok(claims) |
| 259 | } | 297 | } |
| ... | @@ -560,17 +598,8 @@ async fn handle(app: &App, request: Request) -> Result<Response> { | ... | @@ -560,17 +598,8 @@ async fn handle(app: &App, request: Request) -> Result<Response> { |
| 560 | headers | 598 | headers |
| 561 | }) | 599 | }) |
| 562 | .build()?; | 600 | .build()?; |
| 563 | let (subject, name) = exchange(&http, provider, &client, &secret, code, &callback, &flow) | 601 | let (subject, name) = |
| 564 | .await | 602 | exchange(&http, provider, &client, &secret, code, &callback, &flow).await?; |
| 565 | .map_err(|error| { | | |
| 566 | eprintln!( | | |
| 567 | "guest rejected code: {provider} length {} whitespace {} dots {}", | | |
| 568 | code.len(), | | |
| 569 | code.bytes().any(|c| c.is_ascii_whitespace()), | | |
| 570 | code.bytes().filter(|c| *c == b'.').count() | | |
| 571 | ); | | |
| 572 | error | | |
| 573 | })?; | | |
| 574 | let id = account(auth, provider, &subject, &name)?; | 603 | let id = account(auth, provider, &subject, &name)?; |
| 575 | auth.create_session(&id, "dashboard", headers, None) | 604 | auth.create_session(&id, "dashboard", headers, None) |
| 576 | } | 605 | } |
| ... | @@ -578,11 +607,8 @@ async fn handle(app: &App, request: Request) -> Result<Response> { | ... | @@ -578,11 +607,8 @@ async fn handle(app: &App, request: Request) -> Result<Response> { |
| 578 | match result { | 607 | match result { |
| 579 | Ok(session) => Ok(( | 608 | Ok(session) => Ok(( |
| 580 | StatusCode::FOUND, | 609 | StatusCode::FOUND, |
| 581 | [ | 610 | [("location", next), ("set-cookie", session)], |
| 582 | ("location", next), | 611 | axum::response::AppendHeaders([("set-cookie", auth::set_cookie(COOKIE, "", 0))]), |
| 583 | ("set-cookie", session), | | |
| 584 | ("set-cookie", auth::set_cookie(COOKIE, "", 0)), | | |
| 585 | ], | | |
| 586 | ) | 612 | ) |
| 587 | .into_response()), | 613 | .into_response()), |
| 588 | Err(error) => { | 614 | Err(error) => { |