A better Rust ATProto crate
1use crate::jose::jws::RegisteredHeader;
2use crate::jose::jwt::Claims;
3use crate::jose::signing;
4use jacquard_common::CowStr;
5use jose_jwa::{Algorithm, Signing};
6use jose_jwk::{Class, EcCurves, OkpCurves, crypto};
7use jose_jwk::{Jwk, JwkSet, Key};
8use std::collections::HashSet;
9use thiserror::Error;
10
11/// Errors that can occur when constructing or using a [`Keyset`].
12#[derive(Error, Debug)]
13#[non_exhaustive]
14pub enum Error {
15 /// Two keys in the set share the same `kid`, which would make key selection ambiguous.
16 #[error("duplicate kid: {0}")]
17 DuplicateKid(String),
18 /// A keyset with no keys cannot sign anything.
19 #[error("keys must not be empty")]
20 EmptyKeys,
21 /// Each key must carry a `kid` so it can be referenced in JWS headers.
22 #[error("key at index {0} must have a `kid`")]
23 EmptyKid(usize),
24 /// No key in the set matches any of the requested signing algorithms.
25 #[error("no signing key found for algorithms: {0:?}")]
26 NotFound(Vec<Signing>),
27 /// Only secret (private) keys may be used for signing; a public key was provided.
28 #[error("key for signing must be a secret key")]
29 PublicKey,
30 /// The key type or curve is not supported for signing.
31 #[error("unsupported key type for signing")]
32 UnsupportedKey,
33 /// The private key (`d` parameter) is missing from the JWK.
34 #[error("missing private key material")]
35 MissingPrivateKey,
36 /// An error from the underlying JWK cryptographic operation.
37 #[error("crypto error: {0:?}")]
38 JwkCrypto(crypto::Error),
39 /// The raw key bytes have an invalid length or format.
40 #[error("invalid key material: {0}")]
41 InvalidKey(String),
42 /// JSON serialization of a JWT header or claims payload failed.
43 #[error(transparent)]
44 SerdeJson(#[from] serde_json::Error),
45}
46
47/// Convenience result type for keyset operations.
48pub type Result<T> = core::result::Result<T, Error>;
49
50/// Signing algorithm preference order for AT Protocol OAuth.
51///
52/// EdDSA and ES256K are preferred for their security properties, followed by
53/// the NIST curves. This order matches common AT Protocol server expectations.
54const PREFERRED_SIGNING_ALGORITHMS: [Signing; 4] = [
55 Signing::EdDsa,
56 Signing::Es256K,
57 Signing::Es256,
58 Signing::Es384,
59];
60
61/// A validated collection of JWK secret keys used for signing DPoP proofs and client assertions.
62///
63/// Key selection follows [`PREFERRED_SIGNING_ALGORITHMS`] when multiple keys match.
64/// Supported algorithms: EdDSA (Ed25519), ES256K (secp256k1), ES256 (P-256), ES384 (P-384).
65#[derive(Clone, Debug, Default, PartialEq, Eq)]
66pub struct Keyset(Vec<Jwk>);
67
68impl Keyset {
69 /// Returns a [`JwkSet`] containing the public halves of all keys in this keyset.
70 pub fn public_jwks(&self) -> JwkSet {
71 let mut keys = Vec::with_capacity(self.0.len());
72 for mut key in self.0.clone() {
73 match key.key {
74 Key::Ec(ref mut ec) => {
75 ec.d = None;
76 }
77 Key::Okp(ref mut okp) => {
78 okp.d = None;
79 }
80 _ => {}
81 }
82 keys.push(key);
83 }
84 JwkSet { keys }
85 }
86
87 /// Signs a JWT with the best available key that matches one of the requested algorithms.
88 ///
89 /// Returns [`Error::NotFound`] if no key in the keyset supports any of the given algorithms.
90 pub fn create_jwt(&self, algs: &[Signing], claims: Claims) -> Result<CowStr<'static>> {
91 let Some(jwk) = self.find_key(algs, Class::Signing) else {
92 return Err(Error::NotFound(algs.to_vec()));
93 };
94 self.create_jwt_with_key(jwk, claims)
95 }
96
97 fn find_key(&self, algs: &[Signing], cls: Class) -> Option<&Jwk> {
98 let candidates = self
99 .0
100 .iter()
101 .filter_map(|key| {
102 if key.prm.cls.is_some_and(|c| c != cls) {
103 return None;
104 }
105 let alg = alg_for_key(&key.key)?;
106 Some((alg, key)).filter(|(alg, _)| algs.contains(alg))
107 })
108 .collect::<Vec<_>>();
109 for pref_alg in PREFERRED_SIGNING_ALGORITHMS {
110 for (alg, key) in &candidates {
111 if *alg == pref_alg {
112 return Some(key);
113 }
114 }
115 }
116 None
117 }
118
119 fn create_jwt_with_key(&self, key: &Jwk, claims: Claims) -> Result<CowStr<'static>> {
120 let kid = key.prm.kid.clone().unwrap();
121 match &key.key {
122 Key::Ec(ec) => {
123 let d = ec.d.as_ref().ok_or(Error::MissingPrivateKey)?;
124 let d_bytes: &[u8] = d.as_ref();
125 match ec.crv {
126 EcCurves::P256 => {
127 let signing_key = p256::ecdsa::SigningKey::from_bytes(d_bytes.into())
128 .map_err(|e| Error::InvalidKey(e.to_string()))?;
129 let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::Es256));
130 header.kid = Some(kid.into());
131 Ok(signing::create_signed_jwt_es256(
132 signing_key,
133 header.into(),
134 claims,
135 )?)
136 }
137 EcCurves::P384 => {
138 let signing_key = p384::ecdsa::SigningKey::from_bytes(d_bytes.into())
139 .map_err(|e| Error::InvalidKey(e.to_string()))?;
140 let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::Es384));
141 header.kid = Some(kid.into());
142 Ok(signing::create_signed_jwt_es384(
143 signing_key,
144 header.into(),
145 claims,
146 )?)
147 }
148 EcCurves::P256K => {
149 let signing_key = k256::ecdsa::SigningKey::from_bytes(d_bytes.into())
150 .map_err(|e| Error::InvalidKey(e.to_string()))?;
151 let mut header =
152 RegisteredHeader::from(Algorithm::Signing(Signing::Es256K));
153 header.kid = Some(kid.into());
154 Ok(signing::create_signed_jwt_es256k(
155 signing_key,
156 header.into(),
157 claims,
158 )?)
159 }
160 _ => Err(Error::UnsupportedKey),
161 }
162 }
163 Key::Okp(okp) => match okp.crv {
164 OkpCurves::Ed25519 => {
165 let d = okp.d.as_ref().ok_or(Error::MissingPrivateKey)?;
166 let d_bytes: &[u8] = d.as_ref();
167 let signing_key = ed25519_dalek::SigningKey::try_from(d_bytes)
168 .map_err(|e| Error::InvalidKey(e.to_string()))?;
169 let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::EdDsa));
170 header.kid = Some(kid.into());
171 Ok(signing::create_signed_jwt_eddsa(
172 signing_key,
173 header.into(),
174 claims,
175 )?)
176 }
177 _ => Err(Error::UnsupportedKey),
178 },
179 _ => Err(Error::UnsupportedKey),
180 }
181 }
182}
183
184/// Returns the signing algorithm for the given JWK key type, if supported.
185fn alg_for_key(key: &Key) -> Option<Signing> {
186 match key {
187 Key::Ec(ec) => match ec.crv {
188 EcCurves::P256 => Some(Signing::Es256),
189 EcCurves::P384 => Some(Signing::Es384),
190 EcCurves::P256K => Some(Signing::Es256K),
191 _ => None,
192 },
193 Key::Okp(okp) => match okp.crv {
194 OkpCurves::Ed25519 => Some(Signing::EdDsa),
195 _ => None,
196 },
197 _ => None,
198 }
199}
200
201/// Parses a string-based algorithm name into a [`Signing`] variant, if it maps to
202/// an algorithm this crate supports.
203pub fn parse_signing_alg(s: &str) -> Option<Signing> {
204 match s {
205 "ES256" => Some(Signing::Es256),
206 "ES384" => Some(Signing::Es384),
207 "ES256K" => Some(Signing::Es256K),
208 "EdDSA" => Some(Signing::EdDsa),
209 _ => None,
210 }
211}
212
213impl TryFrom<Vec<Jwk>> for Keyset {
214 type Error = Error;
215
216 fn try_from(keys: Vec<Jwk>) -> Result<Self> {
217 if keys.is_empty() {
218 return Err(Error::EmptyKeys);
219 }
220 let mut v = Vec::with_capacity(keys.len());
221 let mut hs = HashSet::with_capacity(keys.len());
222 for (i, key) in keys.into_iter().enumerate() {
223 if let Some(kid) = key.prm.kid.clone() {
224 if hs.contains(&kid) {
225 return Err(Error::DuplicateKid(kid));
226 }
227 hs.insert(kid);
228
229 // Validate that the key has private material and is a supported type.
230 match &key.key {
231 Key::Ec(ec) => {
232 if ec.d.is_none() {
233 return Err(Error::PublicKey);
234 }
235 if alg_for_key(&key.key).is_none() {
236 return Err(Error::UnsupportedKey);
237 }
238 }
239 Key::Okp(okp) => {
240 if okp.d.is_none() {
241 return Err(Error::PublicKey);
242 }
243 if alg_for_key(&key.key).is_none() {
244 return Err(Error::UnsupportedKey);
245 }
246 }
247 _ => return Err(Error::UnsupportedKey),
248 }
249
250 v.push(key);
251 } else {
252 return Err(Error::EmptyKid(i));
253 }
254 }
255 Ok(Self(v))
256 }
257}