ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
secure.h
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 secure.h
6 * @brief Secure pool accessor - borrows that hold key material.
7 *
8 * The same pool mechanism as the plaintext side (::pc_arena, mmgr/arena), instantiated a second
9 * time. The resource and its mechanics are identical - one instance per worker slot, compile-time
10 * sized, double-ended, fail-closed, high-water reported. What differs is the access and control
11 * layer, and the difference is security:
12 *
13 * - **Release wipes.** pc_secure_release() and pc_secure_reset() zero the region being reclaimed
14 * before it becomes available again. On the plaintext side reclaiming is just an offset move; a
15 * secret must not outlive its borrow, so here the wipe IS the release. That makes the rule
16 * structural instead of a discipline every caller has to remember on every return path - the
17 * form that had already been missed on two of the SSH key-exchange error paths.
18 *
19 * - **Disjoint region.** The two pools occupy different addresses, so pc_secure_owns() and
20 * pc_scratch_owns() are mutually exclusive by construction. A secure borrow can never be
21 * accepted where a plaintext one is expected, or the reverse, with no tagging and no metadata.
22 *
23 * **What belongs here.** Anything whose bytes are key material: shared secrets, private scalars,
24 * derived keys, and the working state of an operation over them. Public wire values (a peer's
25 * public point, a ciphertext about to be transmitted, a staging buffer for an outbound frame) belong
26 * in the plaintext pool - putting them here only shrinks the room left for real secrets.
27 *
28 * **Lifetime is not the axis.** Both pools carry long-lived and ephemeral allocations; the pool a
29 * borrow comes from is decided by whether its contents are secret, nothing else.
30 *
31 * @author Douglas Quigg (dstroy0)
32 * @date 2026
33 */
34
35#ifndef PROTOCORE_SECURE_H
36#define PROTOCORE_SECURE_H
37
38#include "protocore_config.h"
40#include <stddef.h>
41#include <stdint.h>
42
43/**
44 * @brief Securely zero @p len bytes at @p ptr with a volatile store the compiler cannot elide.
45 *
46 * The canonical wipe. Use this, never memset(), for any buffer that held key material: a plain
47 * memset() whose result is never observed (the buffer dies at return) is a dead store and may be
48 * optimized away, leaving the bytes in memory. The volatile write forces it even when the memory is
49 * never read again.
50 *
51 * It lives here because wiping is a memory-manager operation, not a cryptographic one. It is the
52 * secure pool's own reclaim primitive, and it is equally what any owner needs for storage that was
53 * never in a pool at all - session key material, a caller's own buffer. Crypto is a consumer of it,
54 * not its home.
55 *
56 * @param ptr Buffer to wipe.
57 * @param len Number of bytes to zero.
58 */
59static inline void pc_secure_wipe(void *ptr, size_t len)
60{
61 // Machine-width stores, with byte head/tail only for unaligned edges. Both edges are normally
62 // empty - pool borrows are aligned and their lengths rounded up - so this is the word loop.
63 // volatile is per-access, so a volatile word store is exactly as un-elidable as a volatile byte
64 // store; the guarantee is unchanged and the store count drops by the width.
65 volatile uint8_t *b = (volatile uint8_t *)ptr;
66 while (len != 0 && (((uintptr_t)b & (sizeof(uintptr_t) - 1)) != 0))
67 {
68 *b++ = 0;
69 len--;
70 }
71 volatile uintptr_t *w = (volatile uintptr_t *)b;
72 while (len >= sizeof(uintptr_t))
73 {
74 *w++ = 0;
75 len -= sizeof(uintptr_t);
76 }
77 b = (volatile uint8_t *)w;
78 while (len != 0)
79 {
80 *b++ = 0;
81 len--;
82 }
83}
84
85/**
86 * @brief Borrow @p n bytes of secure storage, aligned to @p align.
87 *
88 * Returns uninitialized memory (the pool wipes on release, not on hand-out). Returns nullptr if the
89 * request does not fit - callers MUST handle null and fail closed.
90 *
91 * @param n bytes requested.
92 * @param align required alignment in bytes, a power of two (0 selects the platform default).
93 */
94void *pc_secure_alloc(size_t n, size_t align);
95
96/**
97 * @brief Borrow @p n secure bytes as a span whose capacity is bound to the allocation.
98 *
99 * The preferred form: one argument sets both fields, so the capacity cannot drift from what was
100 * reserved. An over-budget request yields an empty span, so an omitted pc_span_ok() check writes
101 * nothing rather than dereferencing null.
102 */
103pc_span pc_secure_span(size_t n, size_t align);
104
105/** @brief Capture the current position, to be handed to pc_secure_release(). */
106size_t pc_secure_mark(void);
107
108/**
109 * @brief Wipe and reclaim everything borrowed since @p mark.
110 *
111 * The wipe happens BEFORE the position moves, so the bytes are already zero at the instant they
112 * become available again - there is no window in which a subsequent borrow could be handed memory
113 * still holding the previous tenant's key material. Use SecureScope rather than calling this by
114 * hand, so no return path can skip it.
115 */
116void pc_secure_release(size_t mark);
117
118/** @brief Wipe and reclaim the whole slot. */
119void pc_secure_reset(void);
120
121/** @brief Bytes currently handed out. */
122size_t pc_secure_used(void);
123
124/** @brief Peak bytes ever handed out, for sizing PC_SECURE_ARENA_SIZE. */
125size_t pc_secure_high_water(void);
126
127/** @brief Total per-slot capacity in bytes (PC_SECURE_ARENA_SIZE). */
128size_t pc_secure_capacity(void);
129
130/**
131 * @brief True if @p p points inside the secure pool.
132 *
133 * One unsigned subtract and compare: the slot count and slot size are compile-time, so the pool is
134 * one region of known extent. Mutually exclusive with pc_scratch_owns() because the regions are
135 * disjoint - which is the whole access control, with no per-allocation bookkeeping.
136 */
137bool pc_secure_owns(const void *p);
138
139/** @brief Which secure slot owns @p p, or -1 if @p p is not in the secure pool. */
140int pc_secure_slot_of(const void *p);
141
142/**
143 * @brief A secure borrow whose acquire and release are one call each.
144 *
145 * SecureScope + pc_secure_span crosses this module's boundary three times (mark, alloc, release);
146 * mark and alloc always happen together, so one of those is structure rather than work. Measured on
147 * an ESP32-S3: 129 cycles against 172 for the three-call form.
148 *
149 * Releases exactly like SecureScope - the region is wiped BEFORE the position moves, so no later
150 * borrow can be handed memory still holding this one's key material.
151 */
153{
154 public:
155 SecureBorrow(size_t n, size_t align);
157 SecureBorrow(const SecureBorrow &) = delete;
159
160 /** @brief The borrowed region; empty if the pool could not satisfy it. */
161 const pc_span &span() const
162 {
163 return m_span;
164 }
165
166 private:
167 size_t m_mark;
168 pc_span m_span;
169};
170
171/**
172 * @brief RAII scope guard for secure borrows - marks on entry, wipes and reclaims on every exit.
173 *
174 * The reason to prefer this over pc_secure_release(): a secret's wipe must happen on *every* path,
175 * including the early returns taken when a peer sends something malformed, and those are exactly the
176 * paths a hand-written wipe gets forgotten on.
177 */
179{
180 public:
182 {
183 }
185 {
186 pc_secure_release(m_mark);
187 }
188 SecureScope(const SecureScope &) = delete;
190
191 private:
192 size_t m_mark;
193};
194
195#endif // PROTOCORE_SECURE_H
A secure borrow whose acquire and release are one call each.
Definition secure.h:153
SecureBorrow(const SecureBorrow &)=delete
SecureBorrow & operator=(const SecureBorrow &)=delete
const pc_span & span() const
The borrowed region; empty if the pool could not satisfy it.
Definition secure.h:161
RAII scope guard for secure borrows - marks on entry, wipes and reclaims on every exit.
Definition secure.h:179
SecureScope(const SecureScope &)=delete
SecureScope()
Definition secure.h:181
~SecureScope()
Definition secure.h:184
SecureScope & operator=(const SecureScope &)=delete
User-facing configuration for ProtoCore.
size_t pc_secure_used(void)
Bytes currently handed out.
Definition secure.cpp:172
int pc_secure_slot_of(const void *p)
Which secure slot owns p, or -1 if p is not in the secure pool.
Definition secure.cpp:202
void pc_secure_reset(void)
Wipe and reclaim the whole slot.
Definition secure.cpp:161
size_t pc_secure_mark(void)
Capture the current position, to be handed to pc_secure_release().
Definition secure.cpp:147
bool pc_secure_owns(const void *p)
True if p points inside the secure pool.
Definition secure.cpp:197
pc_span pc_secure_span(size_t n, size_t align)
Borrow n secure bytes as a span whose capacity is bound to the allocation.
Definition secure.cpp:142
void pc_secure_release(size_t mark)
Wipe and reclaim everything borrowed since mark.
Definition secure.cpp:154
void * pc_secure_alloc(size_t n, size_t align)
Borrow n bytes of secure storage, aligned to align.
Definition secure.cpp:134
size_t pc_secure_high_water(void)
Peak bytes ever handed out, for sizing PC_SECURE_ARENA_SIZE.
Definition secure.cpp:178
size_t pc_secure_capacity(void)
Total per-slot capacity in bytes (PC_SECURE_ARENA_SIZE).
Definition secure.cpp:192
A byte region whose run length is bound in both directions.
A writable byte region: the storage, the capacity that belongs to it, and what has been produced into...
Definition span.h:65