···3434- `knownValues` generation now aligned with AT Protocol spec and triggers more frequently
3535- Improved feature dependency tracking for API crate features
36363737+**Additional signing algorithms** (`jacquard-oauth`)
3838+- Keyset signing now supports ES384 (P-384), ES256K (secp256k1), and EdDSA (Ed25519) in addition to ES256
3939+- `Keyset::create_jwt` now accepts `&[Signing]` (from `jose_jwa`) instead of string-based algorithm names
4040+4141+**Documentation** (`jacquard-oauth`, `jacquard-identity`)
4242+- Doc comments across all public items in both crates (thanks Claude, but I played editor pretty heavily)
4343+3744### Fixed
38453946**Identity resolution** (`jacquard-identity`)
···22pub mod jws;
33/// JWT (JSON Web Token) claims types.
44pub mod jwt;
55-/// Signed JWT creation using ES256 keys.
55+/// Signed JWT creation for supported algorithms (ES256, ES384, ES256K, EdDSA).
66pub mod signing;
7788use serde::{Deserialize, Serialize};
···1818 Jws(jws::Header<'a>),
1919}
20202121-pub use self::signing::create_signed_jwt;
+55-12
crates/jacquard-oauth/src/jose/signing.rs
···11use base64::Engine;
22use base64::engine::general_purpose::URL_SAFE_NO_PAD;
33use jacquard_common::CowStr;
44-use p256::ecdsa::{Signature, SigningKey, signature::Signer};
5465use super::{Header, jwt::Claims};
7688-/// Creates a compact-serialized signed JWT using an ES256 (P-256 ECDSA) key.
99-pub fn create_signed_jwt(
1010- key: SigningKey,
77+/// Builds the base64url-encoded `header.payload` signing input.
88+fn signing_input(header: &Header, claims: &Claims) -> serde_json::Result<(String, String)> {
99+ let h = URL_SAFE_NO_PAD.encode(serde_json::to_string(header)?);
1010+ let p = URL_SAFE_NO_PAD.encode(serde_json::to_string(claims)?);
1111+ Ok((h, p))
1212+}
1313+1414+/// Assembles a compact JWS from pre-encoded parts and raw signature bytes.
1515+fn assemble(header: &str, payload: &str, sig: &[u8]) -> CowStr<'static> {
1616+ format!("{header}.{payload}.{}", URL_SAFE_NO_PAD.encode(sig)).into()
1717+}
1818+1919+/// Creates a compact-serialized signed JWT using ES256 (P-256 ECDSA with SHA-256).
2020+pub fn create_signed_jwt_es256(
2121+ key: p256::ecdsa::SigningKey,
2222+ header: Header,
2323+ claims: Claims,
2424+) -> serde_json::Result<CowStr<'static>> {
2525+ use p256::ecdsa::signature::Signer;
2626+ let (h, p) = signing_input(&header, &claims)?;
2727+ let sig: p256::ecdsa::Signature = key.sign(format!("{h}.{p}").as_bytes());
2828+ Ok(assemble(&h, &p, &sig.to_bytes()))
2929+}
3030+3131+/// Creates a compact-serialized signed JWT using ES384 (P-384 ECDSA with SHA-384).
3232+pub fn create_signed_jwt_es384(
3333+ key: p384::ecdsa::SigningKey,
3434+ header: Header,
3535+ claims: Claims,
3636+) -> serde_json::Result<CowStr<'static>> {
3737+ use p384::ecdsa::signature::Signer;
3838+ let (h, p) = signing_input(&header, &claims)?;
3939+ let sig: p384::ecdsa::Signature = key.sign(format!("{h}.{p}").as_bytes());
4040+ Ok(assemble(&h, &p, &sig.to_bytes()))
4141+}
4242+4343+/// Creates a compact-serialized signed JWT using ES256K (secp256k1 ECDSA with SHA-256).
4444+pub fn create_signed_jwt_es256k(
4545+ key: k256::ecdsa::SigningKey,
4646+ header: Header,
4747+ claims: Claims,
4848+) -> serde_json::Result<CowStr<'static>> {
4949+ use k256::ecdsa::signature::Signer;
5050+ let (h, p) = signing_input(&header, &claims)?;
5151+ let sig: k256::ecdsa::Signature = key.sign(format!("{h}.{p}").as_bytes());
5252+ Ok(assemble(&h, &p, &sig.to_bytes()))
5353+}
5454+5555+/// Creates a compact-serialized signed JWT using EdDSA (Ed25519).
5656+pub fn create_signed_jwt_eddsa(
5757+ key: ed25519_dalek::SigningKey,
1158 header: Header,
1259 claims: Claims,
1360) -> serde_json::Result<CowStr<'static>> {
1414- let header = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header)?);
1515- let payload = URL_SAFE_NO_PAD.encode(serde_json::to_string(&claims)?);
1616- let signature: Signature = key.sign(format!("{header}.{payload}").as_bytes());
1717- Ok(format!(
1818- "{header}.{payload}.{}",
1919- URL_SAFE_NO_PAD.encode(signature.to_bytes())
2020- )
2121- .into())
6161+ use ed25519_dalek::Signer;
6262+ let (h, p) = signing_input(&header, &claims)?;
6363+ let sig = key.sign(format!("{h}.{p}").as_bytes());
6464+ Ok(assemble(&h, &p, &sig.to_bytes()))
2265}
+148-36
crates/jacquard-oauth/src/keyset.rs
···11-use crate::jose::create_signed_jwt;
21use crate::jose::jws::RegisteredHeader;
32use crate::jose::jwt::Claims;
44-use jacquard_common::{CowStr, IntoStatic};
33+use crate::jose::signing;
44+use jacquard_common::CowStr;
55use jose_jwa::{Algorithm, Signing};
66-use jose_jwk::{Class, EcCurves, crypto};
66+use jose_jwk::{Class, EcCurves, OkpCurves, crypto};
77use jose_jwk::{Jwk, JwkSet, Key};
88use std::collections::HashSet;
99use thiserror::Error;
···2323 EmptyKid(usize),
2424 /// No key in the set matches any of the requested signing algorithms.
2525 #[error("no signing key found for algorithms: {0:?}")]
2626- NotFound(Vec<CowStr<'static>>),
2626+ NotFound(Vec<Signing>),
2727 /// Only secret (private) keys may be used for signing; a public key was provided.
2828 #[error("key for signing must be a secret key")]
2929 PublicKey,
3030+ /// The key type or curve is not supported for signing.
3131+ #[error("unsupported key type for signing")]
3232+ UnsupportedKey,
3333+ /// The private key (`d` parameter) is missing from the JWK.
3434+ #[error("missing private key material")]
3535+ MissingPrivateKey,
3036 /// An error from the underlying JWK cryptographic operation.
3137 #[error("crypto error: {0:?}")]
3238 JwkCrypto(crypto::Error),
3939+ /// The raw key bytes have an invalid length or format.
4040+ #[error("invalid key material: {0}")]
4141+ InvalidKey(String),
3342 /// JSON serialization of a JWT header or claims payload failed.
3443 #[error(transparent)]
3544 SerdeJson(#[from] serde_json::Error),
···3847/// Convenience result type for keyset operations.
3948pub type Result<T> = core::result::Result<T, Error>;
40495050+/// Signing algorithm preference order for AT Protocol OAuth.
5151+///
5252+/// EdDSA and ES256K are preferred for their security properties, followed by
5353+/// the NIST curves. This order matches common AT Protocol server expectations.
5454+const PREFERRED_SIGNING_ALGORITHMS: [Signing; 4] = [
5555+ Signing::EdDsa,
5656+ Signing::Es256K,
5757+ Signing::Es256,
5858+ Signing::Es384,
5959+];
6060+4161/// A validated collection of JWK secret keys used for signing DPoP proofs and client assertions.
4262///
4343-/// Key selection follows a preference order defined in [`PREFERRED_SIGNING_ALGORITHMS`](Self::PREFERRED_SIGNING_ALGORITHMS),
4444-/// though currently only P-256 (ES256) keys are supported.
6363+/// Key selection follows [`PREFERRED_SIGNING_ALGORITHMS`] when multiple keys match.
6464+/// Supported algorithms: EdDSA (Ed25519), ES256K (secp256k1), ES256 (P-256), ES384 (P-384).
4565#[derive(Clone, Debug, Default, PartialEq, Eq)]
4666pub struct Keyset(Vec<Jwk>);
47674868impl Keyset {
4949- const PREFERRED_SIGNING_ALGORITHMS: [&'static str; 9] = [
5050- "EdDSA", "ES256K", "ES256", "PS256", "PS384", "PS512", "HS256", "HS384", "HS512",
5151- ];
5269 /// Returns a [`JwkSet`] containing the public halves of all keys in this keyset.
5370 pub fn public_jwks(&self) -> JwkSet {
5471 let mut keys = Vec::with_capacity(self.0.len());
···5774 Key::Ec(ref mut ec) => {
5875 ec.d = None;
5976 }
6060- _ => unimplemented!(),
7777+ Key::Okp(ref mut okp) => {
7878+ okp.d = None;
7979+ }
8080+ _ => {}
6181 }
6282 keys.push(key);
6383 }
6484 JwkSet { keys }
6585 }
8686+6687 /// Signs a JWT with the best available key that matches one of the requested algorithms.
6788 ///
6889 /// Returns [`Error::NotFound`] if no key in the keyset supports any of the given algorithms.
6969- pub fn create_jwt(&self, algs: &[CowStr], claims: Claims) -> Result<CowStr<'static>> {
9090+ pub fn create_jwt(&self, algs: &[Signing], claims: Claims) -> Result<CowStr<'static>> {
7091 let Some(jwk) = self.find_key(algs, Class::Signing) else {
7171- return Err(Error::NotFound(algs.to_vec().into_static()));
9292+ return Err(Error::NotFound(algs.to_vec()));
7293 };
7394 self.create_jwt_with_key(jwk, claims)
7495 }
7575- fn find_key(&self, algs: &[CowStr], cls: Class) -> Option<&Jwk> {
9696+9797+ fn find_key(&self, algs: &[Signing], cls: Class) -> Option<&Jwk> {
7698 let candidates = self
7799 .0
78100 .iter()
···80102 if key.prm.cls.is_some_and(|c| c != cls) {
81103 return None;
82104 }
8383- let alg = match &key.key {
8484- Key::Ec(ec) => match ec.crv {
8585- EcCurves::P256 => "ES256",
8686- _ => unimplemented!(),
8787- },
8888- _ => unimplemented!(),
8989- };
9090- Some((alg, key)).filter(|(alg, _)| algs.contains(&CowStr::Borrowed(&alg)))
105105+ let alg = alg_for_key(&key.key)?;
106106+ Some((alg, key)).filter(|(alg, _)| algs.contains(alg))
91107 })
92108 .collect::<Vec<_>>();
9393- for pref_alg in Self::PREFERRED_SIGNING_ALGORITHMS {
109109+ for pref_alg in PREFERRED_SIGNING_ALGORITHMS {
94110 for (alg, key) in &candidates {
9595- if alg == &pref_alg {
111111+ if *alg == pref_alg {
96112 return Some(key);
97113 }
98114 }
99115 }
100116 None
101117 }
118118+102119 fn create_jwt_with_key(&self, key: &Jwk, claims: Claims) -> Result<CowStr<'static>> {
103120 let kid = key.prm.kid.clone().unwrap();
104104- match crypto::Key::try_from(&key.key).map_err(Error::JwkCrypto)? {
105105- crypto::Key::P256(crypto::Kind::Secret(secret_key)) => {
106106- let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::Es256));
107107- header.kid = Some(kid.into());
108108- Ok(create_signed_jwt(secret_key.into(), header.into(), claims)?)
121121+ match &key.key {
122122+ Key::Ec(ec) => {
123123+ let d = ec.d.as_ref().ok_or(Error::MissingPrivateKey)?;
124124+ let d_bytes: &[u8] = d.as_ref();
125125+ match ec.crv {
126126+ EcCurves::P256 => {
127127+ let signing_key = p256::ecdsa::SigningKey::from_bytes(d_bytes.into())
128128+ .map_err(|e| Error::InvalidKey(e.to_string()))?;
129129+ let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::Es256));
130130+ header.kid = Some(kid.into());
131131+ Ok(signing::create_signed_jwt_es256(
132132+ signing_key,
133133+ header.into(),
134134+ claims,
135135+ )?)
136136+ }
137137+ EcCurves::P384 => {
138138+ let signing_key = p384::ecdsa::SigningKey::from_bytes(d_bytes.into())
139139+ .map_err(|e| Error::InvalidKey(e.to_string()))?;
140140+ let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::Es384));
141141+ header.kid = Some(kid.into());
142142+ Ok(signing::create_signed_jwt_es384(
143143+ signing_key,
144144+ header.into(),
145145+ claims,
146146+ )?)
147147+ }
148148+ EcCurves::P256K => {
149149+ let signing_key = k256::ecdsa::SigningKey::from_bytes(d_bytes.into())
150150+ .map_err(|e| Error::InvalidKey(e.to_string()))?;
151151+ let mut header =
152152+ RegisteredHeader::from(Algorithm::Signing(Signing::Es256K));
153153+ header.kid = Some(kid.into());
154154+ Ok(signing::create_signed_jwt_es256k(
155155+ signing_key,
156156+ header.into(),
157157+ claims,
158158+ )?)
159159+ }
160160+ _ => Err(Error::UnsupportedKey),
161161+ }
109162 }
110110- _ => unimplemented!(),
163163+ Key::Okp(okp) => match okp.crv {
164164+ OkpCurves::Ed25519 => {
165165+ let d = okp.d.as_ref().ok_or(Error::MissingPrivateKey)?;
166166+ let d_bytes: &[u8] = d.as_ref();
167167+ let signing_key = ed25519_dalek::SigningKey::try_from(d_bytes)
168168+ .map_err(|e| Error::InvalidKey(e.to_string()))?;
169169+ let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::EdDsa));
170170+ header.kid = Some(kid.into());
171171+ Ok(signing::create_signed_jwt_eddsa(
172172+ signing_key,
173173+ header.into(),
174174+ claims,
175175+ )?)
176176+ }
177177+ _ => Err(Error::UnsupportedKey),
178178+ },
179179+ _ => Err(Error::UnsupportedKey),
111180 }
112181 }
113182}
114183184184+/// Returns the signing algorithm for the given JWK key type, if supported.
185185+fn alg_for_key(key: &Key) -> Option<Signing> {
186186+ match key {
187187+ Key::Ec(ec) => match ec.crv {
188188+ EcCurves::P256 => Some(Signing::Es256),
189189+ EcCurves::P384 => Some(Signing::Es384),
190190+ EcCurves::P256K => Some(Signing::Es256K),
191191+ _ => None,
192192+ },
193193+ Key::Okp(okp) => match okp.crv {
194194+ OkpCurves::Ed25519 => Some(Signing::EdDsa),
195195+ _ => None,
196196+ },
197197+ _ => None,
198198+ }
199199+}
200200+201201+/// Parses a string-based algorithm name into a [`Signing`] variant, if it maps to
202202+/// an algorithm this crate supports.
203203+pub fn parse_signing_alg(s: &str) -> Option<Signing> {
204204+ match s {
205205+ "ES256" => Some(Signing::Es256),
206206+ "ES384" => Some(Signing::Es384),
207207+ "ES256K" => Some(Signing::Es256K),
208208+ "EdDSA" => Some(Signing::EdDsa),
209209+ _ => None,
210210+ }
211211+}
212212+115213impl TryFrom<Vec<Jwk>> for Keyset {
116214 type Error = Error;
117215···127225 return Err(Error::DuplicateKid(kid));
128226 }
129227 hs.insert(kid);
130130- // ensure that the key is a secret key
131131- if match crypto::Key::try_from(&key.key).map_err(Error::JwkCrypto)? {
132132- crypto::Key::P256(crypto::Kind::Public(_)) => true,
133133- crypto::Key::P256(crypto::Kind::Secret(_)) => false,
134134- _ => unimplemented!(),
135135- } {
136136- return Err(Error::PublicKey);
228228+229229+ // Validate that the key has private material and is a supported type.
230230+ match &key.key {
231231+ Key::Ec(ec) => {
232232+ if ec.d.is_none() {
233233+ return Err(Error::PublicKey);
234234+ }
235235+ if alg_for_key(&key.key).is_none() {
236236+ return Err(Error::UnsupportedKey);
237237+ }
238238+ }
239239+ Key::Okp(okp) => {
240240+ if okp.d.is_none() {
241241+ return Err(Error::PublicKey);
242242+ }
243243+ if alg_for_key(&key.key).is_none() {
244244+ return Err(Error::UnsupportedKey);
245245+ }
246246+ }
247247+ _ => return Err(Error::UnsupportedKey),
137248 }
249249+138250 v.push(key);
139251 } else {
140252 return Err(Error::EmptyKid(i));