Credentials for MoonBit: JWT, JWK and X.509.
moon add moonbitstack/mooncred// The key is whatever signs. Here, HMAC over SHA-256.
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)
// The instant the claims are judged at, in Unix seconds: data, not a clock.
let seconds = 1793232000L
// The algorithm is the caller's, not the token's.
let got = @jwt.verify(token[:], HS256, key, now=At(seconds),
issuer="moonbitstack", audience=["moonapi"])| Package | What | Specification |
|---|---|---|
| jwt | JWS in both serialisations, with unencoded payloads; JWT; all thirteen JOSE algorithms | RFC 7515, 7518, 7519, 7797, 8037 |
| jwk | JSON Web Key and key sets: reading, writing, choosing | RFC 7517, RFC 7518 §6 |
| jwe | JSON Web Encryption in both serialisations: every RFC 7518 key-management algorithm, several recipients, additional data, compression | RFC 7516, 7518, 3394, 1951 |
| token | A signed string for a cookie or a URL: expiry on verify, a salt per purpose, key rotation | — |
| x509 | X.509 v3 certificates: read one down to its extensions, check its signature under RSA, ECDSA, Ed25519 or ML-DSA, match it against a host, validate a chain to a set of trust anchors; build a self-signed one for any key; read the private key that goes with it | RFC 5280, RFC 6125, RFC 7468, RFC 5958, RFC 5915, RFC 9881 |
| csr | Certificate signing requests: read one, check its signature, see what it asks for, and write one | RFC 2986, RFC 2985 §5.4 |
| pbe | Encrypted private keys: PBES2 with PBKDF2, and the older PBES1 and PKCS#12 derivations openssl still writes | RFC 8018, RFC 7292 §B.2 |
| pkcs12 | PFX containers: the certificate chain and the key, with a MAC, read and written | RFC 7292, RFC 5652 |
| revoke | Revocation lists and OCSP responses: read one and judge a certificate good, revoked or unknown | RFC 5280 §5, RFC 6960 |
| policy | Certificate policies over a chain: the §6.1 tree, mappings and the three counters | RFC 5280 §6.1 |
| cbor | CBOR, the subset COSE and CWT are written in | RFC 8949 |
| cose | COSE_Sign1, COSE_Mac0, COSE_Encrypt0, and CWT claim sets | RFC 9052, 9053, 8392 |
| roots | The Mozilla CA store as trust anchors, from curl's extract | — |
@jwt.policy < the policy you pass < the arguments you pass// A policy for the deployment, assembled once.
let ours = @jwt.Policy::new(issuer="moonbitstack", audience=["moonapi"])
// The same thing written as a record update.
let same : @jwt.Policy = { ..@jwt.policy, issuer: Some("moonbitstack"), audience: ["moonapi"] }
// Used as it is, and used with one thing changed for this call.
let a = @jwt.verify(token[:], HS256, key, now=At(seconds), policy=ours)
let b = @jwt.verify(token[:], HS256, key, now=At(seconds), policy=ours, leeway=60)
// Reading a token without asking whether it has expired.
let c = @jwt.verify(token[:], HS256, key, now=Ignored)let signer = @jwt.mac(fn() {
@hmac.Hmac::new(b"a shared secret nobody else has"[:], fn() { @sha2.Hasher::new() })
})
// b64: false, and the payload travels beside the token (RFC 7797 §4).
let detached = @jwt.sign_bytes(b"$.02", HS256, signer, b64=false, detached=true)
let octets = @jwt.verify_bytes(detached[:], HS256, signer, b64=false, payload=b"$.02")
assert_eq(octets, b"$.02")
// A payload that is not ASCII cannot travel in the compact form unencoded, so
// the compact form encodes it; the JSON serialisation can carry it either way.
let binary = @jwt.sign_bytes(b"\x00\x01\x02", HS256, signer)
assert_eq(@jwt.verify_bytes(binary[:], HS256, signer), b"\x00\x01\x02")fn kid_of(kid : String) -> Map[String, Json] {
let out : Map[String, Json] = Map([])
out["kid"] = Json::string(kid)
out
}let message = @jwt.Message::new(b"{\"iss\":\"moonbitstack\"}")
// The key's name travels unprotected; `alg` is always signed.
.sign(HS256, signer, header=kid_of("2026-09"))
// A second signature by the same key with no signed header at all
// (RFC 7515 §A.5): the payload is authenticated and nothing about it is.
.sign(HS256, signer, header=kid_of("2027-03"), bare=true)
let general = @json.dumps(message.json())
let flattened = @json.dumps(message.flattened()) // refused unless there is one
ignore(flattened)
// Each signature is offered with the algorithm and key that may open it, and
// nothing else is tried.
let checked = @jwt.verify_json(general[:], checks=[@jwt.Check::new(HS256, signer)])
assert_eq(checked.payload, b"{\"iss\":\"moonbitstack\"}")let published = @jwk.Jwk::parse(
"{\"kty\":\"OKP\",\"crv\":\"Ed25519\",\"x\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\"}",
)
let verifier : &@spec.Verifier = match published.material() {
Okp(x~, ..) => (@ed25519.PublicKey::new(x[:]) : &@spec.Verifier)
_ => abort("not an Ed25519 key")
}let material = @jwk.Jwk::parse(
"{\"kty\":\"oct\",\"k\":\"aGVsbG8gdGhlcmUgdGhpcyBpcyBhIGtleQ\",\"alg\":\"HS256\"}",
)
assert_eq(material.alg(), Some("HS256"))
assert_eq(material.crt(), None)let rules = @jwt.Policy::new(issuer="https://issuer.example", leeway=30)
let checked = @jwt.verify(token[:], HS256, key, now=At(seconds), issuer="https://issuer.example")
ignore(rules)
ignore(checked)let plain = @jwt.sign(claims, HS256, key) // typ: "JWT"
let access = @jwt.sign(claims, HS256, key, typ=Some("at+jwt")) // typ: "at+jwt"
let bare = @jwt.sign(claims, HS256, key, typ=None) // no typ header
ignore(plain)
ignore(access)
ignore(bare)let seed = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
let issue_key : @x509.PrivateKey = MlDsa(@mldsa.PrivateKey::from_seed(kind=D65, seed))
let der = @x509.self_signed(
key=issue_key,
cn="example.com",
serial=b"\x01",
not_before=1793232000L, // seconds since the Unix epoch
not_after=1824768000L,
exts={ ..@x509.Exts::empty(), san: [Dns("example.com")] },
)
let chain : Array[Bytes] = [der]
let anchors = @x509.Roots::new(certs=chain[:])
let now = 1793232000L// The peer's leaf first, then whatever intermediates it sent, in any order.
let path = @x509.verify(chain[:], anchors, now~, host="example.com")
// path: the leaf, the intermediates it runs through, the anchor
// Or one certificate on its own.
let cert = @x509.parse(der[:])
let serves = cert.matches("example.com") // subjectAltName names and addresses
let signed = cert.signed_by(cert.spki) // raises for an algorithm it does not check
let every_anchor = @roots.mozilla() // the bundle, its own package
assert_true(serves)
assert_true(signed)
assert_eq(path.length(), 1)
assert_true(every_anchor.length() > 100)| Rule | From |
|---|---|
| Names come from subjectAltName only, never the commonName; an IP literal is held to the iPAddress entries | RFC 6125, Go, webpki |
| Every issuer is a CA, has keyCertSign when it lists key usages, respects its pathLenConstraint; a CA is not a leaf | RFC 5280 §4.2.1.9, webpki |
| Validity on every certificate but the anchor | RFC 5280 §6.1.3 |
| The leaf allows the purpose asked for; an issuer that lists purposes allows it too | RFC 5280 §4.2.1.12, Go |
| An unknown critical extension refuses the chain | RFC 5280 §4.2 |
| Name constraints over dNSName, iPAddress, rfc822Name, directoryName and URI hosts | RFC 5280 §4.2.1.10 |
| RSA PKCS#1 and PSS, ECDSA on four curves, Ed25519, ML-DSA at all three sets; no SHA-1 | webpki, Go, RFC 9881 |
| An ML-DSA key asserts no keyEncipherment or keyAgreement, which a signing key cannot do | RFC 9881 §5 |
| At most six intermediates and a hundred signature checks | webpki |
let text = issue_key.pkcs8_pem() // what a PRIVATE KEY file contains
let loaded = @x509.PrivateKey::pem(text) // PRIVATE KEY, EC PRIVATE KEY, RSA PRIVATE KEY
let pairs = cert.spki.holds(loaded.public()) // whether the two belong together
assert_true(pairs)// A request to be signed, and the same request read back.
let pem = @csr.self_signed_pem(issue_key, subject=@x509.common_name("csr.example.com"))
let request = @csr.Request::pem(pem)
let sound = request.verified() // the signature, over the octets that arrived
let mine = request.asks_for(issue_key.public()) // whether it asks for this key
assert_true(sound)
assert_true(mine)// A key at rest, and the container that holds it.
let salt = b"0123456789abcdef"
let passphrase = "a passphrase for the container"
let locked = issue_key.encrypt(passphrase~, salt~, iv=salt[:], scheme=Aes256Cbc)
let back = @x509.PrivateKey::read_encrypted(locked[:], passphrase~)
let pfx = @pkcs12.build(
certs=chain[:],
key=Some(issue_key.pkcs8()),
passphrase~,
salt~,
iv=salt[:],
mac_salt=salt[:],
)
let inside = @pkcs12.parse(pfx[:]).contents(passphrase~)
assert_true(back.public() is MlDsa(_))
assert_true(inside.key is Some(_))fn standing(crl_pem : String, cert : @x509.Cert, at : Int64) -> String raise {
let list = @revoke.Crl::pem(crl_pem)
if !list.fresh(at) {
return "stale"
}
match list.status(cert) {
Good => "good"
Revoked(_) => "revoked"
Unknown => "unknown"
}
}let keys = @token.Keys::new(b"the key that signs session cookies")
let text = @token.sign(b"user=heke1228", keys~, salt="session", at=seconds)
let value = @token.verify(text[:], keys~, salt="session", max_age=86400L, now=seconds)
assert_eq(value, b"user=heke1228")| Key management | What the recipient holds | Wrapped key on the wire |
|---|---|---|
| dir | the content key itself | nothing |
| A128KW, A192KW, A256KW | a shared key-encryption key | the content key, AES key wrap (RFC 3394) |
| A128GCMKW, A192GCMKW, A256GCMKW | a shared key-encryption key | the content key under AES-GCM; nonce and tag in the header |
| RSA-OAEP-256 | an RSA key | the content key under RSAES-OAEP with SHA-256 |
| ECDH-ES | an elliptic-curve key | nothing: the content key is derived |
| ECDH-ES+A128KW, +A192KW, +A256KW | an elliptic-curve key | the content key under a derived key-encryption key |
| PBES2-HS256+A128KW, HS384+A192KW, HS512+A256KW | a password | the content key under a key derived from it |
let shared = b"a 16-octet key"
let content = b"0001020304050607"
let iv = b"twelve bytes"
// dir: the shared key is the content key.
let direct = @jwe.encrypt(
b"card ending 4242",
key=Shared(shared),
alg=Dir,
enc=A128Gcm,
iv~,
)
assert_eq(@jwe.decrypt(direct[:], key=Shared(shared), alg=Dir, enc=A128Gcm), b"card ending 4242")
// A key wrapped under a password: the salt and the count are the caller's,
// and the key is derived rather than held.
let sealed = @jwe.encrypt(
b"card ending 4242",
key=Password(b"a passphrase for the auditor"),
alg=Pbes2Hs256A128Kw,
enc=A128Gcm,
cek=content,
iv~,
salt=b"sixteen byte sal",
count=4096,
)
let opened = @jwe.decrypt(
sealed[:],
key=Password(b"a passphrase for the auditor"),
alg=Pbes2Hs256A128Kw,
enc=A128Gcm,
)
assert_eq(opened, b"card ending 4242")
// Every algorithm is accepted by default.
assert_eq(@jwe.policy.rsa1_5, true)
assert_eq(@jwe.policy.rsa_oaep, true)
// A deployment that will not answer for the two says so once, and both
// directions obey it.
let only_modern = @jwe.strict
assert_eq(only_modern.rsa1_5, false)
// Or one at a time.
let no_rsa1_5 = @jwe.Policy::new(rsa1_5=false)
assert_eq(no_rsa1_5.rsa_oaep, true)let content = b"0001020304050607"
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",
)
let document = @json.dumps(message)
let opened = @jwe.open(
document[:],
enc=A128Gcm,
unseal=[@jwe.Unseal::new(A128Kw, Shared(b"sixteen byte key"))],
)
assert_eq(opened.index, 0) // which recipient opened it
assert_eq(opened.aad, b"tenant=42") // authenticated, not carried
assert_eq(opened.plain, b"card ending 4242")let signature_key = @ed25519.PrivateKey::new(seed)
let message = @cose.sign1(b"payload", alg=EdDSA, signature_key, kid=b"2026-09")
let opened = @cose.verify1(message[:], alg=EdDSA, signature_key.public())
assert_eq(opened, b"payload")
let claims : @cose.Claims = { ..@cose.Claims::new(), iss: Some("moonbitstack"), exp: Some(4102444800L) }
let cwt = @cose.signed(claims, alg=EdDSA, signature_key)
let checked = @cose.verified(cwt[:], alg=EdDSA, signature_key.public(), now=At(seconds))
assert_eq(checked.iss, Some("moonbitstack"))let certificates = []
for one in path {
certificates.push(@policy.read(one.raw))
}
let held = @policy.policies(certificates, initial=["2.23.140.1.2.1"])
assert_eq(held.length(), 0) // this certificate carries no policy of that name| The rule | Cases |
|---|---|
| The trusted anchor is a name and a key: its own validity, extensions and criticality are not examined. | rfc5280::aki::critical-aki, rfc5280::aki::cross-signed-root-missing-aki, rfc5280::nc::permitted-dns-match-noncritical, rfc5280::root-inconsistent-ca-extensions, rfc5280::root-missing-basic-constraints, rfc5280::root-non-critical-basic-constraints, rfc5280::ski::root-missing-ski, rfc5280::unknown-critical-extension-root, rfc5280::validity::expired-root, webpki::aki::root-with-aki-all-fields, webpki::aki::root-with-aki-authoritycertissuer, webpki::aki::root-with-aki-authoritycertserialnumber, webpki::aki::root-with-aki-missing-keyidentifier, webpki::aki::root-with-aki-ski-mismatch, webpki::eku::root-has-eku |
| A leaf certificate that is itself a CA is refused. | pathlen::validation-ignores-pathlen-in-leaf, rfc5280::ca-as-leaf |
| The profile rules that decide how a certificate may be issued, rather than whether a path is trustworthy: a common name that repeats its subject alternative name character for character, the criticality of a subject alternative name or a key purpose, the shape of an authority information access extension, wildcards over a public suffix. | webpki::cn::case-mismatch, webpki::cn::ipv4-hex-mismatch, webpki::cn::ipv4-leading-zeros-mismatch, webpki::cn::ipv6-non-rfc5952-mismatch, webpki::cn::ipv6-uncompressed-mismatch, webpki::cn::ipv6-uppercase-mismatch, webpki::cn::not-in-san, webpki::cn::punycode-not-in-san, webpki::cn::utf8-vs-punycode-mismatch, webpki::eku::ee-anyeku, webpki::eku::ee-critical-eku, webpki::eku::ee-without-eku, webpki::malformed-aia, webpki::san::public-suffix-multi-label-wildcard-san, webpki::san::public-suffix-private-namespace-wildcard-san, webpki::san::san-critical-with-nonempty-subject |
| An authority key identifier or a subject key identifier, which the path algorithm never reads. | rfc5280::aki::intermediate-missing-aki, rfc5280::aki::leaf-missing-aki, rfc5280::ski::critical-ski, rfc5280::ski::intermediate-missing-ski |
| The strength of the key that signs, which policy.bits bounds, and not the shape of the key the leaf carries. | webpki::forbidden-dsa-leaf, webpki::forbidden-p192-leaf, webpki::forbidden-rsa-key-not-divisible-by-8-in-leaf, webpki::forbidden-rsa-not-divisible-by-8-in-root, webpki::forbidden-weak-rsa-in-leaf |
| The issuance rules for a certificate's own extensions, which a validator is not asked to audit: a serial number of zero or over twenty octets, keyCertSign on an end entity, name constraints in an end entity, the criticality of policy constraints. | rfc5280::ca-empty-subject, rfc5280::leaf-ku-keycertsign, rfc5280::nc::not-allowed-in-ee-critical, rfc5280::nc::not-allowed-in-ee-noncritical, rfc5280::pc::ica-noncritical-pc, rfc5280::san::noncritical-with-empty-subject, rfc5280::san::underscore-dns, rfc5280::serial::too-long, rfc5280::serial::zero |
| A constraint written with a leading period, read as every name below it. | rfc5280::nc::invalid-dnsname-leading-period |
Install
Download zipCredentials for MoonBit: JWT, JWK and X.509.