Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

slightly okay-er jwks treatment

bleh

Changed files
+34 -37
who-am-i
+29 -10
who-am-i/src/jwt.rs
··· 1 + use elliptic_curve::SecretKey; 2 + use jose_jwk::{Class, Jwk, Key, Parameters}; 1 3 use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, errors::Error as JWTError}; 4 + use pkcs8::DecodePrivateKey; 2 5 use serde::Serialize; 3 6 use std::fs; 4 7 use std::io::Error as IOError; ··· 27 30 28 31 pub struct Tokens { 29 32 encoding_key: EncodingKey, 30 - jwks: String, 33 + jwk: Jwk, 31 34 } 32 35 33 36 impl Tokens { 34 - pub fn from_files( 35 - priv_f: impl AsRef<Path>, 36 - jwks_f: impl AsRef<Path>, 37 - ) -> Result<Self, TokensSetupError> { 37 + pub fn from_files(priv_f: impl AsRef<Path>) -> Result<Self, TokensSetupError> { 38 38 let private_key_data: Vec<u8> = 39 39 fs::read(priv_f).map_err(TokensSetupError::ReadPrivateKey)?; 40 40 let encoding_key = 41 41 EncodingKey::from_ec_pem(&private_key_data).map_err(TokensSetupError::PrivateKey)?; 42 42 43 - let jwks_data: Vec<u8> = fs::read(jwks_f).map_err(TokensSetupError::ReadJwks)?; 44 - let jwks = String::from_utf8(jwks_data).map_err(TokensSetupError::DecodeJwks)?; 43 + let jwk_key_string = String::from_utf8(private_key_data).unwrap(); 44 + let mut jwk = SecretKey::<p256::NistP256>::from_pkcs8_pem(&jwk_key_string) 45 + .map(|secret_key| Jwk { 46 + key: Key::from(&secret_key.into()), 47 + prm: Parameters { 48 + kid: Some("who-am-i-00".to_string()), 49 + cls: Some(Class::Signing), 50 + ..Default::default() 51 + }, 52 + }) 53 + .expect("to get private key"); 54 + 55 + // CRITICAL: this is what turns the private jwk into a public one: the 56 + // `d` parameter is the secret for an EC key; a pubkey just has no `d`. 57 + // 58 + // this feels baaaadd but hey we're just copying atrium 59 + // https://github.com/atrium-rs/atrium/blob/b48810f84d83d037ee89b79b8566df9e0f2a6dae/atrium-oauth/src/keyset.rs#L41 60 + let Key::Ec(ref mut ec) = jwk.key else { 61 + unimplemented!() 62 + }; 63 + ec.d = None; // CRITICAL 45 64 46 - Ok(Self { encoding_key, jwks }) 65 + Ok(Self { encoding_key, jwk }) 47 66 } 48 67 49 68 pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> { ··· 62 81 )?) 63 82 } 64 83 65 - pub fn jwks(&self) -> String { 66 - self.jwks.clone() 84 + pub fn jwk(&self) -> Jwk { 85 + self.jwk.clone() 67 86 } 68 87 } 69 88
+1 -16
who-am-i/src/main.rs
··· 31 31 /// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem 32 32 #[arg(long)] 33 33 jwt_private_key: PathBuf, 34 - /// path to pubkeys file (jwks format) 35 - /// 36 - /// get pem of pubkey from private key with: 37 - /// 38 - /// openssl ec -in <PATH-TO-PRIV-KEY>.pem -pubout 39 - /// 40 - /// then convert to a jwk, probably with something less sketchy than an [online tool](https://jwkset.com/generate) 41 - /// 42 - /// wrap the jwk in an array, then in an object under "keys": 43 - /// 44 - /// { "keys": [<JWK obj>] } 45 - /// 46 - /// TODO: remove this, serve automatically 47 - #[arg(long)] 48 - jwks: PathBuf, 49 34 /// this server's client-reachable base url, for oauth redirect + jwt check 50 35 /// 51 36 /// required unless running in localhost mode with --dev ··· 100 85 println!(" - {host}"); 101 86 } 102 87 103 - let tokens = Tokens::from_files(args.jwt_private_key, args.jwks).unwrap(); 88 + let tokens = Tokens::from_files(args.jwt_private_key).unwrap(); 104 89 105 90 if let Err(e) = install_metrics_server() { 106 91 eprintln!("failed to install metrics server: {e:?}");
+4 -11
who-am-i/src/server.rs
··· 99 99 .route("/auth", get(start_oauth)) 100 100 .route("/authorized", get(complete_oauth)) 101 101 .route("/disconnect", post(disconnect)) 102 - .route("/.well-known/at-jwks.json", get(at_jwks)) // todo combine jwks eps (key id is enough?) 103 102 .route("/.well-known/jwks.json", get(jwks)) 104 103 .with_state(state); 105 104 ··· 315 314 Json(oauth.client_metadata()) 316 315 } 317 316 318 - async fn at_jwks(State(AppState { oauth, .. }): State<AppState>) -> Json<JwkSet> { 319 - Json(oauth.jwks()) 320 - } 321 - 322 317 #[derive(Debug, Deserialize)] 323 318 struct BeginOauthParams { 324 319 handle: String, ··· 450 445 (jar, Json(json!({ "ok": true }))) 451 446 } 452 447 453 - async fn jwks(State(AppState { tokens, .. }): State<AppState>) -> impl IntoResponse { 454 - let headers = [ 455 - (CONTENT_TYPE, "application/json"), 456 - // (CACHE_CONTROL, "") // TODO 457 - ]; 458 - (headers, tokens.jwks()) 448 + async fn jwks(State(AppState { oauth, tokens, .. }): State<AppState>) -> Json<JwkSet> { 449 + let mut jwks = oauth.jwks(); 450 + jwks.keys.push(tokens.jwk()); 451 + Json(jwks) 459 452 }