Each sheet adds one thing to the last: a token, the ways it is refused, the settings behind a check, keys from JSON, then certificates and the files around them. Every listing is cut from one program, src/examples/guide, and every box under a listing is what that program prints.
$ moon add moonbitstack/mooncred
The keys come from mooncrypt and are passed in; mooncred chooses no algorithm and draws no randomness of its own.
Mooncredjwtii
A token, signed and checked.
The key is anything that signs. @jwt.mac turns a MAC into a signer and a verifier at once, so one HMAC secret both issues and checks. verify is told the algorithm, the time and what the claims must say; the token is not asked.
let secret = b"a shared secret nobody else has"
let key = @jwt.mac(fn() {
@hmac.Hmac::new(secret[:], fn() { @sha2.Hasher::new() })
})
let claims : Map[String, Json] = {
"sub": "heke1228",
"iss": "moonbitstack",
"aud": "moonapi",
"exp": 1793235600,
}
let token = @jwt.sign(claims, HS256, key)
println(token)
let got = @jwt.verify(
token[:],
HS256,
key,
now=At(now),
issuer="moonbitstack",
audience=["moonapi"],
)
if got.get("sub") is Some(String(sub)) {
println("subject: \{sub}")
}
A JWS carries octets, and RFC 7797 lets those octets travel as themselves rather than as a base64url copy of themselves. The signature covers whatever the payload is — a document, a firmware image, a byte string a protocol defined — and the token can leave the payload out entirely, for a caller that sends it by another road.
let key = @jwt.mac(fn() {
@hmac.Hmac::new(b"a shared secret nobody else has"[:], fn() {
@sha2.Hasher::new()
})
})
// A payload that is not a claims set, and that travels as itself (RFC 7797): the// token carries no payload at all, and the octets are handed to the verifier.
let token = @jwt.sign_bytes(b"$.02", HS256, key, b64=false, detached=true)
println(token)
let back = @jwt.verify_bytes(token[:], HS256, key, b64=false, payload=b"$.02")
println("recovered: \{@utf8.decode(back)}")
// The JSON serialisation, where the name of the key travels beside the signature// instead of inside it.
let message = @jwt.Message::new(b"{\"iss\":\"moonbitstack\"}").sign(
HS256,
key,
header=kid_of("2026-09"),
)
println(@json.dumps(message.flattened()))
A token naming another algorithm than the one the caller expects is refused before its signature is looked at; that closes both alg: none and the HMAC-with-a-public-key confusion. Time is checked unless the caller switches the clock off, and saying so is the only way to read a lapsed token.
// The same bytes, offered as another algorithm.
try @jwt.verify(token[:], HS384, key, now=At(now)) |> ignore catch {
Algorithm(want~, got~) => println("refused: \{got} is not \{want}")
_ => println("refused")
}
// Two hours on, the token has lapsed.
try @jwt.verify(token[:], HS256, key, now=At(now + 7200)) |> ignore catch {
Expired(at~) => println("refused: expired at \{at.to_int64()}")
_ => println("refused")
}
// Read anyway, with the clock switched off on purpose.
let old = @jwt.verify(token[:], HS256, key, now=Ignored)
println("read with no clock: \{old.length()} claims")
refused: HS256 is not HS384
refused: expired at 1793235600
read with no clock: 4 claims
Mooncredjwtiv
Settings, in three layers.
Everything a verification checks is a setting, given in a Policy assembled once or as an argument to one call. The more specific layer wins:
wins=Base turns the last two around, so the record decides. clash says what happens when one setting arrives twice with different values: Ignore merges quietly, Warn(f) merges and passes one line to f, Panic stops, Handle(f) hands both to your function. Both types come from moonknob, shared by the whole family. now is data, not a setting, so it is always an argument.
// Assembled once for a deployment.
let ours = @jwt.Policy::new(
issuer="moonbitstack",
audience=["moonapi"],
leeway=30,
)
// The same record, written as an update of the preset.
let same : @jwt.Policy = {
..@jwt.policy,
issuer: Some("moonbitstack"),
audience: ["moonapi"],
leeway: 30,
}
println("same policy: \{ours == same}")
// A hundred seconds past `exp`: outside the policy's leeway.
let late : @jwt.Clock = At(1793235700L)
// This one call allows two minutes; the argument beats the record.
let got = @jwt.verify(token[:], HS256, key, now=late, policy=ours, leeway=120)
println("accepted with leeway 120: \{got.length()} claims")
// `wins=Base` turns that around, and the record's 30 s stands.
try
@jwt.verify(
token[:],
HS256,
key,
now=late,
policy=ours,
leeway=120,
wins=Base,
)
|> ignore
catch {
Expired(..) => println("refused with wins=Base: the policy decides")
_ => println("refused")
}
same policy: true
accepted with leeway 120: 4 claims
refused with wins=Base: the policy decides
Mooncredjwkv
Keys from a JWK set.
jwk reads key material as bytes, not as keys: building a key means choosing an implementation, and the program is what knows which one it links. pick finds the key for an algorithm and an id; a verifier reads kid from the unverified header to know which one to fetch.
let set = @jwk.Set::parse(
(
#|{"keys":[{"kty":"OKP","crv":"Ed25519","kid":"2026-09","alg":"EdDSA",
#|"d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A",
#|"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"}]}
),
)
guard set.pick(alg="EdDSA", id="2026-09") is Some(jwk) else { return }
guard jwk.material() is Okp(x~, secret=Some(seed), ..) else { return }
let signer = @ed25519.PrivateKey::new(seed[:])
let token = @jwt.sign({ "sub": "heke1228" }, EdDSA, signer, extra={
"kid": "2026-09",
})
// A verifier reads `kid` first, fetches that key, then verifies.
guard @jwt.header(token[:]).get("kid") is Some(String(kid)) else { return }
guard set.by_id(kid) is Some(found) else { return }
guard found.material() is Okp(x=public, ..) else { return }
println("kid \{kid}, same key: \{public == x}")
let got = @jwt.verify(
token[:],
EdDSA,
@ed25519.PublicKey::new(public[:]),
now=At(now),
)
println("EdDSA verified: \{got.length()} claim")
kid 2026-09, same key: true
EdDSA verified: 1 claim
Mooncredtokenvi
A signed string for a cookie.
When the reader is your own server, a JWT is more than the job needs. token signs a value with a timestamp and an HMAC; the salt keeps a session cookie from passing as a password-reset link, max_age bounds its life, and rotate moves to a new key while the old one still reads.
let keys = @token.Keys::new(b"the key that signs session cookies")
let cookie = @token.sign(b"user=heke1228", keys~, salt="session", at=now)
println(cookie)
let value = @token.verify(
cookie[:],
keys~,
salt="session",
max_age=3600L,
now~,
)
println("value: \{value.length()} bytes")
// A cookie signed for one purpose does not pass for another.
try @token.verify(cookie[:], keys~, salt="reset", now~) |> ignore catch {
Signature => println("refused under salt \"reset\"")
_ => println("refused")
}
// A new key signs from now on; cookies under the old one still read.
let next = keys.rotate(b"next quarter's session key")
let kept = @token.verify(cookie[:], keys=next, salt="session", now~)
println("after rotation: \{kept.length()} bytes")
dXNlcj1oZWtlMTIyOA.AAAAAGrijIA.TH9WhyXWbCafkk-MDH0K5PDDFyUhjeHiwgID1G6-HLc
value: 13 bytes
refused under salt "reset"
after rotation: 13 bytes
Mooncredx509vii
A certificate chain.
The certificates below were written by openssl: a test authority and a server certificate it issued for leaf.example.com. verify builds the path to a trust anchor, checks each signature, the validity period, the constraints, and the name the server was asked for.
let anchors = @x509.Roots::pem(authority)
let chain = [@x509.pem(good)]
let path = @x509.verify(chain[:], anchors, now~, host="leaf.example.com")
for cert in path {
println(cert.subject.cn().unwrap_or("?"))
}
try
@x509.verify(chain[:], anchors, now~, host="other.example.com") |> ignore
catch {
Host(name) => println("refused: not issued for \{name}")
_ => println("refused")
}
good.example.com
Mooncred Test CA
refused: not issued for other.example.com
One certificate, read.
parse reads a certificate down to its extensions; its fields are plain data.
let cert = @x509.parse(@x509.pem(good))
println("subject \{cert.subject.cn().unwrap_or("?")}")
println("issuer \{cert.issuer.cn().unwrap_or("?")}")
for name in cert.exts.san {
if name is Dns(host) {
println("dns \{host}")
}
}
println("serves leaf.example.com: \{cert.matches("leaf.example.com")}")
println("valid now: \{cert.valid_at(now)}")
let ca = @x509.parse(@x509.pem(authority))
println("signed by the CA: \{cert.signed_by(ca.spki)}")
subject good.example.com
issuer Mooncred Test CA
dns leaf.example.com
serves leaf.example.com: true
valid now: true
signed by the CA: true
Mooncredx509 · csrviii
A certificate and a request.
For tests and local services, self_signed writes a certificate. The signature algorithm follows the key, so the same call with an ML-DSA key writes a post-quantum one. What a real authority wants is a request: csr writes one signed by the key it asks a certificate for, and reads it back to check that signature.
// RFC 6979's P-256 test key; a real one comes from a random source.
let key = @ecdsa.PrivateKey::new(
@base16.decode(
"C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721",
),
)
// The instants are seconds since the Unix epoch, the same unit a certificate's validity is// read back in, and the extensions are what a client compares and obeys.
let der = @x509.self_signed(
key=Ec(key),
cn="moonbitstack.example",
serial=b"\x01",
not_before=1793232000L,
not_after=1824768000L,
exts={ ..@x509.Exts::empty(), san: [Dns("moonbitstack.example")], },
)
let cert = @x509.parse(der)
println(
"self-signed \{cert.subject.cn().unwrap_or("?")}: \{cert.signed_by(cert.spki)}",
)
println("names its own host: \{cert.matches("moonbitstack.example")}")
// What a CA would ask for instead: a request, signed by the same key.
let pem = @csr.self_signed_pem(
Ec(key),
subject=@x509.common_name("moonbitstack.example"),
)
let request = @csr.Request::pem(pem)
println("request verified: \{request.verified()}")
println("asks for this key: \{request.asks_for(Ec(key.public()))}")
// The same call with a post-quantum key. The identifier in the certificate and the signature// over it both follow the key, so nothing else changes.
let seed = @base16.decode(
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
)
let pq : @x509.PrivateKey = MlDsa(
@mldsa.PrivateKey::from_seed(kind=D65, seed),
)
let pq_cert = @x509.parse(
@x509.self_signed(
key=pq,
cn="pqc.example",
serial=b"\x02",
not_before=1793232000L,
not_after=1824768000L,
),
)
println(
"ML-DSA certificate: \{pq_cert.alg}, \{pq_cert.sig.bytes.length()} octets of signature",
)
self-signed moonbitstack.example: true
names its own host: true
request verified: true
asks for this key: true
ML-DSA certificate: 2.16.840.1.101.3.4.3.18, 3309 octets of signature
Mooncredpbe · pkcs12ix
Keys at rest, and containers.
A private key on disk is kept behind a passphrase: PKCS#8 encrypted with PBES2, the form openssl writes. Salt and IV are arguments, because this library draws no randomness; in production they come from a random source such as moonrand.
// Salt and IV are arguments: fresh random bytes in production.
let pem = @x509.PrivateKey::Ec(key).encrypt_pem(
passphrase="correct horse",
salt=b"0123456789abcdef",
iv=b"fedcba9876543210",
)
println(pem.split("\n").next().unwrap_or(""))
let back = @x509.PrivateKey::pem_encrypted(pem, passphrase="correct horse")
println(
"same key: \{back.public() is Ec(public) && public.bytes() == key.public().bytes()}",
)
try
@x509.PrivateKey::pem_encrypted(pem, passphrase="battery staple") |> ignore
catch {
_ => println("wrong passphrase: refused")
}
-----BEGIN ENCRYPTED PRIVATE KEY-----
same key: true
wrong passphrase: refused
A PKCS#12 container.
A key and its chain travel together in one file, under the same passphrase and a MAC over the whole.
let pfx = @pkcs12.build(
certs=[der],
key=Some(@x509.PrivateKey::Ec(key).pkcs8()),
passphrase="correct horse",
salt=b"0123456789abcdef",
iv=b"fedcba9876543210",
mac_salt=b"mac salt",
)
let read = @pkcs12.parse(pfx)
println("MAC checks out: \{read.verify(passphrase="correct horse")}")
let inside = read.contents(passphrase="correct horse")
println(
"\{inside.certs.length()} certificate, key inside: \{inside.key is Some(_)}",
)
MAC checks out: true
1 certificate, key inside: true
Mooncredrevokex
Revocation.
revoke reads a revocation list or an OCSP answer and judges a certificate by it. Fetching either is the caller's business: nothing here opens a socket. A list is only trusted when its issuer signed it and it has not aged past its next update.
let list = @revoke.Crl::pem(crl)
let ca = @x509.parse(@x509.pem(authority))
println("list signed by the CA: \{list.signed_by(ca.spki)}")
println("list still fresh: \{list.fresh(now)}")
for name in ["good", "revoked"] {
let cert = @x509.parse(
@x509.pem(if name == "good" { good } else { revoked }),
)
match list.status(cert) {
Good => println("\{name}: good")
Revoked(_) => println("\{name}: revoked")
Unknown => println("\{name}: unknown")
}
}
list signed by the CA: true
list still fresh: true
good: good
revoked: revoked
Mooncredjwe · cosexi
JWE and COSE.
A JWT is signed, not hidden. When the claims must stay private, jwe encrypts them: here directly under a shared key with AES-GCM, the IV again an argument.
let shared = b"a 32-byte key for A256GCM, here."
let jwe = @jwe.encrypt(
b"card ending 4242",
key=Shared(shared),
alg=Dir,
enc=A256Gcm,
iv=b"twelve bytes",
)
println(jwe)
let opened = @jwe.decrypt(jwe[:], key=Shared(shared), alg=Dir, enc=A256Gcm)
println("opened: \{opened == b"card ending 4242"}")
Several recipients, and data that is not the payload.
The JSON serialisation carries one ciphertext for several recipients: each has its own copy of the content key under its own key-management algorithm, and any one of them opens the same payload. aad is authenticated without being carried in the header, which is how a tenant name or a form identifier is bound to a message without being readable in it.
let content = b"0001020304050607"[:].to_owned()
let message = @jwe.seal(
b"card ending 4242",
enc=A128Gcm,
recipients=[
{
..@jwe.Recipient::new(A128Kw, Shared(b"sixteen byte key")),
cek: Some(content),
header: kid_of("ops"),
},
{
..@jwe.Recipient::new(
Pbes2Hs256A128Kw,
Password(b"a passphrase for the auditor"),
),
cek: Some(content),
salt: Some(b"sixteen byte sal"),
count: Some(4096),
header: kid_of("audit"),
},
],
iv=b"twelve bytes",
aad=b"tenant=42",
)
println(@json.dumps(message))
let opened = @jwe.open(@json.dumps(message)[:], enc=A128Gcm, unseal=[
@jwe.Unseal::new(A128Kw, Shared(b"sixteen byte key")),
])
println(
"opened by recipient \{opened.index}, aad \{@utf8.decode(opened.aad)}",
)
{"protected":"eyJlbmMiOiJBMTI4R0NNIn0","recipients":[{"header":{"alg":"A128KW","kid":"ops"},"encrypted_key":"FM5tULvWg_gbWfRHZyl-N_n89AE8tRf8"},{"header":{"alg":"PBES2-HS256+A128KW","kid":"audit","p2s":"c2l4dGVlbiBieXRlIHNhbA","p2c":4096},"encrypted_key":"G-WqPS_nQyLcf7zhznTfmyRu8H4yaWs8"}],"aad":"dGVuYW50PTQy","iv":"dHdlbHZlIGJ5dGVz","ciphertext":"NjujYqAb3DUJULt64fdveQ","tag":"60szW1olBDtE4tRcphFkNg"}
opened by recipient 0, aad tenant=42
A CBOR Web Token.
COSE is the same idea written in CBOR, for devices that count bytes. A CWT carries the claims a JWT would, signed here with Ed25519.
// RFC 8032's first Ed25519 test key.
let signer = @ed25519.PrivateKey::new(
@base16.decode(
"9D61B19DEFFD5A60BA844AF492EC2CC44449C5697B326919703BAC031CAE7F60",
),
)
let claims = @cose.Claims::new(
iss="moonbitstack",
sub="heke1228",
exp=1793235600L,
)
let token = @cose.signed(claims, alg=EdDSA, signer)
println("a CWT of \{token.length()} bytes")
let back = @cose.verified(token, alg=EdDSA, signer.public(), now=At(now))
println("issuer: \{back.iss.unwrap_or("?")}")
a CWT of 106 bytes
issuer: moonbitstack
Mooncredrootsxii
Trust anchors.
To check a public server, the anchors are the Mozilla CA store, built from curl's extract. Build it once and keep it; pass it to @x509.verify in place of the test authority above.
let mozilla = @roots.mozilla()
println("\{mozilla.length()} anchors, Mozilla store of \{@roots.version}")
118 anchors, Mozilla store of 2026-10-20
Every type and function used here, and the ones that were not, are in the reference.