fn0-worker 0.3.16

Worker binary for the fn0 FaaS platform
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use hmac::{Hmac, Mac};
use serde::Deserialize;
use sha2::Sha256;

type HmacSha256 = Hmac<Sha256>;

#[derive(Deserialize)]
pub struct AdminTokenClaims {
    pub subdomain: String,
    pub task: String,
    pub github_login: String,
    pub iat: i64,
    pub exp: i64,
}

#[derive(Debug)]
pub enum VerifyError {
    Malformed,
    BadSignature,
    Expired,
    SubdomainMismatch,
}

pub fn decode_key_base64(s: &str) -> Result<[u8; 32], String> {
    let bytes = base64::engine::general_purpose::STANDARD
        .decode(s)
        .map_err(|e| format!("not valid base64: {e}"))?;
    bytes
        .try_into()
        .map_err(|v: Vec<u8>| format!("must be 32 bytes, got {}", v.len()))
}

pub fn verify_token(
    token: &str,
    key: &[u8; 32],
    expected_subdomain: &str,
    now_unix: i64,
) -> Result<AdminTokenClaims, VerifyError> {
    let (payload_b64, sig_b64) = token.split_once('.').ok_or(VerifyError::Malformed)?;
    let sig_bytes = URL_SAFE_NO_PAD
        .decode(sig_b64)
        .map_err(|_| VerifyError::Malformed)?;

    let mut mac = HmacSha256::new_from_slice(key).expect("HMAC key");
    mac.update(payload_b64.as_bytes());
    mac.verify_slice(&sig_bytes)
        .map_err(|_| VerifyError::BadSignature)?;

    let payload_json = URL_SAFE_NO_PAD
        .decode(payload_b64)
        .map_err(|_| VerifyError::Malformed)?;
    let claims: AdminTokenClaims =
        serde_json::from_slice(&payload_json).map_err(|_| VerifyError::Malformed)?;

    if claims.exp < now_unix {
        return Err(VerifyError::Expired);
    }
    if claims.subdomain != expected_subdomain {
        return Err(VerifyError::SubdomainMismatch);
    }
    Ok(claims)
}