Skip to content

Conversation

@ctz
Copy link
Owner

@ctz ctz commented Nov 23, 2025

This is work-in-progress Ed25519 support. Much of the work is thanks to @phlip9 in #74

Draft while I do these items:

  • review coverage
  • self-review and write this list

fixes #74

ctz and others added 25 commits November 23, 2025 09:32
Ex: `#define foo()   bl edwards25519_decode_alt_mul_p25519`

Before this change, the generated rust macro didn't wire up the local label
reference properly:

```rust
macro_rules! foo { () => { Q!(
    "bl " Label!("edwards25519_decode_alt_mul_p25519", 0, Before)
)} }
```

After this change:

```rust
macro_rules! foo { () => { Q!(
    "bl " Label!("edwards25519_decode_alt_mul_p25519", 4, After)
)} }
```

To support this, we now track which macros reference labels. We then track
which blocks call these macros. As long as all macro callsites are uniformly
before or after the labels they reference, we can safely replace the local label
id and search direction in the original macro definition.

It was challenging to track macro callsites in the main `RustFormatter` and also
difficult to "pre-locate" callsites while dealing with hoisting, so the actual
macro fixing happens on the generated Rust code from the `RustFormatter` pass.
@ctz ctz marked this pull request as draft November 23, 2025 09:57
@codecov
Copy link

codecov bot commented Nov 23, 2025

Codecov Report

❌ Patch coverage is 99.59641% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.31%. Comparing base (9796ac5) to head (01938d4).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
rustls-graviola/src/sign.rs 83.87% 5 Missing ⚠️
graviola/src/mid/ed25519.rs 99.25% 2 Missing ⚠️
graviola/src/error.rs 0.00% 1 Missing ⚠️
graviola/src/high/ed25519.rs 98.96% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #120      +/-   ##
==========================================
+ Coverage   99.14%   99.31%   +0.17%     
==========================================
  Files         171      185      +14     
  Lines       39165    50337   +11172     
==========================================
+ Hits        38830    49994   +11164     
- Misses        335      343       +8     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codspeed-hq
Copy link

codspeed-hq bot commented Nov 23, 2025

CodSpeed Performance Report

Merging #120 will not alter performance

Comparing jbp-ed25519 (01938d4) with main (9796ac5)

Summary

✅ 143 untouched
🆕 12 new

Benchmarks breakdown

Benchmark BASE HEAD Change
🆕 aws-lc-rs N/A 77.5 µs N/A
🆕 dalek N/A 112.9 µs N/A
🆕 graviola N/A 62.5 µs N/A
🆕 ring N/A 116.3 µs N/A
🆕 aws-lc-rs N/A 82.7 µs N/A
🆕 dalek N/A 115.9 µs N/A
🆕 graviola N/A 62.7 µs N/A
🆕 ring N/A 122.5 µs N/A
🆕 aws-lc-rs N/A 195.7 µs N/A
🆕 dalek N/A 149.8 µs N/A
🆕 graviola N/A 141 µs N/A
🆕 ring N/A 233.7 µs N/A

Copy link
Contributor

@phlip9 phlip9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a ton for bringing this over the finish line! And I apologize for the mess of WIP commits 😅

Figured I would leave some comments as a potential user of both rustls-graviola and graviola::signing::eddsa


pkcs8::Key::decode(bytes, &asn1::oid::id_ed25519, None)
.and_then(|k| asn1::OctetString::from_bytes(k.private_key()).map_err(Error::Asn1Error))
.and_then(|pk| Self::from_bytes(pk.as_octets()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to sanity check that the serialized pubkey in the pkcs8 document matches the derived compressed pubkey

https://github.com/briansmith/ring/blob/main/src/ec/curve25519/ed25519/signing.rs#L136

pub fn public_key(&self) -> Ed25519VerifyingKey {
let _entry = Entry::new_secret();
Ed25519VerifyingKey(self.0.verifying_key())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is obviously committing to the signing key containing the decompressed verifying key, but it would be huge if this could just return a &Ed25519VerifyingKey and not have to clear registers:

impl Ed25519SigningKey {
    pub fn public_key(&self) -> &Ed25519VerifyingKey {
        Ed25519VerifyingKey::from_ref(self.0.verifying_key())
    }
}

/// An Ed25519 verification public key.
#[derive(Debug)]
#[repr(transparent)]
pub struct Ed25519VerifyingKey(ed25519::VerifyingKey);

impl Ed25519VerifyingKey {
    #[inline]
    const fn from_ref(vk: &ed25519::VerifyingKey) -> &Self {
        // Assert that both types have the same layout
        const _: [(); std::mem::size_of::<Self>()] =
            [(); std::mem::size_of::<ed25519::VerifyingKey>()];
        const _: [(); std::mem::align_of::<Self>()] =
            [(); std::mem::align_of::<ed25519::VerifyingKey>()];

        // SAFETY: Both types have the same layout and the newtype uses
        // #[repr(transparent)], so their references are equivalent.
        // See <https://docs.rs/ref_cast>.
        unsafe { &*(vk as *const ed25519::VerifyingKey as *const Self) }
    }
}

.try_into()
.map(|seed| Self(ed25519::SigningKey::from_seed(&seed)))
.map_err(|_| Error::WrongLength)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

somewhat unfortunate that this fn has to become fallible when in principle it's infallible

pub fn from_seed(bytes: &[u8; 32]) -> Self { /* .. */ }

the struggles of API stability 😢

let _entry = Entry::new_secret();
Ed25519VerifyingKey(self.0.verifying_key())
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likewise, if I can get access to the seed after, that would be amazing:

impl Ed25519SigningKey {
    #[inline]
    pub fn as_seed(&self) -> &[u8; 32] { /* .. */ }
}

this is a problem I have with ring's interface; it doesn't expose the seed, which means I have to duplicate it outside: https://github.com/lexe-app/lexe-public/blob/master/common/src/ed25519.rs#L77

/// An ed25519 secret key and public key.
///
/// Applications should always sign with a *key pair* rather than passing in
/// the secret key and public key separately, to avoid attacks like
/// [attacker controlled pubkey signing](https://github.com/MystenLabs/ed25519-unsafe-libs).
pub struct KeyPair {
    /// The ring key pair for actually signing things.
    key_pair: ring::signature::Ed25519KeyPair,

    /// Unfortunately, [`ring`] doesn't expose the `seed` after construction,
    /// so we need to hold on to the seed if we ever need to serialize the key
    /// pair later.
    seed: [u8; 32],
}


impl SigningKey {
pub(crate) fn from_seed(seed: &[u8; 32]) -> Self {
let _entry = low::Entry::new_secret();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this still need let _entry = low::Entry::new_secret();?

low::ct::into_public(sig)
}

pub(crate) fn verifying_key(&self) -> VerifyingKey {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can probably return a &VerifyingKey now


let mut sig = [0u8; 64];
sig[0..32].clone_from_slice(&sig_r.0);
sig[32..64].clone_from_slice(&s.to_le_bytes());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: the compiler specializes this correctly, but .copy_from_slice(..) is less surprising as a reader

low::zeroise(&mut self.prefix);
// clearly not necessary, but makes zeroisation easier to test
low::zeroise(&mut self.verifying_key.point.0);
low::zeroise(&mut self.verifying_key.bytes);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: could gate zeroising public data on #[cfg(debug_assertions)]


/// `PureEd25519` signature verification
pub(crate) fn verify(&self, sig: &[u8; 64], msg: &[u8]) -> Result<(), Error> {
let _entry = low::Entry::new_public();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likewise, does this need entry if it's not a public API?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants