ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
hmac_sha256.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file hmac_sha256.cpp
6 * @brief HMAC-SHA2-256 implementation (RFC 2104).
7 *
8 * Implemented in terms of the pc_sha256 streaming functions so it compiles identically on Arduino and
9 * native. The inner SHA-256 hardware acceleration (where present) is transparent through those calls.
10 *
11 * RFC 2104 construction: HMAC(K, m) = H((K XOR opad) || H((K XOR ipad) || m)), H = SHA-256, ipad = 0x36
12 * repeated, opad = 0x5c repeated. Keys > 64 bytes are pre-hashed; keys <= 64 are zero-padded to the
13 * 64-byte block. SSH-derived MAC keys are 32 bytes, so they are padded, not pre-hashed.
14 *
15 * The transient working memory that touches the key (the padded ipad/opad blocks, the intermediate inner
16 * digest, and the one outer / one-shot hash context) lives in the shared crypto scratch (HMAC-256 region)
17 * and is wiped on the way out - never on the stack. The caller-owned streaming context (pc_hmac_sha256_ctx:
18 * the opad key block + the inner hash state) is per-session state the caller wipes at teardown, so it is
19 * NOT kept in the scratch (a long-lived value there would be clobbered by the next op).
20 */
21
23#include "crypto/crypto_opt.h"
24#include "server/mmgr/secure.h" // the secure pool: HMAC working state, wiped on release
25#include <string.h>
26// HMAC-SHA256 is HW-SHA-dominated; the only -O lever is its SW key-block glue. On the P4 that rides the per-die
27// -O3 default (whose win is -O3's loop-unroll parameter budget). The S3's ~4% O3 edge is the same parameter
28// class (bisected on-device: -fpeel-loops and -funswitch-loops both no-op), not a single transform, and not
29// worth -O3's code-size / miscompile baggage on a HW-dominated MAC - so the S3 keeps the -O2 default. Capturing
30// that 4% deliberately would take a source #pragma GCC unroll on the key-block loops (a code change, not a flag).
31// See crypto_opt.h caveat 1.
33
34namespace
35{
36// Transient HMAC-SHA256 working set, borrowed from the secure pool per call and wiped on release. No
37// hand-assigned address: HMAC runs nested under the KDFs (SP800-108 / HKDF / TLS1.3), whose own
38// borrows are still live, so the pool separates them by construction. The two 64-byte key blocks
39// double as key-padding scratch for build_key_block.
41{
42 uint8_t opad[64]; ///< one-shot opad block (persists inner->outer); else key-pad scratch
43 uint8_t ipad[64]; ///< ipad block; also key-pad scratch once its ipad is consumed
44 uint8_t inner_digest[PC_SHA256_DIGEST_LEN]; ///< H((K XOR ipad) || m)
45 pc_sha256_ctx hash; ///< transient hash: one-shot inner then outer; streaming final outer
46};
47static_assert(sizeof(HmacWork) <= PC_WORK_HMAC_SHA256,
48 "HmacWork outgrew PC_WORK_HMAC_SHA256 - raise it in protocore_config.h, which derives "
49 "PC_SECURE_ARENA_SIZE from it");
50
51// Build one 64-byte HMAC key block into @p block (RFC 2104 sec 2), using @p scratch (64 bytes) to hold the
52// zero-padded / pre-hashed key. Both @p block and @p scratch are pool-resident, never the stack.
53void build_key_block(const uint8_t *key, size_t key_len, uint8_t block[64], uint8_t pad_byte, uint8_t scratch[64])
54{
55 memset(scratch, 0, 64);
56 if (key_len > 64)
57 {
58 pc_sha256(key, key_len, scratch); // keys longer than the block are replaced by their SHA-256 hash
59 }
60 else
61 {
62 for (size_t i = 0; i < key_len; i++)
63 {
64 scratch[i] = key[i];
65 }
66 }
67 for (int i = 0; i < 64; i++)
68 {
69 block[i] = scratch[i] ^ pad_byte;
70 }
71}
72} // namespace
73
74void pc_hmac_sha256_init(pc_hmac_sha256_ctx *ctx, const uint8_t *key, size_t key_len)
75{
76 SecureBorrow ws_b(sizeof(HmacWork), alignof(HmacWork));
77 const pc_span &ws = ws_b.span();
78 if (!pc_span_ok(ws))
79 {
80 return; // pool exhausted: the empty borrow is the supported failure signal
81 }
82 HmacWork *w = reinterpret_cast<HmacWork *>(ws.buf);
83 build_key_block(key, key_len, w->ipad, 0x36u, w->opad); // ipad -> scratch (opad slot holds the padded key)
84 build_key_block(key, key_len, ctx->okey, 0x5cu, w->opad); // opad -> caller ctx (stored for the final step)
85
86 pc_sha256_init(&ctx->inner);
87 pc_sha256_update(&ctx->inner, w->ipad, 64);
88}
89
90void pc_hmac_sha256_update(pc_hmac_sha256_ctx *ctx, const uint8_t *data, size_t len)
91{
92 pc_sha256_update(&ctx->inner, data, len);
93}
94
96{
97 SecureBorrow ws_b(sizeof(HmacWork), alignof(HmacWork));
98 const pc_span &ws = ws_b.span();
99 if (!pc_span_ok(ws))
100 {
101 return; // pool exhausted: the empty borrow is the supported failure signal
102 }
103 HmacWork *w = reinterpret_cast<HmacWork *>(ws.buf);
104 pc_sha256_final(&ctx->inner, w->inner_digest);
105
106 // Outer hash: H(okey || inner_digest)
107 pc_sha256_init(&w->hash);
108 pc_sha256_update(&w->hash, ctx->okey, 64);
109 pc_sha256_update(&w->hash, w->inner_digest, PC_SHA256_DIGEST_LEN);
110 pc_sha256_final(&w->hash, mac);
111}
112
113void pc_hmac_sha256(const uint8_t *key, size_t key_len, const uint8_t *data, size_t len,
114 uint8_t mac[PC_HMAC_SHA256_LEN])
115{
116 // Self-contained (does not build a caller-facing context): ipad block first, fold it into the inner hash,
117 // then reuse its slot as the opad key-padding scratch - so no key block ever lands on the stack.
118 SecureBorrow ws_b(sizeof(HmacWork), alignof(HmacWork));
119 const pc_span &ws = ws_b.span();
120 if (!pc_span_ok(ws))
121 {
122 return; // pool exhausted: the empty borrow is the supported failure signal
123 }
124 HmacWork *w = reinterpret_cast<HmacWork *>(ws.buf);
125 build_key_block(key, key_len, w->ipad, 0x36u, w->opad); // ipad block (opad slot as key-pad scratch)
126 pc_sha256_init(&w->hash);
127 pc_sha256_update(&w->hash, w->ipad, 64);
128 pc_sha256_update(&w->hash, data, len);
129 pc_sha256_final(&w->hash, w->inner_digest); // inner = H((K XOR ipad) || m)
130
131 build_key_block(key, key_len, w->opad, 0x5cu, w->ipad); // opad block (ipad slot now free as scratch)
132 pc_sha256_init(&w->hash);
133 pc_sha256_update(&w->hash, w->opad, 64);
134 pc_sha256_update(&w->hash, w->inner_digest, PC_SHA256_DIGEST_LEN);
135 pc_sha256_final(&w->hash, mac); // HMAC = H((K XOR opad) || inner)
136}
A secure borrow whose acquire and release are one call each.
Definition secure.h:153
const pc_span & span() const
The borrowed region; empty if the pool could not satisfy it.
Definition secure.h:161
Per-translation-unit optimization override for hot, pure-integer crypto.
void pc_hmac_sha256(const uint8_t *key, size_t key_len, const uint8_t *data, size_t len, uint8_t mac[PC_HMAC_SHA256_LEN])
Compute HMAC-SHA2-256 over a single contiguous buffer.
void pc_hmac_sha256_update(pc_hmac_sha256_ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes into the running HMAC.
void pc_hmac_sha256_init(pc_hmac_sha256_ctx *ctx, const uint8_t *key, size_t key_len)
Initialize a streaming HMAC-SHA2-256 context.
void pc_hmac_sha256_final(pc_hmac_sha256_ctx *ctx, uint8_t mac[PC_HMAC_SHA256_LEN])
Finalize and write the 32-byte MAC.
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1) - streaming context and one-shot API.
#define PC_HMAC_SHA256_LEN
HMAC-SHA2-256 output length in bytes.
Definition hmac_sha256.h:30
void build_key_block(const uint8_t *key, size_t key_len, uint8_t block[64], uint8_t pad_byte, uint8_t scratch[64])
#define PC_WORK_HMAC_SHA256
Secure pool accessor - borrows that hold key material.
PC_CRYPTO_HOT void pc_sha256_init(pc_sha256_ctx *ctx)
Initialize a streaming SHA-256 context (ctx must not be NULL).
Definition sha256.cpp:29
void pc_sha256_final(pc_sha256_ctx *ctx, uint8_t digest[PC_SHA256_DIGEST_LEN])
Finalize the hash and write the 32-byte digest. The context is undefined afterwards; call init() agai...
Definition sha256.cpp:48
void pc_sha256_update(pc_sha256_ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes of data into the running hash.
Definition sha256.cpp:39
void pc_sha256(const uint8_t *data, size_t len, uint8_t digest[PC_SHA256_DIGEST_LEN])
One-shot SHA-256: hash len bytes of data into digest (32 bytes).
Definition sha256.cpp:58
#define PC_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
Definition sha256.h:25
bool pc_span_ok(const pc_span &s)
True when the span refers to real storage and every write so far has fit.
Definition span.h:114
pc_sha256_ctx hash
transient hash: one-shot inner then outer; streaming final outer
Streaming HMAC-SHA2-256 context.
Definition hmac_sha256.h:57
uint8_t okey[64]
Outer key block (key XOR opad), stored for the final step.
Definition hmac_sha256.h:59
pc_sha256_ctx inner
Inner hash context (key XOR ipad prepended).
Definition hmac_sha256.h:58
Streaming SHA-256 context.
Definition sha256.h:40
A writable byte region: the storage, the capacity that belongs to it, and what has been produced into...
Definition span.h:65
uint8_t * buf
first byte, or nullptr when the region could not be obtained
Definition span.h:66