A pure-MoonBit, zero-dependency native cryptographic library for the MoonBit
ecosystem. No FFI, no C — every primitive is implemented in plain MoonBit and
verified against official standard vectors.
Highlights
Correct — every algorithm is checked against FIPS / NIST / RFC test
vectors, and cross-validated against reference implementations
(pycryptodome, cryptography, hashlib, libsodium, zlib) plus randomized
differential testing. 555 tests, run with moon test --deny-warn.
Fast where it matters — hex / Base64 encoding are O(n); AES MixColumns
uses precomputed GF(2^8) tables (~5x over bit-sliced math); throughput is
measured by moon bench.
Fail-fast input validation — AES / ChaCha20 / SHAKE / hex functions abort
with a clear message on wrong key / IV / nonce / tag lengths instead of
producing garbage. Graceful Result-returning variants (hex_to_bytes_or,
base64_decode_or) are provided for callers that prefer to branch on error.
Single source of truth — one-shot hash entry points delegate to the
streaming hashers, so the incremental and one-shot paths share one
implementation.
Ed25519ctx / Ed25519ph (RFC 8032 §7.2/§7.3) — context-bound and
pre-hashed variants (dom2 domain separation)
Field arithmetic over GF(2^255-19) uses @bigint; the twisted-Edwards
base point is recovered from y = 4/5. Signing is deterministic (no RNG).
Key agreement (X25519)
X25519 (RFC 7748) — Diffie-Hellman over Curve25519 via the
x-coordinate-only Montgomery ladder. Reuses the GF(2^255-19) field ops.
Encoding
Base64 (RFC 4648) — standard alphabet with padding
Hex — bytes ↔ lowercase hex
Streaming API
MD5, SHA-224/256/384/512, SHA3-224/256/384/512, and SHAKE128/256 support
incremental new / update / finalize for streaming or large inputs.
For byte-at-a-time streams prefer sha256_update_byte /
sha224_update_byte over update(hasher, Bytes::make(1, b)): no Bytes
allocation and a minimal hot path (~1.3x one-shot cost vs ~3.7x for
1-byte update chunks).
Installation
moon add cc06b/mooncry
Quick start
Create a new project, add the dependency, then import the lib package and
call its functions through the @lib alias. Dependencies are declared in
moon.pkg (per-package), not as a top-level import statement, and
assert_true is only available inside test blocks — so use println in
main.
fn main {
// SHA-256 one-shot
let digest = @lib.sha256(b"Hello, world!")
println("SHA-256: " + @lib.bytes_to_hex(digest))
// AES-GCM round-trip (256-bit key, 96-bit nonce, with AAD)
let key = Bytes::make(32, b'\x00')
let iv = Bytes::make(12, b'\x00')
let (ciphertext, tag) = @lib.aes_gcm_encrypt(b"secret data", key, iv, b"aad")
let (plaintext, ok) = @lib.aes_gcm_decrypt(ciphertext, key, iv, b"aad", tag)
let status = if ok { "OK" } else { "FAIL" }
println("AES-GCM round-trip: " + status)
println("Recovered: " + @lib.bytes_to_hex(plaintext))
// Sealed-box envelope: HKDF-SHA256 derives the AES-256-GCM key from a
// master key + context, then encrypts into a versioned envelope.
let master = Bytes::make(32, b'\x07')
let nonce = Bytes::make(12, b'\x01')
let envelope = @lib.sealed_box_seal(master, nonce, b"plaintext", b"aad", b"tenant-1")
match @lib.sealed_box_open(master, envelope, b"aad", b"tenant-1") {
Ok(pt) => println("Sealed box: " + @lib.bytes_to_hex(pt))
Err(msg) => println("Sealed box failed: " + msg)
}
}
Run it:
moon run cmd/main
SHA-256: 315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3
AES-GCM round-trip: OK
Recovered: 7365637265742064617461
A larger runnable example that validates the implementation against NIST/RFC
standard vectors lives in cmd/main of the repository
itself. Run it with moon run cmd/main.
Public API
All functions live in the lib package (cc06b/mooncry/lib), called as
@lib.<fn> after declaring the import in your moon.pkg.
Streaming hashers (<algo>_new / sha3_update / sha3_finalize /
shake_finalize) are available for MD5, SHA-224/256/384/512, SHA3-224/256/384/512,
and SHAKE128/256, plus SHA-512/224 / SHA-512/256 (sha512_224_new /
sha512_256_new with sha512_update). For SHA-3/SHAKE, sha3_update is shared and the finalize
method depends on the variant (sha3_finalize for fixed-length, shake_finalize(h, out_len) for XOF).
AES-CBC/GCM/CTR keys may be 128, 192, or 256 bits; the nonce for GCM and
ChaCha20 is 96 bits (12 bytes), the recommended length per spec. Wrong lengths
cause an abort with a descriptive message.
Security & performance boundaries
Not audited. The code is correct against known vectors but has had no
formal security review. Do not use it to protect high-value assets without an
independent audit.
AES is not constant-time. MixColumns uses precomputed GF(2^8) lookup
tables (mul2/3/9/11/13/14) for ~5x throughput. This leaks key-dependent
table indices through the CPU cache — acceptable for many use cases but
not side-channel-safe against a local attacker. Since v0.19.0 GHASH also
uses precomputed 4-bit tables (keyed by the GCM hash subkey), so it is no
longer bit-sliced and is likewise not side-channel-safe; the GCM
tag comparison and CBC PKCS#7 verification remain constant-time
(no early exit on mismatch).
Nonce reuse is catastrophic for AES-GCM and ChaCha20(-Poly1305). Never
reuse a (key, nonce) pair. The library does not track nonces — generate a
fresh one per message (e.g. a counter or CSPRNG).
PBKDF2 is a KDF, not a password hasher. For interactive password storage
prefer Argon2 / bcrypt / scrypt elsewhere; PBKDF2 is included for
compatibility with existing protocols.
MD5 is collision-broken. It is included for legacy compatibility only —
do not sign or authenticate with it.
BLAKE3 supports arbitrary-length input via the tree-Merkle mode (verified
vs the reference blake3 Python package up to 5000 bytes).
Inputs are validated, not silently padded. Wrong key / IV / nonce / tag
lengths abort immediately rather than producing wrong output.
No RNG. The library provides deterministic primitives; obtain keys, IVs,
and nonces from a secure source.
Performance
Throughput is measured by the lib benchmark suite (moon bench) on 1 KiB
inputs. Figures below are from the development sandbox; absolute numbers vary
by host — run moon bench locally for comparable figures.
moon bench
Algorithm
1 KiB (approx.)
MD5
~8.5 µs
SHA-256
~16 µs
SHA-512
~14 µs
SHA3-256
~140 µs
SHAKE128 1KiB (out=32)
~80 µs
SHAKE256 1KiB (out=64)
~92 µs
BLAKE2b
~27 µs
BLAKE3
~46 µs
HMAC-SHA256
~23 µs
HMAC-SHA3-256
~213 µs
HMAC-SHA3-512
~338 µs
AES-128-CMAC
~347 µs
SipHash-2-4
~4.2 µs
CRC32 / CRC32C
~4.7 µs
sealed_box_seal
~588 µs (HKDF + AES-256-GCM)
scrypt (N=1024,r=8,p=1,dk32)
~84 ms (memory-hard KDF)
Argon2id (t=1,m=64,p=1,dk16)
~1 ms (memory-hard KDF)
ECDSA P-256 sign
~13 ms (Jacobian, was ~270 ms affine)
ECDSA P-256 verify
~24 ms (Jacobian, was ~540 ms affine)
AES-256-SIV encrypt 1KiB
~950 µs (S2V + AES-CTR)
AES-128-KW wrap 32B
~146 µs
ChaCha20
~48 µs
ChaCha20-Poly1305 encrypt
~73 µs (was ~456 µs)
Poly1305 MAC
~7 µs (was ~387 µs, BigInt)
AES-256-CBC
~315 µs (table-based GF mul)
AES-256-GCM
~487 µs (GHASH 4-bit tables)
Base64 encode
~10 µs
Hex encode
~6.7 µs
v0.18.0 perf pass. The Keccak-f[1600] state was flattened from a
nested 5×5 Array[Array[UInt64]] to a flat 25-lane array (removing the
inner-array indirection from the hot permutation loop), and ChaCha20 now
expands the key/nonce words once per call instead of per block. Measured
on 1 KiB inputs: SHAKE128 ~123 → ~80 µs (-35%), SHAKE256 ~136 → ~92 µs
(-32%), ChaCha20 ~54 → ~48 µs (-11%).
v0.19.0 perf pass. GHASH (the GF(2^128) multiply inside AES-GCM) was
rewritten from a 128-iteration bit-serial loop to a precomputed 4-bit
table (32 nibble lookups + XORs per multiply), cutting AES-256-GCM from
~507 to ~487 µs/KiB. Poly1305 was rewritten from per-block @bigint
arithmetic to 5-limb radix-2^26 arithmetic with UInt64 partial products
(“donna” style): Poly1305 drops from ~387 to ~7 µs/KiB (-98%), which
takes ChaCha20-Poly1305 AEAD from ~456 to ~73 µs/KiB (-84%) and also
speeds up XChaCha20-Poly1305 and the sealed-box envelope.
v0.20.0 perf pass. ECDSA P-256 point arithmetic moved from affine to
Jacobian projective coordinates. Affine double/add each need one
Fermat-exponentiation modular inverse (256 field muls), so a 256-bit
scalar multiplication cost ~384 inverses; Jacobian double/add need none,
inverting only once when converting back to affine. All field
subtractions stay non-negative (the X25519 BigInt lesson). Measured on
one host: sign ~270 → ~13 ms and verify ~540 → ~24 ms (**21-23x**).
Signatures are byte-identical to before (RFC 6979 deterministic k is
unchanged), so no compatibility break.
v0.21.0 perf pass. Two fronts. (1) SHA-256 streaming: the hasher now
reuses one 64-word message schedule instead of allocating 16- and 64-word
arrays per block, reads buffer words without an intermediate Bytes, and
finalizes with in-place padding (no padded-array allocation); one-shot
SHA-256 1KiB drops 26.9 → ~20.7 µs (-23%). New sha256_update_byte /
sha224_update_byte give byte-at-a-time streams a zero-allocation hot
path: ~26.3 µs/KiB, only ~1.3x one-shot (the old 1-byte update pattern
is ~3.7x). (2) RSA: all four private-key operations (PKCS1-v1.5
sign/decrypt, OAEP decrypt, PSS sign) gain _crt variants taking the CRT
key (p, q, dP, dQ, qInv) and doing two half-width exponentiations
instead of one full-width (RFC 8017 §5.1.2) — ~17.6-18.5 ms → ~6.8-7.0 ms
on RSA-1024 (**2.5-2.6x**). CRT output is byte-identical to the
single-exponentiation path (deterministic padding), verified in-tree.
The PSS/OAEP emBits computation no longer allocates a ~k-char binary
string per call.
v0.22.0 features. Three additions. (1) ECDSA secp256k1 (Bitcoin
curve, y² = x³ + 7): sign/verify/public-key with the same RFC 6979 +
SHA-256 deterministic construction as P-256. The point/RFC-6979 code is
now curve-parameterized (P-256 keeps its fast a = −3 doubling; secp256k1
uses the a = 0 specialization), and P-256 signatures stay byte-identical.
Verified against the Python ecdsa library
(sign_digest_deterministic) plus cryptography cross-verification;
signatures are not low-S-normalized (verify accepts any s in [1, n−1]);
use ecdsa_secp256k1_sign_low_s for BIP-62 canonical output (v0.23.0).
(2) Multi-hash RSA signatures: rsa_pkcs1_v15_sign_with /
rsa_pkcs1_v15_verify_with / rsa_pss_sign_with / rsa_pss_verify_with
take an RsaHash (SHA-1/256/384/512) for the message hash, DigestInfo
(v1.5) and MGF1 (PSS). The plain functions stay fixed to SHA-256. Verified
byte-for-byte against pycryptodome. (3) Incremental GMAC
(gmac_new / gmac_update / gmac_finalize): chunked AAD over the same
table-accelerated GHASH, equal to the one-shot gmac tag (verified
in-tree and against pycryptodome GCM-empty tags).
v0.23.0 features. Two additions. (1) secp256k1 low-S signatures
(ecdsa_secp256k1_sign_low_s): BIP-62 canonical form — if s > n/2 it is
replaced by n − s (r unchanged, still valid and deterministic). Verified
against the Python ecdsa library’s sigencode_string_canonize plus
cryptography. (2) Multi-hash RSA-OAEP: rsa_oaep_encrypt_with /
rsa_oaep_decrypt_with take an RsaHash for lHash and MGF1 (the plain
functions stay fixed to SHA-256). Exact ciphertexts verified against
pycryptodome (fixed seed via its randfunc hook) plus random-seed interop;
SHA-512 vectors use a dedicated 2048-bit key since OAEP-SHA512 needs
k ≥ 2·hLen + 2 + |M| (impossible on 1024 bits).
v0.24.0 features. Two additions. (1) P-256 low-S signatures
(ecdsa_p256_sign_low_s): the WebCrypto / widely-mandated canonical form —
if s > n/2 it is replaced by n − s (r unchanged, still valid and
deterministic); ecdsa_p256_verify accepts both forms. Verified against
the Python ecdsa library’s sigencode_string_canonize plus
cryptography, with a property test asserting s ≤ n/2 byte-wise. (2)
GMAC with any IV length (gmac_iv / gmac_iv_verify one-shot and
gmac_new_iv incremental): SP 800-38D §8.2.1 — 96-bit IVs keep the fast
J0 = IV || 0³¹ || 1 path; other lengths derive J0 via GHASH over
IV || 0^(s+64) || [len(IV)]₆₄. Verified against pycryptodome GCM-empty
tags for IV lengths 1/8/16/20/32 plus 12-byte fast-path equality and
streaming-vs-one-shot properties.
v0.25.0 features.CRT × multi-hash combinations: the CRT speed of
v0.21.0 and the hash choice of v0.22.0 are now combinable —
rsa_pkcs1_v15_sign_with_crt and rsa_pss_sign_with_crt take both the
CRT key (p, q, dP, dQ, qInv) and an RsaHash. Deterministic padding makes
them byte-identical to the pycryptodome-verified _with paths (transitive
verification across all four hashes, plus a salt_len=0 deterministic case).
v0.26.0 features.The 448-bit suite. (1) Ed448 (RFC 8032 §5.2):
pure EdDSA over edwards448 (Goldilocks field p = 2⁴⁴⁸ − 2²²⁴ − 1), SHAKE256-based hashing with dom4 domain separation,
context support (ed448_sign_ctx / ed448_verify_ctx), cofactor
verification equation [4][S]B = [4]R + [4][k]A. Verified against all nine
official RFC 8032 §7.4 vectors incl. the 1023-octet message and the
context-carrying one, plus tamper/context-mismatch/S≥L negative tests.
(2) X448 (RFC 7748): Montgomery-ladder Diffie-Hellman over curve448
with RFC §5 clamping. Verified against the official RFC 7748 §5.2 vectors,
the §6.1 DH triple, and the single-iteration K=u=5 vector, plus an ECDH
commutativity property.
v0.27.0 features.Post-quantum: ML-KEM (FIPS 203) — the NIST
standard module-lattice KEM, all three parameter sets (ML-KEM-512/768/1024).
Pure MoonBit on UInt (q = 3329), NTT-domain arithmetic with the standard
zeta/gamma tables, rejection sampling from a true incremental SHAKE128
stream, CBD noise sampling, Compress/ByteEncode for d = 1/4/5/10/11/12,
and the full K-PKE + ML-KEM algorithm set (KeyGen/Encaps/Decaps_internal,
implicit rejection included). The API is the deterministic internal form:
callers supply CSPRNG randomness (d, z for keygen; m for encaps).
Lesson learned during this work: the FIPS 203 final text instantiates
G = SHA3-512 and H = SHA3-256 (not SHAKE256 as in earlier drafts) — only
J and PRF remain SHAKE256. Verified against the official NIST ACVP vectors
(keyGen + encapsulation + decapsulation, incl. implicit-rejection cases)
for all three parameter sets, plus round-trip and tamper properties.
v0.28.0 features.Post-quantum: ML-DSA (FIPS 204) — the NIST
module-lattice signature standard, all three parameter sets
(ML-DSA-44/65/87). Pure-MoonBit port: NTT over q = 8380417 (Int64-widening
multiplies — Int is 32-bit on wasm-gc), rejection sampling
(SampleNTT/SamplePolyCBD/ExpandMask), Power2Round/Decompose/HighBits/
LowBits/MakeHint/UseHint, hint bit-pack/unpack, and the full
K-PKE + ML-DSA KeyGen/Sign/Verify internal algorithms (pure message
interface, hedged or deterministic via the rnd input). Verified against
the official NIST ACVP vectors: keyGen (all sets), sigGen pure
deterministic, and the full sigVer external-pure suite including every
rejection category. HashML-DSA and external-mu followed in v0.29.0/v0.30.0.
v0.29.0 features.ML-DSA HashML-DSA + external-mu interfaces —
completes the FIPS 204 algorithm surface. HashML-DSA (Algorithms 4/5):
M’ = 0x01 || octet(|ctx|) || ctx || OID(PH) || PH(M) with the DER OID
tag of the pre-hash function (all 12 NIST digests/XOFs; SHAKE-128
pre-hash output 32 B, SHAKE-256 64 B). External-mu (Algorithms 7/8):
sign/verify with the 64-byte message digest mu supplied externally.
Verified against the official NIST ACVP vector sets (FIPS204-tr1 sigGen
pre-hash, deterministic, byte-exact; full sigVer pre-hash suite; all
rejection categories).
v0.30.0 features.ML-DSA external-mu ACVP coverage — the
external-mu entry points shipped in v0.29.0 (*_sign_mu /
*_verify_mu) are now pinned against the official NIST ACVP
external-mu vector groups: sigGen deterministic (byte-exact, mu
supplied) and the full sigVer external-mu suite including all rejection
categories. ML-DSA’s complete FIPS 204 surface — pure, pre-hash and
external-mu interfaces — is now vector-verified (789 tests).
v0.31.0 features.SLH-DSA (SPHINCS+, FIPS 205) — the NIST
stateless hash-based signature standard, all 12 approved parameter sets
(SLH-DSA-{SHA2,SHAKE}-{128,192,256}{s,f}). Pure-MoonBit port: WOTS+
chains, XMSS Merkle trees, the hypertree, FORS few-time signatures,
the SHA2 instance (MGF1-SHA-256/512 based H_msg, compressed 22-byte
addresses, category-dependent hash selection) and the SHAKE instance
(SHAKE256 based). Verified against the official NIST ACVP vectors:
keyGen (all sets), sigGen deterministic (pure, pre-hash with OID
tagging, and internal/raw-M’ semantics) and sigVer. Note: SLH-DSA is
deliberately expensive — signing runs thousands of hash calls; the full
849-test suite takes ~17 minutes.
v0.32.0 features.SLH-DSA speed-up (~16%) + hasher cloning.
SLH signing now runs through a per-key hash suite: the SHA2/SHAKE
PK.seed prefix is absorbed once and reused via zero-allocation
working hashers (state reset per call, prefix bytes restored). New
public utilities sha256_clone / sha512_clone / sha3_clone
deep-copy streaming hashers for hashing many messages that share a
prefix. Note a subtle trap now documented in the test history: when a
buffered prefix is shorter than one block, multi-block absorptions
overwrite the prefix region of the buffer, so a reused hasher must
restore it.
v0.33.0 features.ML-KEM hybrid encryption (KEM + DEM) —
ml_kem_{512,768,1024}_hybrid_seal / hybrid_open plus streaming
hybrid_seal_init + ml_kem_hybrid_stream_update / stream_final.
Composition: (K, c) = Encaps(ek, m); (key, nonce) = HKDF-SHA256(salt =
c, ikm = K, info = “mooncry/ml-kem-hybrid/v1”, 44); blob = c ||
ChaCha20-Poly1305(key, nonce, aad, msg). open returns None on any
failure (KEM implicit rejection surfaces as a tag mismatch). New
incremental Poly1305 (poly1305_new / update / finalize; the one-shot
is now built on it). Also fixes a latent HMAC bug: keys longer than
one block were not zero-padded after hashing (ipad/opad truncated);
RFC 4231 TC6 regression vectors added. Verification: KEM I/O from the
official NIST ACVP vectors (FIPS 203); the composition layer is
cross-checked against pycryptodome.
v0.34.0 features.SLH-DSA ~25% faster (suite 17 min → 13 min).
Two allocation-removal passes on the per-node hash hot path:
sha256_finalize_n / sha512_finalize_n write truncated digests
directly (one allocation instead of finalize + slice), and T_l over
WOTS+/FORS public values now feeds the pre-split blocks into the hasher
without concatenating. Regression-tested against truncated one-shot
digests. (A moonc 0827 compiler ICE forced the finalize_n internals to
be factored into a shared padding helper — noted for future toolchains.)
v0.35.0 features.X-Wing hybrid KEM
(draft-connolly-cfrg-xwing-kem) — the ML-KEM-768 + X25519 composite
KEM: xwing_keygen (32-byte seed → 1216-byte pk), xwing_encaps
(derandomized in a 64-byte eseed) and xwing_decaps (implicit
rejection inherited from ML-KEM). Verified against the official draft
test vectors (keygen, encapsulation and decapsulation for all three
published vectors).
v0.36.0 features.TurboSHAKE + KangarooTwelve (RFC 9861) —
turbo_shake_128 / turbo_shake_256 (Keccak-p[1600,12] sponge with a
domain byte; 12-round permutation factored out of the SHA-3 core) and
kangaroo_twelve_128 / kangaroo_twelve_256 (Sakura tree hash over
8192-byte chunks with a customization string). Verified against every
applicable RFC 9861 test vector, including the 8191/8192-byte tree
boundary and customization-string cases.
v0.37.0 features.HPKE — Hybrid Public Key Encryption (RFC
9180). DHKEM(X25519, HKDF-SHA256) and DHKEM(X448, HKDF-SHA512);
KDFs HKDF-SHA256/SHA512; AEADs AES-128-GCM, AES-256-GCM and
ChaCha20Poly1305; all four modes (base / psk / auth / auth+psk).
hpke_setup_s / hpke_setup_r (derandomized via explicit ephemeral
IKM), hpke_seal / hpke_open with automatic sequence numbers
(None on authentication failure), and hpke_export. Verified
against the official RFC 9180 Appendix A vectors (every mode of the
X25519 suites, including the sequence-number carry at 255/256);
X448 covered by round-trip tests.
v0.38.0 features.HPKE completes: DHKEM(P-256, HKDF-SHA256)
with rejection-sampling key derivation and uncompressed-point
encoding, covering the remaining official RFC 9180 Appendix A suites
(A.3-A.5, all four modes each), including the mixed
DHKEM(P-256)+HKDF-SHA512 ciphersuite where the KEM-internal KDF
differs from the suite KDF. All three DHKEMs the RFC defines vectors
for are now vector-verified.
v0.39.0 features.AES-GCM-SIV (RFC 8452) — the
nonce-misuse-resistant AEAD: aes_gcm_siv_encrypt / aes_gcm_siv_decrypt for AEAD_AES_128_GCM_SIV and
AEAD_AES_256_GCM_SIV. POLYVAL is implemented via the RFC Appendix A
equivalence with GHASH (ByteReverse + mulX), reusing the library’s
table-accelerated GF(2^128) multiplier. Verified against all 48
official RFC 8452 Appendix C vectors (encrypt byte-exact + decrypt
round-trip).
v0.40.0 features.HPKE completes: DHKEM(P-521, HKDF-SHA512) —
rejection sampling with the 0x01 bitmask over 66-byte candidates,
133-byte uncompressed points, and the library’s generic BigInt scalar
multiplication (P-521 also uses a = -3). The official RFC 9180
Appendix A.6 vectors (all four modes) now pass — every HPKE ciphersuite
the RFC publishes vectors for is vector-verified.
v0.41.0 features.HPKE DHKEM(P-384, HKDF-SHA384) + HMAC/HKDF-SHA384
— the HPKE KEM table is now complete (P-256/P-384/P-521/X25519/X448).
New public hmac_sha384 (with the long-key zero-padding handled) and
hkdf_sha384 / hkdf_sha384_extract, regression-tested against
pycryptodome (RFC 4231 TC6-style long key). P-384 HPKE is verified by
differential vectors from an independent Python oracle (the HPKE
composition machinery itself is RFC-vector-verified via P-256/P-521).
v0.53.0 features.Performance: Ed448 Shamir verification +
housekeeping. Ed448 verify rewrites the cofactor equation
[4][S]B = [4]R + [4][k]A as [4](S*B - k*A - R) = O and evaluates it
with one interleaved-window double-scalar multiplication instead of
two independent ones (using L-k for -k is cofactor-safe: the
difference is 4 = O for ANY point since the group order is
4L). Ed448 verify 15.2 → 12.0 ms; the base point is hoisted to a
constant. ML-DSA’s dq_mul keeps i64.rem after THREE measured-negative
alternatives (f64 reciprocal, folding with a subtraction loop, and
branch-free folding over q = 2^23-2^13+1) — all documented in-code so
nobody retries them. Also normalized 118 stray NUL bytes inside byte
literals across 22 files to escapes (semantics unchanged; the
files are text-clean for grep/diff again).
v0.52.0 features.Performance: word-oriented scrypt. The
Salsa20/8 core, scryptBlockMix and ROMix now operate on UInt word
arrays in place (V table = a single flat allocation; XORs and
interleaving at word granularity; no per-block Bytes conversions) —
~1.4x on the memory-hard loop. RFC 7914 vectors unchanged.
v0.51.0 features.Performance: native P-256 field and curve
arithmetic. New eight-32-bit-limb implementation of GF(p256):
schoolbook multiply with UInt64 accumulators (provably overflow-free)
and the NIST Solinas fast reduction — the fold table for
2^256 = 2^224 - 2^192 - 2^96 + 1 is DERIVED at init by a fixpoint
loop (cross-validated in Python against modular arithmetic on random
x < p^2) and the prime words come from the existing p256_curve
constant. Jacobian doubling/addition (a = -3), 4-bit windowed scalar
multiplication and Shamir double-scalar multiplication mirror the
vector-verified generic code; ec_scalar_mult and
ec_double_scalar_mult_jac dispatch to the native path whenever the
curve modulus is P-256’s, so ECDSA, low-S signing, and HPKE P-256 DH
all benefit. ECDSA P-256 sign 5.6 → 3.6 ms, verify 7.1 → 4.5 ms
(3.5x / 5x versus the pre-optimization BigInt baseline). Differential
tests against the generic BigInt path (64 random field multiplies,
10 scalar multiples) run permanently in the suite.
v0.50.0 features.Performance: matrix-expansion caching for the
lattice schemes. ML-DSA re-expanded the full k x l A matrix (FIPS 204
ExpandA, k*l rejection-sampled polynomials) on every sign, verify and
keygen, and ML-KEM did the same with its k x k A-hat (FIPS 203 XOF
rejection sampling); both now live in single-slot caches keyed by
(rho, dimensions) — A depends only on the key’s public seed, never on
the message. ML-DSA-65 verify 1.78 ms → 0.61 ms (2.9x), sign 8.4 →
7.2 ms; ML-KEM-768 keygen 620 → 364 µs (1.7x), encaps 623 → 389 µs
(1.6x), decaps 583 µs. New benchmarks: ml_dsa_65 sign/verify,
ml_kem_768 keygen/encaps/decaps. Cached matrices are contractually
read-only for all consumers.
v0.49.0 features.Performance: native Curve448-Goldilocks field
arithmetic (radix 2^28, sixteen signed Int64 limbs). p448 = 2^448 -
2^224 - 1 folds via 2^448 = 2^224 + 1; the fold coefficients are
derived per (i,j,k) by pure arithmetic in the multiply inner loop
(verified term-by-term against the recursive definition — no tables,
no hand-copied constants), the subtraction bias is the limb
decomposition of p448 computed at init. X448 runs a native Montgomery
ladder (a24 = 39081, RFC 7748 clamp); Ed448 point arithmetic (RFC
8032 A.4 projective formulas), encode/decode and the decoding square
root all move onto the new limbs, with 4-bit window scalar
multiplication. Ed448 sign 27.2 → 13.2 ms (2.1x), verify 29.8 →
15.2 ms; X448 10.4 → 6.1 ms (1.7x). Validated by a Python prototype
against the official RFC 7748 §6.2 X448 vectors (Alice/Bob/shared
secret) and the §7.1 iteration value before porting; all RFC 8032
§7.4 Ed448 vectors green.
v0.48.0 features.Performance: division-free Ed448/X448 field
arithmetic — the Barrett playbook from v0.46 applied to the 448-bit
Goldilocks prime: conditional add/sub, Barrett modular multiply, and
precomputed MSB-first bit chains for inversion (p-2) and the decoding
square root (exponent (p+1)/4). Ed448 sign
27.2 → 21.3 ms, verify 29.8 → 23.5 ms; X448 10.4 → 7.9 ms. New
benchmarks for Ed448/X448.
v0.47.0 features.XMSS / XMSS^MT eXtended Merkle Signatures
(RFC 8391) — the second major stateful hash-based family (with
LMS/HSS), ported 1:1 from the official reference implementation
(github.com/XMSS/xmss-reference, CC0, by the RFC authors). WOTS+
(w=16) with PRF_keygen-derived chain seeds, tweakable hashes thash_f/
thash_h with per-address PRF keys/masks, L-tree WOTS-pk compression,
stack-based treehash with auth-path extraction, and the XMSS^MT
layer machinery (d layers, per-layer subtree addresses). All five
domain-tagged hash instantiations: SHA2 with n=24/32/64 and SHAKE128/
SHAKE256 with n=32/64. Verified against the reference’s deterministic
KATs (seed[i]=i, msg=0x25, index=2^(H-1)): WOTS+ pk/sign/pk-from-sig/
leaf byte-exact for all 7 parameter sets; XMSS^MT (H=20/2_256 family,
4 layers, all 7 sets) keygen+sign byte-exact; XMSS H10 verify for all
7 sets plus byte-exact keygen+sign for SHA2_10_256. Completes the
three-family post-quantum signature coverage: ML-DSA (lattice,
stateless), SLH-DSA (hash, stateless), LMS+XMSS (hash, stateful).
v0.46.0 features.Performance: division-free EC arithmetic.
Every modular multiply on the NIST curves (P-256, P-384, P-521) and
secp256k1 now reduces with a per-curve Barrett constant
(mu = floor(2^2k / p), derived once at init) instead of BigInt
division; field add/sub became conditional add/sub (no reduction op at
all); modular inversion (field and scalar order) runs a precomputed
MSB-first bit chain over the Barrett multiply instead of the generic
modular exponentiation. ECDSA P-256 sign 8.2 → 5.6 ms (1.46x),
verify 9.5 → 7.1 ms (1.33x); secp256k1 and all HPKE NIST-curve DH
benefit identically.
v0.45.0 features.Performance: native Curve25519 field
arithmetic (radix 2^25.5, ten signed Int64 limbs — donna/ref10 style).
New fe25519 core with multiplication coefficients DERIVED at init
from the shift table (never hand-copied), precomputed fixed-exponent
bit patterns (p-2 inversion, (p+3)/8 square root), and one raw
ladder pass. X25519 moves off the generic BigInt ladder:
4.34 → 1.80 ms (2.4x). Ed25519 group arithmetic (extended
coordinates), point decoding (native sqrt instead of two BigInt
modular exponentiations) and encoding all run on the new limbs:
sign 10.3 → 4.3 ms (2.4x), verify 9.9 → 3.7 ms (2.7x); Ed25519ctx/ph
ride the same path. Keccak gained flat lane-index tables (rho/pi/chi
without nested lookups or per-round mod-5) and copy-free SHA3 absorb.
New tests: RFC 7748 §6.1 vectors + iteration-1, ECDH commutativity
property, and public-key consistency (the BigInt ladder is retained
in the test tree as a differential reference).
v0.44.0 features.Performance: T-table AES core + LMS fast
chains (benchmarked, wasm-gc). The AES block core is rewritten in
the classic OpenSSL T-table style (four 256-entry UInt tables fusing
SubBytes+ShiftRows+MixColumns into one lookup per output word; the
decryption key schedule applies InvMixColumns to rounds 1..nr-1 per
the equivalent inverse cipher). Every AES mode benefits:
AES-256-GCM 439 → 104 µs/KiB (4.2x), CTR 63.5, CBC 79.4, SIV 188,
CMAC 68.8 µs/KiB; GCM-SIV, CCM and KW ride the same core. LMS/HSS
chain hashing moved to a shared pre-padded SHA-256 block template
(one raw compression per Winternitz step, zero per-hash allocations):
LMS test workload ~1.9x, key generation several times faster.
v0.43.0 features.LMS / HSS hash-based signatures (RFC 8554)
— the Leighton-Micali Signature scheme and its Hierarchical variant,
SHA256 parameter sets (LMOTS_SHA256_N32_W1/W2/W4/W8 x
LMS_SHA256_M32_H5/H10/H15/H20/H25). Stateful: lms_sign consumes one
Merkle leaf per signature and aborts at exhaustion; HSS chains L
levels with fixed inter-level signatures (caller re-keys on bottom-level
exhaustion). Pseudorandom key generation follows RFC 8554 Appendix A
(SEED/I). Verified against the official RFC 8554 Appendix F test cases
(HSS L=2 verify + public-key regeneration from SEED/I) plus 12
oracle-generated vectors covering all four Winternitz widths, tree
heights 5/10/15, non-zero leaf counters, and HSS L=1/2/3, and
sign/verify round-trips. Completes the hash-based signature families
alongside SLH-DSA (stateless).
v0.42.0 features.Performance polish (benchmarked, wasm-gc).
Ed25519 rewritten on extended twisted Edwards coordinates (X:Y:Z:T,
Hisil et al. formulas) with 4-bit fixed-window scalar multiplication
and Shamir double-scalar verification: sign 855 ms → 10.3 ms (83x),
verify 870 ms → 9.9 ms (88x). ECDSA P-256 (and secp256k1, and the
HPKE NIST-curve DH) switched from string double-and-add to 4-bit
windows + Shamir verify: P-256 sign 12.4 → 10.2 ms, verify 22.7 →
12.3 ms. BLAKE3 chunk compression made zero-allocation (ping-pong
message schedule): 64 → 20 µs/KiB (3.2x), now faster than BLAKE2b.
New benchmarks for Ed25519/X25519.
Hashes, ChaCha20, and hex/Base64 are throughput-bound by the algorithm; AES
trades constant-time property for ~5x speed via lookup tables (see
Security & performance boundaries).
Testing
Every algorithm is verified against official standard vectors:
The CI (.github/workflows/moonbit-ci.yml) installs the latest MoonBit
toolchain and runs the four required checks — moon check --deny-warn,
moon fmt --check, moon info, moon test --deny-warn — and verifies that
no build artifacts are tracked. Run them locally:
moon check --deny-warn
moon fmt --check
moon info
moon test --deny-warn
moon bench # run the benchmark suite
The module manifest is moon.mod (TOML); per-package manifests are moon.pkg
(TOML). Build outputs (_build/, generated .mbti) are gitignored and must
not be committed.
Publishing (maintainers)
moon.mod declares name = "cc06b/mooncry", license Apache-2.0. Publishing
requires the owner of the cc06b namespace to be logged in:
moon login # one time, with the account that owns cc06b
moon publish # publishes the current version
Before publishing, ensure all four checks above pass and the tree is clean.
mooncry
A pure-MoonBit, zero-dependency native cryptographic library for the MoonBit ecosystem. No FFI, no C — every primitive is implemented in plain MoonBit and verified against official standard vectors.
Highlights
moon test --deny-warn._withvariants**)**, ECDSA P-256 / secp256k1, Ed25519 (incl. Ed25519ctx / Ed25519ph), Ed448 (incl. contexts), X25519 / X448, ML-KEM-512/768/1024 (FIPS 203 post-quantum KEM), HOTP/TOTP (incl. SHA-256/SHA-512 variants), SipHash-2-4, CRC32/CRC32C/CRC-64/Adler-32, a sealed-box AEAD envelope, Base64, Hex.moon bench.Result-returning variants (hex_to_bytes_or,base64_decode_or) are provided for callers that prefer to branch on error.Repositories
Both are kept in sync. To add the mirror locally:
Algorithms
Hash functions
Extendable-output functions (XOF)
Message authentication
gmac_new/gmac_update/gmac_finalize)Symmetric ciphers / AEAD
Key derivation
Checksums / PRFs
Composite envelope
version(1) ‖ nonce(12) ‖ ciphertext ‖ tag(16).Asymmetric (RSA)
Asymmetric signatures (Ed25519)
@bigint; the twisted-Edwards base point is recovered from y = 4/5. Signing is deterministic (no RNG).Key agreement (X25519)
Encoding
Streaming API
MD5, SHA-224/256/384/512, SHA3-224/256/384/512, and SHAKE128/256 support incremental
new/update/finalizefor streaming or large inputs. For byte-at-a-time streams prefersha256_update_byte/sha224_update_byteoverupdate(hasher, Bytes::make(1, b)): noBytesallocation and a minimal hot path (~1.3x one-shot cost vs ~3.7x for 1-byteupdatechunks).Installation
Quick start
Create a new project, add the dependency, then import the
libpackage and call its functions through the@libalias. Dependencies are declared inmoon.pkg(per-package), not as a top-levelimportstatement, andassert_trueis only available insidetestblocks — so useprintlninmain.Edit
cmd/main/moon.pkgto import the library:Edit
cmd/main/main.mbt:Run it:
A larger runnable example that validates the implementation against NIST/RFC standard vectors lives in
cmd/mainof the repository itself. Run it withmoon run cmd/main.Public API
All functions live in the
libpackage (cc06b/mooncry/lib), called as@lib.<fn>after declaring the import in yourmoon.pkg.md5(data : Bytes) -> Bytessha224 / sha256 / sha384 / sha512(data : Bytes) -> Bytessha512_224 / sha512_256(data : Bytes) -> Bytesripemd160(data : Bytes) -> Bytessha3_224 / sha3_256 / sha3_384 / sha3_512(data : Bytes) -> Byteskeccak_256(data : Bytes) -> Bytesshake_128 / shake_256(data : Bytes, out_len : Int) -> Bytesout_lenbytescshake_128 / cshake_256(data, n, s : Bytes, out_len : Int) -> Byteskmac_128 / kmac_256(key, data, s : Bytes, out_len : Int) -> Byteskmac_xof_128 / kmac_xof_256(key, data, s : Bytes, out_len : Int) -> Bytesblake2b(data : Bytes, out_len : Int) -> Bytesout_len1..64blake2b_keyed(data, key : Bytes, out_len : Int) -> Bytesblake2s(data : Bytes, out_len : Int) -> Bytesout_len1..32blake2s_keyed(data, key : Bytes, out_len : Int) -> Bytesblake3(data : Bytes) -> Bytesblake3_xof(data : Bytes, out_len : Int) -> Byteshmac_sha256 / hmac_sha512(key, msg : Bytes) -> Byteshmac_sha3_256 / hmac_sha3_512(key, msg : Bytes) -> Byteshmac_sha3_224 / hmac_sha3_384(key, msg : Bytes) -> Bytespoly1305(key, msg : Bytes) -> Bytescmac_aes(data, key : Bytes) -> Bytesaes_encrypt_cbc / aes_decrypt_cbc(data, key, iv) -> Bytesaes_gcm_encrypt(pt, key, iv, aad) -> (Bytes, Bytes)aes_gcm_decrypt(ct, key, iv, aad, tag) -> (Bytes, Bool)aes_ccm_encrypt(pt, key, nonce, aad, mac_len) -> Bytesaes_ccm_decrypt(input, key, nonce, aad, mac_len) -> Bytesgmac(key, iv, aad) -> Bytesgmac_verify(key, iv, aad, tag) -> Boolgmac_new(key, iv) -> GmacStategmac_new_iv(key, iv) -> GmacStategmac_update(st, aad_chunk)gmac_finalize(st) -> Bytesgmac_iv(key, iv, aad) -> Bytesgmac_iv_verify(key, iv, aad, tag) -> Boolaes_ctr(data, key, iv) -> Byteschacha20_xor(input, key, nonce, counter) -> Bytessalsa20_keystream_block(key, nonce, counter) -> Bytessalsa20_xor(key, nonce, counter, data) -> Byteschacha20_poly1305_encrypt(key, nonce, aad, pt) -> Byteschacha20_poly1305_decrypt(key, nonce, aad, input) -> Byteshchacha20(key, in16 : Bytes) -> Bytesxchacha20_xor(input, key, nonce24, counter) -> Bytesxchacha20_poly1305_encrypt(key, nonce24, aad, pt) -> Bytesxchacha20_poly1305_decrypt(key, nonce24, aad, input) -> Byteshotp_sha256 / hotp_sha512(key, counter, digits) -> Stringtotp_sha256 / totp_sha512(key, unix_time, step, digits) -> Stringed25519ctx_sign(seed, msg, ctx) / ed25519ctx_verify(pk, msg, sig, ctx)ed25519ph_sign(seed, msg, ctx) / ed25519ph_verify(pk, msg, sig, ctx)adler32(data : Bytes) -> Byteshkdf_sha256(salt, ikm, info, len) -> Byteshkdf_sha512(salt, ikm, info, len) -> Bytespbkdf2_hmac_sha256(password, salt, iterations, len) -> Bytespbkdf2_hmac_sha512(password, salt, iterations, len) -> Bytespbkdf2_hmac_sha1(password, salt, iterations, len) -> Byteshkdf_sha3_256(salt, ikm, info, len) -> Bytespbkdf2_hmac_sha3_256(password, salt, iterations, len) -> Bytesscrypt(password, salt, n, r, p, dklen) -> Bytesnpower of tworsa_pkcs1_v15_encrypt(msg, n, e, rand_ps) -> Bytesrsa_pkcs1_v15_decrypt(ct, n, d) -> Bytesrsa_pkcs1_v15_sign(msg, n, d) -> Bytesrsa_pkcs1_v15_verify(msg, sig, n, e) -> Boolrsa_oaep_encrypt(msg, n, e, seed, label) -> Bytesrsa_oaep_decrypt(ct, n, d, label) -> Bytesrsa_oaep_encrypt_with(msg, n, e, seed, label, hash) -> Bytesrsa_oaep_decrypt_with(ct, n, d, label, hash) -> Bytesrsa_pss_sign(msg, n, d, salt) -> Bytesrsa_pss_verify(msg, sig, n, e, salt_len) -> Boolrsa_pkcs1_v15_sign_crt(msg, p, q, dp, dq, qinv) -> Bytesrsa_pkcs1_v15_decrypt_crt(ct, p, q, dp, dq, qinv) -> Bytesrsa_oaep_decrypt_crt(ct, p, q, dp, dq, qinv, label) -> Bytesrsa_pss_sign_crt(msg, p, q, dp, dq, qinv, salt) -> Bytesrsa_pkcs1_v15_sign_with(msg, n, d, hash) -> Bytesrsa_pkcs1_v15_verify_with(msg, sig, n, e, hash) -> Boolrsa_pss_sign_with(msg, n, d, salt, hash) -> Bytesrsa_pss_verify_with(msg, sig, n, e, salt_len, hash) -> Boolrsa_pkcs1_v15_sign_with_crt(msg, p, q, dp, dq, qinv, hash) -> Bytesrsa_pss_sign_with_crt(msg, p, q, dp, dq, qinv, salt, hash) -> Bytesed25519_public_key(seed) -> Bytesed25519_sign(seed, message) -> Bytesed25519_verify(public_key, message, sig) -> Boolecdsa_p256_public_key(sk) -> Bytesecdsa_p256_sign(sk, message) -> Bytesecdsa_p256_sign_low_s(sk, message) -> Bytesecdsa_p256_verify(pk, message, sig) -> Boolecdsa_secp256k1_public_key(sk) -> Bytesecdsa_secp256k1_sign(sk, message) -> Bytesecdsa_secp256k1_sign_low_s(sk, message) -> Bytesecdsa_secp256k1_verify(pk, message, sig) -> Boolx25519(scalar, u) -> Bytesx25519_public_key(private_key) -> Bytesed448_public_key(seed) -> Bytesed448_sign(seed, message) -> Bytesed448_sign_ctx(seed, message, ctx) -> Bytesed448_verify(pk, message, sig) -> Booled448_verify_ctx(pk, message, sig, ctx) -> Boolx448(scalar, u) -> Bytesx448_public_key(private_key) -> Bytesml_kem_512_keygen(d, z) -> (ek, dk)ml_kem_512_encaps(ek, m) -> (K, c)ml_kem_512_decaps(dk, c) -> Bytesml_kem_768_keygen / encaps / decapsml_kem_1024_keygen / encaps / decapsml_kem_768_hybrid_seal(ek, m, aad, msg)ml_kem_768_hybrid_open(dk, blob, aad) -> Option[Bytes]Noneon any failureml_kem_768_hybrid_seal_init + ml_kem_hybrid_stream_update/finalpoly1305_new / poly1305_update / poly1305_finalizeaes_gcm_siv_encrypt(key, nonce, aad, pt)aes_gcm_siv_decrypt(key, nonce, aad, ct) -> Option[Bytes]xwing_keygen(seed) -> (pk, sk)xwing_encaps(pk, eseed) -> (ss, ct)xwing_decaps(ct, sk) -> Bytesturbo_shake_128(data, d, out_len)kangaroo_twelve_128(m, c, out_len)turbo_shake_256 / kangaroo_twelve_256hpke_setup_s(suite, mode, pk_r, ikm_e, info, psk, psk_id, sk_s)hpke_seal(ctx, aad, pt) / hpke_open(ctx, aad, ct)hpke_export(ctx, exporter_context, len)hpke_x25519_* / hpke_p256_* / hpke_p521_* / hpke_x448_*hpke_p384_hkdf_sha384_aes256gcmhmac_sha384 / hkdf_sha384 / hkdf_sha384_extractml_dsa_44_keygen(seed) -> (pk, sk)ml_dsa_44_sign(sk, msg, rnd, ctx) -> Bytesml_dsa_44_verify(pk, msg, sig, ctx) -> Boolml_dsa_65_keygen / sign / verifyml_dsa_87_keygen / sign / verifyml_dsa_44_sign_prehash(sk, msg, rnd, ctx, ph)ml_dsa_44_verify_prehash(pk, msg, sig, ctx, ph)ml_dsa_44_sign_mu(sk, mu, rnd) / verify_mu(pk, mu, sig)dsa_prehash_variants / dsa_prehash_sha2_256 / ...slh_keygen(slh_sha2_128s, sk_seed, sk_prf, pk_seed)slh_sign(params, sk, msg, ctx) / slh_sign_hedged(...)slh_verify(params, pk, msg, sig, ctx)slh_sign_prehash / slh_verify_prehashslh_sign_raw / slh_verify_rawslh_sha2_128s ... slh_shake_256fcrc32 / crc32c(data : Bytes) -> Bytescrc64_xz / crc64_go_iso(data : Bytes) -> Bytessiphash_2_4(key, data : Bytes) -> Bytessealed_box_seal(master_key, nonce, pt, aad, ctx) -> Bytessealed_box_open(master_key, envelope, aad, ctx) -> Result[Bytes, String]Erron auth failurebase64_encode(data : Bytes) -> Stringbase64_decode(encoded : String) -> Bytesbase64_decode_or(encoded : String) -> Result[Bytes, String]Erron malformed inputbytes_to_hex(data : Bytes) -> Stringhex_to_bytes(hex : String) -> Byteshex_to_bytes_or(hex : String) -> Result[Bytes, String]Erron bad inputbytes_equal(a, b : Bytes) -> BoolStreaming hashers (
<algo>_new/sha3_update/sha3_finalize/shake_finalize) are available for MD5, SHA-224/256/384/512, SHA3-224/256/384/512, and SHAKE128/256, plus SHA-512/224 / SHA-512/256 (sha512_224_new/sha512_256_newwithsha512_update). For SHA-3/SHAKE,sha3_updateis shared and the finalize method depends on the variant (sha3_finalizefor fixed-length,shake_finalize(h, out_len)for XOF).AES-CBC/GCM/CTR keys may be 128, 192, or 256 bits; the nonce for GCM and ChaCha20 is 96 bits (12 bytes), the recommended length per spec. Wrong lengths cause an
abortwith a descriptive message.Security & performance boundaries
mul2/3/9/11/13/14) for ~5x throughput. This leaks key-dependent table indices through the CPU cache — acceptable for many use cases but not side-channel-safe against a local attacker. Since v0.19.0 GHASH also uses precomputed 4-bit tables (keyed by the GCM hash subkey), so it is no longer bit-sliced and is likewise not side-channel-safe; the GCM tag comparison and CBC PKCS#7 verification remain constant-time (no early exit on mismatch).blake3Python package up to 5000 bytes).abortimmediately rather than producing wrong output.Performance
Throughput is measured by the
libbenchmark suite (moon bench) on 1 KiB inputs. Figures below are from the development sandbox; absolute numbers vary by host — runmoon benchlocally for comparable figures.v0.18.0 perf pass. The Keccak-f[1600] state was flattened from a nested 5×5
Array[Array[UInt64]]to a flat 25-lane array (removing the inner-array indirection from the hot permutation loop), and ChaCha20 now expands the key/nonce words once per call instead of per block. Measured on 1 KiB inputs: SHAKE128 ~123 → ~80 µs (-35%), SHAKE256 ~136 → ~92 µs (-32%), ChaCha20 ~54 → ~48 µs (-11%).v0.19.0 perf pass. GHASH (the GF(2^128) multiply inside AES-GCM) was rewritten from a 128-iteration bit-serial loop to a precomputed 4-bit table (32 nibble lookups + XORs per multiply), cutting AES-256-GCM from ~507 to ~487 µs/KiB. Poly1305 was rewritten from per-block
@bigintarithmetic to 5-limb radix-2^26 arithmetic with UInt64 partial products (“donna” style): Poly1305 drops from ~387 to ~7 µs/KiB (-98%), which takes ChaCha20-Poly1305 AEAD from ~456 to ~73 µs/KiB (-84%) and also speeds up XChaCha20-Poly1305 and the sealed-box envelope.v0.20.0 perf pass. ECDSA P-256 point arithmetic moved from affine to Jacobian projective coordinates. Affine double/add each need one Fermat-exponentiation modular inverse (
256 field muls), so a 256-bit scalar multiplication cost ~384 inverses; Jacobian double/add need none, inverting only once when converting back to affine. All field subtractions stay non-negative (the X25519 BigInt lesson). Measured on one host: sign ~270 → ~13 ms and verify ~540 → ~24 ms (**21-23x**). Signatures are byte-identical to before (RFC 6979 deterministic k is unchanged), so no compatibility break.v0.21.0 perf pass. Two fronts. (1) SHA-256 streaming: the hasher now reuses one 64-word message schedule instead of allocating 16- and 64-word arrays per block, reads buffer words without an intermediate
Bytes, and finalizes with in-place padding (no padded-array allocation); one-shot SHA-256 1KiB drops26.9 → ~20.7 µs (-23%). New2.5-2.6x**). CRT output is byte-identical to the single-exponentiation path (deterministic padding), verified in-tree. The PSS/OAEPsha256_update_byte/sha224_update_bytegive byte-at-a-time streams a zero-allocation hot path: ~26.3 µs/KiB, only ~1.3x one-shot (the old 1-byteupdatepattern is ~3.7x). (2) RSA: all four private-key operations (PKCS1-v1.5 sign/decrypt, OAEP decrypt, PSS sign) gain_crtvariants taking the CRT key(p, q, dP, dQ, qInv)and doing two half-width exponentiations instead of one full-width (RFC 8017 §5.1.2) — ~17.6-18.5 ms → ~6.8-7.0 ms on RSA-1024 (**emBitscomputation no longer allocates a ~k-char binary string per call.v0.22.0 features. Three additions. (1) ECDSA secp256k1 (Bitcoin curve, y² = x³ + 7): sign/verify/public-key with the same RFC 6979 + SHA-256 deterministic construction as P-256. The point/RFC-6979 code is now curve-parameterized (P-256 keeps its fast a = −3 doubling; secp256k1 uses the a = 0 specialization), and P-256 signatures stay byte-identical. Verified against the Python
ecdsalibrary (sign_digest_deterministic) pluscryptographycross-verification; signatures are not low-S-normalized (verify accepts any s in [1, n−1]); useecdsa_secp256k1_sign_low_sfor BIP-62 canonical output (v0.23.0). (2) Multi-hash RSA signatures:rsa_pkcs1_v15_sign_with/rsa_pkcs1_v15_verify_with/rsa_pss_sign_with/rsa_pss_verify_withtake anRsaHash(SHA-1/256/384/512) for the message hash, DigestInfo (v1.5) and MGF1 (PSS). The plain functions stay fixed to SHA-256. Verified byte-for-byte against pycryptodome. (3) Incremental GMAC (gmac_new/gmac_update/gmac_finalize): chunked AAD over the same table-accelerated GHASH, equal to the one-shotgmactag (verified in-tree and against pycryptodome GCM-empty tags).v0.23.0 features. Two additions. (1) secp256k1 low-S signatures (
ecdsa_secp256k1_sign_low_s): BIP-62 canonical form — if s > n/2 it is replaced by n − s (r unchanged, still valid and deterministic). Verified against the Pythonecdsalibrary’ssigencode_string_canonizepluscryptography. (2) Multi-hash RSA-OAEP:rsa_oaep_encrypt_with/rsa_oaep_decrypt_withtake anRsaHashfor lHash and MGF1 (the plain functions stay fixed to SHA-256). Exact ciphertexts verified against pycryptodome (fixed seed via itsrandfunchook) plus random-seed interop; SHA-512 vectors use a dedicated 2048-bit key since OAEP-SHA512 needs k ≥ 2·hLen + 2 + |M| (impossible on 1024 bits).v0.24.0 features. Two additions. (1) P-256 low-S signatures (
ecdsa_p256_sign_low_s): the WebCrypto / widely-mandated canonical form — if s > n/2 it is replaced by n − s (r unchanged, still valid and deterministic);ecdsa_p256_verifyaccepts both forms. Verified against the Pythonecdsalibrary’ssigencode_string_canonizepluscryptography, with a property test asserting s ≤ n/2 byte-wise. (2) GMAC with any IV length (gmac_iv/gmac_iv_verifyone-shot andgmac_new_ivincremental): SP 800-38D §8.2.1 — 96-bit IVs keep the fast J0 = IV || 0³¹ || 1 path; other lengths derive J0 via GHASH over IV || 0^(s+64) || [len(IV)]₆₄. Verified against pycryptodome GCM-empty tags for IV lengths 1/8/16/20/32 plus 12-byte fast-path equality and streaming-vs-one-shot properties.v0.25.0 features. CRT × multi-hash combinations: the CRT speed of v0.21.0 and the hash choice of v0.22.0 are now combinable —
rsa_pkcs1_v15_sign_with_crtandrsa_pss_sign_with_crttake both the CRT key (p, q, dP, dQ, qInv) and anRsaHash. Deterministic padding makes them byte-identical to the pycryptodome-verified_withpaths (transitive verification across all four hashes, plus a salt_len=0 deterministic case).v0.26.0 features. The 448-bit suite. (1) Ed448 (RFC 8032 §5.2): pure EdDSA over edwards448 (Goldilocks field p = 2⁴⁴⁸ − 2²²⁴ − 1), SHAKE256-based hashing with dom4 domain separation, context support (
ed448_sign_ctx/ed448_verify_ctx), cofactor verification equation [4][S]B = [4]R + [4][k]A. Verified against all nine official RFC 8032 §7.4 vectors incl. the 1023-octet message and the context-carrying one, plus tamper/context-mismatch/S≥L negative tests. (2) X448 (RFC 7748): Montgomery-ladder Diffie-Hellman over curve448 with RFC §5 clamping. Verified against the official RFC 7748 §5.2 vectors, the §6.1 DH triple, and the single-iteration K=u=5 vector, plus an ECDH commutativity property.v0.27.0 features. Post-quantum: ML-KEM (FIPS 203) — the NIST standard module-lattice KEM, all three parameter sets (ML-KEM-512/768/1024). Pure MoonBit on UInt (q = 3329), NTT-domain arithmetic with the standard zeta/gamma tables, rejection sampling from a true incremental SHAKE128 stream, CBD noise sampling, Compress/ByteEncode for d = 1/4/5/10/11/12, and the full K-PKE + ML-KEM algorithm set (KeyGen/Encaps/Decaps_internal, implicit rejection included). The API is the deterministic internal form: callers supply CSPRNG randomness (d, z for keygen; m for encaps). Lesson learned during this work: the FIPS 203 final text instantiates G = SHA3-512 and H = SHA3-256 (not SHAKE256 as in earlier drafts) — only J and PRF remain SHAKE256. Verified against the official NIST ACVP vectors (keyGen + encapsulation + decapsulation, incl. implicit-rejection cases) for all three parameter sets, plus round-trip and tamper properties.
v0.28.0 features. Post-quantum: ML-DSA (FIPS 204) — the NIST module-lattice signature standard, all three parameter sets (ML-DSA-44/65/87). Pure-MoonBit port: NTT over q = 8380417 (Int64-widening multiplies — Int is 32-bit on wasm-gc), rejection sampling (SampleNTT/SamplePolyCBD/ExpandMask), Power2Round/Decompose/HighBits/ LowBits/MakeHint/UseHint, hint bit-pack/unpack, and the full K-PKE + ML-DSA KeyGen/Sign/Verify internal algorithms (pure message interface, hedged or deterministic via the
rndinput). Verified against the official NIST ACVP vectors: keyGen (all sets), sigGen pure deterministic, and the full sigVer external-pure suite including every rejection category. HashML-DSA and external-mu followed in v0.29.0/v0.30.0.v0.29.0 features. ML-DSA HashML-DSA + external-mu interfaces — completes the FIPS 204 algorithm surface. HashML-DSA (Algorithms 4/5): M’ = 0x01 || octet(|ctx|) || ctx || OID(PH) || PH(M) with the DER OID tag of the pre-hash function (all 12 NIST digests/XOFs; SHAKE-128 pre-hash output 32 B, SHAKE-256 64 B). External-mu (Algorithms 7/8): sign/verify with the 64-byte message digest mu supplied externally. Verified against the official NIST ACVP vector sets (FIPS204-tr1 sigGen pre-hash, deterministic, byte-exact; full sigVer pre-hash suite; all rejection categories).
v0.30.0 features. ML-DSA external-mu ACVP coverage — the external-mu entry points shipped in v0.29.0 (
*_sign_mu/*_verify_mu) are now pinned against the official NIST ACVP external-mu vector groups: sigGen deterministic (byte-exact, mu supplied) and the full sigVer external-mu suite including all rejection categories. ML-DSA’s complete FIPS 204 surface — pure, pre-hash and external-mu interfaces — is now vector-verified (789 tests).v0.31.0 features. SLH-DSA (SPHINCS+, FIPS 205) — the NIST stateless hash-based signature standard, all 12 approved parameter sets (SLH-DSA-{SHA2,SHAKE}-{128,192,256}{s,f}). Pure-MoonBit port: WOTS+ chains, XMSS Merkle trees, the hypertree, FORS few-time signatures, the SHA2 instance (MGF1-SHA-256/512 based H_msg, compressed 22-byte addresses, category-dependent hash selection) and the SHAKE instance (SHAKE256 based). Verified against the official NIST ACVP vectors: keyGen (all sets), sigGen deterministic (pure, pre-hash with OID tagging, and internal/raw-M’ semantics) and sigVer. Note: SLH-DSA is deliberately expensive — signing runs thousands of hash calls; the full 849-test suite takes ~17 minutes.
v0.32.0 features. SLH-DSA speed-up (~16%) + hasher cloning. SLH signing now runs through a per-key hash suite: the SHA2/SHAKE
PK.seedprefix is absorbed once and reused via zero-allocation working hashers (state reset per call, prefix bytes restored). New public utilitiessha256_clone/sha512_clone/sha3_clonedeep-copy streaming hashers for hashing many messages that share a prefix. Note a subtle trap now documented in the test history: when a buffered prefix is shorter than one block, multi-block absorptions overwrite the prefix region of the buffer, so a reused hasher must restore it.v0.33.0 features. ML-KEM hybrid encryption (KEM + DEM) —
ml_kem_{512,768,1024}_hybrid_seal / hybrid_openplus streaminghybrid_seal_init+ml_kem_hybrid_stream_update / stream_final. Composition: (K, c) = Encaps(ek, m); (key, nonce) = HKDF-SHA256(salt = c, ikm = K, info = “mooncry/ml-kem-hybrid/v1”, 44); blob = c || ChaCha20-Poly1305(key, nonce, aad, msg).openreturnsNoneon any failure (KEM implicit rejection surfaces as a tag mismatch). New incremental Poly1305 (poly1305_new / update / finalize; the one-shot is now built on it). Also fixes a latent HMAC bug: keys longer than one block were not zero-padded after hashing (ipad/opad truncated); RFC 4231 TC6 regression vectors added. Verification: KEM I/O from the official NIST ACVP vectors (FIPS 203); the composition layer is cross-checked against pycryptodome.v0.34.0 features. SLH-DSA ~25% faster (suite 17 min → 13 min). Two allocation-removal passes on the per-node hash hot path:
sha256_finalize_n/sha512_finalize_nwrite truncated digests directly (one allocation instead of finalize + slice), andT_lover WOTS+/FORS public values now feeds the pre-split blocks into the hasher without concatenating. Regression-tested against truncated one-shot digests. (A moonc 0827 compiler ICE forced the finalize_n internals to be factored into a shared padding helper — noted for future toolchains.)v0.35.0 features. X-Wing hybrid KEM (draft-connolly-cfrg-xwing-kem) — the ML-KEM-768 + X25519 composite KEM:
xwing_keygen(32-byte seed → 1216-byte pk),xwing_encaps(derandomized in a 64-byte eseed) andxwing_decaps(implicit rejection inherited from ML-KEM). Verified against the official draft test vectors (keygen, encapsulation and decapsulation for all three published vectors).v0.36.0 features. TurboSHAKE + KangarooTwelve (RFC 9861) —
turbo_shake_128 / turbo_shake_256(Keccak-p[1600,12] sponge with a domain byte; 12-round permutation factored out of the SHA-3 core) andkangaroo_twelve_128 / kangaroo_twelve_256(Sakura tree hash over 8192-byte chunks with a customization string). Verified against every applicable RFC 9861 test vector, including the 8191/8192-byte tree boundary and customization-string cases.v0.37.0 features. HPKE — Hybrid Public Key Encryption (RFC 9180). DHKEM(X25519, HKDF-SHA256) and DHKEM(X448, HKDF-SHA512); KDFs HKDF-SHA256/SHA512; AEADs AES-128-GCM, AES-256-GCM and ChaCha20Poly1305; all four modes (base / psk / auth / auth+psk).
hpke_setup_s / hpke_setup_r(derandomized via explicit ephemeral IKM),hpke_seal / hpke_openwith automatic sequence numbers (Noneon authentication failure), andhpke_export. Verified against the official RFC 9180 Appendix A vectors (every mode of the X25519 suites, including the sequence-number carry at 255/256); X448 covered by round-trip tests.v0.38.0 features. HPKE completes: DHKEM(P-256, HKDF-SHA256) with rejection-sampling key derivation and uncompressed-point encoding, covering the remaining official RFC 9180 Appendix A suites (A.3-A.5, all four modes each), including the mixed DHKEM(P-256)+HKDF-SHA512 ciphersuite where the KEM-internal KDF differs from the suite KDF. All three DHKEMs the RFC defines vectors for are now vector-verified.
v0.39.0 features. AES-GCM-SIV (RFC 8452) — the nonce-misuse-resistant AEAD:
aes_gcm_siv_encrypt / aes_gcm_siv_decryptfor AEAD_AES_128_GCM_SIV and AEAD_AES_256_GCM_SIV. POLYVAL is implemented via the RFC Appendix A equivalence with GHASH (ByteReverse + mulX), reusing the library’s table-accelerated GF(2^128) multiplier. Verified against all 48 official RFC 8452 Appendix C vectors (encrypt byte-exact + decrypt round-trip).v0.40.0 features. HPKE completes: DHKEM(P-521, HKDF-SHA512) — rejection sampling with the 0x01 bitmask over 66-byte candidates, 133-byte uncompressed points, and the library’s generic BigInt scalar multiplication (P-521 also uses a = -3). The official RFC 9180 Appendix A.6 vectors (all four modes) now pass — every HPKE ciphersuite the RFC publishes vectors for is vector-verified.
v0.41.0 features. HPKE DHKEM(P-384, HKDF-SHA384) + HMAC/HKDF-SHA384 — the HPKE KEM table is now complete (P-256/P-384/P-521/X25519/X448). New public
hmac_sha384(with the long-key zero-padding handled) andhkdf_sha384/hkdf_sha384_extract, regression-tested against pycryptodome (RFC 4231 TC6-style long key). P-384 HPKE is verified by differential vectors from an independent Python oracle (the HPKE composition machinery itself is RFC-vector-verified via P-256/P-521).v0.53.0 features. Performance: Ed448 Shamir verification + housekeeping. Ed448 verify rewrites the cofactor equation [4][S]B = [4]R + [4][k]A as [4](S*B - k*A - R) = O and evaluates it with one interleaved-window double-scalar multiplication instead of two independent ones (using L-k for -k is cofactor-safe: the difference is 4 = O for ANY point since the group order is 4L). Ed448 verify 15.2 → 12.0 ms; the base point is hoisted to a constant. ML-DSA’s dq_mul keeps i64.rem after THREE measured-negative alternatives (f64 reciprocal, folding with a subtraction loop, and branch-free folding over q = 2^23-2^13+1) — all documented in-code so nobody retries them. Also normalized 118 stray NUL bytes inside byte literals across 22 files to
escapes (semantics unchanged; the files are text-clean for grep/diff again).v0.52.0 features. Performance: word-oriented scrypt. The Salsa20/8 core, scryptBlockMix and ROMix now operate on UInt word arrays in place (V table = a single flat allocation; XORs and interleaving at word granularity; no per-block Bytes conversions) — ~1.4x on the memory-hard loop. RFC 7914 vectors unchanged.
v0.51.0 features. Performance: native P-256 field and curve arithmetic. New eight-32-bit-limb implementation of GF(p256): schoolbook multiply with UInt64 accumulators (provably overflow-free) and the NIST Solinas fast reduction — the fold table for 2^256 = 2^224 - 2^192 - 2^96 + 1 is DERIVED at init by a fixpoint loop (cross-validated in Python against modular arithmetic on random x < p^2) and the prime words come from the existing p256_curve constant. Jacobian doubling/addition (a = -3), 4-bit windowed scalar multiplication and Shamir double-scalar multiplication mirror the vector-verified generic code; ec_scalar_mult and ec_double_scalar_mult_jac dispatch to the native path whenever the curve modulus is P-256’s, so ECDSA, low-S signing, and HPKE P-256 DH all benefit. ECDSA P-256 sign 5.6 → 3.6 ms, verify 7.1 → 4.5 ms (3.5x / 5x versus the pre-optimization BigInt baseline). Differential tests against the generic BigInt path (64 random field multiplies, 10 scalar multiples) run permanently in the suite.
v0.50.0 features. Performance: matrix-expansion caching for the lattice schemes. ML-DSA re-expanded the full k x l A matrix (FIPS 204 ExpandA, k*l rejection-sampled polynomials) on every sign, verify and keygen, and ML-KEM did the same with its k x k A-hat (FIPS 203 XOF rejection sampling); both now live in single-slot caches keyed by (rho, dimensions) — A depends only on the key’s public seed, never on the message. ML-DSA-65 verify 1.78 ms → 0.61 ms (2.9x), sign 8.4 → 7.2 ms; ML-KEM-768 keygen 620 → 364 µs (1.7x), encaps 623 → 389 µs (1.6x), decaps 583 µs. New benchmarks: ml_dsa_65 sign/verify, ml_kem_768 keygen/encaps/decaps. Cached matrices are contractually read-only for all consumers.
v0.49.0 features. Performance: native Curve448-Goldilocks field arithmetic (radix 2^28, sixteen signed Int64 limbs). p448 = 2^448 - 2^224 - 1 folds via 2^448 = 2^224 + 1; the fold coefficients are derived per (i,j,k) by pure arithmetic in the multiply inner loop (verified term-by-term against the recursive definition — no tables, no hand-copied constants), the subtraction bias is the limb decomposition of p448 computed at init. X448 runs a native Montgomery ladder (a24 = 39081, RFC 7748 clamp); Ed448 point arithmetic (RFC 8032 A.4 projective formulas), encode/decode and the decoding square root all move onto the new limbs, with 4-bit window scalar multiplication. Ed448 sign 27.2 → 13.2 ms (2.1x), verify 29.8 → 15.2 ms; X448 10.4 → 6.1 ms (1.7x). Validated by a Python prototype against the official RFC 7748 §6.2 X448 vectors (Alice/Bob/shared secret) and the §7.1 iteration value before porting; all RFC 8032 §7.4 Ed448 vectors green.
v0.48.0 features. Performance: division-free Ed448/X448 field arithmetic — the Barrett playbook from v0.46 applied to the 448-bit Goldilocks prime: conditional add/sub, Barrett modular multiply, and precomputed MSB-first bit chains for inversion (p-2) and the decoding square root (exponent (p+1)/4). Ed448 sign 27.2 → 21.3 ms, verify 29.8 → 23.5 ms; X448 10.4 → 7.9 ms. New benchmarks for Ed448/X448.
v0.47.0 features. XMSS / XMSS^MT eXtended Merkle Signatures (RFC 8391) — the second major stateful hash-based family (with LMS/HSS), ported 1:1 from the official reference implementation (github.com/XMSS/xmss-reference, CC0, by the RFC authors). WOTS+ (w=16) with PRF_keygen-derived chain seeds, tweakable hashes thash_f/ thash_h with per-address PRF keys/masks, L-tree WOTS-pk compression, stack-based treehash with auth-path extraction, and the XMSS^MT layer machinery (d layers, per-layer subtree addresses). All five domain-tagged hash instantiations: SHA2 with n=24/32/64 and SHAKE128/ SHAKE256 with n=32/64. Verified against the reference’s deterministic KATs (seed[i]=i, msg=0x25, index=2^(H-1)): WOTS+ pk/sign/pk-from-sig/ leaf byte-exact for all 7 parameter sets; XMSS^MT (H=20/2_256 family, 4 layers, all 7 sets) keygen+sign byte-exact; XMSS H10 verify for all 7 sets plus byte-exact keygen+sign for SHA2_10_256. Completes the three-family post-quantum signature coverage: ML-DSA (lattice, stateless), SLH-DSA (hash, stateless), LMS+XMSS (hash, stateful).
v0.46.0 features. Performance: division-free EC arithmetic. Every modular multiply on the NIST curves (P-256, P-384, P-521) and secp256k1 now reduces with a per-curve Barrett constant (mu = floor(2^2k / p), derived once at init) instead of BigInt division; field add/sub became conditional add/sub (no reduction op at all); modular inversion (field and scalar order) runs a precomputed MSB-first bit chain over the Barrett multiply instead of the generic modular exponentiation. ECDSA P-256 sign 8.2 → 5.6 ms (1.46x), verify 9.5 → 7.1 ms (1.33x); secp256k1 and all HPKE NIST-curve DH benefit identically.
v0.45.0 features. Performance: native Curve25519 field arithmetic (radix 2^25.5, ten signed Int64 limbs — donna/ref10 style). New
fe25519core with multiplication coefficients DERIVED at init from the shift table (never hand-copied), precomputed fixed-exponent bit patterns (p-2 inversion, (p+3)/8 square root), and one raw ladder pass. X25519 moves off the generic BigInt ladder: 4.34 → 1.80 ms (2.4x). Ed25519 group arithmetic (extended coordinates), point decoding (native sqrt instead of two BigInt modular exponentiations) and encoding all run on the new limbs: sign 10.3 → 4.3 ms (2.4x), verify 9.9 → 3.7 ms (2.7x); Ed25519ctx/ph ride the same path. Keccak gained flat lane-index tables (rho/pi/chi without nested lookups or per-round mod-5) and copy-free SHA3 absorb. New tests: RFC 7748 §6.1 vectors + iteration-1, ECDH commutativity property, and public-key consistency (the BigInt ladder is retained in the test tree as a differential reference).v0.44.0 features. Performance: T-table AES core + LMS fast chains (benchmarked, wasm-gc). The AES block core is rewritten in the classic OpenSSL T-table style (four 256-entry UInt tables fusing SubBytes+ShiftRows+MixColumns into one lookup per output word; the decryption key schedule applies InvMixColumns to rounds 1..nr-1 per the equivalent inverse cipher). Every AES mode benefits: AES-256-GCM 439 → 104 µs/KiB (4.2x), CTR 63.5, CBC 79.4, SIV 188, CMAC 68.8 µs/KiB; GCM-SIV, CCM and KW ride the same core. LMS/HSS chain hashing moved to a shared pre-padded SHA-256 block template (one raw compression per Winternitz step, zero per-hash allocations): LMS test workload ~1.9x, key generation several times faster.
v0.43.0 features. LMS / HSS hash-based signatures (RFC 8554) — the Leighton-Micali Signature scheme and its Hierarchical variant, SHA256 parameter sets (LMOTS_SHA256_N32_W1/W2/W4/W8 x LMS_SHA256_M32_H5/H10/H15/H20/H25). Stateful:
lms_signconsumes one Merkle leaf per signature and aborts at exhaustion; HSS chains L levels with fixed inter-level signatures (caller re-keys on bottom-level exhaustion). Pseudorandom key generation follows RFC 8554 Appendix A (SEED/I). Verified against the official RFC 8554 Appendix F test cases (HSS L=2 verify + public-key regeneration from SEED/I) plus 12 oracle-generated vectors covering all four Winternitz widths, tree heights 5/10/15, non-zero leaf counters, and HSS L=1/2/3, and sign/verify round-trips. Completes the hash-based signature families alongside SLH-DSA (stateless).v0.42.0 features. Performance polish (benchmarked, wasm-gc). Ed25519 rewritten on extended twisted Edwards coordinates (X:Y:Z:T, Hisil et al. formulas) with 4-bit fixed-window scalar multiplication and Shamir double-scalar verification: sign 855 ms → 10.3 ms (83x), verify 870 ms → 9.9 ms (88x). ECDSA P-256 (and secp256k1, and the HPKE NIST-curve DH) switched from string double-and-add to 4-bit windows + Shamir verify: P-256 sign 12.4 → 10.2 ms, verify 22.7 → 12.3 ms. BLAKE3 chunk compression made zero-allocation (ping-pong message schedule): 64 → 20 µs/KiB (3.2x), now faster than BLAKE2b. New benchmarks for Ed25519/X25519.
Hashes, ChaCha20, and hex/Base64 are throughput-bound by the algorithm; AES trades constant-time property for ~5x speed via lookup tables (see Security & performance boundaries).
Testing
Every algorithm is verified against official standard vectors:
Coverage: MD5 (RFC 1321), SHA-2 family (FIPS 180-4 + million-
a), SHA-3 (NIST KAT), SHAKE (FIPS 202), BLAKE2b (RFC 7693 + hashlib), BLAKE3 (python blake3), HMAC (RFC 4231), HMAC-SHA3 (hashlib), Poly1305 (RFC 8439), AES-CMAC (NIST SP 800-38B + pycryptodome), ChaCha20-Poly1305 (RFC 8439 + pycryptodome), HKDF (RFC 5869), PBKDF2 (RFC 6070), scrypt (RFC 7914 + hashlib.scrypt), AES-CBC/GCM/CTR (NIST SP 800-38A/D), ChaCha20 (RFC 8439), RSA (RFC 8017 PKCS1-v1.5/OAEP/PSS + pycryptodome), Ed25519 (RFC 8032 + cryptography lib), X25519 (RFC 7748 + cryptography lib), CRC32/CRC32C (zlib + manual ref), SipHash-2-4 (Python reference), Salsa20 (eSTREAM + pycryptodome), SHA-1 (hashlib) + HOTP/TOTP (RFC 4226/6238), Base64 (RFC 4648), hex round-trip, Keccak-256 / cSHAKE128/256 / KMAC128/256/XOF (NIST SP 800-185 official samples + pycryptodome, differential-tested), CRC-64/XZ + CRC-64/GO-ISO (CRC RevEng check values), SHA-512/224 / SHA-512/256 (FIPS 180-4 + hashlib), BLAKE2s (RFC 7693 + hashlib), HMAC-SHA3-224/384 (stdlib hmac), XChaCha20 / XChaCha20-Poly1305 (draft-irtf-cfrg-xchacha official vectors + libsodium, differential-tested), TOTP-SHA256/512 (RFC 6238 Table 1, all 12 rows), HKDF-SHA512 / PBKDF2-HMAC-SHA512 (RFC 5869 construction anchored on TC1 + hashlib), RIPEMD-160 (official paper suite incl. million-a, + hashlib), AES-CCM (RFC 3610 Packet Vector #1 + pycryptodome), Ed25519ctx / Ed25519ph (RFC 8032 §7.2/§7.3 official vectors), GMAC (pycryptodome GCM) + incremental-vs-one-shot, keyed BLAKE2b/2s (hashlib keyed), Adler-32 (zlib), PBKDF2-HMAC-SHA1 (RFC 6070 official suite), ECDSA secp256k1 (PythonecdsaRFC 6979 +cryptographycross-verify, sk = 1 ⇒ pubkey = G anchor; low-S BIP-62 viasigencode_string_canonize), multi-hash RSA PKCS1-v1.5/PSS/OAEP (pycryptodome, SHA-1/384/512 exact + random-salt/seed interop; OAEP-SHA512 on a 2048-bit key), sealed-box round-trip + tamper, and property-based round-trip checks (deterministic PRNG) for every cipher + streaming-vs-one-shot consistency. 555 tests.Development
The CI (
.github/workflows/moonbit-ci.yml) installs the latest MoonBit toolchain and runs the four required checks —moon check --deny-warn,moon fmt --check,moon info,moon test --deny-warn— and verifies that no build artifacts are tracked. Run them locally:The module manifest is
moon.mod(TOML); per-package manifests aremoon.pkg(TOML). Build outputs (_build/, generated.mbti) are gitignored and must not be committed.Publishing (maintainers)
moon.moddeclaresname = "cc06b/mooncry", licenseApache-2.0. Publishing requires the owner of thecc06bnamespace to be logged in:Before publishing, ensure all four checks above pass and the tree is clean.
License
Apache-2.0