Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::auth_jwt_decode_jwt::check_jwt_secret; ← decodeJwt, another function of this group · built into the same file, even by a slim install
use super::crypto_hmac_sha256::hmac_sha256; ← from crypto.hmac-sha256 ^1.0.0 · built alongside by fune
use super::encoding_base64_base64_url_encode::base64_url_encode;
/// base64url of {"alg":"HS256","typ":"JWT"}: the header is fixed.
const HEADER: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
const MAX_DEPTH: usize = 32;
const MAX_SAFE: i64 = 9007199254740991;
fn quote(text: &str, out: &mut String) {
out.push('"');
for ch in text.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{8}' => out.push_str("\\b"),
'\u{c}' => out.push_str("\\f"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
}
/// One canonical JSON text: no spaces, object keys sorted by code point
/// (Rust's `String` order), whole numbers only, non-ASCII written as UTF-8.
fn canonical(value: &Value, depth: usize, out: &mut String) {
const WHOLE: &str = "claims may only hold whole numbers from -(2^53 - 1) to 2^53 - 1";
match value {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Int(i) => {
if i.abs() > MAX_SAFE {
panic!("{}", WHOLE);
}
out.push_str(&i.to_string());
}
Value::Float(f) => {
if !f.is_finite() || f.fract() != 0.0 || f.abs() > MAX_SAFE as f64 {
panic!("{}", WHOLE);
}
out.push_str(&(*f as i64).to_string());
}
Value::Str(s) => quote(s, out),
Value::Arr(items) => {
if depth > MAX_DEPTH {
panic!("claims are nested more than 32 deep");
}
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
canonical(item, depth + 1, out);
}
out.push(']');
}
Value::Obj(pairs) => {
if depth > MAX_DEPTH {
panic!("claims are nested more than 32 deep");
}
let mut sorted: Vec<&(String, Value)> = pairs.iter().collect();
sorted.sort_by(|a, b| a.0.cmp(&b.0));
out.push('{');
for (i, (key, item)) in sorted.into_iter().enumerate() {
if i > 0 {
out.push(',');
}
quote(key, out);
out.push(':');
canonical(item, depth + 1, out);
}
out.push('}');
}
}
}
/// An HS256 JWT for the claims: header {"alg":"HS256","typ":"JWT"}, the
/// claims as canonical JSON, and the HMAC-SHA256 of the two under the secret.
///
/// # Panics
/// Panics if the secret is under 32 bytes, the claims are not an object, or
/// they hold a fraction, an unsafe integer or nesting past 32.
pub fn sign_jwt(claims: &Value, secret: &[i64]) -> String {
check_jwt_secret(secret);
if !matches!(claims, Value::Obj(_)) {
panic!("claims must be a JSON object");
}
let mut payload = String::new();
canonical(claims, 1, &mut payload);
let payload_bytes: Vec<i64> = payload.bytes().map(|b| b as i64).collect();
let signing_input = format!("{}.{}", HEADER, base64_url_encode(&payload_bytes));
let input: Vec<i64> = signing_input.bytes().map(|b| b as i64).collect();
format!("{}.{}", signing_input, base64_url_encode(&hmac_sha256(secret, &input)))
}
pub fn fune_vector(args: &[Value]) -> Value {
let secret: Vec<i64> = match &args[1] {
Value::Arr(items) => items
.iter()
.map(|item| match item {
Value::Int(i) => *i,
_ => panic!("secret must be a list of integers from 0 to 255"),
})
.collect(),
_ => panic!("secret must be a list of integers from 0 to 255"),
};
Value::str(&sign_jwt(&args[0], &secret))
}