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 keyhmac_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 outputhmac_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>
| key | int[] | the secret, any length; one longer than 64 bytes is hashed first, as RFC 2104 says |
| message | int[] | the bytes to authenticate |
| returns | int[] | 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(…)
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
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,691 |
| impl/python.py | 842 |
| impl/rust.rs | 1,771 |
| impl/typescript.ts | 1,319 |
| vectors.json | 6,795 |