use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
use crate::jwt::{JwtError, JwtSigner, JwtVerifier};
pub const ALG: &str = "EdDSA";
pub struct EdDsaSigner {
signing_key: SigningKey,
kid: Option<String>,
}
impl EdDsaSigner {
pub fn from_bytes(private_key: &[u8]) -> Result<Self, JwtError> {
let seed: [u8; 32] = private_key
.try_into()
.map_err(|_| JwtError::Signing("Ed25519 private key must be 32 bytes".into()))?;
Ok(Self {
signing_key: SigningKey::from_bytes(&seed),
kid: None,
})
}
pub fn generate() -> Self {
Self::generate_with_rng(&mut rand_core::OsRng)
}
pub fn generate_with_rng<R>(rng: &mut R) -> Self
where
R: rand_core::CryptoRng + rand_core::RngCore,
{
Self {
signing_key: SigningKey::generate(rng),
kid: None,
}
}
pub fn with_kid(mut self, kid: impl Into<String>) -> Self {
self.kid = Some(kid.into());
self
}
pub fn public_key_bytes(&self) -> Vec<u8> {
self.signing_key.verifying_key().to_bytes().to_vec()
}
pub fn public_key_jwk(&self) -> serde_json::Value {
serde_json::json!({
"kty": "OKP",
"crv": "Ed25519",
"x": URL_SAFE_NO_PAD.encode(self.signing_key.verifying_key().to_bytes()),
})
}
}
impl JwtSigner for EdDsaSigner {
fn algorithm(&self) -> &str {
ALG
}
fn key_id(&self) -> Option<&str> {
self.kid.as_deref()
}
fn sign(&self, data: &[u8]) -> Result<Vec<u8>, JwtError> {
let signature: Signature = self.signing_key.sign(data);
Ok(signature.to_bytes().to_vec())
}
}
pub struct EdDsaVerifier {
verifying_key: VerifyingKey,
}
impl EdDsaVerifier {
pub fn from_bytes(public_key: &[u8]) -> Result<Self, JwtError> {
let bytes: [u8; 32] = public_key
.try_into()
.map_err(|_| JwtError::Verification("Ed25519 public key must be 32 bytes".into()))?;
let verifying_key = VerifyingKey::from_bytes(&bytes)
.map_err(|e| JwtError::Verification(format!("invalid Ed25519 public key: {e}")))?;
Ok(Self { verifying_key })
}
pub fn from_jwk(jwk: &serde_json::Value) -> Result<Self, JwtError> {
match jwk.get("kty").and_then(|v| v.as_str()) {
Some("OKP") => {}
Some(other) => {
return Err(JwtError::Verification(format!(
"expected JWK kty OKP, got {other}"
)));
}
None => return Err(JwtError::Verification("JWK missing kty".into())),
}
match jwk.get("crv").and_then(|v| v.as_str()) {
Some("Ed25519") => {}
Some(other) => {
return Err(JwtError::Verification(format!(
"expected OKP crv Ed25519, got {other}"
)));
}
None => return Err(JwtError::Verification("JWK missing crv".into())),
}
let x = jwk
.get("x")
.and_then(|v| v.as_str())
.ok_or_else(|| JwtError::Verification("JWK missing x".into()))?;
let x_bytes = URL_SAFE_NO_PAD
.decode(x)
.map_err(|e| JwtError::Verification(format!("x decode: {e}")))?;
Self::from_bytes(&x_bytes)
}
}
impl JwtVerifier for EdDsaVerifier {
fn verify(&self, data: &[u8], signature: &[u8]) -> Result<(), JwtError> {
let sig = Signature::from_slice(signature)
.map_err(|e| JwtError::Verification(format!("invalid signature: {e}")))?;
self.verifying_key
.verify_strict(data, &sig)
.map_err(|_| JwtError::Verification("EdDSA signature verification failed".into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jwt::{decode_compact_jws_verified_with_algs, encode_compact_jws};
use serde_json::json;
#[test]
fn eddsa_sign_verify_jwt() {
let signer = EdDsaSigner::from_bytes(&[7u8; 32]).unwrap().with_kid("k1");
let verifier = EdDsaVerifier::from_bytes(&signer.public_key_bytes()).unwrap();
let header = json!({"alg": "EdDSA", "typ": "JWT", "kid": "k1"});
let payload = json!({"sub": "user123", "name": "Alice"});
let jws = encode_compact_jws(&header, &payload, &signer).unwrap();
let (decoded_header, decoded_payload) =
decode_compact_jws_verified_with_algs(&jws, &verifier, &["EdDSA"]).unwrap();
assert_eq!(decoded_header["alg"], "EdDSA");
assert_eq!(decoded_payload["sub"], "user123");
}
#[test]
fn eddsa_wrong_key_fails() {
let signer = EdDsaSigner::from_bytes(&[1u8; 32]).unwrap();
let other = EdDsaSigner::from_bytes(&[2u8; 32]).unwrap();
let verifier = EdDsaVerifier::from_bytes(&other.public_key_bytes()).unwrap();
let jws = encode_compact_jws(&json!({"alg": "EdDSA"}), &json!({"x": 1}), &signer).unwrap();
assert!(decode_compact_jws_verified_with_algs(&jws, &verifier, &["EdDSA"]).is_err());
}
#[test]
fn eddsa_from_jwk_roundtrip() {
let signer = EdDsaSigner::from_bytes(&[3u8; 32]).unwrap();
let jwk = signer.public_key_jwk();
assert_eq!(jwk["kty"], "OKP");
assert_eq!(jwk["crv"], "Ed25519");
let verifier = EdDsaVerifier::from_jwk(&jwk).unwrap();
let jws =
encode_compact_jws(&json!({"alg": "EdDSA"}), &json!({"test": true}), &signer).unwrap();
let (_, payload) =
decode_compact_jws_verified_with_algs(&jws, &verifier, &["EdDSA"]).unwrap();
assert_eq!(payload["test"], true);
}
#[test]
fn eddsa_from_jwk_rejects_wrong_curve() {
let jwk = json!({"kty": "OKP", "crv": "X25519", "x": "AAAA"});
assert!(EdDsaVerifier::from_jwk(&jwk).is_err());
}
#[test]
fn eddsa_from_jwk_rejects_wrong_kty() {
let jwk = json!({"kty": "EC", "crv": "Ed25519", "x": "AAAA"});
assert!(EdDsaVerifier::from_jwk(&jwk).is_err());
}
#[test]
fn eddsa_signature_is_64_bytes() {
let signer = EdDsaSigner::from_bytes(&[5u8; 32]).unwrap();
assert_eq!(signer.sign(b"test data").unwrap().len(), 64);
}
#[test]
fn eddsa_generate_produces_working_keypair() {
let signer = EdDsaSigner::generate();
let verifier = EdDsaVerifier::from_bytes(&signer.public_key_bytes()).unwrap();
let jws = encode_compact_jws(&json!({"alg": "EdDSA"}), &json!({"ok": 1}), &signer).unwrap();
assert!(decode_compact_jws_verified_with_algs(&jws, &verifier, &["EdDSA"]).is_ok());
}
#[test]
fn eddsa_from_bytes_rejects_wrong_length() {
assert!(EdDsaSigner::from_bytes(&[0u8; 16]).is_err());
assert!(EdDsaVerifier::from_bytes(&[0u8; 16]).is_err());
}
}