mjwt

A JWT (JSON Web Token) library for MoonBit with extensible trait-based signers

jwt
json-web-token
crypto
hmac
rsa
ecdsa
moon add RabitLogic/mjwt@0.2.0
Download zip
Version
0.2.0
License
MIT
Last updated
last month
Downloads
25

Dependencies

README

#RabitLogic/mjwt — JWT library for MoonBit

A JSON Web Token (JWT) library written in pure MoonBit, with extensible trait-based signer architecture. Passes 32 unit tests and is cross-validated against a Python reference implementation — HMAC tokens are verified to interoperate in both directions.

Requires MoonBit v0.10.4+ (uses extend syntax for explicit trait method mounting; the old implicit method mounting behavior from v0.10.3 and earlier is deprecated in this version).

#Features

AlgorithmTypeHashTest coverage
HS256HMAC symmetricSHA-256✅ RFC 4231
HS384HMAC symmetricSHA-384†✅ RFC 4231
HS512HMAC symmetricSHA-512†✅ RFC 4231
RS256RSA PKCS#1 v1.5SHA-256✅ format
ES256ECDSASHA-256✅ format

† SHA-384 / SHA-512 are self-implemented per FIPS 180-4 and verified with NIST known-answer tests (empty, short, multi-block) and RFC 4231 HMAC vectors.

#Quick start

let claims = JwtClaims::new()
claims.set_subject("user123")

// --- HS256 convenience ---
let token = @mjwt.encode(claims, "my-secret")
let decoded = @mjwt.decode(token, "my-secret")
@mjwt.verify(token, "my-secret") // true

// Inspect without verification (debugging/diagnostic use only)
let raw = @mjwt.decode_without_verify(token)
raw.claims.get_subject() // Some("user123")

#Trait-based API (extensible)

///|
let signer = HmacSigner::new("HS256", "my-secret")

///|
let verifier = HmacVerifier::new("HS256", "my-secret")

///|
let token = @mjwt.encode_with(signer, claims)

///|
let decoded = @mjwt.decode_with(verifier, token)

#Expiration check

claims.set_expiration(1_800_000_000L)
claims.is_expired() // false
claims.is_now_valid() // true

#Supported algorithms

#HS256 / HS384 / HS512 (HMAC)

Symmetric signing — same key for sign & verify.

let s = HmacSigner::new("HS256", "my-secret")?
let v = HmacVerifier::new("HS256", "my-secret")?

Uses MoonBit's @crypto.hmac with @crypto.SHA256 (built-in) for HS256, and with our own Sha384 / Sha512 (FIPS 180-4) for HS384 / HS512.

#RS256 (RSA PKCS#1 v1.5)

Asymmetric signing — private key signs, public key verifies.

let signer = RsaSigner::new(n_bytes, d_bytes, "RS256")?
let verifier = RsaVerifier::new(n_bytes, e_bytes, "RS256")?
let token = @mjwt.encode_with(signer, claims)?
let decoded = @mjwt.decode_with(verifier, token)?

Key material: big-endian byte arrays for n (modulus), d (private exponent), e (public exponent). Minimum key size: 2048 bits for RS256.

#ES256 (ECDSA P-256)

Asymmetric signing using the NIST P-256 (secp256r1) curve.

let signer = EcSigner::new_p256(priv_bytes, pub_bytes)?
let verifier = EcVerifier::new_p256(pub_bytes)?
let token = @mjwt.encode_with(signer, claims)?

Key format: 32-byte private key, 64-byte uncompressed public key (x ‖ y).

#Standard claims API

MethodClaimRFC 7519
set_subject / get_subjectsub§4.1.2
set_issuer / get_issueriss§4.1.1
set_audience / get_audienceaud§4.1.3
set_expiration / get_expirationexp§4.1.4
set_not_before / get_not_beforenbf§4.1.5
set_issued_at / get_issued_atiat§4.1.6
set_jwt_id / get_jwt_idjti§4.1.7
is_expiredChecks exp against @env.now()
is_now_validChecks both nbf and exp
set / getarbitraryAny custom key-value

#Architecture

┌──────────────────────┐ │ JwtSigner trait │ │ JwtVerifier trait │ └──────────┬───────────┘ │ implements ┌─────────────────────┼──────────────────────┐ ▼ ▼ ▼ HmacSigner RsaSigner EcSigner HmacVerifier RsaVerifier EcVerifier (HS256/384/512) (RS256) (ES256 P-256)

#File structure

FileResponsibility
mjwt.mbtCore: errors, traits, JwtHeader, JwtClaims, JwtToken, Base64URL, public API
mjwt_hash_sha512.mbtSHA-384 / SHA-512 (FIPS 180-4, @crypto.CryptoHasher)
mjwt_signer_hmac.mbtHmacSigner / HmacVerifier
mjwt_signer_rsa.mbtRsaSigner / RsaVerifier
mjwt_signer_ecdsa.mbtEcSigner / EcVerifier (P-256)
mjwt_test.mbt32 unit tests
mjwt_bench_test.mbt4 benchmarks
examples/example_usage.mbtRunnable usage examples (12 tests)
examples/py_compare.pyPython cross-validation script

#Adding a custom signer

Implement the JwtSigner / JwtVerifier traits on any type, and use extend to expose trait methods via dot syntax (required since MoonBit v0.10.4):

struct MySigner { key : Bytes }
impl JwtSigner for MySigner with fn alg_name(_) -> String { "HS256" }
impl JwtSigner for MySigner with fn sign(self, msg) -> .. { .. }

// Mount trait methods as dot-syntax methods on MySigner
pub MySigner with JwtSigner::{alg_name, sign}

let token = @mjwt.encode_with(MySigner { key }, claims)

#Development

moon test # run 32 tests moon bench # run 4 benchmarks moon check # check for warnings moon fmt # format code moon info # update interface (.mbti) files

#Cross-validation

# Full interop test (Python + MoonBit) python3 examples/exact_validate.py # Legacy format check python3 examples/py_compare.py

#Performance

AlgorithmOperationTime (mean ± σ)Notes
HS256encode4.10 µs ± 260 ns
HS256decode4.13 µs ± 339 ns
RS256sign29.79 ms ± 684 µs
ES256sign (v0.2)26.56 ms ± 2.06 ms~28× faster than v0.1

ES256 was optimized in v0.2.0 with Jacobian projective coordinates, eliminating 384 modular inversions from the inner loop. Run benchmarks: moon bench

#License

MIT

#
JwtSigner

pub(open) trait JwtSigner {
fn alg_name(Self) -> String
fn sign(Self, BytesView) -> FixedArray[Byte] raise JwtError
}

Pluggable JWT signer.

Implement this on any type that holds key material.

struct RsaSigner { n: BigUint, d: BigUint }
impl JwtSigner for RsaSigner with fn alg_name(_) -> String { "RS256" }
impl JwtSigner for RsaSigner with fn sign(self, msg) -> .. { .. }

#
JwtVerifier

pub(open) trait JwtVerifier {
fn alg_name(Self) -> String
fn verify(Self, BytesView, BytesView) -> Bool
}

Pluggable JWT verifier.

struct RsaVerifier { n: BigUint, e: BigUint }
impl JwtVerifier for RsaVerifier with fn alg_name(_) -> String { "RS256" }
impl JwtVerifier for RsaVerifier with fn verify(self, msg, sig) -> .. { .. }

#
JwtError

pub suberror JwtError {
InvalidFormat(String)
InvalidBase64(String)
InvalidJson(String)
InvalidUtf8(String)
InvalidHeader(String)
SignatureMismatch
UnsupportedAlgorithm(String)
CryptoError(String)
} derive(Eq,
Debug
)

Errors that can occur during JWT encoding, decoding, or verification.

#
JwtError::equal

fn JwtError::equal(JwtError, JwtError) -> Bool

#
JwtError::not_equal

fn JwtError::not_equal(x : JwtError, y : JwtError) -> Bool

#
JwtError::to_repr

#
JwtError::to_string

fn JwtError::to_string(self : JwtError) -> String

#
EcSigner

pub struct EcSigner {
private_key : Scalar256
public_key_x : Scalar256
public_key_y : Scalar256
}

ECDSA signer for P-256 (ES256).

#
EcSigner::alg_name

fn EcSigner::alg_name(_ : EcSigner) -> String

#
EcSigner::new_p256

fn EcSigner::new_p256(priv_bytes : BytesView, pub_bytes : BytesView) -> EcSigner raise JwtError

Create an EcSigner for ES256.

  • priv_bytes — 32 bytes (big-endian scalar)
  • pub_bytes — 64 bytes (uncompressed: x || y, each 32 bytes big-endian)

#
EcSigner::sign

fn EcSigner::sign(self : EcSigner, message : BytesView) -> FixedArray[Byte] raise JwtError

#
EcVerifier

pub struct EcVerifier {
public_key_x : Scalar256
public_key_y : Scalar256
}

ECDSA verifier for P-256 (ES256).

#
EcVerifier::alg_name

fn EcVerifier::alg_name(_ : EcVerifier) -> String

#
EcVerifier::new_p256

fn EcVerifier::new_p256(pub_bytes : BytesView) -> EcVerifier raise JwtError

Create an EcVerifier for ES256.

  • pub_bytes — 64 bytes (uncompressed: x || y)

#
EcVerifier::verify

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

#
HmacSigner

pub struct HmacSigner {
alg_name : String
key : Bytes
}

Symmetric HMAC signer. The same secret is used for both signing and verification.

#
HmacSigner::alg_name

fn HmacSigner::alg_name(self : HmacSigner) -> String

#
HmacSigner::new

fn HmacSigner::new(alg : String, key : StringView) -> HmacSigner raise JwtError

Create an HmacSigner.

Parameters

  • alg"HS256", "HS384", or "HS512"
  • key — shared secret

Errors

UnsupportedAlgorithm when alg is not recognised.

#
HmacSigner::sign

fn HmacSigner::sign(self : HmacSigner, message : BytesView) -> FixedArray[Byte] raise JwtError

#
HmacVerifier

pub struct HmacVerifier {
alg_name : String
key : Bytes
}

Symmetric HMAC verifier.

#
HmacVerifier::alg_name

fn HmacVerifier::alg_name(self : HmacVerifier) -> String

#
HmacVerifier::new

fn HmacVerifier::new(alg : String, key : StringView) -> HmacVerifier raise JwtError

Create an HmacVerifier.

#
HmacVerifier::verify

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

#
JwtClaims

pub(all) struct JwtClaims {
data : Map[String, Json]
}

#
JwtClaims::get

fn JwtClaims::get(self : JwtClaims, key : String) -> Json?

Retrieve a previously stored claim by name. Returns None when the key does not exist.

#
JwtClaims::get_audience

fn JwtClaims::get_audience(self : JwtClaims) -> String?

Returns the aud (audience) value, or None if absent.

#
JwtClaims::get_expiration

fn JwtClaims::get_expiration(self : JwtClaims) -> Int64?

Returns the exp (expiration time) value, or None if absent.

#
JwtClaims::get_issued_at

fn JwtClaims::get_issued_at(self : JwtClaims) -> Int64?

Returns the iat (issued at) value, or None if absent.

#
JwtClaims::get_issuer

fn JwtClaims::get_issuer(self : JwtClaims) -> String?

Returns the iss (issuer) value, or None if absent.

#
JwtClaims::get_jwt_id

fn JwtClaims::get_jwt_id(self : JwtClaims) -> String?

Returns the jti (JWT ID) value, or None if absent.

#
JwtClaims::get_not_before

fn JwtClaims::get_not_before(self : JwtClaims) -> Int64?

Returns the nbf (not before) value, or None if absent.

#
JwtClaims::get_subject

fn JwtClaims::get_subject(self : JwtClaims) -> String?

Returns the sub (subject) value, or None if absent.

#
JwtClaims::is_expired

fn JwtClaims::is_expired(self : JwtClaims) -> Bool

Returns true when the current time (UTC) is past the exp claim. Returns false if exp is not set, so it is safe to call on any claims.

#
JwtClaims::is_now_valid

fn JwtClaims::is_now_valid(self : JwtClaims) -> Bool

Checks both nbf (not-before) and exp (expiration) against the current UTC time.

Returns true iff:
  • nbf is absent OR now >= nbf
  • exp is absent OR now < exp

#
JwtClaims::new

fn JwtClaims::new() -> JwtClaims

#
JwtClaims::set

fn JwtClaims::set(self : JwtClaims, key : String, value : Json) -> Unit

Store an arbitrary key-value pair in the claims payload.
  • key – claim name, e.g. "custom-claim"
  • value – any JSON value (Json::string(...), Json::number(...), etc.)

#
JwtClaims::set_audience

fn JwtClaims::set_audience(self : JwtClaims, aud : String) -> Unit

Registered claim aud (audience) — identifies the recipients that the JWT is intended for. Per RFC 7519 §4.1.3.

#
JwtClaims::set_expiration

fn JwtClaims::set_expiration(self : JwtClaims, exp : Int64) -> Unit

Registered claim exp (expiration time) — Unix timestamp after which the JWT MUST NOT be accepted. Per RFC 7519 §4.1.4.

#
JwtClaims::set_issued_at

fn JwtClaims::set_issued_at(self : JwtClaims, iat : Int64) -> Unit

Registered claim iat (issued at) — Unix timestamp when the JWT was created. Per RFC 7519 §4.1.6.

#
JwtClaims::set_issuer

fn JwtClaims::set_issuer(self : JwtClaims, iss : String) -> Unit

Registered claim iss (issuer) — identifies the principal that issued the JWT. Per RFC 7519 §4.1.1.

#
JwtClaims::set_jwt_id

fn JwtClaims::set_jwt_id(self : JwtClaims, jti : String) -> Unit

Registered claim jti (JWT ID) — a unique identifier for the JWT. Per RFC 7519 §4.1.7.

#
JwtClaims::set_not_before

fn JwtClaims::set_not_before(self : JwtClaims, nbf : Int64) -> Unit

Registered claim nbf (not before) — Unix timestamp before which the JWT MUST NOT be accepted. Per RFC 7519 §4.1.5.

#
JwtClaims::set_subject

fn JwtClaims::set_subject(self : JwtClaims, sub : String) -> Unit

Registered claim sub (subject) — identifies the principal that is the subject of the JWT. Per RFC 7519 §4.1.2.

#
JwtHeader

pub(all) struct JwtHeader {
alg : String
typ : String
kid : String?
}

#
JwtHeader::new

fn JwtHeader::new(alg : String, kid? : String) -> JwtHeader

#
JwtToken

pub(all) struct JwtToken {
header : JwtHeader
claims : JwtClaims
signature : FixedArray[Byte]
}

#
RsaSigner

pub struct RsaSigner {
n :
BigInt

d :
BigInt

hash_len : Int
alg : String
}

RSA signer using PKCS#1 v1.5 signature scheme.

  • n — RSA modulus
  • d — private exponent
  • hash_len — output length of the hash (32=SHA256, 48=SHA384, 64=SHA512)

#
RsaSigner::alg_name

fn RsaSigner::alg_name(self : RsaSigner) -> String

#
RsaSigner::new

fn RsaSigner::new(n_bytes : BytesView, d_bytes : BytesView, alg : String) -> RsaSigner raise JwtError

#
RsaSigner::sign

fn RsaSigner::sign(self : RsaSigner, message : BytesView) -> FixedArray[Byte] raise JwtError

#
RsaVerifier

pub struct RsaVerifier {
n :
BigInt

e :
BigInt

hash_len : Int
alg : String
}

RSA verifier using PKCS#1 v1.5 signature scheme.

#
RsaVerifier::alg_name

fn RsaVerifier::alg_name(self : RsaVerifier) -> String

#
RsaVerifier::new

fn RsaVerifier::new(n_bytes : BytesView, e_bytes : BytesView, alg : String) -> RsaVerifier raise JwtError

Create an RsaVerifier.

  • n_bytes — modulus in big-endian bytes
  • e_bytes — public exponent in big-endian bytes (usually 0x010001 = 65537)
  • alg"RS256", "RS384", or "RS512"

#
RsaVerifier::verify

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

#
Scalar256

pub struct Scalar256 {
limbs : FixedArray[UInt64]
}

#
Scalar256::from_bytes

fn Scalar256::from_bytes(be : BytesView) -> Scalar256

#
Scalar256::one

fn Scalar256::one() -> Scalar256

#
Scalar256::to_bytes

fn Scalar256::to_bytes(self : Scalar256) -> FixedArray[Byte]

#
Scalar256::zero

fn Scalar256::zero() -> Scalar256

#
Sha384

pub struct Sha384 {
ctx : Sha512Ctx
}

#
Sha384::block_size

fn Sha384::block_size(_ : Sha384) -> Int

#
Sha384::finalize_into

fn Sha384::finalize_into(self : Sha384, buf : FixedArray[Byte], offset~ : Int) -> Unit

#
Sha384::new

fn Sha384::new() -> Sha384

#
Sha384::reset

fn Sha384::reset(self : Sha384) -> Unit

#
Sha384::size

fn Sha384::size(_ : Sha384) -> Int

#
Sha384::update

fn Sha384::update(self : Sha384, data : BytesView) -> Unit

#
Sha512

pub struct Sha512 {
ctx : Sha512Ctx
}

#
Sha512::block_size

fn Sha512::block_size(_ : Sha512) -> Int

#
Sha512::finalize_into

fn Sha512::finalize_into(self : Sha512, buf : FixedArray[Byte], offset~ : Int) -> Unit

#
Sha512::new

fn Sha512::new() -> Sha512

#
Sha512::reset

fn Sha512::reset(self : Sha512) -> Unit

#
Sha512::size

fn Sha512::size(_ : Sha512) -> Int

#
Sha512::update

fn Sha512::update(self : Sha512, data : BytesView) -> Unit

#
Sha512Ctx

type Sha512Ctx

#
decode

fn decode(sv : StringView, secret : String) -> JwtToken raise JwtError

Decode and verify HMAC-SHA256.

#
decode_with

fn[V : JwtVerifier] decode_with(verifier : V, sv : StringView) -> JwtToken raise JwtError

Verify and decode a compact JWT string using verifier.

let v = HmacVerifier::new("HS256", "my-secret")?
let tok = mjwt::decode_with(v, token)?

#
decode_without_verify

fn decode_without_verify(sv : StringView) -> JwtToken raise JwtError

Decode without signature verification. Use for inspecting tokens only.

#
encode

fn encode(claims : JwtClaims, secret : String) -> String raise JwtError

Encode using HMAC-SHA256.

#
encode_with

fn[S : JwtSigner] encode_with(signer : S, claims : JwtClaims) -> String raise JwtError

Encode claims into a compact JWT string signed by signer.

let s = HmacSigner::new("HS256", "my-secret")?
let t = mjwt::encode_with(s, claims)?

#
sha384

fn sha384(data : BytesView) -> FixedArray[Byte]

Convenience: one-shot SHA-384 hash.

#
sha512

fn sha512(data : BytesView) -> FixedArray[Byte]

Convenience: one-shot SHA-512 hash.

#
verify

fn verify(sv : StringView, secret : String) -> Bool

Verify HMAC-SHA256 signature. Returns true / false (no panic).