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 jsonwebtoken::{Algorithm, EncodingKey, Header, encode, errors::Error as JWTError}; 2 use serde::Serialize; 3 use std::fs; 4 use std::io::Error as IOError; ··· 27 28 pub struct Tokens { 29 encoding_key: EncodingKey, 30 - jwks: String, 31 } 32 33 impl Tokens { 34 - pub fn from_files( 35 - priv_f: impl AsRef<Path>, 36 - jwks_f: impl AsRef<Path>, 37 - ) -> Result<Self, TokensSetupError> { 38 let private_key_data: Vec<u8> = 39 fs::read(priv_f).map_err(TokensSetupError::ReadPrivateKey)?; 40 let encoding_key = 41 EncodingKey::from_ec_pem(&private_key_data).map_err(TokensSetupError::PrivateKey)?; 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)?; 45 46 - Ok(Self { encoding_key, jwks }) 47 } 48 49 pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> { ··· 62 )?) 63 } 64 65 - pub fn jwks(&self) -> String { 66 - self.jwks.clone() 67 } 68 } 69
··· 1 + use elliptic_curve::SecretKey; 2 + use jose_jwk::{Class, Jwk, Key, Parameters}; 3 use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, errors::Error as JWTError}; 4 + use pkcs8::DecodePrivateKey; 5 use serde::Serialize; 6 use std::fs; 7 use std::io::Error as IOError; ··· 30 31 pub struct Tokens { 32 encoding_key: EncodingKey, 33 + jwk: Jwk, 34 } 35 36 impl Tokens { 37 + pub fn from_files(priv_f: impl AsRef<Path>) -> Result<Self, TokensSetupError> { 38 let private_key_data: Vec<u8> = 39 fs::read(priv_f).map_err(TokensSetupError::ReadPrivateKey)?; 40 let encoding_key = 41 EncodingKey::from_ec_pem(&private_key_data).map_err(TokensSetupError::PrivateKey)?; 42 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 64 65 + Ok(Self { encoding_key, jwk }) 66 } 67 68 pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> { ··· 81 )?) 82 } 83 84 + pub fn jwk(&self) -> Jwk { 85 + self.jwk.clone() 86 } 87 } 88
+1 -16
who-am-i/src/main.rs
··· 31 /// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem 32 #[arg(long)] 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 /// this server's client-reachable base url, for oauth redirect + jwt check 50 /// 51 /// required unless running in localhost mode with --dev ··· 100 println!(" - {host}"); 101 } 102 103 - let tokens = Tokens::from_files(args.jwt_private_key, args.jwks).unwrap(); 104 105 if let Err(e) = install_metrics_server() { 106 eprintln!("failed to install metrics server: {e:?}");
··· 31 /// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem 32 #[arg(long)] 33 jwt_private_key: PathBuf, 34 /// this server's client-reachable base url, for oauth redirect + jwt check 35 /// 36 /// required unless running in localhost mode with --dev ··· 85 println!(" - {host}"); 86 } 87 88 + let tokens = Tokens::from_files(args.jwt_private_key).unwrap(); 89 90 if let Err(e) = install_metrics_server() { 91 eprintln!("failed to install metrics server: {e:?}");
+4 -11
who-am-i/src/server.rs
··· 99 .route("/auth", get(start_oauth)) 100 .route("/authorized", get(complete_oauth)) 101 .route("/disconnect", post(disconnect)) 102 - .route("/.well-known/at-jwks.json", get(at_jwks)) // todo combine jwks eps (key id is enough?) 103 .route("/.well-known/jwks.json", get(jwks)) 104 .with_state(state); 105 ··· 315 Json(oauth.client_metadata()) 316 } 317 318 - async fn at_jwks(State(AppState { oauth, .. }): State<AppState>) -> Json<JwkSet> { 319 - Json(oauth.jwks()) 320 - } 321 - 322 #[derive(Debug, Deserialize)] 323 struct BeginOauthParams { 324 handle: String, ··· 450 (jar, Json(json!({ "ok": true }))) 451 } 452 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()) 459 }
··· 99 .route("/auth", get(start_oauth)) 100 .route("/authorized", get(complete_oauth)) 101 .route("/disconnect", post(disconnect)) 102 .route("/.well-known/jwks.json", get(jwks)) 103 .with_state(state); 104 ··· 314 Json(oauth.client_metadata()) 315 } 316 317 #[derive(Debug, Deserialize)] 318 struct BeginOauthParams { 319 handle: String, ··· 445 (jar, Json(json!({ "ok": true }))) 446 } 447 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) 452 }