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
···
+
use elliptic_curve::SecretKey;
+
use jose_jwk::{Class, Jwk, Key, Parameters};
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, errors::Error as JWTError};
+
use pkcs8::DecodePrivateKey;
use serde::Serialize;
use std::fs;
use std::io::Error as IOError;
···
pub struct Tokens {
encoding_key: EncodingKey,
-
jwks: String,
+
jwk: Jwk,
}
impl Tokens {
-
pub fn from_files(
-
priv_f: impl AsRef<Path>,
-
jwks_f: impl AsRef<Path>,
-
) -> Result<Self, TokensSetupError> {
+
pub fn from_files(priv_f: impl AsRef<Path>) -> Result<Self, TokensSetupError> {
let private_key_data: Vec<u8> =
fs::read(priv_f).map_err(TokensSetupError::ReadPrivateKey)?;
let encoding_key =
EncodingKey::from_ec_pem(&private_key_data).map_err(TokensSetupError::PrivateKey)?;
-
let jwks_data: Vec<u8> = fs::read(jwks_f).map_err(TokensSetupError::ReadJwks)?;
-
let jwks = String::from_utf8(jwks_data).map_err(TokensSetupError::DecodeJwks)?;
+
let jwk_key_string = String::from_utf8(private_key_data).unwrap();
+
let mut jwk = SecretKey::<p256::NistP256>::from_pkcs8_pem(&jwk_key_string)
+
.map(|secret_key| Jwk {
+
key: Key::from(&secret_key.into()),
+
prm: Parameters {
+
kid: Some("who-am-i-00".to_string()),
+
cls: Some(Class::Signing),
+
..Default::default()
+
},
+
})
+
.expect("to get private key");
+
+
// CRITICAL: this is what turns the private jwk into a public one: the
+
// `d` parameter is the secret for an EC key; a pubkey just has no `d`.
+
//
+
// this feels baaaadd but hey we're just copying atrium
+
// https://github.com/atrium-rs/atrium/blob/b48810f84d83d037ee89b79b8566df9e0f2a6dae/atrium-oauth/src/keyset.rs#L41
+
let Key::Ec(ref mut ec) = jwk.key else {
+
unimplemented!()
+
};
+
ec.d = None; // CRITICAL
-
Ok(Self { encoding_key, jwks })
+
Ok(Self { encoding_key, jwk })
}
pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> {
···
)?)
}
-
pub fn jwks(&self) -> String {
-
self.jwks.clone()
+
pub fn jwk(&self) -> Jwk {
+
self.jwk.clone()
}
}
+1 -16
who-am-i/src/main.rs
···
/// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem
#[arg(long)]
jwt_private_key: PathBuf,
-
/// path to pubkeys file (jwks format)
-
///
-
/// get pem of pubkey from private key with:
-
///
-
/// openssl ec -in <PATH-TO-PRIV-KEY>.pem -pubout
-
///
-
/// then convert to a jwk, probably with something less sketchy than an [online tool](https://jwkset.com/generate)
-
///
-
/// wrap the jwk in an array, then in an object under "keys":
-
///
-
/// { "keys": [<JWK obj>] }
-
///
-
/// TODO: remove this, serve automatically
-
#[arg(long)]
-
jwks: PathBuf,
/// this server's client-reachable base url, for oauth redirect + jwt check
///
/// required unless running in localhost mode with --dev
···
println!(" - {host}");
}
-
let tokens = Tokens::from_files(args.jwt_private_key, args.jwks).unwrap();
+
let tokens = Tokens::from_files(args.jwt_private_key).unwrap();
if let Err(e) = install_metrics_server() {
eprintln!("failed to install metrics server: {e:?}");
+4 -11
who-am-i/src/server.rs
···
.route("/auth", get(start_oauth))
.route("/authorized", get(complete_oauth))
.route("/disconnect", post(disconnect))
-
.route("/.well-known/at-jwks.json", get(at_jwks)) // todo combine jwks eps (key id is enough?)
.route("/.well-known/jwks.json", get(jwks))
.with_state(state);
···
Json(oauth.client_metadata())
}
-
async fn at_jwks(State(AppState { oauth, .. }): State<AppState>) -> Json<JwkSet> {
-
Json(oauth.jwks())
-
}
-
#[derive(Debug, Deserialize)]
struct BeginOauthParams {
handle: String,
···
(jar, Json(json!({ "ok": true })))
}
-
async fn jwks(State(AppState { tokens, .. }): State<AppState>) -> impl IntoResponse {
-
let headers = [
-
(CONTENT_TYPE, "application/json"),
-
// (CACHE_CONTROL, "") // TODO
-
];
-
(headers, tokens.jwks())
+
async fn jwks(State(AppState { oauth, tokens, .. }): State<AppState>) -> Json<JwkSet> {
+
let mut jwks = oauth.jwks();
+
jwks.keys.push(tokens.jwk());
+
Json(jwks)
}