ed25519

    A pure MoonBit Ed25519 signing and verification package.

    ed25519
    signature
    crypto
    license
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    5 months ago
    Downloads
    18K

    #hustcer/ed25519

    A pure MoonBit implementation of plain Ed25519 signing and verification.

    The package implements deterministic Ed25519 over Array[UInt] byte arrays. It is intended for small payloads such as locally signed license data, plus tests and interoperability checks against RFC 8032 vectors and OpenSSL.

    This package is not audited and is not written to be constant-time. Use a mature audited cryptography library for high-volume, network-facing, or side-channel-sensitive signing systems.

    #Module

    Add the module dependency in moon.mod.json:

    { "deps": { "hustcer/ed25519": "0.2.0" } }

    Import it from moon.pkg with an alias:

    import {
    "hustcer/ed25519" @ed25519,
    }

    This repository itself depends on Tigls/mb-hash for SHA-512.

    #API

    The public API is generated in pkg.generated.mbti and currently consists of:

    pub fn derive_public_key(BytesView) -> Result[Bytes, String]
    pub fn sign(BytesView, BytesView) -> Result[Bytes, String]
    pub fn verify(BytesView, BytesView, BytesView) -> Bool
    pub fn verify_result(BytesView, BytesView, BytesView) -> Result[Bool, String]

    pub struct SigningKey
    pub fn SigningKey::from_seed(BytesView) -> Result[SigningKey, String]
    pub fn SigningKey::public_key(SigningKey) -> Bytes
    pub fn SigningKey::sign(SigningKey, BytesView) -> Bytes

    pub struct VerifyingKey
    pub fn VerifyingKey::from_public_key(BytesView) -> Result[VerifyingKey, String]
    pub fn VerifyingKey::public_key(VerifyingKey) -> Bytes
    pub fn VerifyingKey::verify(VerifyingKey, BytesView, BytesView) -> Bool
    pub fn VerifyingKey::verify_result(
    VerifyingKey,
    BytesView,
    BytesView,
    ) -> Result[Bool, String]

    #Data Model

    • Private keys are 32-byte Ed25519 seeds.
    • Public keys are 32 bytes.
    • Signatures are 64 bytes.
    • Messages are BytesView (Bytes is implicitly convertible to BytesView).

    The implementation validates byte lengths, canonical point encodings, public-key subgroup membership, signature R subgroup membership, and signature S < L. The Result returning functions report malformed inputs as Err(String).

    verify and VerifyingKey::verify return false on malformed input or an invalid signature. Use verify_result or VerifyingKey::verify_result when the caller needs to distinguish malformed input from a valid-but-rejected signature.

    #Usage

    One-off signing and verification:

    ///|
    let seed : Bytes = b"\x9d\x61\xb1\x9d\xef\xfd\x5a\x60\xba\x84\x4a\xf4\x92\xec\x2c\xc4\x44\x49\xc5\x69\x7b\x32\x69\x19\x70\x3b\xac\x03\x1c\xae\x7f\x60"

    ///|
    let message : Bytes = b"license"

    ///|
    let public_key = @ed25519.derive_public_key(seed).unwrap()

    ///|
    let signature = @ed25519.sign(seed, message).unwrap()

    ///|
    let ok = @ed25519.verify(public_key, message, signature)

    For repeated signing with the same seed, create a SigningKey once. It caches the expanded scalar, prefix, and derived public key:

    ///|
    let signing_key = @ed25519.SigningKey::from_seed(seed).unwrap()

    ///|
    let public_key = signing_key.public_key()

    ///|
    let signature = signing_key.sign(message)

    For repeated verification with the same public key, create a VerifyingKey once. It caches the decoded public key and verification table:

    ///|
    let verifying_key = @ed25519.VerifyingKey::from_public_key(public_key).unwrap()

    ///|
    let ok = verifying_key.verify(message, signature)

    #Implementation Notes

    This is a plain Ed25519 implementation using SHA-512 from Tigls/mb-hash. It does not expose Ed25519ph or Ed25519ctx variants.

    Verification is intentionally strict: non-canonical point encodings, small-order public keys, small-order signature R points, and non-canonical S scalars are rejected as malformed inputs.

    The curve arithmetic uses MoonBit BigInt, extended Edwards coordinates, fixed-length 5-bit scalar windows, a cached basepoint table, and an interleaved double-scalar verification path. The fixed scalar schedule removes window-length and zero-digit branches, but this package is still not constant-time because BigInt arithmetic and table access remain data-dependent. The cached key types avoid repeated setup work when signing or verifying multiple messages with the same key material.

    #Development

    Useful commands from the repository root:

    moon fmt moon check moon test moon info

    moon info regenerates pkg.generated.mbti, which is the easiest way to review public API changes.

    Run benchmarks with:

    moon bench --release

    See BENCHMARK.md for the benchmark cases and local historical measurements. Benchmark numbers are local measurements, not performance guarantees.

    #OpenSSL Interop Check

    The interop check requires Nushell and OpenSSL:

    nu tools/openssl-interop.nu

    The script runs six scenario groups against a fresh OpenSSL Ed25519 install and the MoonBit interop binary in cmd/openssl-interop:

    1. Length matrix — signs and cross-verifies messages of 1, 2, 32, 64, 111, 112, 119, 120, 127, 128, 129, 200, 256, and 1024 bytes against one fresh key. Sizes 111/112/119/120/127/128/129 straddle the SHA-512 block + padding boundary; sizes ≥ 256 cover every byte value including NUL and high-bit.
    2. Multi-iteration — repeats the basic round-trip with a freshly generated OpenSSL key on each iteration (default 4, controlled by --iterations N).
    3. Tamper rejection — for one signed message, flips one byte in the message, the signature R, and the signature S, and asserts that both OpenSSL pkeyutl -verify and MoonBit verify reject each variant.
    4. Public-key tamper rejection — flips one byte of the verifying public key at positions 0 and 31, wraps the result into a fresh SPKI DER, and asserts both sides reject the legitimate signature under the tampered pubkey (whether decode fails or the curve math fails downstream).
    5. Reverse pubkey load — wraps the MoonBit-derived 32-byte public key into an Ed25519 SubjectPublicKeyInfo DER, loads it with OpenSSL, and verifies the MoonBit signature using that reconstructed PEM. Proves MoonBit pubkey bytes are independently OpenSSL-loadable.
    6. External-seed injection — builds an OpenSSL DER private key from an RFC 8032 vector-1 seed, derives the public key via OpenSSL, asserts it matches the RFC vector, and asserts MoonBit produces the RFC vector-1 signature for the empty message. Anchors the seed → key → signature pipeline at known coordinates. (The empty-message signing happens through MoonBit because openssl pkeyutl -sign -rawin refuses 0-byte input.)

    Pass --keep-temp to retain the working directory for inspection.

    #License

    Apache-2.0. See LICENSE.

    SigningKey

    pub struct SigningKey {
    // private fields
    }

    Expanded Ed25519 signing key for repeated signing with the same seed.

    SigningKey::from_seed

    fn SigningKey::from_seed(seed : BytesView) -> Result[SigningKey, String]

    Create an expanded signing key from a 32-byte Ed25519 private seed.

    SigningKey::public_key

    fn SigningKey::public_key(self : SigningKey) -> Bytes

    Return the 32-byte public key for this signing key.

    SigningKey::sign

    fn SigningKey::sign(self : SigningKey, message : BytesView) -> Bytes

    Sign a message with a pre-expanded Ed25519 signing key.

    VerifyingKey

    pub struct VerifyingKey {
    // private fields
    }

    Expanded Ed25519 verifying key for repeated verification.

    VerifyingKey::from_public_key

    fn VerifyingKey::from_public_key(public_key : BytesView) -> Result[VerifyingKey, String]

    Create an expanded verifying key from a 32-byte Ed25519 public key.

    VerifyingKey::public_key

    fn VerifyingKey::public_key(self : VerifyingKey) -> Bytes

    Return the 32-byte public key for this verifying key.

    VerifyingKey::verify

    fn VerifyingKey::verify(self : VerifyingKey, message : BytesView, signature : BytesView) -> Bool

    Verify a signature with a pre-expanded verifying key.

    VerifyingKey::verify_result

    fn VerifyingKey::verify_result(self : VerifyingKey, message : BytesView, signature : BytesView) -> Result[Bool, String]

    Verify a signature with a pre-expanded verifying key and report malformed inputs.

    derive_public_key

    fn derive_public_key(seed : BytesView) -> Result[Bytes, String]

    Derive a 32-byte Ed25519 public key from a 32-byte private seed.

    sign

    fn sign(seed : BytesView, message : BytesView) -> Result[Bytes, String]

    Sign a message with a 32-byte Ed25519 private seed.

    verify

    fn verify(public_key : BytesView, message : BytesView, signature : BytesView) -> Bool

    Verify a 64-byte Ed25519 signature for a message and 32-byte public key.

    verify_result

    fn verify_result(public_key : BytesView, message : BytesView, signature : BytesView) -> Result[Bool, String]

    Verify a signature and return an error for malformed public inputs.

    Source Files