forked from
microcosm.blue/microcosm-rs
Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm
1use elliptic_curve::SecretKey;
2use jose_jwk::{Class, Jwk, Key, Parameters};
3use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, errors::Error as JWTError};
4use pkcs8::DecodePrivateKey;
5use serde::Serialize;
6use std::fs;
7use std::io::Error as IOError;
8use std::path::Path;
9use std::string::FromUtf8Error;
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11use thiserror::Error;
12
13#[derive(Debug, Error)]
14pub enum TokensSetupError {
15 #[error("failed to read private key")]
16 ReadPrivateKey(IOError),
17 #[error("failed to retrieve private key: {0}")]
18 PrivateKey(JWTError),
19 #[error("failed to read private key")]
20 ReadJwks(IOError),
21 #[error("failed to retrieve jwks: {0}")]
22 DecodeJwks(FromUtf8Error),
23}
24
25#[derive(Debug, Error)]
26pub enum TokenMintingError {
27 #[error("failed to mint: {0}")]
28 EncodingError(#[from] JWTError),
29}
30
31pub struct Tokens {
32 encoding_key: EncodingKey,
33 jwk: Jwk,
34}
35
36impl 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> {
69 let sub = t.to_string();
70
71 let dt_now = SystemTime::now()
72 .duration_since(UNIX_EPOCH)
73 .expect("unix epoch is in the past");
74 let dt_exp = dt_now + Duration::from_secs(30 * 86_400);
75 let exp = dt_exp.as_secs();
76
77 let mut header = Header::new(Algorithm::ES256);
78 header.kid = Some("who-am-i-00".to_string());
79 // todo: consider setting jku?
80
81 Ok(encode(&header, &Claims { sub, exp }, &self.encoding_key)?)
82 }
83
84 pub fn jwk(&self) -> Jwk {
85 self.jwk.clone()
86 }
87}
88
89#[derive(Debug, Serialize)]
90struct Claims {
91 sub: String,
92 exp: u64,
93}