raisin.crypto
Cryptographic primitives available inside a
function, in both the
JavaScript (QuickJS) and Starlark runtimes. The JavaScript calls are
synchronous; await is harmless but not needed.
This is the complete surface. There is no md5, sha1, randomUUID or HMAC
binding.
Methods
| JavaScript | Starlark | Returns |
|---|---|---|
crypto.uuid() | crypto.uuid() | UUID v4 string |
crypto.randomBytes(n) | crypto.random_bytes(n) | base64url (unpadded) string of n random bytes, n in 1..=64 |
crypto.hash(input, alg?) | crypto.hash(input, alg?) | lowercase hex digest |
crypto.generateKeyPair(alg?) | crypto.generate_key_pair(alg?) | { alg, publicJwk, privateJwk } |
crypto.signJwt(claims, privateJwk, opts?) | crypto.sign_jwt(...) | compact JWS string |
crypto.verifyJwt(token, opts?) | crypto.verify_jwt(token, opts?) | { valid, claims?, error? } |
raisin.crypto.uuid(); // "20e0fd9d-85c2-4d0d-9355-c9cf1639a13e"
raisin.crypto.randomBytes(16); // "MuK5ndZzYd2sPdh9_r0Mgw"
raisin.crypto.hash("hello"); // "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
randomBytes(n)
n cryptographically secure random bytes from the operating system,
base64url-encoded without padding, so the result can go straight into a URL,
a filename or a JWS segment. n outside 1..=64 is an error
([crypto:invalid_length]). This and uuid() are the only CSPRNG sources in
the runtime; Math.random() is not one.
hash(input, alg?)
Lowercase hex digest of a UTF-8 string. alg is "sha256" (default) or
"sha512"; anything else is [crypto:unsupported_alg].
raisin.crypto.hash("hello"); // sha256
raisin.crypto.hash("hello", "sha512"); // sha512
A bare digest is not a message authentication code. To authenticate a payload
(for example a webhook body), sign it with signJwt rather than hashing
secret + body.
generateKeyPair(alg?)
Generates a signing key pair. alg defaults to "ES256" (ECDSA on P-256)
and is currently the only supported value.
const { alg, publicJwk, privateJwk } = raisin.crypto.generateKeyPair();
// publicJwk: { kty: "EC", crv: "P-256", alg: "ES256", use: "sig", kid, x, y }
// privateJwk: the same plus d
Publish publicJwk in a JWKS document; pass privateJwk whole to signJwt.
kid is derived from the public point, so the same key always has the same
id. The runtime never logs the private key; where you store it is up to you
(see below).
signJwt(claims, privateJwk, opts?)
Signs claims (an object) into a compact JWS header.payload.signature.
| Option | Type | Meaning |
|---|---|---|
alg | string | Must be "ES256" if given |
kid | string | Header kid. Defaults to the private JWK's own kid |
expiresInSec | integer | Sets exp = now + expiresInSec. Must be a positive whole number |
- The header is always
{"alg":"ES256","typ":"JWT","kid":...}; the caller cannot changealg. iatis added when the claims do not carry one.expiresInSecoverrides anyexpalready present inclaims.- A non-integer, zero, negative or string
expiresInSecis rejected with[crypto:invalid_options]rather than ignored (600.0is accepted,899.5is not), because a token withoutexpis read as never expiring by most verifiers. - Passing options positionally (
signJwt(claims, key, 600)) is an error.
verifyJwt(token, opts?)
Verifies an RS256 or ES256 token against a JWKS.
opts = { jwks_url, issuer?, audience?, algorithms? }. Returns
{ valid: true, claims } or { valid: false, error }; an invalid or expired
token, or a missing jwks_url, is a valid: false result rather than an
exception.
The call throws only when the JWKS host is not allowed by the function's
network_policy ([crypto:policy_denied], checked before any connection is
opened) or the JWKS cannot be fetched or parsed ([crypto:jwks_unreachable],
[crypto:jwks_invalid]). Key sets are cached for five minutes; redirects are
not followed.
Signature encoding
An ES256 signature is the JOSE fixed-width form: r and s as 32 raw bytes
each, concatenated into 64 bytes and base64url-encoded without padding. This
is what RFC 7515 and 7518 specify and what JOSE libraries (jose,
jsonwebtoken, PyJWT, go-jose) expect. If you verify with a raw ECDSA
API instead, convert the 64 bytes to DER first. All JWS segments and the JWK
x, y and d members are base64url without padding.
Sign and verify an ES256 token
Mint an offline-verifiable ticket token, then verify it against the JWKS the same tenant publishes.
export async function issueTicket(input) {
// 1. A signing key. Generate once, store the private half in the secret
// store, publish the public half at /.well-known/jwks.json.
const privateJwk = JSON.parse(raisin.secrets.get("tickets/signing-key"));
// 2. Mint the token. exp comes from expiresInSec, iat is automatic,
// kid is taken from the key so the verifier can select it.
const token = raisin.crypto.signJwt(
{
iss: "https://tickets.example.com",
aud: "door-scanner",
sub: input.ticket_id,
seat: input.seat,
jti: raisin.crypto.randomBytes(16),
},
privateJwk,
{ expiresInSec: 24 * 60 * 60 }
);
// 3. Verify, the same call the door scanner makes.
const result = raisin.crypto.verifyJwt(token, {
jwks_url: "https://tickets.example.com/.well-known/jwks.json",
issuer: "https://tickets.example.com",
audience: "door-scanner",
algorithms: ["ES256"],
});
if (!result.valid) {
throw new Error(`ticket token rejected: ${result.error}`);
}
return { token, claims: result.claims };
}
The function's network_policy must allow tickets.example.com, or step 3
fails with [crypto:policy_denied].
Generating the key pair is a one-off:
const { publicJwk, privateJwk } = raisin.crypto.generateKeyPair("ES256");
// publish { keys: [publicJwk] }; keep privateJwk in the secret store
Reading a signing key from raisin.secrets
A private JWK is a credential. Keep it in the
secret store, either as a named secret or in a
node property declared encrypted: true, and not in an ordinary property or
a log line.
Reading it back requires a secret_policy on the function. Without a
matching grant raisin.secrets.get() fails with [secrets:policy_denied]
and signJwt is never reached:
# functions/issue-ticket/.node.yaml
secret_policy:
enabled: true
allowed_names: ["tickets/*"]
const privateJwk = JSON.parse(raisin.secrets.get("tickets/signing-key"));
Two things to plan for:
- Rotation is a secret-store operation. Secrets are versioned: publish
the new
publicJwkin the JWKS alongside the old one, rotate the secret, and let in-flight tokens age out. Thekidin each header tells the verifier which key to use. cryptoitself has no policy gate.uuid,randomBytes,hash,generateKeyPairandsignJwttouch nothing outside the process, so any function can call them. The capability checks are thesecret_policyon the key and thenetwork_policyonverifyJwt'sjwks_url. A function that can read the key can mint any claims, so keepallowed_namesnarrow.
Errors
Failures carry a machine-readable tag as a prefix of the message:
| Tag | Cause |
|---|---|
[crypto:unsupported_alg] | alg other than ES256, or a digest other than sha256/sha512 |
[crypto:invalid_length] | randomBytes(n) with n outside 1..=64 |
[crypto:invalid_key] | Malformed EC private JWK. The message does not say which component was wrong |
[crypto:invalid_claims] | Claims are not an object or not JSON-serializable |
[crypto:invalid_options] | opts is not an object, or expiresInSec is not a positive integer |
[crypto:invalid_expiry] | expiresInSec is not greater than zero |
[crypto:policy_denied] | verifyJwt's jwks_url is not allowed by network_policy |
[crypto:jwks_unreachable], [crypto:jwks_invalid] | The JWKS could not be fetched or parsed |
[crypto:rng_failed], [crypto:keygen_failed], [crypto:sign_failed] | The underlying primitive failed |
See also
- Creating Functions
- Secrets
- Virtual Node Adapters, which use
verifyJwtin the signed-push path