Functional Weave
Code in Rust

crypto.hmac-sha256

HMAC-SHA256 of a message under a secret key (RFC 2104, RFC 4231), in pure code that also runs in the browser.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 13 tests, run in TypeScript, Python and Rust.

What it does

`hmacSha256(key, message)` is the 32-byte HMAC-SHA256 tag of `message` under `key`, as RFC 2104 defines HMAC and RFC 4231 pins it for SHA-256. It is what signs an HS256 JWT (`auth.jwt`), a webhook payload or a signed cookie.

Bytes in and out are lists of integers 0 to 255, as everywhere in the registry (see `encoding.hex`). A text secret or message goes through `encoding.utf8` first.

For example

  • hmac_sha256(11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 72, 105, 32, 84, 104, 101, 114, 101) → 176, 52, 76, 97, 216, 219, 56, 83, 92, 168, 175, 206, 175, 11, 241, 43, 136, 29, 194, 0, 201, 131, 61, 167, 38, 233, 55, 108, 46, 50, 207, 247 RFC 4231 test case 1: a 20-byte key
  • hmac_sha256(74, 101, 102, 101, 119, 104, 97, 116, 32, 100, 111, 32, 121, 97, 32, 119, 97, 110, 116, 32, 102, 111, 114, 32, 110, 111, 116, 104, 105, 110, 103, 63) → 91, 220, 193, 70, 191, 96, 117, 78, 106, 4, 36, 38, 8, 149, 117, 199, 90, 0, 63, 8, 157, 39, 57, 131, 157, 236, 88, 185, 100, 236, 56, 67 RFC 4231 test case 2: a key shorter than the output
  • hmac_sha256(170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,…) → 119, 62, 169, 30, 54, 128, 14, 70, 133, 77, 184, 235, 208, 145, 129, 167, 41, 89, 9, 139, 62, 248, 193, 34, 217, 99, 85, 20, 206, 213, 101, 254 RFC 4231 test case 3: 50 bytes of 0xdd under 20 bytes of 0xaa

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

pub fn hmac_sha256(key: &[i64], message: &[i64]) -> Vec<i64>
keyint[]the secret, any length; one longer than 64 bytes is hashed first, as RFC 2104 says
messageint[]the bytes to authenticate
returnsint[]the 32-byte tag; compare tags with crypto.constant-time-equal, never ==

Your code names it in one line, in the file that uses it

fune!(crypto.hmac-sha256@^1);  // then call hmac_sha256(…)
impl/rust.rs · 56 lines · open · raw

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::crypto_sha256::sha256;  ← from crypto.sha256 ^1.0.0 · built alongside by fune

const BLOCK: usize = 64;

fn check_bytes(value: &[i64], name: &str) {
    if value.iter().any(|b| !(0..=255).contains(b)) {
        panic!("{} must be a list of integers from 0 to 255", name);
    }
}

/// HMAC-SHA256 (RFC 2104): H((K ^ opad) || H((K ^ ipad) || message)), with a
/// key longer than the 64-byte block hashed down to 32 bytes first.
///
/// # Panics
/// Panics if any key or message value is outside 0-255.
pub fn hmac_sha256(key: &[i64], message: &[i64]) -> Vec<i64> {
    check_bytes(key, "key");
    check_bytes(message, "message");
    let hashed;
    let k: &[i64] = if key.len() > BLOCK {
        hashed = sha256(key);
        &hashed
    } else {
        key
    };
    let mut inner: Vec<i64> = Vec::with_capacity(BLOCK + message.len());
    let mut outer: Vec<i64> = Vec::with_capacity(BLOCK + 32);
    for i in 0..BLOCK {
        let b = if i < k.len() { k[i] } else { 0 };
        inner.push(b ^ 0x36);
        outer.push(b ^ 0x5c);
    }
    inner.extend_from_slice(message);
    outer.extend(sha256(&inner));
    sha256(&outer)
}

fn bytes_from_value(value: &Value, name: &str) -> Vec<i64> {
    match value {
        Value::Arr(items) => items
            .iter()
            .map(|item| match item {
                Value::Int(i) => *i,
                _ => panic!("{} must be a list of integers from 0 to 255", name),
            })
            .collect(),
        _ => panic!("{} must be a list of integers from 0 to 255", name),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    let key = bytes_from_value(&args[0], "key");
    let message = bytes_from_value(&args[1], "message");
    Value::Arr(hmac_sha256(&key, &message).into_iter().map(Value::Int).collect())
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add crypto.hmac-sha256
Download for Rust crypto.hmac-sha256-1.0.0-rust.fune · 11,999 bytes sha256 b67fe2226a3ecf4b20c98252358c3e167a7fb4df72dd0679425faf2c8503fee2

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./crypto.hmac-sha256-1.0.0-rust.fune, or fetch it from a terminal with fune pull crypto.hmac-sha256@1.0.0:rust.

The whole function, every language, is one file too: crypto.hmac-sha256-1.0.0.fune, 14,277 bytes, sha256 aefbaef231a3cc8016bbff5c5226b4169f41ea671335aac1093c35e6d5bbf81e. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before crypto.hmac-sha256

after — your function gets the result and the arguments, and returns the final result.

// fune: after crypto.hmac-sha256

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace crypto.sha256 in crypto.hmac-sha256

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show crypto.hmac-sha256 --steps.

// fune: step crypto.hmac-sha256 after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
RFC 4231 test case 1: a 20-byte key 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 72, 105, 32, 84, 104, 101, 114, 101 → 176, 52, 76, 97, 216, 219, 56, 83, 92, 168, 175, 206, 175, 11, 241, 43, 136, 29, 194, 0, 201, 131, 61, 167, 38, 233, 55, 108, 46, 50, 207, 247
RFC 4231 test case 2: a key shorter than the output 74, 101, 102, 101, 119, 104, 97, 116, 32, 100, 111, 32, 121, 97, 32, 119, 97, 110, 116, 32, 102, 111, 114, 32, 110, 111, 116, 104, 105, 110, 103, 63 → 91, 220, 193, 70, 191, 96, 117, 78, 106, 4, 36, 38, 8, 149, 117, 199, 90, 0, 63, 8, 157, 39, 57, 131, 157, 236, 88, 185, 100, 236, 56, 67
RFC 4231 test case 3: 50 bytes of 0xdd under 20 bytes of 0xaa 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,… → 119, 62, 169, 30, 54, 128, 14, 70, 133, 77, 184, 235, 208, 145, 129, 167, 41, 89, 9, 139, 62, 248, 193, 34, 217, 99, 85, 20, 206, 213, 101, 254
RFC 4231 test case 4: a 25-byte counting key 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205… → 130, 85, 138, 56, 154, 68, 60, 14, 164, 204, 129, 152, 153, 242, 8, 58, 133, 240, 250, 163, 229, 120, 248, 7, 122, 46, 63, 244, 103, 41, 102, 91
RFC 4231 test case 5: the full tag, whose first 16 bytes the RFC publishes (a3b61674...) 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 84, 101, 115, 116, 32, 87, 105, 116, 104, 32, 84, 114, 117, 110, 99, 97, 116, 105, 111, 110 → 163, 182, 22, 116, 115, 16, 14, 224, 110, 12, 121, 108, 41, 85, 85, 43, 250, 111, 124, 10, 106, 138, 239, 139, 147, 248, 96, 170, 176, 205, 32, 197
RFC 4231 test case 6: a 131-byte key is hashed first 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170,… → 96, 228, 49, 89, 30, 224, 182, 127, 13, 138, 38, 170, 203, 245, 183, 127, 142, 11, 198, 33, 55, 40, 197, 20, 5, 70, 4, 15, 14, 227, 127, 84
RFC 4231 test case 7: a long key and a message longer than a block 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170,… → 155, 9, 255, 167, 27, 148, 47, 203, 39, 99, 95, 188, 213, 176, 233, 68, 191, 220, 99, 100, 79, 7, 19, 147, 138, 127, 81, 83, 92, 58, 53, 226
the empty key and the empty message , → 182, 19, 103, 154, 8, 20, 217, 236, 119, 47, 149, 215, 120, 195, 95, 197, 255, 22, 151, 196, 147, 113, 86, 83, 198, 199, 18, 20, 66, 146, 197, 173
a key of exactly one block (64 bytes) is used as is, not hashed (Python hmac as the reference) 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 4… → 106, 181, 65, 180, 134, 157, 202, 113, 196, 202, 17, 216, 187, 27, 2, 83, 59, 120, 154, 85, 117, 131, 22, 20, 41, 41, 44, 116, 4, 188, 33, 246
a key one byte over the block is hashed (Python hmac as the reference) 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 4… → 223, 191, 254, 228, 103, 27, 173, 0, 237, 93, 30, 25, 153, 213, 94, 211, 176, 204, 119, 74, 195, 87, 249, 235, 246, 73, 193, 97, 36, 20, 252, 236
Show the other 3 tests
CaseArgumentsExpected
a key byte above 255 1, 2, 256, 97 → error: key must be a list of integers from 0 to 255
a fractional message byte 1, 2, 3, 97.5 → error: message must be a list of integers from 0 to 255
a message given as text rather than bytes 1, 2, 3, abc → error: message must be a list of integers from 0 to 255

More from the author

The construction is `H((K xor opad) || H((K xor ipad) || message))` with the key zero-padded to SHA-256's 64-byte block, or first hashed to 32 bytes when it is longer than 64. That last rule is the one a naive implementation misses, and RFC 4231 test cases 6 and 7 (a 131-byte key) are among the vectors. The empty key is allowed, as the RFC allows it, but a real key should be at least 32 random bytes (RFC 2104 section 3; RFC 7518 requires it for HS256).

**Compare tags with `crypto.constant-time-equal`.** Comparing with `==` stops at the first differing byte, and the time that takes tells an attacker how many leading bytes of a forged tag were right.

TypeScript and Rust build on `crypto.sha256` and so run anywhere, the browser included, with no `node:crypto` or `crypto.subtle`; Python uses the standard library's `hmac`. Test case 5 of RFC 4231 publishes only the first 16 bytes of its tag; its vector holds the full 32, computed with Python's `hmac` module as the reference, and begins with the RFC's 16.

Source: RFC 2104, HMAC: Keyed-Hashing for Message Authentication (https://www.rfc-editor.org/rfc/rfc2104); RFC 4231, Identifiers and Test Vectors for HMAC-SHA-224, HMAC-SHA-256, HMAC-SHA-384, and HMAC-SHA-512, section 4 (https://www.rfc-editor.org/rfc/rfc4231).

Files

PathBytes
README.md1,691
impl/python.py842
impl/rust.rs1,771
impl/typescript.ts1,319
vectors.json6,795