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