ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
esp.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 esp.cpp
6 * @brief ESP (RFC 4303) packet transform with AES-256-GCM (RFC 4106) - see esp.h.
7 */
8
10
11#if PC_ENABLE_IKEV2
12
13#include "crypto/aead/aesgcm.h"
14#include "server/mmgr/secure.h" // SecureBorrow: the per-call GCM context
15#include <string.h>
16
17namespace
18{
19void put32be(uint8_t *p, uint32_t v)
20{
21 p[0] = (uint8_t)(v >> 24);
22 p[1] = (uint8_t)(v >> 16);
23 p[2] = (uint8_t)(v >> 8);
24 p[3] = (uint8_t)v;
25}
26uint32_t get32be(const uint8_t *p)
27{
28 return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3];
29}
30void esp_nonce(uint8_t nonce[PC_AESGCM_IV_LEN], const uint8_t *salt, const uint8_t *iv)
31{
32 memcpy(nonce, salt, PC_ESP_SALT_LEN);
33 memcpy(nonce + PC_ESP_SALT_LEN, iv, PC_ESP_IV_LEN);
34}
35// Bytes the ESP header + IV occupy before the ciphertext (also the AAD length = SPI|Seq).
36#define PC_ESP_CT_OFF (PC_ESP_HDR_LEN + PC_ESP_IV_LEN)
37} // namespace
38
39size_t pc_esp_gcm_encapsulate(uint32_t spi, uint32_t seq, const uint8_t key[PC_ESP_KEY_LEN],
40 const uint8_t salt[PC_ESP_SALT_LEN], const uint8_t iv[PC_ESP_IV_LEN], uint8_t next_header,
41 const uint8_t *payload, size_t payload_len, uint8_t *out, size_t out_cap)
42{
43 if (!key || !salt || !iv || !out || (payload_len && !payload))
44 {
45 return 0;
46 }
47
48 // Plaintext = Payload | Padding | Pad Length | Next Header, padded so Pad Length + Next Header (the
49 // 2-octet trailer) land on a 4-octet boundary (RFC 4303 §2.4).
50 size_t padn = (4 - (payload_len + 2) % 4) % 4;
51 size_t pt_len = payload_len + padn + 2;
52 size_t total = PC_ESP_CT_OFF + pt_len + PC_ESP_ICV_LEN;
53 if (out_cap < total)
54 {
55 return 0;
56 }
57
58 put32be(out, spi);
59 put32be(out + 4, seq);
60 memcpy(out + PC_ESP_HDR_LEN, iv, PC_ESP_IV_LEN);
61
62 uint8_t *pt = out + PC_ESP_CT_OFF;
63 if (payload_len)
64 {
65 memcpy(pt, payload, payload_len);
66 }
67 for (size_t i = 0; i < padn; i++)
68 {
69 pt[payload_len + i] = (uint8_t)(i + 1); // RFC 4303 monotonic padding 1, 2, 3 ...
70 }
71 pt[payload_len + padn] = (uint8_t)padn; // Pad Length
72 pt[payload_len + padn + 1] = next_header; // Next Header
73
74 // Encrypt in place: AAD = SPI | Seq (the first 8 octets), plaintext -> ciphertext || ICV at PC_ESP_CT_OFF.
75 uint8_t nonce[PC_AESGCM_IV_LEN];
76 esp_nonce(nonce, salt, iv);
77 {
78 // Per-call context: this path is not hot enough to justify a per-session one. The cost is the
79 // ~9,200-cycle lifecycle documented in aesgcm.h - hoist the context into session state if it
80 // ever shows up in a profile.
82 pc_aesgcm_key *gcm = pc_aesgcm_key_init(gcm_b.span().buf, key);
83 pc_aesgcm_seal(gcm, nonce, out, PC_ESP_HDR_LEN, pt, pt_len, pt, pt + pt_len);
85 }
86 return total;
87}
88
89bool pc_esp_gcm_decapsulate(const uint8_t key[PC_ESP_KEY_LEN], const uint8_t salt[PC_ESP_SALT_LEN], uint8_t *packet,
90 size_t len, uint32_t *spi_out, uint32_t *seq_out, uint8_t *next_header_out,
91 const uint8_t **payload_out, size_t *payload_len_out)
92{
93 if (!key || !salt || !packet || !payload_out || !payload_len_out)
94 {
95 return false;
96 }
97 // Minimum: header + IV + at least the 2-octet trailer (Pad Length + Next Header) + ICV.
98 if (len < PC_ESP_CT_OFF + 2 + PC_ESP_ICV_LEN)
99 {
100 return false;
101 }
102
103 const uint8_t *iv = packet + PC_ESP_HDR_LEN;
104 uint8_t *ct = packet + PC_ESP_CT_OFF;
105 size_t ct_len = len - PC_ESP_CT_OFF - PC_ESP_ICV_LEN;
106 const uint8_t *tag = ct + ct_len;
107
108 uint8_t nonce[PC_AESGCM_IV_LEN];
109 esp_nonce(nonce, salt, iv);
110 bool ok = false;
111 {
112 // Per-call context: this path is not hot enough to justify a per-session one. The cost is the
113 // ~9,200-cycle lifecycle documented in aesgcm.h - hoist the context into session state if it
114 // ever shows up in a profile.
116 pc_aesgcm_key *gcm = pc_aesgcm_key_init(gcm_b.span().buf, key);
117 ok = pc_aesgcm_open(gcm, nonce, packet, PC_ESP_HDR_LEN, ct, ct_len, tag, ct); // AAD = SPI | Seq
119 }
120 if (!ok)
121 {
122 return false;
123 }
124
125 // Trailer: the last octet is Next Header, the one before it is Pad Length.
126 uint8_t next_header = ct[ct_len - 1];
127 uint8_t pad_len = ct[ct_len - 2];
128 if ((size_t)pad_len + 2 > ct_len) // padding + trailer cannot exceed the plaintext
129 {
130 return false;
131 }
132
133 if (spi_out)
134 {
135 *spi_out = get32be(packet);
136 }
137 if (seq_out)
138 {
139 *seq_out = get32be(packet + 4);
140 }
141 if (next_header_out)
142 {
143 *next_header_out = next_header;
144 }
145 *payload_out = ct;
146 *payload_len_out = ct_len - 2 - pad_len;
147 return true;
148}
149
150// ── ESP anti-replay window (RFC 4303 §3.4.3) ───────────────────────────────────────────────────
151
152void pc_esp_replay_init(EspReplay *r)
153{
154 if (!r)
155 {
156 return;
157 }
158 r->highest = 0;
159 r->bitmap = 0;
160 r->seen_any = false;
161}
162
163bool pc_esp_replay_check(EspReplay *r, uint32_t seq)
164{
165 if (!r || seq == 0) // sequence 0 is never valid (ESP counts from 1)
166 {
167 return false;
168 }
169
170 if (!r->seen_any)
171 {
172 r->highest = seq;
173 r->bitmap = 1; // bit 0 = this (the new highest)
174 r->seen_any = true;
175 return true;
176 }
177
178 if (seq > r->highest)
179 {
180 // A new highest: slide the window up, then mark the new top bit. A jump >= the window clears it.
181 uint32_t shift = seq - r->highest;
182 r->bitmap = (shift >= PC_ESP_REPLAY_WINDOW) ? 0u : (r->bitmap << shift);
183 r->bitmap |= 1u;
184 r->highest = seq;
185 return true;
186 }
187
188 uint32_t offset = r->highest - seq;
189 if (offset >= PC_ESP_REPLAY_WINDOW) // left of the window -> too old
190 {
191 return false;
192 }
193 uint64_t mask = (uint64_t)1 << offset;
194 if (r->bitmap & mask) // already accepted -> replay
195 {
196 return false;
197 }
198 r->bitmap |= mask;
199 return true;
200}
201
202#endif // PC_ENABLE_IKEV2
AES-256-GCM AEAD (RFC 5116) - stateless, detached-tag API.
#define PC_AESGCM_IV_LEN
GCM nonce length (bytes) = fixed_field(4) || invocation_counter(8).
Definition aesgcm.h:40
A secure borrow whose acquire and release are one call each.
Definition secure.h:153
ESP (RFC 4303) packet transform with AES-256-GCM (RFC 4106) - the IPsec datapath's crypto core.
uint32_t mask(uint8_t width)
Mask of width low bits (width 32 handled without a 32-bit shift, which is UB).
Definition crc.h:61
bool pc_aesgcm_open(pc_aesgcm_key *k, const uint8_t nonce[PC_AESGCM_IV_LEN], const uint8_t *aad, size_t aad_len, const uint8_t *ct, size_t ct_len, const uint8_t tag[PC_AESGCM_TAG_LEN], uint8_t *out)
Open one record: verify tag over aad || ct in constant time, then (only on success) decrypt ct into o...
pc_aesgcm_key * pc_aesgcm_key_init(void *storage, const uint8_t key[PC_AESGCM_KEY_LEN])
Bind storage as a context keyed with key.
pc_cspan pc_aesgcm_seal(pc_aesgcm_key *k, const uint8_t nonce[PC_AESGCM_IV_LEN], const uint8_t *aad, size_t aad_len, const uint8_t *pt, size_t pt_len, uint8_t *ct_out, uint8_t tag_out[PC_AESGCM_TAG_LEN])
Seal one record under k and nonce.
void pc_aesgcm_key_wipe(pc_aesgcm_key *k)
Wipe the expanded schedule. Call on rekey and on close; the storage stays the caller's.
#define PC_WORK_AESGCM
Secure pool accessor - borrows that hold key material.