A better Rust ATProto crate
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}