ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ikev2.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 ikev2.cpp
6 * @brief IKEv2 (RFC 7296) message + payload codec (pure, host-tested).
7 */
8
10
11#if PC_ENABLE_IKEV2
12
13#include "crypto/aead/aesgcm.h" // SK-payload AEAD (AES-256-GCM-16)
14#include "crypto/asymmetric/curve25519.h" // D-H group 31 (X25519, RFC 7748)
15#include "crypto/asymmetric/ecdsa.h" // ECDSA-P256 certificate AUTH
16#include "crypto/asymmetric/rsa.h" // RSA-2048 certificate AUTH verify
17#include "crypto/hash/sha256.h" // anti-DoS COOKIE hash (RFC 7296 §2.6)
18#include "crypto/mac/hmac_sha256.h" // PRF = HMAC-SHA2-256
19#include "server/mmgr/secure.h" // SecureBorrow: the per-call GCM context
20#include <string.h> // memcpy / memset (framing is hand-rolled)
21
22// ── big-endian scalar helpers ─────────────────────────────────────────────────────────────────
23static inline void put16(uint8_t *p, uint16_t v)
24{
25 p[0] = (uint8_t)(v >> 8);
26 p[1] = (uint8_t)v;
27}
28static inline void put32(uint8_t *p, uint32_t v)
29{
30 p[0] = (uint8_t)(v >> 24);
31 p[1] = (uint8_t)(v >> 16);
32 p[2] = (uint8_t)(v >> 8);
33 p[3] = (uint8_t)v;
34}
35static inline uint16_t get16(const uint8_t *p)
36{
37 return (uint16_t)(((uint16_t)p[0] << 8) | p[1]);
38}
39static inline uint32_t get32(const uint8_t *p)
40{
41 return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3];
42}
43
44// Write a generic payload header (next-payload, no critical bit, 2-byte total length); false on overflow.
45static bool put_pl_hdr(uint8_t *buf, size_t cap, IkePayloadType next_payload, size_t total_len)
46{
47 if (total_len > 0xFFFF || cap < total_len)
48 {
49 return false;
50 }
51 buf[0] = (uint8_t)next_payload;
52 buf[1] = 0x00;
53 put16(buf + 2, (uint16_t)total_len);
54 return true;
55}
56
57// ── IKE header ────────────────────────────────────────────────────────────────────────────────
58
59size_t pc_ike_hdr_build(uint8_t *buf, size_t cap, const IkeHeader *h)
60{
61 if (!buf || !h || cap < PC_IKE_HDR_LEN)
62 {
63 return 0;
64 }
65 memcpy(buf, h->init_spi, PC_IKE_SPI_LEN);
66 memcpy(buf + 8, h->resp_spi, PC_IKE_SPI_LEN);
67 buf[16] = (uint8_t)h->next_payload;
68 buf[17] = h->version;
69 buf[18] = (uint8_t)h->exchange;
70 buf[19] = h->flags;
71 put32(buf + 20, h->message_id);
72 put32(buf + 24, h->length);
73 return PC_IKE_HDR_LEN;
74}
75
76bool pc_ike_hdr_parse(const uint8_t *buf, size_t len, IkeHeader *out)
77{
78 if (!out)
79 {
80 return false;
81 }
82 memset(out, 0, sizeof(*out));
83 if (!buf || len < PC_IKE_HDR_LEN)
84 {
85 return false;
86 }
87 memcpy(out->init_spi, buf, PC_IKE_SPI_LEN);
88 memcpy(out->resp_spi, buf + 8, PC_IKE_SPI_LEN);
89 out->next_payload = (IkePayloadType)buf[16];
90 out->version = buf[17];
91 out->exchange = (IkeExchange)buf[18];
92 out->flags = buf[19];
93 out->message_id = get32(buf + 20);
94 out->length = get32(buf + 24);
95 return true;
96}
97
98bool pc_ike_set_length(uint8_t *buf, size_t buf_cap, uint32_t total_len)
99{
100 if (!buf || buf_cap < PC_IKE_HDR_LEN)
101 {
102 return false;
103 }
104 put32(buf + 24, total_len);
105 return true;
106}
107
108// ── generic payload chain ─────────────────────────────────────────────────────────────────────
109
110void pc_ike_payload_iter_init(IkePayloadIter *it, IkePayloadType first_type, const uint8_t *area, size_t area_len)
111{
112 if (!it)
113 {
114 return;
115 }
116 it->area = area;
117 it->len = area_len;
118 it->off = 0;
119 it->next_type = first_type;
120}
121
122bool pc_ike_payload_next(IkePayloadIter *it, IkePayload *out)
123{
124 if (!out)
125 {
126 return false;
127 }
128 out->type = IkePayloadType::IKE_PL_NONE;
129 out->next_payload = IkePayloadType::IKE_PL_NONE;
130 out->critical = false;
131 out->body = nullptr;
132 out->body_len = 0;
133 if (!it || !it->area || it->next_type == IkePayloadType::IKE_PL_NONE)
134 {
135 return false;
136 }
137 if (it->off + PC_IKE_PAYLOAD_HDR_LEN > it->len)
138 {
139 return false;
140 }
141 const uint8_t *p = it->area + it->off;
142 uint8_t next = p[0];
143 bool critical = (p[1] & PC_IKE_CRITICAL) != 0;
144 uint16_t plen = get16(p + 2);
145 if (plen < PC_IKE_PAYLOAD_HDR_LEN || it->off + plen > it->len)
146 {
147 return false;
148 }
149 out->type = it->next_type;
150 out->next_payload = (IkePayloadType)next;
151 out->critical = critical;
152 out->body = p + PC_IKE_PAYLOAD_HDR_LEN;
153 out->body_len = (size_t)plen - PC_IKE_PAYLOAD_HDR_LEN;
154 it->next_type = (IkePayloadType)next;
155 it->off += plen;
156 return true;
157}
158
159size_t pc_ike_payload_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, bool critical, const uint8_t *body,
160 size_t body_len)
161{
162 if (!buf || (body_len && !body))
163 {
164 return 0;
165 }
166 size_t total = PC_IKE_PAYLOAD_HDR_LEN + body_len;
167 if (total > 0xFFFF || cap < total)
168 {
169 return 0;
170 }
171 buf[0] = (uint8_t)next_payload;
172 buf[1] = critical ? PC_IKE_CRITICAL : 0x00;
173 put16(buf + 2, (uint16_t)total);
174 if (body_len)
175 {
176 memcpy(buf + PC_IKE_PAYLOAD_HDR_LEN, body, body_len);
177 }
178 return total;
179}
180
181// ── typed payload builders ────────────────────────────────────────────────────────────────────
182
183size_t pc_ike_sa_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, uint8_t proposal_num,
184 IkeProtocol protocol_id, const uint8_t *spi, uint8_t spi_size, const IkeTransform *transforms,
185 uint8_t num_transforms)
186{
187 if (!buf || !transforms || num_transforms == 0)
188 {
189 return 0;
190 }
191 if (spi_size && !spi)
192 {
193 return 0;
194 }
195
196 const size_t prop_hdr = 8; // last+res+len(2)+num+proto+spisize+numtrans
197 size_t off = PC_IKE_PAYLOAD_HDR_LEN + prop_hdr + spi_size;
198 if (cap < off)
199 {
200 return 0;
201 }
202
203 size_t tstart = off;
204 for (uint8_t i = 0; i < num_transforms; i++)
205 {
206 bool has_key = transforms[i].key_length >= 0;
207 size_t tlen = 8 + (has_key ? 4 : 0);
208 if (off + tlen > cap)
209 {
210 return 0;
211 }
212 buf[off + 0] = (i + 1 == num_transforms) ? 0 : 3; // 0 = last, 3 = more
213 buf[off + 1] = 0;
214 put16(buf + off + 2, (uint16_t)tlen);
215 buf[off + 4] = (uint8_t)transforms[i].type;
216 buf[off + 5] = 0;
217 put16(buf + off + 6, transforms[i].id);
218 if (has_key)
219 {
220 put16(buf + off + 8, (uint16_t)(0x8000u | IKE_ATTR_KEY_LENGTH)); // TV form, AF bit set
221 put16(buf + off + 10, (uint16_t)transforms[i].key_length);
222 }
223 off += tlen;
224 }
225
226 size_t prop_len = prop_hdr + spi_size + (off - tstart);
227 size_t sa_total = PC_IKE_PAYLOAD_HDR_LEN + prop_len;
228 if (prop_len > 0xFFFF || sa_total > 0xFFFF) // GCOVR_EXCL_LINE unreachable: spi_size and num_transforms are both
229 {
230 return 0; // GCOVR_EXCL_LINE uint8_t, so prop_len <= 8+255+255*12 = 3323 and sa_total <= 3327
231 }
232
233 buf[0] = (uint8_t)next_payload;
234 buf[1] = 0;
235 put16(buf + 2, (uint16_t)sa_total);
236 uint8_t *pr = buf + PC_IKE_PAYLOAD_HDR_LEN;
237 pr[0] = 0; // last (only) proposal
238 pr[1] = 0;
239 put16(pr + 2, (uint16_t)prop_len);
240 pr[4] = proposal_num;
241 pr[5] = (uint8_t)protocol_id;
242 pr[6] = spi_size;
243 pr[7] = num_transforms;
244 if (spi_size)
245 {
246 memcpy(pr + prop_hdr, spi, spi_size);
247 }
248 return sa_total;
249}
250
251size_t pc_ike_ke_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, uint16_t dh_group, const uint8_t *data,
252 size_t data_len)
253{
254 if (!buf || (data_len && !data))
255 {
256 return 0;
257 }
258 size_t total = PC_IKE_PAYLOAD_HDR_LEN + 4 + data_len;
259 if (!put_pl_hdr(buf, cap, next_payload, total))
260 {
261 return 0;
262 }
263 put16(buf + 4, dh_group);
264 buf[6] = 0;
265 buf[7] = 0;
266 if (data_len)
267 {
268 memcpy(buf + 8, data, data_len);
269 }
270 return total;
271}
272
273size_t pc_ike_nonce_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, const uint8_t *nonce, size_t nonce_len)
274{
275 if (!buf || (nonce_len && !nonce))
276 {
277 return 0;
278 }
279 size_t total = PC_IKE_PAYLOAD_HDR_LEN + nonce_len;
280 if (!put_pl_hdr(buf, cap, next_payload, total))
281 {
282 return 0;
283 }
284 if (nonce_len)
285 {
286 memcpy(buf + 4, nonce, nonce_len);
287 }
288 return total;
289}
290
291size_t pc_ike_id_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, IkeIdType id_type, const uint8_t *data,
292 size_t data_len)
293{
294 if (!buf || (data_len && !data))
295 {
296 return 0;
297 }
298 size_t total = PC_IKE_PAYLOAD_HDR_LEN + 4 + data_len;
299 if (!put_pl_hdr(buf, cap, next_payload, total))
300 {
301 return 0;
302 }
303 buf[4] = (uint8_t)id_type;
304 buf[5] = 0;
305 buf[6] = 0;
306 buf[7] = 0;
307 if (data_len)
308 {
309 memcpy(buf + 8, data, data_len);
310 }
311 return total;
312}
313
314size_t pc_ike_auth_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, IkeAuthMethod auth_method,
315 const uint8_t *data, size_t data_len)
316{
317 if (!buf || (data_len && !data))
318 {
319 return 0;
320 }
321 size_t total = PC_IKE_PAYLOAD_HDR_LEN + 4 + data_len;
322 if (!put_pl_hdr(buf, cap, next_payload, total))
323 {
324 return 0;
325 }
326 buf[4] = (uint8_t)auth_method;
327 buf[5] = 0;
328 buf[6] = 0;
329 buf[7] = 0;
330 if (data_len)
331 {
332 memcpy(buf + 8, data, data_len);
333 }
334 return total;
335}
336
337size_t pc_ike_cert_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, uint8_t cert_encoding,
338 const uint8_t *data, size_t data_len)
339{
340 if (!buf || (data_len && !data))
341 {
342 return 0;
343 }
344 size_t total = PC_IKE_PAYLOAD_HDR_LEN + 1 + data_len;
345 if (!put_pl_hdr(buf, cap, next_payload, total))
346 {
347 return 0;
348 }
349 buf[4] = cert_encoding;
350 if (data_len)
351 {
352 memcpy(buf + 5, data, data_len);
353 }
354 return total;
355}
356
357size_t pc_ike_notify_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, IkeProtocol protocol_id,
358 const uint8_t *spi, uint8_t spi_size, uint16_t notify_type, const uint8_t *data,
359 size_t data_len)
360{
361 if (!buf || (spi_size && !spi) || (data_len && !data))
362 {
363 return 0;
364 }
365 size_t total = PC_IKE_PAYLOAD_HDR_LEN + 4 + spi_size + data_len;
366 if (!put_pl_hdr(buf, cap, next_payload, total))
367 {
368 return 0;
369 }
370 buf[4] = (uint8_t)protocol_id;
371 buf[5] = spi_size;
372 put16(buf + 6, notify_type);
373 size_t off = 8;
374 if (spi_size)
375 {
376 memcpy(buf + off, spi, spi_size);
377 off += spi_size;
378 }
379 if (data_len)
380 {
381 memcpy(buf + off, data, data_len);
382 }
383 return total;
384}
385
386size_t pc_ike_delete_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, IkeProtocol protocol_id,
387 uint8_t spi_size, const uint8_t *spis, uint16_t num_spis)
388{
389 if (!buf)
390 {
391 return 0;
392 }
393 size_t spis_len = (size_t)spi_size * num_spis;
394 if (spis_len && !spis)
395 {
396 return 0;
397 }
398 size_t total = PC_IKE_PAYLOAD_HDR_LEN + 4 + spis_len;
399 if (!put_pl_hdr(buf, cap, next_payload, total))
400 {
401 return 0;
402 }
403 buf[4] = (uint8_t)protocol_id;
404 buf[5] = spi_size;
405 put16(buf + 6, num_spis);
406 if (spis_len)
407 {
408 memcpy(buf + 8, spis, spis_len);
409 }
410 return total;
411}
412
413size_t pc_ike_ts_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, const IkeTrafficSelector *sels,
414 uint8_t num)
415{
416 if (!buf || !sels || num == 0)
417 {
418 return 0;
419 }
420 size_t off = PC_IKE_PAYLOAD_HDR_LEN + 4; // generic hdr + num(1) + reserved(3)
421 if (cap < off)
422 {
423 return 0;
424 }
425 for (uint8_t i = 0; i < num; i++)
426 {
427 const IkeTrafficSelector *s = &sels[i];
428 if ((s->addr_len != 4 && s->addr_len != 16) || !s->start_addr || !s->end_addr)
429 {
430 return 0;
431 }
432 size_t sel_len = 8 + 2 * s->addr_len;
433 if (off + sel_len > cap)
434 {
435 return 0;
436 }
437 buf[off + 0] = (uint8_t)s->ts_type;
438 buf[off + 1] = s->ip_protocol;
439 put16(buf + off + 2, (uint16_t)sel_len);
440 put16(buf + off + 4, s->start_port);
441 put16(buf + off + 6, s->end_port);
442 memcpy(buf + off + 8, s->start_addr, s->addr_len);
443 memcpy(buf + off + 8 + s->addr_len, s->end_addr, s->addr_len);
444 off += sel_len;
445 }
446 if (off > 0xFFFF) // GCOVR_EXCL_LINE unreachable: num is a uint8_t and addr_len is forced to 4 or 16 above, so
447 {
448 return 0; // GCOVR_EXCL_LINE the widest payload is 8 + 255*(8+2*16) = 10208 bytes
449 }
450 buf[0] = (uint8_t)next_payload;
451 buf[1] = 0;
452 put16(buf + 2, (uint16_t)off);
453 buf[4] = num;
454 buf[5] = 0;
455 buf[6] = 0;
456 buf[7] = 0;
457 return off;
458}
459
460size_t pc_ike_cp_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, IkeCfgType cfg_type,
461 const IkeCfgAttr *attrs, uint8_t num_attrs)
462{
463 if (!buf || (num_attrs && !attrs))
464 {
465 return 0;
466 }
467 size_t attrs_len = 0;
468 for (uint8_t i = 0; i < num_attrs; i++)
469 {
470 if (attrs[i].value_len && !attrs[i].value)
471 {
472 return 0;
473 }
474 attrs_len += 4 + attrs[i].value_len; // type(2) + length(2) + value
475 }
476 size_t total = PC_IKE_PAYLOAD_HDR_LEN + 4 + attrs_len; // generic hdr + CFG Type(1) + reserved(3)
477 if (!put_pl_hdr(buf, cap, next_payload, total))
478 {
479 return 0;
480 }
481 buf[4] = (uint8_t)cfg_type;
482 buf[5] = 0;
483 buf[6] = 0;
484 buf[7] = 0;
485 size_t off = 8;
486 for (uint8_t i = 0; i < num_attrs; i++)
487 {
488 put16(buf + off, (uint16_t)(attrs[i].type & 0x7FFF)); // reserved high bit clear
489 put16(buf + off + 2, attrs[i].value_len);
490 off += 4;
491 if (attrs[i].value_len)
492 {
493 memcpy(buf + off, attrs[i].value, attrs[i].value_len);
494 off += attrs[i].value_len;
495 }
496 }
497 return total;
498}
499
500size_t pc_ike_sk_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, const uint8_t *iv, size_t iv_len,
501 const uint8_t *ciphertext, size_t ct_len, const uint8_t *icv, size_t icv_len)
502{
503 if (!buf || (iv_len && !iv) || (ct_len && !ciphertext) || (icv_len && !icv))
504 {
505 return 0;
506 }
507 size_t total = PC_IKE_PAYLOAD_HDR_LEN + iv_len + ct_len + icv_len;
508 if (!put_pl_hdr(buf, cap, next_payload, total))
509 {
510 return 0;
511 }
512 size_t off = PC_IKE_PAYLOAD_HDR_LEN;
513 if (iv_len)
514 {
515 memcpy(buf + off, iv, iv_len);
516 off += iv_len;
517 }
518 if (ct_len)
519 {
520 memcpy(buf + off, ciphertext, ct_len);
521 off += ct_len;
522 }
523 if (icv_len)
524 {
525 memcpy(buf + off, icv, icv_len);
526 }
527 return total;
528}
529
530// ── message fragmentation (RFC 7383) ──────────────────────────────────────────────────────────
531
532size_t pc_ike_skf_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, uint16_t frag_num, uint16_t total,
533 const uint8_t *iv, size_t iv_len, const uint8_t *ciphertext, size_t ct_len, const uint8_t *icv,
534 size_t icv_len)
535{
536 if (!buf || (iv_len && !iv) || (ct_len && !ciphertext) || (icv_len && !icv))
537 {
538 return 0;
539 }
540 if (frag_num == 0 || total == 0 || frag_num > total) // RFC 7383 §2.5: 1 <= Fragment Number <= Total
541 {
542 return 0;
543 }
544 size_t body = PC_IKE_PAYLOAD_HDR_LEN + 4 + iv_len + ct_len + icv_len; // + Frag Number(2) + Total(2)
545 if (!put_pl_hdr(buf, cap, next_payload, body))
546 {
547 return 0;
548 }
549 put16(buf + PC_IKE_PAYLOAD_HDR_LEN, frag_num);
550 put16(buf + PC_IKE_PAYLOAD_HDR_LEN + 2, total);
551 size_t off = PC_IKE_PAYLOAD_HDR_LEN + 4;
552 if (iv_len)
553 {
554 memcpy(buf + off, iv, iv_len);
555 off += iv_len;
556 }
557 if (ct_len)
558 {
559 memcpy(buf + off, ciphertext, ct_len);
560 off += ct_len;
561 }
562 if (icv_len)
563 {
564 memcpy(buf + off, icv, icv_len);
565 }
566 return body;
567}
568
569bool pc_ike_skf_parse(const uint8_t *body, size_t body_len, uint16_t *frag_num, uint16_t *total, size_t iv_len,
570 size_t icv_len, const uint8_t **iv, const uint8_t **ct, size_t *ct_len, const uint8_t **icv)
571{
572 if (frag_num)
573 {
574 *frag_num = 0;
575 }
576 if (total)
577 {
578 *total = 0;
579 }
580 if (iv)
581 {
582 *iv = nullptr;
583 }
584 if (ct)
585 {
586 *ct = nullptr;
587 }
588 if (ct_len)
589 {
590 *ct_len = 0;
591 }
592 if (icv)
593 {
594 *icv = nullptr;
595 }
596 // Body = Frag Number(2) | Total(2) | IV | ciphertext | ICV; the ciphertext must be non-negative.
597 if (!body || body_len < 4 + iv_len + icv_len)
598 {
599 return false;
600 }
601 uint16_t fn = get16(body);
602 uint16_t tf = get16(body + 2);
603 if (fn == 0 || tf == 0 || fn > tf) // a nonsensical fragment counter
604 {
605 return false;
606 }
607 if (frag_num)
608 {
609 *frag_num = fn;
610 }
611 if (total)
612 {
613 *total = tf;
614 }
615 if (iv)
616 {
617 *iv = body + 4;
618 }
619 if (ct)
620 {
621 *ct = body + 4 + iv_len;
622 }
623 if (ct_len)
624 {
625 *ct_len = body_len - 4 - iv_len - icv_len;
626 }
627 if (icv)
628 {
629 *icv = body + body_len - icv_len;
630 }
631 return true;
632}
633
634void pc_ike_frag_reasm_init(IkeFragReasm *r, uint8_t *pool, size_t pool_cap)
635{
636 if (!r)
637 {
638 return;
639 }
640 r->total = 0;
641 r->count = 0;
642 memset(r->present, 0, sizeof(r->present));
643 r->pool = pool;
644 r->pool_cap = pool_cap;
645 r->pool_used = 0;
646}
647
648bool pc_ike_frag_reasm_add(IkeFragReasm *r, uint16_t frag_num, uint16_t total, const uint8_t *chunk, size_t len)
649{
650 if (!r || !r->pool || (len && !chunk))
651 {
652 return false;
653 }
654 if (frag_num == 0 || total == 0 || frag_num > total || total > PC_IKE_FRAG_MAX)
655 {
656 return false;
657 }
658 if (r->total == 0)
659 {
660 r->total = total;
661 }
662 else if (r->total != total) // every fragment must agree on Total
663 {
664 return false;
665 }
666 uint16_t idx = (uint16_t)(frag_num - 1);
667 if (r->present[idx]) // a duplicate fragment is rejected, not merged
668 {
669 return false;
670 }
671 if (len > r->pool_cap - r->pool_used) // pool overflow (also covers pool_used == cap)
672 {
673 return false;
674 }
675 if (len)
676 {
677 memcpy(r->pool + r->pool_used, chunk, len);
678 }
679 r->off[idx] = r->pool_used;
680 r->len[idx] = len;
681 r->present[idx] = true;
682 r->pool_used += len;
683 r->count++;
684 return true;
685}
686
687bool pc_ike_frag_reasm_complete(const IkeFragReasm *r)
688{
689 return r && r->total != 0 && r->count == r->total;
690}
691
692size_t pc_ike_frag_reasm_assemble(const IkeFragReasm *r, uint8_t *out, size_t out_cap)
693{
694 if (!pc_ike_frag_reasm_complete(r) || !out)
695 {
696 return 0;
697 }
698 size_t off = 0;
699 for (uint16_t i = 0; i < r->total; i++) // concatenate fragments 1..Total in order
700 {
701 if (off + r->len[i] > out_cap)
702 {
703 return 0;
704 }
705 if (r->len[i])
706 {
707 memcpy(out + off, r->pool + r->off[i], r->len[i]);
708 }
709 off += r->len[i];
710 }
711 return off;
712}
713
714// ── anti-DoS COOKIE (RFC 7296 §2.6) ───────────────────────────────────────────────────────────
715
716size_t pc_ike_cookie_compute(uint8_t version, const uint8_t *secret, size_t secret_len, const uint8_t *ni,
717 size_t ni_len, const uint8_t *ipi, size_t ipi_len, const uint8_t spii[PC_IKE_SPI_LEN],
718 uint8_t *out, size_t out_cap)
719{
720 if (!out || out_cap < PC_IKE_COOKIE_LEN || !spii)
721 {
722 return 0;
723 }
724 if ((ni_len && !ni) || (ipi_len && !ipi) || (secret_len && !secret))
725 {
726 return 0;
727 }
728 // Cookie = version | SHA-256( Ni | IPi | SPIi | secret ).
729 pc_sha256_ctx ctx;
730 pc_sha256_init(&ctx);
731 if (ni_len)
732 {
733 pc_sha256_update(&ctx, ni, ni_len);
734 }
735 if (ipi_len)
736 {
737 pc_sha256_update(&ctx, ipi, ipi_len);
738 }
739 pc_sha256_update(&ctx, spii, PC_IKE_SPI_LEN);
740 if (secret_len)
741 {
742 pc_sha256_update(&ctx, secret, secret_len);
743 }
744 out[0] = version;
745 pc_sha256_final(&ctx, out + 1);
746 return PC_IKE_COOKIE_LEN;
747}
748
749bool pc_ike_cookie_verify(const uint8_t *cookie, size_t cookie_len, const uint8_t *secret, size_t secret_len,
750 const uint8_t *ni, size_t ni_len, const uint8_t *ipi, size_t ipi_len,
751 const uint8_t spii[PC_IKE_SPI_LEN])
752{
753 if (!cookie || cookie_len != PC_IKE_COOKIE_LEN)
754 {
755 return false;
756 }
757 uint8_t expect[PC_IKE_COOKIE_LEN];
758 if (pc_ike_cookie_compute(cookie[0], secret, secret_len, ni, ni_len, ipi, ipi_len, spii, expect, sizeof(expect)) !=
759 PC_IKE_COOKIE_LEN)
760 {
761 return false;
762 }
763 // Constant-time compare over the whole cookie (version tag + digest); no early-out.
764 uint8_t diff = 0;
765 for (size_t i = 0; i < PC_IKE_COOKIE_LEN; i++)
766 {
767 diff |= (uint8_t)(expect[i] ^ cookie[i]);
768 }
769 return diff == 0;
770}
771
772size_t pc_ike_cookie_notify_build(uint8_t *buf, size_t cap, IkePayloadType next_payload, const uint8_t *cookie,
773 size_t cookie_len)
774{
775 return pc_ike_notify_build(buf, cap, next_payload, IkeProtocol::IKE_PROTO_NONE, nullptr, 0, PC_IKE_N_COOKIE, cookie,
776 cookie_len);
777}
778
779// ── typed payload parsers ─────────────────────────────────────────────────────────────────────
780
781bool pc_ike_ke_parse(const uint8_t *body, size_t body_len, uint16_t *dh_group, const uint8_t **data, size_t *data_len)
782{
783 if (dh_group)
784 {
785 *dh_group = 0;
786 }
787 if (data)
788 {
789 *data = nullptr;
790 }
791 if (data_len)
792 {
793 *data_len = 0;
794 }
795 if (!body || body_len < 4)
796 {
797 return false;
798 }
799 if (dh_group)
800 {
801 *dh_group = get16(body);
802 }
803 if (data)
804 {
805 *data = body + 4;
806 }
807 if (data_len)
808 {
809 *data_len = body_len - 4;
810 }
811 return true;
812}
813
814bool pc_ike_id_parse(const uint8_t *body, size_t body_len, IkeIdType *id_type, const uint8_t **data, size_t *data_len)
815{
816 if (id_type)
817 {
818 *id_type = IkeIdType::IKE_ID_RESERVED;
819 }
820 if (data)
821 {
822 *data = nullptr;
823 }
824 if (data_len)
825 {
826 *data_len = 0;
827 }
828 if (!body || body_len < 4)
829 {
830 return false;
831 }
832 if (id_type)
833 {
834 *id_type = (IkeIdType)body[0];
835 }
836 if (data)
837 {
838 *data = body + 4;
839 }
840 if (data_len)
841 {
842 *data_len = body_len - 4;
843 }
844 return true;
845}
846
847bool pc_ike_auth_parse(const uint8_t *body, size_t body_len, IkeAuthMethod *auth_method, const uint8_t **data,
848 size_t *data_len)
849{
850 if (auth_method)
851 {
852 *auth_method = IkeAuthMethod::IKE_AUTH_RESERVED;
853 }
854 if (data)
855 {
856 *data = nullptr;
857 }
858 if (data_len)
859 {
860 *data_len = 0;
861 }
862 if (!body || body_len < 4)
863 {
864 return false;
865 }
866 if (auth_method)
867 {
868 *auth_method = (IkeAuthMethod)body[0];
869 }
870 if (data)
871 {
872 *data = body + 4;
873 }
874 if (data_len)
875 {
876 *data_len = body_len - 4;
877 }
878 return true;
879}
880
881bool pc_ike_notify_parse(const uint8_t *body, size_t body_len, IkeProtocol *protocol_id, uint16_t *notify_type,
882 const uint8_t **spi, uint8_t *spi_size, const uint8_t **data, size_t *data_len)
883{
884 if (protocol_id)
885 {
886 *protocol_id = IkeProtocol::IKE_PROTO_NONE;
887 }
888 if (notify_type)
889 {
890 *notify_type = 0;
891 }
892 if (spi)
893 {
894 *spi = nullptr;
895 }
896 if (spi_size)
897 {
898 *spi_size = 0;
899 }
900 if (data)
901 {
902 *data = nullptr;
903 }
904 if (data_len)
905 {
906 *data_len = 0;
907 }
908 if (!body || body_len < 4)
909 {
910 return false;
911 }
912 uint8_t ss = body[1];
913 if (body_len < (size_t)4 + ss)
914 {
915 return false;
916 }
917 if (protocol_id)
918 {
919 *protocol_id = (IkeProtocol)body[0];
920 }
921 if (spi_size)
922 {
923 *spi_size = ss;
924 }
925 if (notify_type)
926 {
927 *notify_type = get16(body + 2);
928 }
929 if (spi)
930 {
931 *spi = ss ? body + 4 : nullptr;
932 }
933 if (data)
934 {
935 *data = body + 4 + ss;
936 }
937 if (data_len)
938 {
939 *data_len = body_len - 4 - ss;
940 }
941 return true;
942}
943
944bool pc_ike_delete_parse(const uint8_t *body, size_t body_len, IkeProtocol *protocol_id, uint8_t *spi_size,
945 uint16_t *num_spis, const uint8_t **spis)
946{
947 if (protocol_id)
948 {
949 *protocol_id = IkeProtocol::IKE_PROTO_NONE;
950 }
951 if (spi_size)
952 {
953 *spi_size = 0;
954 }
955 if (num_spis)
956 {
957 *num_spis = 0;
958 }
959 if (spis)
960 {
961 *spis = nullptr;
962 }
963 if (!body || body_len < 4)
964 {
965 return false;
966 }
967 uint8_t ss = body[1];
968 uint16_t num = get16(body + 2);
969 if (body_len < (size_t)4 + (size_t)ss * num)
970 {
971 return false;
972 }
973 if (protocol_id)
974 {
975 *protocol_id = (IkeProtocol)body[0];
976 }
977 if (spi_size)
978 {
979 *spi_size = ss;
980 }
981 if (num_spis)
982 {
983 *num_spis = num;
984 }
985 if (spis)
986 {
987 *spis = (ss && num) ? body + 4 : nullptr;
988 }
989 return true;
990}
991
992bool pc_ike_sk_parse(const uint8_t *body, size_t body_len, size_t iv_len, size_t icv_len, const uint8_t **iv,
993 const uint8_t **ciphertext, size_t *ct_len, const uint8_t **icv)
994{
995 if (iv)
996 {
997 *iv = nullptr;
998 }
999 if (ciphertext)
1000 {
1001 *ciphertext = nullptr;
1002 }
1003 if (ct_len)
1004 {
1005 *ct_len = 0;
1006 }
1007 if (icv)
1008 {
1009 *icv = nullptr;
1010 }
1011 if (!body || body_len < iv_len + icv_len)
1012 {
1013 return false;
1014 }
1015 if (iv)
1016 {
1017 *iv = iv_len ? body : nullptr;
1018 }
1019 if (ciphertext)
1020 {
1021 *ciphertext = body + iv_len;
1022 }
1023 if (ct_len)
1024 {
1025 *ct_len = body_len - iv_len - icv_len;
1026 }
1027 if (icv)
1028 {
1029 *icv = icv_len ? body + body_len - icv_len : nullptr;
1030 }
1031 return true;
1032}
1033
1034// ── SA / proposal / transform parsing ─────────────────────────────────────────────────────────
1035
1036bool pc_ike_sa_first_proposal(const uint8_t *body, size_t body_len, IkeProposalRef *out)
1037{
1038 if (!out)
1039 {
1040 return false;
1041 }
1042 memset(out, 0, sizeof(*out));
1043 if (!body || body_len < 8)
1044 {
1045 return false;
1046 }
1047 uint16_t plen = get16(body + 2);
1048 uint8_t ss = body[6];
1049 if (plen < 8 || (size_t)plen > body_len || (size_t)8 + ss > plen)
1050 {
1051 return false;
1052 }
1053 out->last = (body[0] == 0); // 0 = last, 2 = more proposals follow
1054 out->proposal_num = body[4];
1055 out->protocol_id = (IkeProtocol)body[5];
1056 out->spi_size = ss;
1057 out->num_transforms = body[7];
1058 out->spi = ss ? body + 8 : nullptr;
1059 out->transforms = body + 8 + ss;
1060 out->transforms_len = (size_t)plen - 8 - ss;
1061 return true;
1062}
1063
1064void pc_ike_transform_iter_init(IkeTransformIter *it, const IkeProposalRef *p)
1065{
1066 if (!it)
1067 {
1068 return;
1069 }
1070 it->area = p ? p->transforms : nullptr;
1071 it->len = p ? p->transforms_len : 0;
1072 it->off = 0;
1073}
1074
1075bool pc_ike_transform_next(IkeTransformIter *it, IkeTransformRef *out)
1076{
1077 if (!out)
1078 {
1079 return false;
1080 }
1081 out->type = IkeTransformType::IKE_TRANSFORM_ENCR;
1082 out->id = 0;
1083 out->key_length = -1;
1084 out->last = true;
1085 if (!it || !it->area || it->off + 8 > it->len)
1086 {
1087 return false;
1088 }
1089 const uint8_t *t = it->area + it->off;
1090 uint16_t tlen = get16(t + 2);
1091 if (tlen < 8 || it->off + tlen > it->len)
1092 {
1093 return false;
1094 }
1095 out->last = (t[0] == 0); // 0 = last, 3 = more
1096 out->type = (IkeTransformType)t[4];
1097 out->id = get16(t + 6);
1098
1099 // Walk the transform attributes (RFC 7296 3.3.5): a 2-byte AF|type; if AF (0x8000) set it is TV
1100 // (a 2-byte value follows), else TLV (a 2-byte length then that many value bytes).
1101 size_t ao = 8;
1102 while (ao + 4 <= tlen)
1103 {
1104 uint16_t af_type = get16(t + ao);
1105 uint16_t atype = af_type & 0x7FFF;
1106 if (af_type & 0x8000)
1107 {
1108 if (atype == IKE_ATTR_KEY_LENGTH)
1109 {
1110 out->key_length = get16(t + ao + 2);
1111 }
1112 ao += 4;
1113 }
1114 else
1115 {
1116 uint16_t alen = get16(t + ao + 2);
1117 ao += (size_t)4 + alen;
1118 }
1119 }
1120 it->off += tlen;
1121 return true;
1122}
1123
1124// ── traffic selector parsing ──────────────────────────────────────────────────────────────────
1125
1126uint8_t pc_ike_ts_count(const uint8_t *body, size_t body_len)
1127{
1128 if (!body || body_len < 4)
1129 {
1130 return 0;
1131 }
1132 return body[0];
1133}
1134
1135bool pc_ike_ts_get(const uint8_t *body, size_t body_len, uint8_t index, IkeTrafficSelector *out)
1136{
1137 if (!out)
1138 {
1139 return false;
1140 }
1141 memset(out, 0, sizeof(*out));
1142 if (!body || body_len < 4)
1143 {
1144 return false;
1145 }
1146 uint8_t num = body[0];
1147 if (index >= num)
1148 {
1149 return false;
1150 }
1151 size_t off = 4;
1152 // the loop never runs to completion: index < num is enforced above, so iteration `index` either
1153 // rejects a malformed selector or returns the match
1154 for (uint8_t i = 0; i < num; i++) // GCOVR_EXCL_LINE loop-exhausted branch unreachable (see above)
1155 {
1156 if (off + 8 > body_len)
1157 {
1158 return false;
1159 }
1160 uint16_t sel_len = get16(body + off + 2);
1161 if (sel_len < 8 || off + sel_len > body_len || ((sel_len - 8) % 2) != 0)
1162 {
1163 return false;
1164 }
1165 if (i == index)
1166 {
1167 size_t addr_len = (size_t)(sel_len - 8) / 2;
1168 out->ts_type = (IkeTsType)body[off];
1169 out->ip_protocol = body[off + 1];
1170 out->start_port = get16(body + off + 4);
1171 out->end_port = get16(body + off + 6);
1172 out->start_addr = body + off + 8;
1173 out->end_addr = body + off + 8 + addr_len;
1174 out->addr_len = addr_len;
1175 return true;
1176 }
1177 off += sel_len;
1178 }
1179 return false; // GCOVR_EXCL_LINE unreachable: the loop above always returns (see the note on the for)
1180}
1181
1182bool pc_ike_cp_parse(const uint8_t *body, size_t body_len, IkeCfgType *cfg_type, const uint8_t **attrs,
1183 size_t *attrs_len)
1184{
1185 if (cfg_type)
1186 {
1187 *cfg_type = IkeCfgType::IKE_CFG_REQUEST;
1188 }
1189 if (attrs)
1190 {
1191 *attrs = nullptr;
1192 }
1193 if (attrs_len)
1194 {
1195 *attrs_len = 0;
1196 }
1197 if (!body || body_len < 4) // CFG Type(1) + reserved(3)
1198 {
1199 return false;
1200 }
1201 if (cfg_type)
1202 {
1203 *cfg_type = (IkeCfgType)body[0];
1204 }
1205 if (attrs)
1206 {
1207 *attrs = body + 4;
1208 }
1209 if (attrs_len)
1210 {
1211 *attrs_len = body_len - 4;
1212 }
1213 return true;
1214}
1215
1216void pc_ike_cp_attr_iter_init(IkeCfgAttrIter *it, const uint8_t *attrs, size_t attrs_len)
1217{
1218 if (!it)
1219 {
1220 return;
1221 }
1222 it->area = attrs;
1223 it->len = attrs_len;
1224 it->off = 0;
1225}
1226
1227bool pc_ike_cp_attr_next(IkeCfgAttrIter *it, IkeCfgAttr *out)
1228{
1229 if (!it || !out || !it->area)
1230 {
1231 return false;
1232 }
1233 if (it->off + 4 > it->len) // no room for another attribute header
1234 {
1235 return false;
1236 }
1237 const uint8_t *p = it->area + it->off;
1238 uint16_t vlen = get16(p + 2);
1239 if (it->off + 4 + vlen > it->len) // truncated value
1240 {
1241 return false;
1242 }
1243 out->type = get16(p) & 0x7FFF; // mask off the reserved high bit
1244 out->value_len = vlen;
1245 out->value = vlen ? (p + 4) : nullptr;
1246 it->off += 4 + vlen;
1247 return true;
1248}
1249
1250// ── tier 2: SKEYSEED / SK_* key derivation (RFC 7296 §2.13-2.14) ───────────────────────────────
1251
1252bool pc_ike_prf_plus(const uint8_t *key, size_t key_len, const uint8_t *seed, size_t seed_len, uint8_t *out,
1253 size_t out_len)
1254{
1255 if (!key || !seed || !out || out_len == 0)
1256 {
1257 return false;
1258 }
1259 // prf+ chains at most 255 blocks: the counter i is a single octet 0x01..0xFF (RFC 7296 §2.13).
1260 if (out_len > (size_t)255 * PC_HMAC_SHA256_LEN)
1261 {
1262 return false;
1263 }
1264
1265 uint8_t t[PC_HMAC_SHA256_LEN];
1266 size_t t_len = 0; // T0 is empty (omitted from the first block's input)
1267 size_t produced = 0;
1268 uint8_t counter = 0;
1269 while (produced < out_len)
1270 {
1271 counter++; // 0x01 for T1, 0x02 for T2, ... (bounded by the out_len guard above)
1273 pc_hmac_sha256_init(&ctx, key, key_len);
1274 if (t_len)
1275 {
1276 pc_hmac_sha256_update(&ctx, t, t_len); // Ti-1
1277 }
1278 pc_hmac_sha256_update(&ctx, seed, seed_len);
1279 pc_hmac_sha256_update(&ctx, &counter, 1);
1280 pc_hmac_sha256_final(&ctx, t);
1281 t_len = PC_HMAC_SHA256_LEN;
1282
1283 size_t take = out_len - produced;
1284 if (take > PC_HMAC_SHA256_LEN)
1285 {
1286 take = PC_HMAC_SHA256_LEN;
1287 }
1288 memcpy(out + produced, t, take);
1289 produced += take;
1290 }
1291 return true;
1292}
1293
1294// Build S = Ni | Nr | SPIi | SPIr into @p s (caller-sized); returns its length, or 0 on a bad nonce length.
1295static size_t build_ni_nr_spi(uint8_t *s, const uint8_t *ni, size_t ni_len, const uint8_t *nr, size_t nr_len,
1296 const uint8_t *spi_i, const uint8_t *spi_r)
1297{
1298 if (ni_len == 0 || ni_len > PC_IKE_NONCE_MAX || nr_len == 0 || nr_len > PC_IKE_NONCE_MAX)
1299 {
1300 return 0;
1301 }
1302 size_t nlen = ni_len + nr_len;
1303 memcpy(s, ni, ni_len);
1304 memcpy(s + ni_len, nr, nr_len);
1305 memcpy(s + nlen, spi_i, PC_IKE_SPI_LEN);
1306 memcpy(s + nlen + PC_IKE_SPI_LEN, spi_r, PC_IKE_SPI_LEN);
1307 return nlen + 2 * PC_IKE_SPI_LEN;
1308}
1309
1310// Given SKEYSEED and S = Ni|Nr|SPIi|SPIr, run prf+ and split it into the seven SK_* keys (shared by the
1311// initial and the rekey schedules - only how SKEYSEED is computed differs).
1312static bool sk_split_from_skeyseed(const uint8_t skeyseed[PC_IKE_PRF_LEN], const uint8_t *s, size_t s_len,
1313 const IkeKeyLengths *lens, IkeKeyMaterial *out)
1314{
1315 // sk_a MAY be 0 (an AEAD cipher carries its own integrity, so there is no separate SK_ai/SK_ar);
1316 // the others must be present.
1317 if (lens->sk_d == 0 || lens->sk_d > PC_IKE_SK_MAX || lens->sk_a > PC_IKE_SK_MAX || lens->sk_e == 0 ||
1318 lens->sk_e > PC_IKE_SK_MAX || lens->sk_p == 0 || lens->sk_p > PC_IKE_SK_MAX)
1319 {
1320 return false;
1321 }
1322 size_t total = lens->sk_d + 2 * lens->sk_a + 2 * lens->sk_e + 2 * lens->sk_p;
1323 uint8_t ks[7 * PC_IKE_SK_MAX];
1324 if (!pc_ike_prf_plus(skeyseed, PC_IKE_PRF_LEN, s, s_len, ks, total))
1325 {
1326 return false;
1327 }
1328
1329 size_t o = 0;
1330 memcpy(out->sk_d, ks + o, lens->sk_d);
1331 o += lens->sk_d;
1332 memcpy(out->sk_ai, ks + o, lens->sk_a);
1333 o += lens->sk_a;
1334 memcpy(out->sk_ar, ks + o, lens->sk_a);
1335 o += lens->sk_a;
1336 memcpy(out->sk_ei, ks + o, lens->sk_e);
1337 o += lens->sk_e;
1338 memcpy(out->sk_er, ks + o, lens->sk_e);
1339 o += lens->sk_e;
1340 memcpy(out->sk_pi, ks + o, lens->sk_p);
1341 o += lens->sk_p;
1342 memcpy(out->sk_pr, ks + o, lens->sk_p);
1343 out->sk_d_len = lens->sk_d;
1344 out->sk_a_len = lens->sk_a;
1345 out->sk_e_len = lens->sk_e;
1346 out->sk_p_len = lens->sk_p;
1347 return true;
1348}
1349
1350bool pc_ike_derive_keys(const uint8_t *dh_secret, size_t dh_len, const uint8_t *ni, size_t ni_len, const uint8_t *nr,
1351 size_t nr_len, const uint8_t *spi_i, const uint8_t *spi_r, const IkeKeyLengths *lens,
1352 IkeKeyMaterial *out)
1353{
1354 if (!dh_secret || !ni || !nr || !spi_i || !spi_r || !lens || !out)
1355 {
1356 return false;
1357 }
1358 // S = Ni | Nr | SPIi | SPIr; its Ni|Nr prefix is also the SKEYSEED HMAC key, so one buffer serves both.
1359 uint8_t s[2 * PC_IKE_NONCE_MAX + 2 * PC_IKE_SPI_LEN];
1360 size_t s_len = build_ni_nr_spi(s, ni, ni_len, nr, nr_len, spi_i, spi_r);
1361 if (s_len == 0)
1362 {
1363 return false;
1364 }
1365 // SKEYSEED = prf(Ni | Nr, g^ir). HMAC pre-hashes a key longer than the 64-byte block (RFC 2104).
1366 uint8_t skeyseed[PC_IKE_PRF_LEN];
1367 pc_hmac_sha256(s, ni_len + nr_len, dh_secret, dh_len, skeyseed);
1368 return sk_split_from_skeyseed(skeyseed, s, s_len, lens, out);
1369}
1370
1371bool pc_ike_rekey_derive_keys(const uint8_t *sk_d_old, size_t sk_d_old_len, const uint8_t *dh_secret, size_t dh_len,
1372 const uint8_t *ni, size_t ni_len, const uint8_t *nr, size_t nr_len, const uint8_t *spi_i,
1373 const uint8_t *spi_r, const IkeKeyLengths *lens, IkeKeyMaterial *out)
1374{
1375 if (!sk_d_old || !dh_secret || !ni || !nr || !spi_i || !spi_r || !lens || !out)
1376 {
1377 return false;
1378 }
1379 if (dh_len == 0 || dh_len > PC_IKE_X25519_LEN || ni_len > PC_IKE_NONCE_MAX || nr_len > PC_IKE_NONCE_MAX)
1380 {
1381 return false;
1382 }
1383
1384 // Rekey SKEYSEED = prf(SK_d(old), g^ir(new) | Ni | Nr) (RFC 7296 §2.18) - the OLD SK_d is the key.
1385 uint8_t seed[PC_IKE_X25519_LEN + 2 * PC_IKE_NONCE_MAX];
1386 size_t sl = 0;
1387 memcpy(seed, dh_secret, dh_len);
1388 sl = dh_len;
1389 memcpy(seed + sl, ni, ni_len);
1390 sl += ni_len;
1391 memcpy(seed + sl, nr, nr_len);
1392 sl += nr_len;
1393 uint8_t skeyseed[PC_IKE_PRF_LEN];
1394 pc_hmac_sha256(sk_d_old, sk_d_old_len, seed, sl, skeyseed);
1395
1396 // Then the identical prf+(SKEYSEED, Ni | Nr | SPIi | SPIr) split with the NEW SPIs.
1397 uint8_t s[2 * PC_IKE_NONCE_MAX + 2 * PC_IKE_SPI_LEN];
1398 size_t s_len = build_ni_nr_spi(s, ni, ni_len, nr, nr_len, spi_i, spi_r);
1399 if (s_len == 0)
1400 {
1401 return false;
1402 }
1403 return sk_split_from_skeyseed(skeyseed, s, s_len, lens, out);
1404}
1405
1406// ── tier 2: SK-payload AEAD (AES-256-GCM-16, RFC 5282) ─────────────────────────────────────────
1407
1408// Build the 12-byte GCM nonce = salt(4) || explicit IV(8) (RFC 5282 §4).
1409static void ike_gcm_nonce(uint8_t nonce[PC_AESGCM_IV_LEN], const uint8_t *salt, const uint8_t *iv)
1410{
1411 memcpy(nonce, salt, PC_IKE_GCM_SALT_LEN);
1412 memcpy(nonce + PC_IKE_GCM_SALT_LEN, iv, PC_IKE_GCM_IV_LEN);
1413}
1414
1415bool pc_ike_sk_aead_seal(const uint8_t key[PC_IKE_AEAD_KEY_LEN], const uint8_t salt[PC_IKE_GCM_SALT_LEN],
1416 const uint8_t iv[PC_IKE_GCM_IV_LEN], const uint8_t *aad, size_t aad_len, const uint8_t *pt,
1417 size_t pt_len, uint8_t *out)
1418{
1419 if (!key || !salt || !iv || !out || (pt_len && !pt) || (aad_len && !aad))
1420 {
1421 return false;
1422 }
1423 uint8_t nonce[PC_AESGCM_IV_LEN];
1424 ike_gcm_nonce(nonce, salt, iv);
1425 // IKEv2 chooses a fresh explicit IV per message, so the GCM context is single-use here (the SSH
1426 // invocation-counter model does not apply): init, seal once, wipe.
1427 {
1428 // Per-call context: this path is not hot enough to justify a per-session one. The cost is the
1429 // ~9,200-cycle lifecycle documented in aesgcm.h - hoist the context into session state if it
1430 // ever shows up in a profile.
1431 SecureBorrow gcm_b(PC_WORK_AESGCM, 8);
1432 pc_aesgcm_key *gcm = pc_aesgcm_key_init(gcm_b.span().buf, key);
1433 pc_aesgcm_seal(gcm, nonce, aad, aad_len, pt, pt_len, out, out + pt_len); // out = ct || 16-byte tag
1434 pc_aesgcm_key_wipe(gcm);
1435 }
1436 return true;
1437}
1438
1439bool pc_ike_sk_aead_open(const uint8_t key[PC_IKE_AEAD_KEY_LEN], const uint8_t salt[PC_IKE_GCM_SALT_LEN],
1440 const uint8_t iv[PC_IKE_GCM_IV_LEN], const uint8_t *aad, size_t aad_len, const uint8_t *ct,
1441 size_t ct_len, const uint8_t tag[PC_IKE_AEAD_ICV_LEN], uint8_t *out)
1442{
1443 if (!key || !salt || !iv || !tag || !out || (ct_len && !ct) || (aad_len && !aad))
1444 {
1445 return false;
1446 }
1447 uint8_t nonce[PC_AESGCM_IV_LEN];
1448 ike_gcm_nonce(nonce, salt, iv);
1449 bool ok = false;
1450 {
1451 // Per-call context: this path is not hot enough to justify a per-session one. The cost is the
1452 // ~9,200-cycle lifecycle documented in aesgcm.h - hoist the context into session state if it
1453 // ever shows up in a profile.
1454 SecureBorrow gcm_b(PC_WORK_AESGCM, 8);
1455 pc_aesgcm_key *gcm = pc_aesgcm_key_init(gcm_b.span().buf, key);
1456 ok = pc_aesgcm_open(gcm, nonce, aad, aad_len, ct, ct_len, tag, out);
1457 pc_aesgcm_key_wipe(gcm);
1458 }
1459 return ok;
1460}
1461
1462// ── tier 2: Diffie-Hellman shared secret (RFC 7296 §2.7) ───────────────────────────────────────
1463
1464size_t pc_ike_dh_public(uint16_t group, const uint8_t *our_priv, size_t priv_len, uint8_t *out, size_t out_cap)
1465{
1466 if (!our_priv || !out)
1467 {
1468 return 0;
1469 }
1470 if (group == IKE_DH_CURVE25519) // RFC 7748 X25519
1471 {
1472 if (priv_len != PC_IKE_X25519_LEN || out_cap < PC_IKE_X25519_LEN)
1473 {
1474 return 0;
1475 }
1476 pc_x25519_base(out, our_priv); // out = our_priv * G
1477 return PC_IKE_X25519_LEN;
1478 }
1479 return 0; // groups 19 (P-256) / 14 (MODP-2048) are a later increment
1480}
1481
1482size_t pc_ike_dh_compute(uint16_t group, const uint8_t *our_priv, size_t priv_len, const uint8_t *peer_pub,
1483 size_t pub_len, uint8_t *out, size_t out_cap)
1484{
1485 if (!our_priv || !peer_pub || !out)
1486 {
1487 return 0;
1488 }
1489 if (group == IKE_DH_CURVE25519)
1490 {
1491 if (priv_len != PC_IKE_X25519_LEN || pub_len != PC_IKE_X25519_LEN || out_cap < PC_IKE_X25519_LEN)
1492 {
1493 return 0;
1494 }
1495 pc_x25519(out, our_priv, peer_pub); // out = our_priv * peer_pub
1496 return PC_IKE_X25519_LEN;
1497 }
1498 return 0;
1499}
1500
1501// ── tier 2: IKE_AUTH pre-shared-key authentication (RFC 7296 §2.15) ─────────────────────────────
1502
1503bool pc_ike_auth_psk(const uint8_t *psk, size_t psk_len, const uint8_t *real_msg, size_t real_len,
1504 const uint8_t *peer_nonce, size_t nonce_len, const uint8_t *sk_p, size_t sk_p_len,
1505 const uint8_t *id_body, size_t id_body_len, uint8_t out[PC_IKE_AUTH_LEN])
1506{
1507 if (!psk || !real_msg || !peer_nonce || !sk_p || !id_body || !out)
1508 {
1509 return false;
1510 }
1511
1512 // MACedID = prf(SK_p, RestOfIDPayload).
1513 uint8_t macid[PC_IKE_AUTH_LEN];
1514 pc_hmac_sha256(sk_p, sk_p_len, id_body, id_body_len, macid);
1515
1516 // keypad = prf(PSK, "Key Pad for IKEv2") - the inner PRF that turns the shared key into a fixed key.
1517 uint8_t keypad[PC_IKE_AUTH_LEN];
1518 static const char pad[] = PC_IKE_PSK_PAD; // 17 octets, no NUL sent
1519 pc_hmac_sha256(psk, psk_len, (const uint8_t *)pad, sizeof(pad) - 1, keypad);
1520
1521 // AUTH = prf(keypad, RealMessage | Nonce | MACedID). Streamed so RealMessage is never re-buffered.
1523 pc_hmac_sha256_init(&ctx, keypad, sizeof(keypad));
1524 pc_hmac_sha256_update(&ctx, real_msg, real_len);
1525 pc_hmac_sha256_update(&ctx, peer_nonce, nonce_len);
1526 pc_hmac_sha256_update(&ctx, macid, sizeof(macid));
1527 pc_hmac_sha256_final(&ctx, out);
1528 return true;
1529}
1530
1531// ── tier 2: IKE_SA_INIT message assembly (RFC 7296 §1.2) ───────────────────────────────────────
1532
1533size_t pc_ike_sa_init_build(uint8_t *buf, size_t cap, const uint8_t init_spi[PC_IKE_SPI_LEN],
1534 const uint8_t resp_spi[PC_IKE_SPI_LEN], uint32_t msg_id, bool is_response,
1535 uint8_t proposal_num, const IkeTransform *transforms, uint8_t num_transforms,
1536 uint16_t dh_group, const uint8_t *ke_data, size_t ke_len, const uint8_t *nonce,
1537 size_t nonce_len)
1538{
1539 if (!buf || !init_spi || !resp_spi || !transforms || num_transforms == 0 || (ke_len && !ke_data) ||
1540 (nonce_len && !nonce))
1541 {
1542 return 0;
1543 }
1544
1545 IkeHeader h;
1546 memcpy(h.init_spi, init_spi, PC_IKE_SPI_LEN);
1547 memcpy(h.resp_spi, resp_spi, PC_IKE_SPI_LEN);
1548 h.next_payload = IkePayloadType::IKE_PL_SA;
1549 h.version = PC_IKE_VERSION;
1550 h.exchange = IkeExchange::IKE_SA_INIT;
1551 h.flags = is_response ? PC_IKE_FLAG_RESPONSE : PC_IKE_FLAG_INITIATOR;
1552 h.message_id = msg_id;
1553 h.length = 0; // patched below
1554
1555 size_t off = pc_ike_hdr_build(buf, cap, &h);
1556 if (off == 0)
1557 {
1558 return 0;
1559 }
1560 // SA -> KE. One proposal, IKE protocol, no SPI in an IKE_SA_INIT SA payload.
1561 size_t n = pc_ike_sa_build(buf + off, cap - off, IkePayloadType::IKE_PL_KE, proposal_num,
1562 IkeProtocol::IKE_PROTO_IKE, nullptr, 0, transforms, num_transforms);
1563 if (n == 0)
1564 {
1565 return 0;
1566 }
1567 off += n;
1568 // KE -> Nonce.
1569 n = pc_ike_ke_build(buf + off, cap - off, IkePayloadType::IKE_PL_NONCE, dh_group, ke_data, ke_len);
1570 if (n == 0)
1571 {
1572 return 0;
1573 }
1574 off += n;
1575 // Nonce -> end of chain.
1576 n = pc_ike_nonce_build(buf + off, cap - off, IkePayloadType::IKE_PL_NONE, nonce, nonce_len);
1577 if (n == 0)
1578 {
1579 return 0;
1580 }
1581 off += n;
1582
1583 pc_ike_set_length(buf, cap, (uint32_t)off);
1584 return off;
1585}
1586
1587bool pc_ike_sa_init_parse(const uint8_t *msg, size_t len, IkeSaInitMsg *out)
1588{
1589 if (!out)
1590 {
1591 return false;
1592 }
1593 memset(out, 0, sizeof(*out));
1594
1595 IkeHeader h;
1596 if (!pc_ike_hdr_parse(msg, len, &h))
1597 {
1598 return false;
1599 }
1600 if (h.exchange != IkeExchange::IKE_SA_INIT)
1601 {
1602 return false;
1603 }
1604 if (h.length < PC_IKE_HDR_LEN || h.length > len) // a truncated / lying Length fails closed
1605 {
1606 return false;
1607 }
1608
1609 memcpy(out->init_spi, h.init_spi, PC_IKE_SPI_LEN);
1610 memcpy(out->resp_spi, h.resp_spi, PC_IKE_SPI_LEN);
1611 out->is_response = (h.flags & PC_IKE_FLAG_RESPONSE) != 0;
1612
1613 IkePayloadIter it;
1614 pc_ike_payload_iter_init(&it, h.next_payload, msg + PC_IKE_HDR_LEN, h.length - PC_IKE_HDR_LEN);
1615 IkePayload pl;
1616 bool have_sa = false, have_ke = false, have_nonce = false;
1617 while (pc_ike_payload_next(&it, &pl))
1618 {
1619 if (pl.type == IkePayloadType::IKE_PL_SA && !have_sa)
1620 {
1621 have_sa = pc_ike_sa_first_proposal(pl.body, pl.body_len, &out->proposal);
1622 }
1623 else if (pl.type == IkePayloadType::IKE_PL_KE && !have_ke)
1624 {
1625 have_ke = pc_ike_ke_parse(pl.body, pl.body_len, &out->dh_group, &out->ke_data, &out->ke_len);
1626 }
1627 else if (pl.type == IkePayloadType::IKE_PL_NONCE && !have_nonce)
1628 {
1629 out->nonce = pl.body; // a Nonce payload body is the raw nonce data
1630 out->nonce_len = pl.body_len;
1631 have_nonce = true;
1632 }
1633 }
1634 return have_sa && have_ke && have_nonce;
1635}
1636
1637// ── tier 2: IKE_AUTH encrypted-message assembly (RFC 7296 §3.14, RFC 5282) ─────────────────────
1638
1639// Byte layout of an SK message: [0,28) IKE header | [28,32) SK generic header | [32,40) IV |
1640// [40, 40+ct) ciphertext | [.., +16) ICV. The AAD is [0,32) (header through the SK generic header).
1641static const size_t IKE_SK_HDR_OFF = PC_IKE_HDR_LEN; // 28
1642static const size_t IKE_SK_IV_OFF = PC_IKE_HDR_LEN + PC_IKE_PAYLOAD_HDR_LEN; // 32 (also the AAD length)
1643static const size_t IKE_SK_CT_OFF = IKE_SK_IV_OFF + PC_IKE_GCM_IV_LEN; // 40
1644
1645// The generic SK-encrypted message builder: HDR(@p exchange, @p flags) | SK{ IV | AEAD(inner | Pad) | ICV }.
1646// pc_ike_auth_msg_build and the INFORMATIONAL builder are thin wrappers that fix the exchange + flags.
1647static size_t sk_message_build(uint8_t *buf, size_t cap, const uint8_t *init_spi, const uint8_t *resp_spi,
1648 uint32_t msg_id, IkeExchange exchange, uint8_t flags, IkePayloadType first_inner_type,
1649 const uint8_t *inner, size_t inner_len, const uint8_t *key, const uint8_t *salt,
1650 const uint8_t *iv)
1651{
1652 if (!buf || !init_spi || !resp_spi || !key || !salt || !iv || (inner_len && !inner))
1653 {
1654 return 0;
1655 }
1656
1657 size_t pt_len = inner_len + 1; // inner payloads + a 1-byte Pad Length (zero padding)
1658 size_t sk_len = PC_IKE_PAYLOAD_HDR_LEN + PC_IKE_GCM_IV_LEN + pt_len + PC_IKE_AEAD_ICV_LEN; // SK payload
1659 size_t total = PC_IKE_HDR_LEN + sk_len;
1660 if (total > 0xFFFF || cap < total)
1661 {
1662 return 0;
1663 }
1664
1665 IkeHeader h;
1666 memcpy(h.init_spi, init_spi, PC_IKE_SPI_LEN);
1667 memcpy(h.resp_spi, resp_spi, PC_IKE_SPI_LEN);
1668 h.next_payload = IkePayloadType::IKE_PL_SK;
1669 h.version = PC_IKE_VERSION;
1670 h.exchange = exchange;
1671 h.flags = flags;
1672 h.message_id = msg_id;
1673 h.length = (uint32_t)total;
1674 if (pc_ike_hdr_build(buf, cap, &h) == 0)
1675 {
1676 return 0;
1677 }
1678
1679 // SK generic header (next = first inner payload type).
1680 buf[IKE_SK_HDR_OFF + 0] = (uint8_t)first_inner_type;
1681 buf[IKE_SK_HDR_OFF + 1] = 0;
1682 put16(buf + IKE_SK_HDR_OFF + 2, (uint16_t)sk_len);
1683
1684 // IV, then the plaintext (inner | Pad Length = 0) built in place at the ciphertext offset.
1685 memcpy(buf + IKE_SK_IV_OFF, iv, PC_IKE_GCM_IV_LEN);
1686 if (inner_len)
1687 {
1688 memcpy(buf + IKE_SK_CT_OFF, inner, inner_len);
1689 }
1690 buf[IKE_SK_CT_OFF + inner_len] = 0x00; // Pad Length (0 padding bytes)
1691
1692 // Encrypt in place: AAD = [0, 32), plaintext -> ciphertext || ICV at IKE_SK_CT_OFF.
1693 pc_ike_sk_aead_seal(key, salt, iv, buf, IKE_SK_IV_OFF, buf + IKE_SK_CT_OFF, pt_len, buf + IKE_SK_CT_OFF);
1694 return total;
1695}
1696
1697size_t pc_ike_auth_msg_build(uint8_t *buf, size_t cap, const uint8_t init_spi[PC_IKE_SPI_LEN],
1698 const uint8_t resp_spi[PC_IKE_SPI_LEN], uint32_t msg_id, bool is_response,
1699 IkePayloadType first_inner_type, const uint8_t *inner, size_t inner_len,
1700 const uint8_t key[PC_IKE_AEAD_KEY_LEN], const uint8_t salt[PC_IKE_GCM_SALT_LEN],
1701 const uint8_t iv[PC_IKE_GCM_IV_LEN])
1702{
1703 // In IKE_AUTH the initiator always sends the request and the responder the response, so the flag is
1704 // exactly INITIATOR-for-request / RESPONSE-for-response.
1705 uint8_t flags = is_response ? PC_IKE_FLAG_RESPONSE : PC_IKE_FLAG_INITIATOR;
1706 return sk_message_build(buf, cap, init_spi, resp_spi, msg_id, IkeExchange::IKE_AUTH, flags, first_inner_type, inner,
1707 inner_len, key, salt, iv);
1708}
1709
1710bool pc_ike_auth_msg_open(uint8_t *msg, size_t len, const uint8_t key[PC_IKE_AEAD_KEY_LEN],
1711 const uint8_t salt[PC_IKE_GCM_SALT_LEN], IkePayloadType *first_inner_type,
1712 const uint8_t **inner_out, size_t *inner_len_out)
1713{
1714 if (!msg || !key || !salt || !inner_out || !inner_len_out)
1715 {
1716 return false;
1717 }
1718 if (first_inner_type)
1719 {
1720 *first_inner_type = IkePayloadType::IKE_PL_NONE;
1721 }
1722 *inner_out = nullptr;
1723 *inner_len_out = 0;
1724
1725 IkeHeader h;
1726 if (!pc_ike_hdr_parse(msg, len, &h))
1727 {
1728 return false;
1729 }
1730 if (h.next_payload != IkePayloadType::IKE_PL_SK) // this helper handles the all-encrypted (IKE_AUTH) shape
1731 {
1732 return false;
1733 }
1734 if (h.length < IKE_SK_CT_OFF || h.length > len)
1735 {
1736 return false;
1737 }
1738
1739 // SK payload starts right after the header; its own length bounds the body.
1740 size_t sk_len = ((size_t)msg[IKE_SK_HDR_OFF + 2] << 8) | msg[IKE_SK_HDR_OFF + 3];
1741 if (sk_len < PC_IKE_PAYLOAD_HDR_LEN + PC_IKE_GCM_IV_LEN + 1 + PC_IKE_AEAD_ICV_LEN ||
1742 IKE_SK_HDR_OFF + sk_len > h.length)
1743 {
1744 return false;
1745 }
1746
1747 const uint8_t *ivp = msg + IKE_SK_IV_OFF;
1748 uint8_t *ct = msg + IKE_SK_CT_OFF;
1749 size_t ct_len = sk_len - PC_IKE_PAYLOAD_HDR_LEN - PC_IKE_GCM_IV_LEN - PC_IKE_AEAD_ICV_LEN;
1750 const uint8_t *tag = ct + ct_len;
1751
1752 // Verify + decrypt in place (AAD = header through the SK generic header).
1753 if (!pc_ike_sk_aead_open(key, salt, ivp, msg, IKE_SK_IV_OFF, ct, ct_len, tag, ct))
1754 {
1755 return false;
1756 }
1757
1758 // Strip the RFC 7296 §3.14 trailer: last plaintext byte is Pad Length, preceded by that many pad bytes.
1759 uint8_t pad_len = ct[ct_len - 1];
1760 if ((size_t)pad_len + 1 > ct_len) // padding + the pad-length byte cannot exceed the plaintext
1761 {
1762 return false;
1763 }
1764 if (first_inner_type)
1765 {
1766 *first_inner_type = (IkePayloadType)msg[IKE_SK_HDR_OFF];
1767 }
1768 *inner_out = ct;
1769 *inner_len_out = ct_len - 1 - pad_len;
1770 return true;
1771}
1772
1773// ── tier 2: IKE_AUTH ECDSA-P256 (certificate) authentication (RFC 7296 §2.15, RFC 7427) ─────────
1774
1775size_t pc_ike_signed_octets(uint8_t *scratch, size_t cap, const uint8_t *real, size_t real_len, const uint8_t *nonce,
1776 size_t nonce_len, const uint8_t *sk_p, size_t sk_p_len, const uint8_t *id_body,
1777 size_t id_body_len)
1778{
1779 if (!scratch || !real || !nonce || !sk_p || !id_body)
1780 {
1781 return 0;
1782 }
1783 size_t total = real_len + nonce_len + PC_IKE_AUTH_LEN; // RealMessage | Nonce | prf(SK_p, id)(=32)
1784 if (total > cap)
1785 {
1786 return 0;
1787 }
1788 memcpy(scratch, real, real_len);
1789 memcpy(scratch + real_len, nonce, nonce_len);
1790 pc_hmac_sha256(sk_p, sk_p_len, id_body, id_body_len, scratch + real_len + nonce_len); // MACedID
1791 return total;
1792}
1793
1794bool pc_ike_auth_sign_ecdsa_p256(uint8_t sig[PC_IKE_ECDSA_P256_SIG_LEN], const uint8_t priv[PC_IKE_ECDSA_P256_PRIV_LEN],
1795 uint8_t *scratch, size_t scratch_cap, const uint8_t *real, size_t real_len,
1796 const uint8_t *nonce, size_t nonce_len, const uint8_t *sk_p, size_t sk_p_len,
1797 const uint8_t *id_body, size_t id_body_len)
1798{
1799 if (!sig || !priv)
1800 {
1801 return false;
1802 }
1803 size_t n = pc_ike_signed_octets(scratch, scratch_cap, real, real_len, nonce, nonce_len, sk_p, sk_p_len, id_body,
1804 id_body_len);
1805 if (n == 0)
1806 {
1807 return false;
1808 }
1809 return pc_ecdsa_p256_sign(sig, scratch, n, priv); // hashes the octets with SHA-256 internally
1810}
1811
1812bool pc_ike_auth_verify_ecdsa_p256(const uint8_t pub[PC_IKE_ECDSA_P256_PUB_LEN],
1813 const uint8_t sig[PC_IKE_ECDSA_P256_SIG_LEN], uint8_t *scratch, size_t scratch_cap,
1814 const uint8_t *real, size_t real_len, const uint8_t *nonce, size_t nonce_len,
1815 const uint8_t *sk_p, size_t sk_p_len, const uint8_t *id_body, size_t id_body_len)
1816{
1817 if (!pub || !sig)
1818 {
1819 return false;
1820 }
1821 size_t n = pc_ike_signed_octets(scratch, scratch_cap, real, real_len, nonce, nonce_len, sk_p, sk_p_len, id_body,
1822 id_body_len);
1823 if (n == 0)
1824 {
1825 return false;
1826 }
1827 return pc_ecdsa_p256_verify(pub, scratch, n, sig);
1828}
1829
1830// ── tier 2: IKE SA context + key material from a completed IKE_SA_INIT ──────────────────────────
1831
1832bool pc_ike_suite_keylengths(const IkeSuite *suite, IkeKeyLengths *out)
1833{
1834 if (!suite || !out)
1835 {
1836 return false;
1837 }
1838 if (suite->prf != IKE_PRF_HMAC_SHA2_256) // the only PRF the key schedule implements
1839 {
1840 return false;
1841 }
1842
1843 out->sk_d = PC_IKE_PRF_LEN; // the PRF key length
1844 out->sk_p = PC_IKE_PRF_LEN;
1845
1846 // Integrity: an AEAD cipher (integ == 0) has no separate SK_ai/SK_ar; HMAC-SHA2-256-128 uses a 32-byte key.
1847 if (suite->integ == 0)
1848 {
1849 out->sk_a = 0;
1850 }
1851 else if (suite->integ == IKE_INTEG_HMAC_SHA2_256_128)
1852 {
1853 out->sk_a = 32;
1854 }
1855 else
1856 {
1857 return false;
1858 }
1859
1860 // Encryption: the key in bytes, plus a 4-byte salt for AES-GCM (RFC 5282).
1861 if (suite->encr_keylen <= 0 || (suite->encr_keylen % 8) != 0)
1862 {
1863 return false;
1864 }
1865 size_t ek = (size_t)(suite->encr_keylen / 8);
1866 if (suite->encr == IKE_ENCR_AES_GCM_16)
1867 {
1868 ek += PC_IKE_GCM_SALT_LEN;
1869 }
1870 if (ek == 0 || ek > PC_IKE_SK_MAX)
1871 {
1872 return false;
1873 }
1874 out->sk_e = ek;
1875 return true;
1876}
1877
1878bool pc_ike_sa_keys_from_init(IkeSa *sa, const uint8_t *our_dh_priv, size_t our_dh_priv_len, const uint8_t *peer_ke,
1879 size_t peer_ke_len, const uint8_t *ni, size_t ni_len, const uint8_t *nr, size_t nr_len)
1880{
1881 if (!sa || !our_dh_priv || !peer_ke || !ni || !nr)
1882 {
1883 return false;
1884 }
1885 IkeKeyLengths lens;
1886 if (!pc_ike_suite_keylengths(&sa->suite, &lens))
1887 {
1888 return false;
1889 }
1890
1891 uint8_t shared[PC_IKE_X25519_LEN]; // the only supported group (31) yields a 32-byte secret
1892 size_t sh =
1893 pc_ike_dh_compute(sa->suite.dh, our_dh_priv, our_dh_priv_len, peer_ke, peer_ke_len, shared, sizeof(shared));
1894 if (sh == 0)
1895 {
1896 return false;
1897 }
1898 // SKEYSEED + SK_* are order-independent in the SPIs/nonces, so both peers derive identical keys.
1899 return pc_ike_derive_keys(shared, sh, ni, ni_len, nr, nr_len, sa->init_spi, sa->resp_spi, &lens, &sa->keys);
1900}
1901
1902// ── tier 2: initiator IKE_SA_INIT handshake driver ─────────────────────────────────────────────
1903
1904size_t pc_ike_initiator_start(IkeHandshake *hs, const uint8_t our_spi[PC_IKE_SPI_LEN],
1905 const uint8_t our_dh_priv[PC_IKE_X25519_LEN], const uint8_t our_dh_pub[PC_IKE_X25519_LEN],
1906 const uint8_t *our_nonce, size_t nonce_len, const IkeSuite *suite,
1907 const IkeTransform *transforms, uint8_t num_transforms, uint8_t *out, size_t out_cap)
1908{
1909 if (!hs || !our_spi || !our_dh_priv || !our_dh_pub || !our_nonce || !suite || !transforms || num_transforms == 0)
1910 {
1911 return 0;
1912 }
1913 if (nonce_len == 0 || nonce_len > PC_IKE_NONCE_MAX)
1914 {
1915 return 0;
1916 }
1917
1918 memset(hs, 0, sizeof(*hs));
1919 memcpy(hs->sa.init_spi, our_spi, PC_IKE_SPI_LEN); // resp_spi stays 0 until the response
1920 hs->sa.is_initiator = true;
1921 hs->sa.suite = *suite;
1922 memcpy(hs->our_dh_priv, our_dh_priv, PC_IKE_X25519_LEN);
1923 memcpy(hs->our_nonce, our_nonce, nonce_len);
1924 hs->our_nonce_len = (uint16_t)nonce_len;
1925
1926 uint8_t zero_spi[PC_IKE_SPI_LEN] = {0};
1927 size_t n = pc_ike_sa_init_build(out, out_cap, our_spi, zero_spi, 0, /*is_response=*/false, 1, transforms,
1928 num_transforms, suite->dh, our_dh_pub, PC_IKE_X25519_LEN, our_nonce, nonce_len);
1929 if (n == 0 || n > PC_IKE_MSG_MAX) // RealMessage1 must fit the stored copy (needed for the AUTH octets)
1930 {
1931 hs->state = IkeState::IKE_ST_FAILED;
1932 return 0;
1933 }
1934 memcpy(hs->init_msg, out, n);
1935 hs->init_msg_len = (uint16_t)n;
1936 hs->state = IkeState::IKE_ST_SA_INIT_SENT;
1937 return n;
1938}
1939
1940bool pc_ike_initiator_on_sa_init(IkeHandshake *hs, const uint8_t *resp, size_t resp_len)
1941{
1942 if (!hs || !resp || hs->state != IkeState::IKE_ST_SA_INIT_SENT)
1943 {
1944 return false;
1945 }
1946
1947 IkeSaInitMsg m;
1948 if (!pc_ike_sa_init_parse(resp, resp_len, &m))
1949 {
1950 hs->state = IkeState::IKE_ST_FAILED;
1951 return false;
1952 }
1953 // It must be a RESPONSE echoing our initiator SPI, offering the group we proposed.
1954 if (!m.is_response || memcmp(m.init_spi, hs->sa.init_spi, PC_IKE_SPI_LEN) != 0 || m.dh_group != hs->sa.suite.dh)
1955 {
1956 hs->state = IkeState::IKE_ST_FAILED;
1957 return false;
1958 }
1959 memcpy(hs->sa.resp_spi, m.resp_spi, PC_IKE_SPI_LEN);
1960
1961 // Capture Nr for the IKE_AUTH octets (must fit our bounded store).
1962 if (m.nonce_len == 0 || m.nonce_len > PC_IKE_NONCE_MAX)
1963 {
1964 hs->state = IkeState::IKE_ST_FAILED;
1965 return false;
1966 }
1967 memcpy(hs->peer_nonce, m.nonce, m.nonce_len);
1968 hs->peer_nonce_len = (uint16_t)m.nonce_len;
1969
1970 // Stash RealMessage2 (the responder's IKE_SA_INIT) - the responder's AUTH later signs over it.
1971 if (resp_len > PC_IKE_MSG_MAX)
1972 {
1973 hs->state = IkeState::IKE_ST_FAILED;
1974 return false;
1975 }
1976 memcpy(hs->resp_msg, resp, resp_len);
1977 hs->resp_msg_len = (uint16_t)resp_len;
1978
1979 // Derive the SA keys: for the initiator, Ni = ours, Nr = the responder's.
1980 if (!pc_ike_sa_keys_from_init(&hs->sa, hs->our_dh_priv, PC_IKE_X25519_LEN, m.ke_data, m.ke_len, hs->our_nonce,
1981 hs->our_nonce_len, m.nonce, m.nonce_len))
1982 {
1983 hs->state = IkeState::IKE_ST_FAILED;
1984 return false;
1985 }
1986 hs->state = IkeState::IKE_ST_SA_INIT_DONE;
1987 return true;
1988}
1989
1990size_t pc_ike_initiator_build_auth_psk(IkeHandshake *hs, IkeIdType idi_type, const uint8_t *idi_data, size_t idi_len,
1991 const uint8_t *psk, size_t psk_len, const uint8_t iv[PC_IKE_GCM_IV_LEN],
1992 uint8_t *out, size_t out_cap)
1993{
1994 if (!hs || !idi_data || !psk || !iv || !out)
1995 {
1996 return 0;
1997 }
1998 if (hs->state != IkeState::IKE_ST_SA_INIT_DONE)
1999 {
2000 return 0;
2001 }
2002
2003 // Build the inner chain IDi(next=AUTH) | AUTH(next=PC_NONE) into a scratch buffer.
2004 uint8_t inner[PC_IKE_MSG_MAX];
2005 size_t idn = pc_ike_id_build(inner, sizeof(inner), IkePayloadType::IKE_PL_AUTH, idi_type, idi_data, idi_len);
2006 if (idn == 0)
2007 {
2008 return 0;
2009 }
2010 // AUTH signs the IDi payload BODY (RestOfIDPayload = the payload minus its 4-byte generic header).
2011 const uint8_t *idi_body = inner + PC_IKE_PAYLOAD_HDR_LEN;
2012 size_t idi_body_len = idn - PC_IKE_PAYLOAD_HDR_LEN;
2013 uint8_t auth[PC_IKE_AUTH_LEN];
2014 if (!pc_ike_auth_psk(psk, psk_len, hs->init_msg, hs->init_msg_len, hs->peer_nonce, hs->peer_nonce_len,
2015 hs->sa.keys.sk_pi, hs->sa.keys.sk_p_len, idi_body, idi_body_len, auth))
2016 {
2017 return 0;
2018 }
2019 size_t an = pc_ike_auth_build(inner + idn, sizeof(inner) - idn, IkePayloadType::IKE_PL_NONE,
2020 IkeAuthMethod::IKE_AUTH_PSK, auth, sizeof(auth));
2021 if (an == 0)
2022 {
2023 return 0;
2024 }
2025
2026 // Wrap IDi | AUTH in the SK envelope: SK_ei = the 32-byte AES key || its 4-byte GCM salt (RFC 5282).
2027 size_t n = pc_ike_auth_msg_build(out, out_cap, hs->sa.init_spi, hs->sa.resp_spi, 1, /*is_response=*/false,
2028 IkePayloadType::IKE_PL_IDI, inner, idn + an, hs->sa.keys.sk_ei,
2029 hs->sa.keys.sk_ei + PC_IKE_AEAD_KEY_LEN, iv);
2030 if (n == 0)
2031 {
2032 return 0;
2033 }
2034 hs->state = IkeState::IKE_ST_AUTH_SENT;
2035 return n;
2036}
2037
2038bool pc_ike_initiator_on_auth_psk(IkeHandshake *hs, const uint8_t *resp, size_t resp_len, const uint8_t *psk,
2039 size_t psk_len)
2040{
2041 if (!hs || !resp || !psk || hs->state != IkeState::IKE_ST_AUTH_SENT)
2042 {
2043 return false;
2044 }
2045 if (resp_len == 0 || resp_len > PC_IKE_MSG_MAX)
2046 {
2047 hs->state = IkeState::IKE_ST_FAILED;
2048 return false;
2049 }
2050
2051 // Decrypt SK{ IDr | AUTH } into a scratch copy (open mutates the buffer) with SK_er (responder->initiator).
2052 uint8_t work[PC_IKE_MSG_MAX];
2053 memcpy(work, resp, resp_len);
2054 IkePayloadType first = IkePayloadType::IKE_PL_NONE;
2055 const uint8_t *inner = nullptr;
2056 size_t inner_len = 0;
2057 if (!pc_ike_auth_msg_open(work, resp_len, hs->sa.keys.sk_er, hs->sa.keys.sk_er + PC_IKE_AEAD_KEY_LEN, &first,
2058 &inner, &inner_len))
2059 {
2060 hs->state = IkeState::IKE_ST_FAILED;
2061 return false;
2062 }
2063
2064 // Locate the IDr + AUTH payloads in the decrypted inner chain.
2065 IkePayloadIter it;
2066 pc_ike_payload_iter_init(&it, first, inner, inner_len);
2067 IkePayload pl;
2068 const uint8_t *idr_body = nullptr, *auth_body = nullptr;
2069 size_t idr_body_len = 0, auth_body_len = 0;
2070 while (pc_ike_payload_next(&it, &pl))
2071 {
2072 if (pl.type == IkePayloadType::IKE_PL_IDR && !idr_body)
2073 {
2074 idr_body = pl.body;
2075 idr_body_len = pl.body_len;
2076 }
2077 else if (pl.type == IkePayloadType::IKE_PL_AUTH && !auth_body)
2078 {
2079 auth_body = pl.body;
2080 auth_body_len = pl.body_len;
2081 }
2082 }
2083 IkeAuthMethod method = IkeAuthMethod::IKE_AUTH_RESERVED;
2084 const uint8_t *authdata = nullptr;
2085 size_t authdata_len = 0;
2086 if (!idr_body || !auth_body || !pc_ike_auth_parse(auth_body, auth_body_len, &method, &authdata, &authdata_len) ||
2087 method != IkeAuthMethod::IKE_AUTH_PSK || authdata_len != PC_IKE_AUTH_LEN)
2088 {
2089 hs->state = IkeState::IKE_ST_FAILED;
2090 return false;
2091 }
2092
2093 // Recompute the responder's AUTH = prf(prf(PSK,pad), RealMessage2 | Ni | prf(SK_pr, IDr')).
2094 uint8_t expect[PC_IKE_AUTH_LEN];
2095 if (!pc_ike_auth_psk(psk, psk_len, hs->resp_msg, hs->resp_msg_len, hs->our_nonce, hs->our_nonce_len,
2096 hs->sa.keys.sk_pr, hs->sa.keys.sk_p_len, idr_body, idr_body_len, expect))
2097 {
2098 hs->state = IkeState::IKE_ST_FAILED;
2099 return false;
2100 }
2101 // Constant-time compare (no early-out on the identity proof).
2102 uint8_t diff = 0;
2103 for (size_t i = 0; i < PC_IKE_AUTH_LEN; i++)
2104 {
2105 diff |= (uint8_t)(expect[i] ^ authdata[i]);
2106 }
2107 if (diff != 0)
2108 {
2109 hs->state = IkeState::IKE_ST_FAILED;
2110 return false;
2111 }
2112
2113 hs->state = IkeState::IKE_ST_ESTABLISHED;
2114 return true;
2115}
2116
2117// ── responder IKE_SA_INIT handshake driver ─────────────────────────────────────────────────────
2118
2119size_t pc_ike_responder_on_sa_init(IkeHandshake *hs, const uint8_t *req, size_t req_len,
2120 const uint8_t our_spi[PC_IKE_SPI_LEN], const uint8_t our_dh_priv[PC_IKE_X25519_LEN],
2121 const uint8_t our_dh_pub[PC_IKE_X25519_LEN], const uint8_t *our_nonce,
2122 size_t nonce_len, const IkeSuite *suite, const IkeTransform *transforms,
2123 uint8_t num_transforms, uint8_t *out, size_t out_cap)
2124{
2125 if (!hs || !req || !our_spi || !our_dh_priv || !our_dh_pub || !our_nonce || !suite || !transforms ||
2126 num_transforms == 0)
2127 {
2128 return 0;
2129 }
2130 if (nonce_len == 0 || nonce_len > PC_IKE_NONCE_MAX || req_len > PC_IKE_MSG_MAX)
2131 {
2132 return 0;
2133 }
2134
2135 IkeSaInitMsg m;
2136 if (!pc_ike_sa_init_parse(req, req_len, &m))
2137 {
2138 return 0;
2139 }
2140 // Must be a REQUEST (not a response) offering the group we accept; the nonce must fit our store.
2141 if (m.is_response || m.dh_group != suite->dh || m.nonce_len == 0 || m.nonce_len > PC_IKE_NONCE_MAX)
2142 {
2143 return 0;
2144 }
2145
2146 memset(hs, 0, sizeof(*hs));
2147 hs->sa.is_initiator = false;
2148 hs->sa.suite = *suite;
2149 memcpy(hs->sa.init_spi, m.init_spi, PC_IKE_SPI_LEN); // the initiator's SPI, echoed
2150 memcpy(hs->sa.resp_spi, our_spi, PC_IKE_SPI_LEN); // our chosen responder SPI
2151 memcpy(hs->our_dh_priv, our_dh_priv, PC_IKE_X25519_LEN);
2152 memcpy(hs->our_nonce, our_nonce, nonce_len); // Nr
2153 hs->our_nonce_len = (uint16_t)nonce_len;
2154 memcpy(hs->peer_nonce, m.nonce, m.nonce_len); // Ni
2155 hs->peer_nonce_len = (uint16_t)m.nonce_len;
2156 memcpy(hs->init_msg, req, req_len); // RealMessage1 = the request
2157 hs->init_msg_len = (uint16_t)req_len;
2158
2159 // Emit the IKE_SA_INIT response (echo the initiator SPI, add ours, our KE + nonce).
2160 size_t n = pc_ike_sa_init_build(out, out_cap, m.init_spi, our_spi, 0, /*is_response=*/true, 1, transforms,
2161 num_transforms, suite->dh, our_dh_pub, PC_IKE_X25519_LEN, our_nonce, nonce_len);
2162 if (n == 0 || n > PC_IKE_MSG_MAX)
2163 {
2164 hs->state = IkeState::IKE_ST_FAILED;
2165 return 0;
2166 }
2167 memcpy(hs->resp_msg, out, n); // RealMessage2 = our response
2168 hs->resp_msg_len = (uint16_t)n;
2169
2170 // Derive the SA keys: Ni = the initiator's nonce (peer), Nr = ours.
2171 if (!pc_ike_sa_keys_from_init(&hs->sa, our_dh_priv, PC_IKE_X25519_LEN, m.ke_data, m.ke_len, hs->peer_nonce,
2172 hs->peer_nonce_len, hs->our_nonce, hs->our_nonce_len))
2173 {
2174 hs->state = IkeState::IKE_ST_FAILED;
2175 return 0;
2176 }
2177 hs->state = IkeState::IKE_ST_SA_INIT_DONE;
2178 return n;
2179}
2180
2181// Constant-time 32-byte equality (no early-out on an identity proof).
2182static bool ike_ct_eq32(const uint8_t *a, const uint8_t *b)
2183{
2184 uint8_t diff = 0;
2185 for (size_t i = 0; i < PC_IKE_AUTH_LEN; i++)
2186 {
2187 diff |= (uint8_t)(a[i] ^ b[i]);
2188 }
2189 return diff == 0;
2190}
2191
2192// Find the IDi/IDr and AUTH payloads in a decrypted inner chain; returns false if either is missing.
2193static bool ike_find_id_auth(IkePayloadType first, const uint8_t *inner, size_t inner_len, IkePayloadType id_type,
2194 const uint8_t **id_body, size_t *id_body_len, const uint8_t **authdata,
2195 size_t *authdata_len)
2196{
2197 IkePayloadIter it;
2198 pc_ike_payload_iter_init(&it, first, inner, inner_len);
2199 IkePayload pl;
2200 const uint8_t *auth_body = nullptr;
2201 size_t auth_body_len = 0;
2202 *id_body = nullptr;
2203 *id_body_len = 0;
2204 while (pc_ike_payload_next(&it, &pl))
2205 {
2206 if (pl.type == id_type && !*id_body)
2207 {
2208 *id_body = pl.body;
2209 *id_body_len = pl.body_len;
2210 }
2211 else if (pl.type == IkePayloadType::IKE_PL_AUTH && !auth_body)
2212 {
2213 auth_body = pl.body;
2214 auth_body_len = pl.body_len;
2215 }
2216 }
2217 IkeAuthMethod method = IkeAuthMethod::IKE_AUTH_RESERVED;
2218 if (!*id_body || !auth_body || !pc_ike_auth_parse(auth_body, auth_body_len, &method, authdata, authdata_len) ||
2219 method != IkeAuthMethod::IKE_AUTH_PSK || *authdata_len != PC_IKE_AUTH_LEN)
2220 {
2221 return false;
2222 }
2223 return true;
2224}
2225
2226size_t pc_ike_responder_on_auth_psk(IkeHandshake *hs, const uint8_t *req, size_t req_len, const uint8_t *psk,
2227 size_t psk_len, IkeIdType idr_type, const uint8_t *idr_data, size_t idr_len,
2228 const uint8_t iv[PC_IKE_GCM_IV_LEN], uint8_t *out, size_t out_cap)
2229{
2230 if (!hs || !req || !psk || !idr_data || !iv || !out)
2231 {
2232 return 0;
2233 }
2234 if (hs->state != IkeState::IKE_ST_SA_INIT_DONE || hs->sa.is_initiator)
2235 {
2236 return 0;
2237 }
2238 if (req_len == 0 || req_len > PC_IKE_MSG_MAX)
2239 {
2240 hs->state = IkeState::IKE_ST_FAILED;
2241 return 0;
2242 }
2243
2244 // Decrypt the initiator's SK{ IDi | AUTH } with SK_ei (initiator->responder).
2245 uint8_t work[PC_IKE_MSG_MAX];
2246 memcpy(work, req, req_len);
2247 IkePayloadType first = IkePayloadType::IKE_PL_NONE;
2248 const uint8_t *inner = nullptr;
2249 size_t inner_len = 0;
2250 const uint8_t *idi_body = nullptr, *authdata = nullptr;
2251 size_t idi_body_len = 0, authdata_len = 0;
2252 if (!pc_ike_auth_msg_open(work, req_len, hs->sa.keys.sk_ei, hs->sa.keys.sk_ei + PC_IKE_AEAD_KEY_LEN, &first, &inner,
2253 &inner_len) ||
2254 !ike_find_id_auth(first, inner, inner_len, IkePayloadType::IKE_PL_IDI, &idi_body, &idi_body_len, &authdata,
2255 &authdata_len))
2256 {
2257 hs->state = IkeState::IKE_ST_FAILED;
2258 return 0;
2259 }
2260
2261 // Verify the initiator's AUTH over RealMessage1 | Nr(our_nonce) | prf(SK_pi, IDi').
2262 uint8_t expect[PC_IKE_AUTH_LEN];
2263 if (!pc_ike_auth_psk(psk, psk_len, hs->init_msg, hs->init_msg_len, hs->our_nonce, hs->our_nonce_len,
2264 hs->sa.keys.sk_pi, hs->sa.keys.sk_p_len, idi_body, idi_body_len, expect) ||
2265 !ike_ct_eq32(expect, authdata))
2266 {
2267 hs->state = IkeState::IKE_ST_FAILED;
2268 return 0;
2269 }
2270
2271 // Build our IDr | AUTH: AUTH over RealMessage2 | Ni(peer_nonce) | prf(SK_pr, IDr').
2272 uint8_t rinner[PC_IKE_MSG_MAX];
2273 size_t ridn = pc_ike_id_build(rinner, sizeof(rinner), IkePayloadType::IKE_PL_AUTH, idr_type, idr_data, idr_len);
2274 if (ridn == 0)
2275 {
2276 hs->state = IkeState::IKE_ST_FAILED;
2277 return 0;
2278 }
2279 uint8_t rauth[PC_IKE_AUTH_LEN];
2280 if (!pc_ike_auth_psk(psk, psk_len, hs->resp_msg, hs->resp_msg_len, hs->peer_nonce, hs->peer_nonce_len,
2281 hs->sa.keys.sk_pr, hs->sa.keys.sk_p_len, rinner + PC_IKE_PAYLOAD_HDR_LEN,
2282 ridn - PC_IKE_PAYLOAD_HDR_LEN, rauth))
2283 {
2284 hs->state = IkeState::IKE_ST_FAILED;
2285 return 0;
2286 }
2287 size_t ran = pc_ike_auth_build(rinner + ridn, sizeof(rinner) - ridn, IkePayloadType::IKE_PL_NONE,
2288 IkeAuthMethod::IKE_AUTH_PSK, rauth, sizeof(rauth));
2289 if (ran == 0)
2290 {
2291 hs->state = IkeState::IKE_ST_FAILED;
2292 return 0;
2293 }
2294 // Wrap IDr | AUTH in SK{} keyed by SK_er (responder->initiator).
2295 size_t n = pc_ike_auth_msg_build(out, out_cap, hs->sa.init_spi, hs->sa.resp_spi, 1, /*is_response=*/true,
2296 IkePayloadType::IKE_PL_IDR, rinner, ridn + ran, hs->sa.keys.sk_er,
2297 hs->sa.keys.sk_er + PC_IKE_AEAD_KEY_LEN, iv);
2298 if (n == 0)
2299 {
2300 hs->state = IkeState::IKE_ST_FAILED;
2301 return 0;
2302 }
2303 hs->state = IkeState::IKE_ST_ESTABLISHED;
2304 return n;
2305}
2306
2307// ── tier 2: post-auth exchanges (INFORMATIONAL / CREATE_CHILD_SA) over an established SA ─────────
2308
2309// Build an SK-encrypted message we are SENDING over @p sa: egress-keyed (SK_ei from the original
2310// initiator, SK_er from the responder), the flags carrying INITIATOR/RESPONSE independently.
2311static size_t sk_send_build(const IkeSa *sa, bool is_response, uint32_t msg_id, IkeExchange exchange,
2312 IkePayloadType first_inner_type, const uint8_t *inner, size_t inner_len, const uint8_t *iv,
2313 uint8_t *out, size_t out_cap)
2314{
2315 if (!sa || !iv || !out)
2316 {
2317 return 0;
2318 }
2319 const uint8_t *key = sa->is_initiator ? sa->keys.sk_ei : sa->keys.sk_er;
2320 uint8_t flags =
2321 (uint8_t)((sa->is_initiator ? PC_IKE_FLAG_INITIATOR : 0) | (is_response ? PC_IKE_FLAG_RESPONSE : 0));
2322 return sk_message_build(out, out_cap, sa->init_spi, sa->resp_spi, msg_id, exchange, flags, first_inner_type, inner,
2323 inner_len, key, key + PC_IKE_AEAD_KEY_LEN, iv);
2324}
2325
2326size_t pc_ike_informational_build(const IkeSa *sa, bool is_response, uint32_t msg_id, IkePayloadType first_inner_type,
2327 const uint8_t *inner, size_t inner_len, const uint8_t iv[PC_IKE_GCM_IV_LEN],
2328 uint8_t *out, size_t out_cap)
2329{
2330 return sk_send_build(sa, is_response, msg_id, IkeExchange::IKE_INFORMATIONAL, first_inner_type, inner, inner_len,
2331 iv, out, out_cap);
2332}
2333
2334size_t pc_ike_create_child_sa_build(const IkeSa *sa, bool is_response, uint32_t msg_id, IkePayloadType first_inner_type,
2335 const uint8_t *inner, size_t inner_len, const uint8_t iv[PC_IKE_GCM_IV_LEN],
2336 uint8_t *out, size_t out_cap)
2337{
2338 return sk_send_build(sa, is_response, msg_id, IkeExchange::IKE_CREATE_CHILD_SA, first_inner_type, inner, inner_len,
2339 iv, out, out_cap);
2340}
2341
2342bool pc_ike_child_keymat(const uint8_t *sk_d, size_t sk_d_len, const uint8_t *dh_secret, size_t dh_len,
2343 const uint8_t *ni, size_t ni_len, const uint8_t *nr, size_t nr_len, uint8_t *out,
2344 size_t out_len)
2345{
2346 if (!sk_d || !ni || !nr || !out || out_len == 0)
2347 {
2348 return false;
2349 }
2350 if (ni_len > PC_IKE_NONCE_MAX || nr_len > PC_IKE_NONCE_MAX || dh_len > PC_IKE_X25519_LEN)
2351 {
2352 return false;
2353 }
2354
2355 // KEYMAT = prf+(SK_d, [g^ir |] Ni | Nr) (RFC 7296 §2.17); g^ir is present only for a PFS rekey.
2356 uint8_t seed[PC_IKE_X25519_LEN + 2 * PC_IKE_NONCE_MAX];
2357 size_t o = 0;
2358 if (dh_secret && dh_len)
2359 {
2360 memcpy(seed, dh_secret, dh_len);
2361 o = dh_len;
2362 }
2363 memcpy(seed + o, ni, ni_len);
2364 o += ni_len;
2365 memcpy(seed + o, nr, nr_len);
2366 o += nr_len;
2367 return pc_ike_prf_plus(sk_d, sk_d_len, seed, o, out, out_len);
2368}
2369
2370bool pc_ike_informational_open(const IkeSa *sa, uint8_t *msg, size_t len, IkePayloadType *first_inner_type,
2371 const uint8_t **inner_out, size_t *inner_len_out)
2372{
2373 if (!sa)
2374 {
2375 return false;
2376 }
2377 // Ingress direction is the peer's egress: SK_er if the peer is the responder (we are the initiator), else SK_ei.
2378 const uint8_t *key = sa->is_initiator ? sa->keys.sk_er : sa->keys.sk_ei;
2379 return pc_ike_auth_msg_open(msg, len, key, key + PC_IKE_AEAD_KEY_LEN, first_inner_type, inner_out, inner_len_out);
2380}
2381
2382// ── tier 2: IKE_AUTH RSA-2048 (certificate) verify (RFC 7296 §2.15, RFC 7427) ───────────────────
2383
2384bool pc_ike_auth_verify_rsa_sha256(const uint8_t *n_be, const uint8_t *e_be4, const uint8_t *sig, size_t sig_len,
2385 uint8_t *scratch, size_t scratch_cap, const uint8_t *real, size_t real_len,
2386 const uint8_t *nonce, size_t nonce_len, const uint8_t *sk_p, size_t sk_p_len,
2387 const uint8_t *id_body, size_t id_body_len)
2388{
2389 if (!n_be || !e_be4 || !sig)
2390 {
2391 return false;
2392 }
2393 size_t n = pc_ike_signed_octets(scratch, scratch_cap, real, real_len, nonce, nonce_len, sk_p, sk_p_len, id_body,
2394 id_body_len);
2395 if (n == 0)
2396 {
2397 return false;
2398 }
2399 // The device signs with its own ECDSA key; this verifies a PEER whose CERT is RSA-2048 (SHA-256).
2400 return pc_rsa_verify(n_be, e_be4, scratch, n, sig, sig_len, pc_rsa_hash::SHA256) == 0;
2401}
2402
2403#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
void pc_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[32])
X25519 scalar multiplication: out = scalar * point (RFC 7748 §5).
void pc_x25519_base(uint8_t out[32], const uint8_t scalar[32])
X25519 with the standard base point u=9: out = scalar * G.
Curve25519 field arithmetic + X25519 (RFC 7748) for the curve25519-sha256 KEX.
bool pc_ecdsa_p256_verify(const uint8_t pub[PC_ECDSA_P256_PUB_LEN], const uint8_t *msg, size_t mlen, const uint8_t sig[PC_ECDSA_P256_SIG_LEN])
Verify a P-256 ECDSA signature (SHA-256) against an uncompressed public point.
Definition ecdsa.cpp:159
bool pc_ecdsa_p256_sign(uint8_t sig[PC_ECDSA_P256_SIG_LEN], const uint8_t *msg, size_t mlen, const uint8_t priv[PC_ECDSA_P256_PRIV_LEN])
Sign mlen bytes of msg with a P-256 private key (ECDSA, SHA-256).
Definition ecdsa.cpp:127
NIST P-256 primitives for SSH: ECDSA signatures and ECDH (RFC 5656 / FIPS 186-4).
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
IKEv2 (RFC 7296) message + payload codec (PC_ENABLE_IKEV2) - a zero-heap builder / parser for the Int...
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
int pc_rsa_verify(const uint8_t n_be[PC_RSA_KEY_BYTES], const uint8_t e_be4[4], const uint8_t *msg, size_t msg_len, const uint8_t *sig, size_t sig_len, pc_rsa_hash hash)
Verify an RSA-2048 PKCS#1 v1.5 signature over msg.
Definition rsa.cpp:55
RSA-2048 PKCS#1 v1.5 signature primitive (RFC 8017) - verify + software sign.
@ SHA256
RSASSA-PKCS1-v1.5 with SHA-256.
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
SHA-256 (FIPS 180-4) - streaming context and one-shot API.
Streaming HMAC-SHA2-256 context.
Definition hmac_sha256.h:57
Streaming SHA-256 context.
Definition sha256.h:40