A better Rust ATProto crate
1

Configure Feed

Select the types of activity you want to include in your feed.

at main 4.4 kB View raw
1use base64::Engine; 2use base64::engine::general_purpose::URL_SAFE_NO_PAD; 3use elliptic_curve::SecretKey; 4use jacquard_common::BosStr; 5use jose_jwk::{Key, crypto}; 6use rand::{CryptoRng, RngCore, rngs::ThreadRng}; 7use sha2::{Digest, Sha256}; 8use smol_str::SmolStr; 9use std::cmp::Ordering; 10 11use crate::{FALLBACK_ALG, types::OAuthAuthorizationServerMetadata}; 12 13/// Generate a fresh JWK secret key using the first algorithm from `allowed_algos` that is 14/// supported, returning `None` if none are supported. 15/// 16/// Currently only `ES256` (P-256 ECDSA) is implemented; other algorithm identifiers are skipped. 17pub fn generate_key(allowed_algos: &[impl AsRef<str>]) -> Option<Key> { 18 for alg in allowed_algos { 19 #[allow(clippy::single_match)] 20 match alg.as_ref() { 21 "ES256" => { 22 return Some(Key::from(&crypto::Key::from( 23 SecretKey::<p256::NistP256>::random(&mut ThreadRng::default()), 24 ))); 25 } 26 _ => { 27 // TODO: Implement other algorithms? 28 } 29 } 30 } 31 None 32} 33 34/// Generate a cryptographically random 16-byte nonce encoded as base64url (no padding). 35pub fn generate_nonce() -> SmolStr { 36 URL_SAFE_NO_PAD 37 .encode(get_random_values::<_, 16>(&mut ThreadRng::default())) 38 .into() 39} 40 41/// Generate a cryptographically random 43-byte PKCE code verifier encoded as base64url (no padding). 42pub fn generate_verifier() -> SmolStr { 43 URL_SAFE_NO_PAD 44 .encode(get_random_values::<_, 43>(&mut ThreadRng::default())) 45 .into() 46} 47 48/// Fill a `LEN`-byte array with cryptographically random bytes from `rng`. 49pub fn get_random_values<R, const LEN: usize>(rng: &mut R) -> [u8; LEN] 50where 51 R: RngCore + CryptoRng, 52{ 53 let mut bytes = [0u8; LEN]; 54 rng.fill_bytes(&mut bytes); 55 bytes 56} 57 58/// Compare two algorithm identifier strings by preference order for DPoP key generation. 59/// 60/// The ordering is: ES256K > ES (256 > 384 > 512) > PS (256 > 384 > 512) > RS (256 > 384 > 512) > other. 61/// Algorithms within the same family are ordered by key length, preferring shorter (faster) keys first. 62pub fn compare_algos(a: &impl AsRef<str>, b: &impl AsRef<str>) -> Ordering { 63 let a = a.as_ref(); 64 let b = b.as_ref(); 65 if a == "ES256K" { 66 return Ordering::Less; 67 } 68 if b == "ES256K" { 69 return Ordering::Greater; 70 } 71 for prefix in ["ES", "PS", "RS"] { 72 if let Some(stripped_a) = a.strip_prefix(prefix) { 73 if let Some(stripped_b) = b.strip_prefix(prefix) { 74 if let (Ok(len_a), Ok(len_b)) = 75 (stripped_a.parse::<u32>(), stripped_b.parse::<u32>()) 76 { 77 return len_a.cmp(&len_b); 78 } 79 } else { 80 return Ordering::Less; 81 } 82 } else if b.starts_with(prefix) { 83 return Ordering::Greater; 84 } 85 } 86 Ordering::Equal 87} 88 89/// Generate a PKCE challenge/verifier pair. 90/// 91/// Returns `(challenge, verifier)` where `challenge` is the base64url-encoded SHA-256 hash 92/// of the verifier, per [RFC 7636 §4.1](https://datatracker.ietf.org/doc/html/rfc7636#section-4.1). 93/// The verifier must be kept secret and sent at the token endpoint; the challenge is sent at 94/// the authorization endpoint. 95pub fn generate_pkce() -> (SmolStr, SmolStr) { 96 // https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 97 let verifier = generate_verifier(); 98 ( 99 URL_SAFE_NO_PAD 100 .encode(Sha256::digest(verifier.as_str())) 101 .into(), 102 verifier, 103 ) 104} 105 106/// Generate a DPoP signing key compatible with the algorithms advertised by the authorization server. 107/// 108/// Reads `dpop_signing_alg_values_supported` from the server metadata, sorts by preference 109/// using [`compare_algos`], and attempts to generate a key for the most preferred supported 110/// algorithm. Falls back to [`crate::FALLBACK_ALG`] if the server does not advertise any algorithms. 111pub fn generate_dpop_key<S: BosStr>( 112 metadata: &mut OAuthAuthorizationServerMetadata<S>, 113) -> Option<Key> { 114 let mut fallback = vec![S::from_static(FALLBACK_ALG)]; 115 let algs = metadata 116 .dpop_signing_alg_values_supported 117 .as_deref_mut() 118 .unwrap_or(&mut fallback); 119 algs.sort_by(compare_algos); 120 generate_key(&algs) 121}