use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use ed25519_dalek::{Signature, VerifyingKey};
use jsonwebtoken::jwk::{AlgorithmParameters, EllipticCurve, Jwk};
use jsonwebtoken::{crypto, Algorithm, DecodingKey};
#[derive(Debug)]
pub enum VerifyFailure {
Key(String),
Backend(String),
}
pub fn verify_jws_signature(
alg: Algorithm,
jwk: &Jwk,
decoding_key: &DecodingKey,
signature_b64: &str,
signing_input: &[u8],
) -> Result<bool, VerifyFailure> {
if alg != Algorithm::EdDSA {
return crypto::verify(signature_b64, signing_input, decoding_key, alg)
.map_err(|e| VerifyFailure::Backend(e.to_string()));
}
let key = ed25519_verifying_key(jwk)?;
let signature_bytes = URL_SAFE_NO_PAD
.decode(signature_b64)
.map_err(|e| VerifyFailure::Backend(format!("signature is not valid base64url: {e}")))?;
let signature = Signature::from_slice(&signature_bytes).map_err(|_| {
VerifyFailure::Backend(format!(
"an Ed25519 signature must be exactly 64 bytes, got {}",
signature_bytes.len()
))
})?;
match key.verify_strict(signing_input, &signature) {
Ok(()) => Ok(true),
Err(_) => {
let r_is_small_order = VerifyingKey::from_bytes(
signature_bytes[..32]
.try_into()
.expect("a 64-byte Ed25519 signature always yields a 32-byte R"),
)
.is_ok_and(|r| r.is_weak());
if r_is_small_order {
tracing::warn!(
target: "authkestra_devsig",
"rejecting EdDSA signature: its R component is a low-order point — strict \
verification refuses these because they carry no proof of possession \
(authkestra#242)"
);
} else {
tracing::debug!(
target: "authkestra_devsig",
"EdDSA signature did not verify under strict verification"
);
}
Ok(false)
}
}
}
fn ed25519_verifying_key(jwk: &Jwk) -> Result<VerifyingKey, VerifyFailure> {
let params = match &jwk.algorithm {
AlgorithmParameters::OctetKeyPair(params) => params,
_ => return Err(VerifyFailure::Key("EdDSA requires an OKP key".to_string())),
};
if params.curve != EllipticCurve::Ed25519 {
return Err(VerifyFailure::Key(format!(
"EdDSA requires the Ed25519 curve, got {:?}",
params.curve
)));
}
match authkestra_crypto_util::parse_ed25519_verifying_key_strict(¶ms.x) {
Ok(key) => Ok(key),
Err(e) => {
if let authkestra_crypto_util::EdDsaKeyError::LowOrderPoint = e {
tracing::warn!(
target: "authkestra_devsig",
"rejecting Ed25519 key: it is a low-order (weak) point, for which no private key \
exists — a signature under it would prove possession of nothing, so it is refused \
before verification is even attempted (authkestra#242)"
);
return Err(VerifyFailure::Key(
"Ed25519 public key is a low-order point; no private key exists for it, so a \
signature under it proves nothing"
.to_string(),
));
}
Err(VerifyFailure::Key(e.to_string()))
}
}
}
#[cfg(test)]
mod tests {
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
const IDENTITY: [u8; 32] = {
let mut b = [0u8; 32];
b[0] = 1;
b
};
#[test]
fn strict_verification_rejects_a_low_order_key_that_non_strict_accepts() {
let weak = VerifyingKey::from_bytes(&IDENTITY).expect("identity is a valid Edwards point");
assert!(weak.is_weak(), "the identity point must be low-order");
let mut forged = [0u8; 64];
forged[..32].copy_from_slice(&IDENTITY);
let forged = Signature::from_bytes(&forged);
assert!(
weak.verify(b"authkestra", &forged).is_ok(),
"precondition: the NON-strict verifier accepts this forgery — if this ever fails, \
upstream changed and this test no longer proves what it claims"
);
assert!(
weak.verify_strict(b"authkestra", &forged).is_err(),
"verify_strict must reject a low-order key that non-strict accepts"
);
}
#[test]
fn strict_verification_still_accepts_a_genuine_signature() {
let signing = SigningKey::from_bytes(&[7u8; 32]);
let key = signing.verifying_key();
assert!(!key.is_weak());
let genuine = signing.sign(b"authkestra");
assert!(key.verify_strict(b"authkestra", &genuine).is_ok());
}
}