Functional Weave
Code in Python

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.

def hmac_sha256(key: Sequence[int], message: Sequence[int]) -> List[int]
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

from fune.crypto.hmac_sha256 import hmac_sha256  # crypto.hmac-sha256@^1
impl/python.py · 23 lines · open · raw
import hashlib
import hmac
from typing import Any, List, Sequence


def _check_bytes(value: Any, name: str) -> bytes:
    if isinstance(value, (str, dict)) or not hasattr(value, "__len__"):
        raise TypeError("%s must be a list of integers from 0 to 255" % name)
    for b in value:
        if type(b) is not int or b < 0 or b > 255:
            raise ValueError("%s must be a list of integers from 0 to 255" % name)
    return bytes(value)


def hmac_sha256(key: Sequence[int], message: Sequence[int]) -> List[int]:
    """HMAC-SHA256 (RFC 2104) from the standard library's hmac module.

    The byte values are checked here so a bad value fails with the same words
    as in TypeScript and Rust.
    """
    k = _check_bytes(key, "key")
    m = _check_bytes(message, "message")
    return list(hmac.new(k, m, hashlib.sha256).digest())

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python 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. Or pin a range in fune.project and build in one step:

fune add crypto.hmac-sha256
Download for Python crypto.hmac-sha256-1.0.0-python.fune · 11,043 bytes sha256 ddac0a8ce5b45c95da02aafafe7cb87729b80bd9c19da3b5fc4065443c62040d

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

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