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 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: Ignore merges quietly, Panic stops, Handle(f) hands both to your function. 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=Some(now),
)
println(cookie)
let value = @token.verify(
cookie[:],
keys~,
salt="session",
max_age=Some(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 P-256 certificate. 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",
),
)
let der = @x509.self_signed(
key, b"\x01", "moonbitstack.example", "261029000000Z", "271029000000Z",
)
let cert = @x509.parse(der)
println(
"self-signed \{cert.subject.cn().unwrap_or("?")}: \{cert.signed_by(cert.spki)}",
)
// 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()))}")
self-signed moonbitstack.example: true
request verified: true
asks for this key: true
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"}")
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.