use ed25519_dalek::{
PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, Signature, Signer as _, SigningKey, VerifyingKey,
};
use subtle::ConstantTimeEq as _;
use zeroize::Zeroizing;
use crate::error::{Error, Result};
use crate::jwa::SigningAlg;
use crate::jwk::{Jwk, KeyMaterial, OkpCurve, OkpKey, ed25519_public_is_valid};
pub(crate) fn sign(alg: &SigningAlg, key: &Jwk, signing_input: &[u8]) -> Result<Vec<u8>> {
let okp = ed25519_key(alg, key)?;
let d = okp.d().ok_or(Error::KeyAlgorithmMismatch)?;
if d.len() != SECRET_KEY_LENGTH {
return Err(Error::InvalidKey("Ed25519 d must be 32 octets"));
}
let mut seed = Zeroizing::new([0u8; SECRET_KEY_LENGTH]);
seed.copy_from_slice(d);
let signing_key = SigningKey::from_bytes(&seed);
let derived = signing_key.verifying_key();
if !bool::from(derived.as_bytes()[..].ct_eq(okp.x())) {
return Err(Error::InvalidKey("Ed25519 d does not match x"));
}
let signature = signing_key
.try_sign(signing_input)
.map_err(|_| Error::InvalidKey("Ed25519 signing failed"))?;
Ok(signature.to_bytes().to_vec())
}
pub(crate) fn verify(
alg: &SigningAlg,
key: &Jwk,
signing_input: &[u8],
signature: &[u8],
) -> Result<()> {
verify_ed25519(ed25519_key(alg, key)?.x(), signing_input, signature)
}
fn verify_ed25519(x: &[u8], signing_input: &[u8], signature: &[u8]) -> Result<()> {
let public = verifying_key(x)?;
let signature = Signature::from_slice(signature).map_err(|_| Error::InvalidSignature)?;
public
.verify_strict(signing_input, &signature)
.map_err(|_| Error::InvalidSignature)
}
fn ed25519_key<'k>(alg: &SigningAlg, key: &'k Jwk) -> Result<&'k OkpKey> {
match (alg, key.material()) {
(SigningAlg::EdDSA | SigningAlg::Ed25519, KeyMaterial::Okp(okp))
if okp.crv() == OkpCurve::Ed25519 =>
{
Ok(okp)
}
_ => Err(Error::KeyAlgorithmMismatch),
}
}
const ED25519_PUBLIC_INVALID: &str = "Ed25519 public key is invalid";
fn verifying_key(x: &[u8]) -> Result<VerifyingKey> {
if !ed25519_public_is_valid(x) {
return Err(Error::InvalidKey(ED25519_PUBLIC_INVALID));
}
let x = <&[u8; PUBLIC_KEY_LENGTH]>::try_from(x)
.map_err(|_| Error::InvalidKey(ED25519_PUBLIC_INVALID))?;
VerifyingKey::from_bytes(x).map_err(|_| Error::InvalidKey(ED25519_PUBLIC_INVALID))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jws::alg::test_keys as keys;
use crate::test_support::wycheproof::{self, Deviation, Outcome};
use crate::util::b64url_decode;
use SigningAlg as A;
const A4_SIGNING_INPUT: &[u8] = b"eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc";
const A4_SIGNATURE: &str =
"hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg";
fn a4_signature() -> Vec<u8> {
b64url_decode(A4_SIGNATURE).unwrap()
}
#[test]
fn rfc8037_a5_verifies_with_the_a1_public_key() {
let public = keys::ed25519("");
for alg in [A::EdDSA, A::Ed25519] {
assert_eq!(
verify(&alg, &public, A4_SIGNING_INPUT, &a4_signature()),
Ok(()),
"{alg}"
);
}
}
const IDENTITY: [u8; 32] = {
let mut p = [0u8; 32];
p[0] = 1;
p
};
const L: [u8; 32] = [
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde,
0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10,
];
fn a1_x() -> Vec<u8> {
b64url_decode(keys::ED25519_X).unwrap()
}
#[test]
fn the_identity_key_and_an_all_zero_signature_are_refused() {
let zero = [0u8; 64];
assert_eq!(
verify_ed25519(&IDENTITY, b"any message", &zero),
Err(Error::InvalidKey(ED25519_PUBLIC_INVALID))
);
let mut identity_r = [0u8; 64];
identity_r[..32].copy_from_slice(&IDENTITY);
assert!(matches!(
verify_ed25519(&IDENTITY, b"any message", &identity_r),
Err(Error::InvalidKey(_))
));
let jwk = format!(
r#"{{"kty":"OKP","crv":"Ed25519","x":"{}"}}"#,
crate::util::b64url_encode(&IDENTITY)
);
assert!(matches!(
Jwk::from_json(jwk.as_bytes()),
Err(Error::InvalidKey(_))
));
}
const MIXED_ORDER: &str = "Fd6zBynrPkZciPEbV9DI026w9nDDsi0vgOpgSe6ywjs";
#[test]
fn a_key_with_a_torsion_component_is_refused_before_the_signature() {
let x = b64url_decode(MIXED_ORDER).unwrap();
let key = VerifyingKey::from_bytes(&<[u8; 32]>::try_from(&x[..]).unwrap()).unwrap();
assert!(!key.is_weak());
assert_eq!(
verify_ed25519(&x, A4_SIGNING_INPUT, &a4_signature()),
Err(Error::InvalidKey(ED25519_PUBLIC_INVALID))
);
let jwk = format!(r#"{{"kty":"OKP","crv":"Ed25519","x":"{MIXED_ORDER}"}}"#);
assert!(matches!(
Jwk::from_json(jwk.as_bytes()),
Err(Error::InvalidKey(_))
));
}
const NON_CANONICAL: &str = "8P_______________________________________38";
#[test]
fn a_non_canonical_key_is_refused_at_verify_as_at_import() {
let x = b64url_decode(NON_CANONICAL).unwrap();
assert!(
VerifyingKey::from_bytes(&<[u8; 32]>::try_from(&x[..]).unwrap()).is_ok(),
"the probe needs a key that ZIP-215 decoding accepts"
);
let jwk = format!(r#"{{"kty":"OKP","crv":"Ed25519","x":"{NON_CANONICAL}"}}"#);
let import = Jwk::from_json(jwk.as_bytes()).err();
assert!(matches!(import, Some(Error::InvalidKey(_))), "{import:?}");
let key = Jwk::okp_unchecked(OkpCurve::Ed25519, &x, None);
for alg in [A::EdDSA, A::Ed25519] {
assert_eq!(
verify(&alg, &key, A4_SIGNING_INPUT, &a4_signature()).err(),
import,
"{alg}"
);
}
}
#[test]
fn the_probe_would_pass_a_lenient_verifier() {
use ed25519_dalek::Verifier as _;
let weak = VerifyingKey::from_bytes(&IDENTITY).unwrap();
let mut identity_r = [0u8; 64];
identity_r[..32].copy_from_slice(&IDENTITY);
let sig = Signature::from_bytes(&identity_r);
assert!(weak.verify(b"any message", &sig).is_ok());
assert!(weak.verify_strict(b"any message", &sig).is_err());
}
#[test]
fn a_small_order_r_is_an_invalid_signature() {
let mut sig = [0u8; 64];
sig[..32].copy_from_slice(&IDENTITY);
assert_eq!(
verify_ed25519(&a1_x(), A4_SIGNING_INPUT, &sig),
Err(Error::InvalidSignature)
);
assert_eq!(
verify_ed25519(&a1_x(), A4_SIGNING_INPUT, &[0u8; 64]),
Err(Error::InvalidSignature)
);
}
#[test]
fn s_at_or_above_the_group_order_is_an_invalid_signature() {
let good = a4_signature();
let mut malleated = good.clone();
let mut carry = 0u16;
for (byte, l) in malleated[32..].iter_mut().zip(L) {
let sum = u16::from(*byte) + u16::from(l) + carry;
*byte = sum as u8;
carry = sum >> 8;
}
assert_eq!(carry, 0, "s + L still fits in 32 octets");
assert_eq!(verify_ed25519(&a1_x(), A4_SIGNING_INPUT, &good), Ok(()));
assert_eq!(
verify_ed25519(&a1_x(), A4_SIGNING_INPUT, &malleated),
Err(Error::InvalidSignature)
);
let mut s_is_l = good;
s_is_l[32..].copy_from_slice(&L);
assert_eq!(
verify_ed25519(&a1_x(), A4_SIGNING_INPUT, &s_is_l),
Err(Error::InvalidSignature)
);
}
#[test]
fn a_signature_of_the_wrong_length_is_invalid() {
let good = a4_signature();
let public = keys::ed25519("");
let mut long = good.clone();
long.push(0);
for sig in [
&[][..],
&good[..32],
&good[..63],
&long[..],
&[good.clone(), good.clone()].concat()[..],
] {
for alg in [A::EdDSA, A::Ed25519] {
assert_eq!(
verify(&alg, &public, A4_SIGNING_INPUT, sig),
Err(Error::InvalidSignature),
"{alg}, {} octets",
sig.len()
);
}
}
}
#[test]
fn a_changed_input_or_signature_is_invalid() {
let public = keys::ed25519("");
let good = a4_signature();
let mut input = A4_SIGNING_INPUT.to_vec();
input[0] ^= 1;
assert_eq!(
verify(&A::EdDSA, &public, &input, &good),
Err(Error::InvalidSignature)
);
for i in [0, 31, 32, 63] {
let mut sig = good.clone();
sig[i] ^= 0x01;
assert_eq!(
verify(&A::EdDSA, &public, A4_SIGNING_INPUT, &sig),
Err(Error::InvalidSignature),
"bit flip in octet {i}"
);
}
}
#[test]
fn a_public_key_of_the_wrong_length_is_an_invalid_key() {
let x = a1_x();
for bad in [&x[..31], &[x.clone(), vec![0]].concat()[..], &[][..]] {
assert!(
matches!(
verify_ed25519(bad, A4_SIGNING_INPUT, &a4_signature()),
Err(Error::InvalidKey(_))
),
"{} octets",
bad.len()
);
}
}
#[test]
fn a_foreign_alg_or_key_is_a_mismatch_not_a_panic() {
assert_eq!(
verify(
&A::ES256,
&keys::ed25519(""),
A4_SIGNING_INPUT,
&a4_signature()
),
Err(Error::KeyAlgorithmMismatch)
);
for key in [keys::x25519(), keys::p256(""), keys::oct(32, "")] {
assert_eq!(
verify(&A::EdDSA, &key, A4_SIGNING_INPUT, &a4_signature()),
Err(Error::KeyAlgorithmMismatch)
);
}
}
#[test]
fn ed448_keys_never_reach_the_family() {
let x = crate::util::b64url_encode(&[0x42; 57]);
let jwk = format!(r#"{{"kty":"OKP","crv":"Ed448","x":"{x}"}}"#);
assert_eq!(
Jwk::from_json(jwk.as_bytes()).err(),
Some(Error::UnsupportedAlgorithm)
);
}
#[test]
fn rfc8037_a4_signs_exactly() {
let private = keys::ed25519_private("");
for alg in [A::EdDSA, A::Ed25519] {
let sig = sign(&alg, &private, A4_SIGNING_INPUT).unwrap();
assert_eq!(sig, a4_signature(), "{alg}");
assert_eq!(sig.len(), 64);
}
}
#[test]
fn signatures_round_trip_and_bind_the_input() {
let private = keys::ed25519_private("");
let public = keys::ed25519("");
for input in [&b""[..], b"x", &[0xff; 1000][..]] {
let sig = sign(&A::Ed25519, &private, input).unwrap();
assert_eq!(verify(&A::Ed25519, &public, input, &sig), Ok(()));
assert_eq!(verify(&A::EdDSA, &private, input, &sig), Ok(()));
assert_eq!(
verify(&A::Ed25519, &public, b"other", &sig),
Err(Error::InvalidSignature)
);
}
}
#[test]
fn a_private_key_that_does_not_match_x_is_an_invalid_key() {
let mismatched = Jwk::okp_unchecked(OkpCurve::Ed25519, &a1_x(), Some(&[7u8; 32]));
let other_d = crate::util::b64url_encode(&[7u8; 32]);
let json = format!(
r#"{{"kty":"OKP","crv":"Ed25519","x":"{}","d":"{other_d}"}}"#,
keys::ED25519_X
);
assert!(matches!(
Jwk::from_json(json.as_bytes()),
Err(Error::InvalidKey(_))
));
assert_eq!(
sign(&A::EdDSA, &mismatched, A4_SIGNING_INPUT),
Err(Error::InvalidKey("Ed25519 d does not match x"))
);
}
#[test]
fn signing_needs_this_familys_private_key() {
assert_eq!(
sign(&A::EdDSA, &keys::ed25519(""), A4_SIGNING_INPUT),
Err(Error::KeyAlgorithmMismatch)
);
assert_eq!(
sign(&A::ES256, &keys::ed25519_private(""), A4_SIGNING_INPUT),
Err(Error::KeyAlgorithmMismatch)
);
for key in [keys::p256_private(""), keys::oct(32, "")] {
assert_eq!(
sign(&A::Ed25519, &key, A4_SIGNING_INPUT),
Err(Error::KeyAlgorithmMismatch)
);
}
}
#[test]
fn a_private_key_of_the_wrong_length_never_imports() {
for len in [0, 31, 33, 64] {
let d = crate::util::b64url_encode(&vec![1u8; len]);
let jwk = format!(
r#"{{"kty":"OKP","crv":"Ed25519","x":"{}","d":"{d}"}}"#,
keys::ED25519_X
);
assert!(
matches!(Jwk::from_json(jwk.as_bytes()), Err(Error::InvalidKey(_))),
"{len}"
);
}
}
const WYCHEPROOF_ED25519: &[Deviation] = &[
Deviation::range(
152,
159,
Outcome::Reject,
"ADR 0026 §6, ADR 0041 §4: a public key of small order (the identity \
and the seven torsion points, RFC 8032 §5.1.3) is rejected with \
InvalidKey at JWK import; verify re-checks is_weak()",
),
Deviation::range(
160,
180,
Outcome::Reject,
"ADR 0043 §1: a public key with a torsion component (mixed order, not \
small order) is rejected with InvalidKey at JWK import, although \
RFC 8032 §5.1.7 and verify_strict accept it, because implementations \
disagree on such keys; verify re-checks is_torsion_free()",
),
];
#[test]
fn wycheproof_ed25519() {
let suite = wycheproof::load("ed25519");
let summary = wycheproof::assert_conforms(&suite, WYCHEPROOF_ED25519, |group, test| {
let (msg, sig) = test.signature();
let id = test.tc_id;
let key = match Jwk::from_json(group.verification_jwk().as_bytes()) {
Ok(key) => key,
Err(e) => {
assert!(
test.has_flag("SmallOrderPublicKey")
|| test.has_flag("MixedOrderPublicKey"),
"tcId {id}: {e:?}"
);
assert_eq!(e.code(), "InvalidKey", "tcId {id}");
return Outcome::Reject;
}
};
let eddsa = verify(&A::EdDSA, &key, msg, sig);
assert_eq!(eddsa, verify(&A::Ed25519, &key, msg, sig), "tcId {id}");
if let Err(e) = &eddsa {
assert_eq!(*e, Error::InvalidSignature, "tcId {id}");
}
Outcome::of(&eddsa)
});
assert_eq!(summary.tests, 210);
assert_eq!(summary.deviations, 29);
assert_eq!(summary.accepted, 117);
assert_eq!(summary.rejected, 64 + 8 + 21);
}
const A4_JWS: &str = "eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc.\
hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg";
#[test]
fn rfc8037_a4_and_a5_end_to_end_through_compact() {
use crate::jws::{
ProtectedHeader, SignOptions, VerifyOptions, sign_compact, verify_compact,
};
let protected = ProtectedHeader::from_encoded("eyJhbGciOiJFZERTQSJ9").unwrap();
let jws = sign_compact(
&protected,
b"Example of Ed25519 signing",
&keys::ed25519_private(""),
&SignOptions::new(),
)
.unwrap();
assert_eq!(jws, A4_JWS);
let opts = VerifyOptions::new(&[A::EdDSA]).unwrap();
let verified = verify_compact(A4_JWS, &keys::ed25519(""), &opts).unwrap();
assert_eq!(verified.alg, A::EdDSA);
assert_eq!(*verified.payload, b"Example of Ed25519 signing");
let tampered = A4_JWS.replace("hgyY", "hgyZ");
assert_eq!(
verify_compact(&tampered, &keys::ed25519(""), &opts).err(),
Some(Error::InvalidSignature)
);
let only_ed25519 = VerifyOptions::new(&[A::Ed25519]).unwrap();
assert_eq!(
verify_compact(A4_JWS, &keys::ed25519(""), &only_ed25519).err(),
Some(Error::AlgorithmNotAllowed)
);
}
#[test]
fn ed25519_end_to_end_through_compact() {
use crate::header::Header;
use crate::jws::{
ProtectedHeader, SignOptions, VerifyOptions, sign_compact, verify_compact,
};
let mut header = Header::new();
header.alg = Some("Ed25519".into());
let protected = ProtectedHeader::new(header).unwrap();
let payload = b"Example of Ed25519 signing";
let private = keys::ed25519_private(r#","alg":"Ed25519""#);
let jws = sign_compact(&protected, payload, &private, &SignOptions::new()).unwrap();
let parts: Vec<&str> = jws.split('.').collect();
assert_eq!(parts.len(), 3);
let input = format!("{}.{}", parts[0], parts[1]);
let expected = sign(&A::EdDSA, &keys::ed25519_private(""), input.as_bytes()).unwrap();
assert_eq!(b64url_decode(parts[2]).unwrap(), expected);
let opts = VerifyOptions::new(&[A::Ed25519]).unwrap();
let public = keys::ed25519(r#","alg":"Ed25519""#);
let verified = verify_compact(&jws, &public, &opts).unwrap();
assert_eq!(verified.alg, A::Ed25519);
assert_eq!(*verified.payload, payload);
let eddsa_key = keys::ed25519(r#","alg":"EdDSA""#);
assert_eq!(
verify_compact(&jws, &eddsa_key, &opts).err(),
Some(Error::KeyAlgorithmMismatch)
);
let detached = sign_compact(
&protected,
payload,
&private,
&SignOptions::new().detached(true),
)
.unwrap();
let opts = VerifyOptions::new(&[A::Ed25519])
.unwrap()
.detached_payload(payload);
assert_eq!(
*verify_compact(&detached, &public, &opts).unwrap().payload,
payload
);
}
}