Credentials for MoonBit: JWT, JWK and X.509.
moon add moonbitstack/mooncred// The key is whatever signs. Here, HMAC over SHA-256.
let key = @jwt.mac(fn() {
@hmac.Hmac::new(secret[:], fn() { @sha2.Hasher::new() })
})
let token = @jwt.sign(claims, HS256, key)
// 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 compact serialisation, all thirteen JOSE algorithms | RFC 7515, 7518, 7519, 8037 |
| jwk | JSON Web Key and key sets: reading, writing, choosing | RFC 7517, RFC 7518 §6 |
| jwe | JSON Web Encryption: five-part compact serialisation, key wrapping, GCM and CBC-HMAC | RFC 7516, RFC 7518, RFC 3394 |
| 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, RFC 9053, RFC 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 verifier = match key.material() {
Okp(x~, ..) => @ed25519.PublicKey::new(x[:])
_ => abort("not an Ed25519 key")
}let policy = @jwt.Policy::new(issuer="https://issuer.example", leeway=30)
let got = @jwt.verify(token[:], HS256, key, now=At(seconds), issuer="https://issuer.example")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// Validate what a server sent, against the Mozilla roots.
let roots = @roots.mozilla() // build once, keep
let path = @x509.verify(chain[:], roots, 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("a.example.com") // subjectAltName names and addresses
let signed = cert.signed_by(issuer.spki) // raises for an algorithm it does not check| 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 key = @x509.PrivateKey::pem(text) // PRIVATE KEY, EC PRIVATE KEY, RSA PRIVATE KEY
let pairs = cert.spki.holds(key.public()) // whether the two belong togetherlet 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 key : @x509.PrivateKey = MlDsa(@mldsa.PrivateKey::from_seed(kind=D65, seed))
let der = @x509.self_signed(
key~,
cn="pqc.example",
serial=b"\x01",
not_before=1793232000L, // seconds since the Unix epoch
not_after=1824768000L,
exts={ ..@x509.Exts::empty(), san: [Dns("pqc.example")] },
)// A request to be signed, and the same request read back.
let pem = @csr.self_signed_pem(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(key.public()) // whether it asks for this key
// A key at rest, and the container that holds it.
let locked = key.encrypt(passphrase~, salt~, iv~, scheme=Aes256Cbc)
let back = @x509.PrivateKey::read_encrypted(locked[:], passphrase~)
let pfx = @pkcs12.build(certs~, key=Some(key.pkcs8()), passphrase~, salt~, iv~, mac_salt~)
let inside = @pkcs12.parse(pfx[:]).contents(passphrase~)
// Revocation, without a network in the library.
let list = @revoke.Crl::pem(text)
let fresh = list.fresh(now) // thisUpdate and nextUpdate, against the caller's clock
let status = list.status(cert) // Good, Revoked(entry), or Unknownlet text = @token.sign(payload, keys~, salt="session", at=now)
let value = @token.verify(text[:], keys~, salt="session", max_age=86400L, now~)let message = @cose.sign1(payload, alg=ES256, signer, kid=kid)
let opened = @cose.verify1(message[:], alg=ES256, verifier)
let claims = @cose.Claims::new(iss="moonbitstack", exp=4102444800L)
let cwt = @cose.signed(claims, alg=HS256, signer)
let checked = @cose.verified(cwt[:], alg=HS256, verifier, now=At(seconds))let path = @x509.verify(chain[:], roots, now~)
let certs = []
for cert in path {
certs.push(@policy.read(cert.raw))
}
let held = @policy.policies(certs, initial=["2.23.140.1.2.1"]) // empty: not valid for it| 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.