Skip to content

PublicKey does not round-trip through as_byte() / TryFrom<&[u8]> #5

Description

@cpkb-bluezoo

PublicKey::as_byte() followed by PublicKey::try_from(&bytes[..]) produces
a different key that no longer verifies a signature the original in-memory
key verifies correctly. The parsed-back key isn't equivalent to the one that
was serialized — this makes any round trip through the public wire format
(the whole point of as_byte()/TryFrom) silently produce a broken key.

Repro (deterministic, no RNG needed)

use ed448_rust::{PrivateKey, PublicKey};

fn main() {
    let seed: [u8; 57] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(3));
    let private = PrivateKey::from(seed);
    let public = PublicKey::from(&private);

    let msg = b"hello world";
    let sig = private.sign(msg, None).unwrap();

    assert!(public.verify(msg, &sig, None).is_ok()); // passes

    let bytes = public.as_byte();
    let public2 = PublicKey::try_from(&bytes[..]).unwrap();

    assert_eq!(bytes, public2.as_byte());       // FAILS
    assert!(public2.verify(msg, &sig, None).is_ok()); // FAILS
}

Output

original key verifies signature: true
bytes == bytes2: false
original bytes:      [aa, 57, 71, d7, 94, 34, ba, 8a, 3e, 0f, 44, 20, 11, 36, 85, 5d, aa, d0, e8, 84, 86, b9, 8b, 0b, 91, cd, 21, 60, de, db, 52, 78, d4, 59, 1f, 97, 04, f6, b7, c4, d3, 16, c7, dc, d3, ed, c1, 89, eb, 8c, 13, 72, 66, 7a, ae, a8, 80]
round-tripped bytes: [45, 52, 37, 50, 42, e8, 51, d9, 79, 8a, dd, d6, 38, b4, 2f, 68, bf, ff, ac, dc, 6a, e9, 77, d1, 40, e7, 4c, bf, a9, d9, ae, 44, 78, a9, 68, b5, dc, 6d, 03, 58, d3, a7, 8a, 10, db, 34, af, 69, 25, a4, 95, 95, 93, 89, cb, 5b, 00]
round-tripped key verifies the SAME signature: false
thread 'main' panicked at src/main.rs:30:5:
assertion `left == right` failed: PublicKey should round-trip through as_byte()/try_from() unchanged

Environment

  • ed448-rust = "0.1.1"
  • rustc 1.93.1, cargo 1.93.1
  • macOS (Darwin), but nothing platform-specific in the repro

Impact

This makes the crate unsafe to use anywhere a public key needs to survive
serialisation — e.g. loading a key from a config file, a DNSSEC DNSKEY
record, or any wire protocol — since TryFrom<&[u8]> silently produces a
key that looks valid (parses without error) but doesn't actually correspond
to the bytes it was built from, and will reject signatures the real key
would accept.

I ran into this while evaluating ed448-rust as an Ed448 backend for a
DNSSEC (RFC 8080) implementation and had to switch to a different crate as a
result. My guess for the root cause, not
yet confirmed, is somewhere in Point::encode/decode or the TryFrom
conversion path in src/public_key.rs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions