ed25519

A pure MoonBit Ed25519 signing and verification package.

ed25519
signature
crypto
license
moon add hustcer/ed25519@0.5.2
Download zip
Author
Version
0.5.2
License
Apache-2.0
Last updated
5 days ago
Downloads
7K

Dependencies

README

#hustcer/ed25519

A pure MoonBit implementation of plain Ed25519 signing and verification.

The package implements deterministic Ed25519 over BytesView inputs and Bytes outputs. 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:

import { "hustcer/ed25519@0.5.2", }

Import the package from moon.pkg; its package name provides the default @ed25519 alias:

import { "hustcer/ed25519", }

#API

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

pub fn derive_public_key(BytesView) -> Bytes raise Ed25519Error
pub fn sign(BytesView, BytesView) -> Bytes raise Ed25519Error
pub fn verify(BytesView, BytesView, BytesView) -> Bool
pub fn verify_result(BytesView, BytesView, BytesView) -> Bool raise Ed25519Error

pub(all) suberror Ed25519Error {
InvalidSeedLength(got~ : Int)
InvalidPublicKeyLength(got~ : Int)
InvalidSignatureLength(got~ : Int)
PointYOutOfRange
PointNotOnCurve
PointNotCanonical
PublicKeySmallOrder
SignatureRSmallOrder
PublicKeyNotPrimeOrder
SignatureSOutOfRange
} derive(Eq, Debug)
pub impl Show for Ed25519Error

pub struct SigningKey
pub fn SigningKey::from_seed(BytesView) -> SigningKey raise Ed25519Error
pub fn SigningKey::public_key(SigningKey) -> Bytes
pub fn SigningKey::sign(SigningKey, BytesView) -> Bytes
pub fn SigningKey::verifying_key(SigningKey) -> VerifyingKey

pub struct VerifyingKey
pub fn VerifyingKey::from_public_key(BytesView) -> VerifyingKey raise Ed25519Error
pub fn VerifyingKey::public_key(VerifyingKey) -> Bytes
pub fn VerifyingKey::verify(VerifyingKey, BytesView, BytesView) -> Bool
pub fn VerifyingKey::verify_result(
VerifyingKey,
BytesView,
BytesView,
) -> Bool raise Ed25519Error

#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 prime-order subgroup membership, rejects small-order signature R points, and enforces signature S < L. Malformed inputs raise Ed25519Error, a checked error whose variants can be pattern matched precisely; its Show instance renders the same human-readable messages as the pre-0.5 string API.

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

#Usage

One-off signing and verification. The fallible functions raise the checked Ed25519Error, so call them from a raise context (or handle locally with catch):

///|
fn issue_and_check_license() -> Bool raise {
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)
let signature = @ed25519.sign(seed, message)
@ed25519.verify(public_key, message, signature)
}

To handle a malformed input precisely, match the error variants:

///|
fn check_strict(public_key : Bytes, message : Bytes, sig : Bytes) -> Bool {
@ed25519.verify_result(public_key, message, sig) catch {
@ed25519.InvalidPublicKeyLength(got~) => {
println("bad public key length: \{got}")
false
}
err => {
println("malformed input: \{err}")
false
}
}
}

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)

///|
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)

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

When you hold the SigningKey, derive its VerifyingKey directly instead of re-decoding the encoded public key. The signing key already validated its own public point, so this constructor is infallible and skips the point decode and prime-order subgroup check:

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

#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, public keys outside the prime-order subgroup, small-order signature R points, and non-canonical S scalars are rejected as malformed inputs. A canonical, non-small-order R outside the prime-order subgroup is rejected by the main verification equation and is reported as a well-formed but invalid signature.

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. SHA-512 inputs are fed in bounded chunks instead of first materializing prefix || message or R || A || message as one large array. The signing and key-derivation path scans the full basepoint table for each scalar window instead of indexing it directly by a secret digit, but this package is still not constant-time because BigInt arithmetic, branching, allocation, and verification 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 seven 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.)
  7. Non-prime-order public key — verifies a legitimate OpenSSL signature against the cofactor-mixed public key B + 4-torsion (5252cc0a…65ea). MoonBit rejects the key as malformed (public key is not in the prime-ordersubgroup) before signature math; OpenSSL loads the SPKI successfully and only fails at pkeyutl -verify.

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

#Node.js Interop Check

The Node.js interop gate requires Nushell and Node.js 22 or newer. With fnm:

fnm use 22 nu tools/node-interop.nu

CI can select an explicit executable with --node-bin <path>. The gate uses only the built-in node:crypto module and covers:

  • RFC 8032 vectors 1/2/3, including exact public-key and signature bytes.
  • Bidirectional Node.js/MoonBit signing and verification for binary messages from 0 through 1024 bytes, including SHA-512 padding boundaries.
  • A fixed application-level domain-separation mirror for context || NUL || payload; correct-context verification succeeds, while wrong-context and raw-payload variants fail on both implementations.

tools/node-interop.mjs rejects Node.js versions older than 22 and does not accept text conversions for messages: all interop inputs cross the process boundary as canonical hex and are decoded to Buffer values before signing.

#License

Apache-2.0. See LICENSE.

#
Ed25519Error

pub(all) suberror Ed25519Error {
InvalidSeedLength(got~ : Int)
InvalidPublicKeyLength(got~ : Int)
InvalidSignatureLength(got~ : Int)
PointYOutOfRange
PointNotOnCurve
PointNotCanonical
PublicKeySmallOrder
SignatureRSmallOrder
PublicKeyNotPrimeOrder
SignatureSOutOfRange
} derive(Eq,
Debug
)

Errors raised by the fallible Ed25519 operations: seed expansion, public key parsing, and strict signature verification. Match on the variants to handle failure modes precisely; Show renders the same human-readable messages the pre-0.5 Result[_, String] API returned.

#
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) -> SigningKey raise Ed25519Error

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.

#
SigningKey::verifying_key

fn SigningKey::verifying_key(self : SigningKey) -> VerifyingKey

Create the matching verifying key without decoding or re-validating the public key. The signing key's public point a*B is in the prime-order subgroup by construction, so this is infallible and skips the point decode (one modular exponentiation) and the prime-order subgroup check (one scalar multiplication) that VerifyingKey::from_public_key performs.

#
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) -> VerifyingKey raise Ed25519Error

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, collapsing malformed inputs to false.

#
VerifyingKey::verify_result

fn VerifyingKey::verify_result(self : VerifyingKey, message : BytesView, signature : BytesView) -> Bool raise Ed25519Error

Verify a signature with a pre-expanded verifying key, raising Ed25519Error for malformed inputs so callers can distinguish them from a valid-but-rejected signature.

#
derive_public_key

fn derive_public_key(seed : BytesView) -> Bytes raise Ed25519Error

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

#
sign

fn sign(seed : BytesView, message : BytesView) -> Bytes raise Ed25519Error

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, collapsing malformed inputs to false.

#
verify_result

fn verify_result(public_key : BytesView, message : BytesView, signature : BytesView) -> Bool raise Ed25519Error

Verify a signature, raising Ed25519Error for malformed public inputs so callers can distinguish them from a valid-but-rejected signature.