ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
snmp_v3.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 pc_snmp_v3.cpp
6 * @brief SNMPv3 USM: message framing, engine discovery, timeliness, auth, privacy.
7 */
8
10
11#if PC_ENABLE_SNMP_V3
12
18#include <string.h>
19
20#if PC_ENABLE_SNMP_TRAP
23#endif
24#if defined(ARDUINO)
25#include <Arduino.h>
26static uint32_t pc_snmp_v3_uptime_s()
27{
28 return (uint32_t)(millis() / 1000ULL);
29}
30#else
31static uint32_t pc_snmp_v3_uptime_s()
32{
33 return 0; // host build: tests drive boots/time via the discovery handshake
34}
35#endif
36
37// usmStats... subtree: 1.3.6.1.6.3.15.1.1.<n>.0
38static const uint32_t kUsmStatsBase[] = {1, 3, 6, 1, 6, 3, 15, 1, 1};
39enum class UsmStat : uint8_t
40{
41 USM_STAT_NOT_IN_TIME = 2,
42 USM_STAT_UNKNOWN_USER = 3,
43 USM_STAT_UNKNOWN_ENGINE = 4,
44 USM_STAT_WRONG_DIGEST = 5,
45 USM_STAT_DECRYPT = 6,
46};
47
48// ---------------------------------------------------------------------------
49// Engine / user state
50// ---------------------------------------------------------------------------
51
52// All SNMPv3 USM engine state, owned by one instance (internal linkage): the engine id/boots,
53// the configured user + localized auth/priv keys, the USM stats counters, and the per-request
54// working buffers (staggered lifetimes; see pc_snmp_v3_process), grouped so it is one named owner,
55// unreachable cross-TU. Single-threaded (the lwIP callback or a test, never reentrant).
56struct SnmpV3Ctx
57{
58 uint8_t engine_id[SNMP_V3_ENGINEID_MAX] = {0x80, 0x00, 0xC0, 0xDE, 0x05, 0x01, 0x02, 0x03, 0x04};
59 size_t engine_id_len = 9;
60 uint32_t boots = 1;
61
62 char user[SNMP_V3_USER_MAX] = "";
63 uint8_t auth_key[SNMP_USM_KEY_LEN];
64 uint8_t priv_key[SNMP_USM_KEY_LEN];
65 bool auth_set = false;
66 bool priv_set = false;
67 uint32_t salt_ctr = 0;
68
69 // USM stats counters (reported in discovery / error Reports)
70 uint32_t stat_unknown_engine = 0;
71 uint32_t stat_unknown_user = 0;
72 uint32_t stat_wrong_digest = 0;
73 uint32_t stat_not_in_time = 0;
74 uint32_t stat_decrypt = 0;
75
76 // Working buffers; lifetimes staggered so they never alias within a single request.
77 uint8_t v3_a[SNMP_MSG_BUF_SIZE]; // auth-verify copy / decrypted scopedPDU
78 uint8_t v3_b[SNMP_MSG_BUF_SIZE]; // inner response PDU
79 uint8_t v3_c[SNMP_MSG_BUF_SIZE]; // outgoing scopedPDU
80 uint8_t v3_d[SNMP_MSG_BUF_SIZE]; // privacy ciphertext
81 uint8_t v3_sec[256]; // msgSecurityParameters scratch
82};
83static SnmpV3Ctx s_v3;
84
85// v3_sec is a fixed 256 bytes but what build_message() packs into it scales with two overridable
86// macros, so pin the worst case at compile time instead of assuming it. The msgSecurityParameters
87// SEQUENCE is: 4 (seq tag + back-patched 3-byte length) + 3 + SNMP_V3_ENGINEID_MAX (engineID
88// OCTET STRING) + 7 + 7 (engineBoots/engineTime INTEGERs, <=5 content octets each) + 3 +
89// SNMP_V3_USER_MAX (userName) + 2 + SNMP_V3_AUTH_PARAM_LEN (authParams) + 2 +
90// SNMP_V3_PRIV_PARAM_LEN (privParams). Raising either max past this turns the "cannot overflow"
91// guard below into a live path, so make that a build error rather than a silent behavior change.
92static_assert(4 + 3 + SNMP_V3_ENGINEID_MAX + 7 + 7 + 3 + SNMP_V3_USER_MAX + 2 + SNMP_V3_AUTH_PARAM_LEN + 2 +
93 SNMP_V3_PRIV_PARAM_LEN <=
94 sizeof(SnmpV3Ctx::v3_sec),
95 "v3_sec is too small for SNMP_V3_ENGINEID_MAX + SNMP_V3_USER_MAX: raise v3_sec or lower the maxima");
96
97// ---------------------------------------------------------------------------
98// Configuration
99// ---------------------------------------------------------------------------
100
101void pc_snmp_v3_init(const uint8_t *engine_id, size_t engine_id_len)
102{
103 if (engine_id && engine_id_len >= 5 && engine_id_len <= SNMP_V3_ENGINEID_MAX)
104 {
105 memcpy(s_v3.engine_id, engine_id, engine_id_len);
106 s_v3.engine_id_len = engine_id_len;
107 }
108 s_v3.user[0] = '\0';
109 s_v3.auth_set = false;
110 s_v3.priv_set = false;
111}
112
113void pc_snmp_v3_set_user(const char *user, const char *auth_pass, const char *priv_pass)
114{
115 strncpy(s_v3.user, user ? user : "", sizeof(s_v3.user) - 1);
116 s_v3.user[sizeof(s_v3.user) - 1] = '\0';
117 s_v3.auth_set = auth_pass && auth_pass[0];
118 s_v3.priv_set = priv_pass && priv_pass[0];
119 if (s_v3.auth_set)
120 {
121 pc_snmp_usm_localize_key(auth_pass, s_v3.engine_id, s_v3.engine_id_len, s_v3.auth_key);
122 }
123 if (s_v3.priv_set)
124 {
125 pc_snmp_usm_localize_key(priv_pass, s_v3.engine_id, s_v3.engine_id_len, s_v3.priv_key);
126 }
127}
128
129void pc_snmp_v3_set_boots(uint32_t boots)
130{
131 s_v3.boots = boots;
132}
133uint32_t pc_snmp_v3_get_boots()
134{
135 return s_v3.boots;
136}
137
138// ---------------------------------------------------------------------------
139// Helpers
140// ---------------------------------------------------------------------------
141
142static bool ct_eq(const uint8_t *a, const uint8_t *b, size_t n)
143{
144 uint8_t r = 0;
145 for (size_t i = 0; i < n; i++)
146 {
147 r |= (uint8_t)(a[i] ^ b[i]);
148 }
149 return r == 0;
150}
151
152// Split a scopedPDU SEQUENCE into its contextName and inner PDU TLV.
153static bool parse_scoped(const uint8_t *buf, size_t len, const uint8_t **ctxname, size_t *ctxname_len,
154 const uint8_t **pdu, size_t *pdu_len)
155{
156 BerDec d;
157 pc_ber_dec_init(&d, buf, len);
158 uint8_t tag;
159 size_t l;
160 if (!pc_ber_read_header(&d, &tag, &l) || tag != (uint8_t)SnmpTag::BER_SEQUENCE)
161 {
162 return false;
163 }
164 if (!pc_ber_read_header(&d, &tag, &l) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING) // contextEngineID
165 {
166 return false;
167 }
168 d.pos += l;
169 if (!pc_ber_read_header(&d, &tag, &l) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING) // contextName
170 {
171 return false;
172 }
173 *ctxname = d.buf + d.pos;
174 *ctxname_len = l;
175 d.pos += l;
176 size_t pdu_start = d.pos;
177 if (!pc_ber_read_header(&d, &tag, &l)) // PDU header (kept)
178 {
179 return false;
180 }
181 *pdu = buf + pdu_start;
182 *pdu_len = (size_t)(d.pos - pdu_start) + l;
183 return d.ok;
184}
185
186// Best-effort read of the inner request-id from a plaintext probe (for Reports).
187static long inner_request_id(const uint8_t *mdata, size_t mdata_len, bool priv)
188{
189 if (priv)
190 {
191 return 0;
192 }
193 const uint8_t *ctxname;
194 const uint8_t *pdu;
195 size_t ctxname_len;
196 size_t pdu_len;
197 if (!parse_scoped(mdata, mdata_len, &ctxname, &ctxname_len, &pdu, &pdu_len))
198 {
199 return 0;
200 }
201 BerDec d;
202 pc_ber_dec_init(&d, pdu, pdu_len);
203 uint8_t tag;
204 size_t l;
205 long rid;
206 // parse_scoped already read this exact header and sized pdu_len as (header bytes + content
207 // length), so re-reading it from the start of pdu always succeeds; only the request-id
208 // INTEGER read below can fail.
209 if (!pc_ber_read_header(&d, &tag, &l) || !pc_ber_read_integer(&d, &rid)) // GCOVR_EXCL_LINE the header
210 {
211 return 0; // re-read cannot fail (see above)
212 }
213 return rid;
214}
215
216// Build a complete v3 message wrapping the already-built scopedPDU bytes.
217static size_t build_message(long msg_id, bool auth, bool priv, const uint8_t *scoped, size_t scoped_len, uint8_t *resp,
218 size_t pc_resp_cap)
219{
220 uint32_t now = pc_snmp_v3_uptime_s();
221 const uint8_t *data_ptr = scoped;
222 size_t data_len = scoped_len;
223 uint8_t salt[SNMP_V3_PRIV_PARAM_LEN];
224
225 if (priv)
226 {
227 pc_wr32be(salt, s_v3.boots);
228 pc_wr32be(salt + 4, ++s_v3.salt_ctr);
229 uint8_t iv[16];
230 pc_wr32be(iv, s_v3.boots);
231 pc_wr32be(iv + 4, now);
232 memcpy(iv + 8, salt, SNMP_V3_PRIV_PARAM_LEN);
233 if (scoped_len > sizeof(s_v3.v3_d)) // GCOVR_EXCL_LINE see below
234 {
235 return 0; // GCOVR_EXCL_LINE scoped is built in v3_c, the same size (SNMP_MSG_BUF_SIZE) as v3_d
236 }
237 pc_snmp_aes128_cfb(s_v3.priv_key, iv, scoped, s_v3.v3_d, scoped_len, true);
238 data_ptr = s_v3.v3_d;
239 }
240
241 // msgSecurityParameters (a SEQUENCE, later wrapped in an OCTET STRING).
242 BerEnc se;
243 pc_ber_enc_init(&se, s_v3.v3_sec, sizeof(s_v3.v3_sec));
244 size_t ss = pc_ber_seq_begin(&se, (uint8_t)SnmpTag::BER_SEQUENCE);
245 pc_ber_put_octet_string(&se, (uint8_t)SnmpTag::BER_OCTET_STRING, s_v3.engine_id, s_v3.engine_id_len);
246 pc_ber_put_integer(&se, (long)s_v3.boots);
247 pc_ber_put_integer(&se, (long)now);
248 pc_ber_put_octet_string(&se, (uint8_t)SnmpTag::BER_OCTET_STRING, (const uint8_t *)s_v3.user,
249 strnlen(s_v3.user, SNMP_V3_USER_MAX));
250 size_t auth_off = 0;
251 if (auth)
252 {
253 auth_off = se.len + 2; // value follows tag + 1-byte length (24 < 128)
254 uint8_t zeros[SNMP_V3_AUTH_PARAM_LEN];
255 memset(zeros, 0, sizeof(zeros));
256 pc_ber_put_octet_string(&se, (uint8_t)SnmpTag::BER_OCTET_STRING, zeros, SNMP_V3_AUTH_PARAM_LEN);
257 }
258 else
259 {
260 pc_ber_put_octet_string(&se, (uint8_t)SnmpTag::BER_OCTET_STRING, nullptr, 0);
261 }
262 if (priv)
263 {
264 pc_ber_put_octet_string(&se, (uint8_t)SnmpTag::BER_OCTET_STRING, salt, SNMP_V3_PRIV_PARAM_LEN);
265 }
266 else
267 {
268 pc_ber_put_octet_string(&se, (uint8_t)SnmpTag::BER_OCTET_STRING, nullptr, 0);
269 }
270 pc_ber_seq_end(&se, ss);
271 if (!se.ok) // GCOVR_EXCL_LINE see below
272 {
273 return 0; // GCOVR_EXCL_LINE the static_assert on v3_sec above makes a secParams overflow a build error,
274 // so se.ok always holds here
275 }
276 size_t sec_len = se.len;
277
278 // Full message.
279 BerEnc e;
280 pc_ber_enc_init(&e, resp, pc_resp_cap);
281 size_t msg = pc_ber_seq_begin(&e, (uint8_t)SnmpTag::BER_SEQUENCE);
282 pc_ber_put_integer(&e, (int)SnmpVersion::SNMP_V3);
283 size_t hdr = pc_ber_seq_begin(&e, (uint8_t)SnmpTag::BER_SEQUENCE);
284 pc_ber_put_integer(&e, msg_id);
285 pc_ber_put_integer(&e, 65507); // msgMaxSize
286 uint8_t fl = (uint8_t)((auth ? 0x01 : 0) | (priv ? 0x02 : 0));
287 pc_ber_put_octet_string(&e, (uint8_t)SnmpTag::BER_OCTET_STRING, &fl, 1);
288 pc_ber_put_integer(&e, 3); // msgSecurityModel = USM
289 pc_ber_seq_end(&e, hdr);
290 pc_ber_put_octet_string(&e, (uint8_t)SnmpTag::BER_OCTET_STRING, s_v3.v3_sec, sec_len);
291 size_t sec_value_pos = e.len - sec_len;
292 if (priv)
293 {
294 pc_ber_put_octet_string(&e, (uint8_t)SnmpTag::BER_OCTET_STRING, data_ptr, data_len);
295 }
296 else
297 {
298 pc_ber_put_raw(&e, data_ptr, data_len);
299 }
300 pc_ber_seq_end(&e, msg);
301 if (!e.ok)
302 {
303 return 0;
304 }
305 size_t total = e.len;
306
307 if (auth)
308 {
309 uint8_t mac[PC_HMAC_SHA256_LEN];
310 pc_hmac_sha256(s_v3.auth_key, SNMP_USM_KEY_LEN, resp, total, mac);
311 memcpy(resp + sec_value_pos + auth_off, mac, SNMP_V3_AUTH_PARAM_LEN);
312 }
313 return total;
314}
315
316// Build a Report PDU (usmStats<stat>.0 = Counter32) and wrap it in a v3 message.
317static size_t build_report(long msg_id, bool auth, uint32_t stat, uint32_t count, long request_id, uint8_t *resp,
318 size_t pc_resp_cap)
319{
320 uint32_t oid[11];
321 for (int i = 0; i < 9; i++)
322 {
323 oid[i] = kUsmStatsBase[i];
324 }
325 oid[9] = stat;
326 oid[10] = 0;
327
328 BerEnc e;
329 pc_ber_enc_init(&e, s_v3.v3_b, sizeof(s_v3.v3_b));
330 size_t pdu = pc_ber_seq_begin(&e, (uint8_t)SnmpTag::SNMP_PDU_REPORT);
331 pc_ber_put_integer(&e, request_id);
332 pc_ber_put_integer(&e, 0);
333 pc_ber_put_integer(&e, 0);
334 size_t vbl = pc_ber_seq_begin(&e, (uint8_t)SnmpTag::BER_SEQUENCE);
335 size_t vb = pc_ber_seq_begin(&e, (uint8_t)SnmpTag::BER_SEQUENCE);
336 pc_ber_put_oid(&e, oid, 11);
337 pc_ber_put_uint(&e, (uint8_t)SnmpTag::SNMP_COUNTER32, count);
338 pc_ber_seq_end(&e, vb);
339 pc_ber_seq_end(&e, vbl);
340 pc_ber_seq_end(&e, pdu);
341 if (!e.ok) // GCOVR_EXCL_LINE see below
342 {
343 return 0; // GCOVR_EXCL_LINE the Report is one fixed usmStats varbind, far under v3_b (SNMP_MSG_BUF_SIZE)
344 }
345
346 BerEnc sc;
347 pc_ber_enc_init(&sc, s_v3.v3_c, sizeof(s_v3.v3_c));
348 size_t s = pc_ber_seq_begin(&sc, (uint8_t)SnmpTag::BER_SEQUENCE);
349 pc_ber_put_octet_string(&sc, (uint8_t)SnmpTag::BER_OCTET_STRING, s_v3.engine_id, s_v3.engine_id_len);
350 pc_ber_put_octet_string(&sc, (uint8_t)SnmpTag::BER_OCTET_STRING, nullptr, 0);
351 pc_ber_put_raw(&sc, s_v3.v3_b, e.len);
352 pc_ber_seq_end(&sc, s);
353 if (!sc.ok) // GCOVR_EXCL_LINE see below
354 {
355 return 0; // GCOVR_EXCL_LINE fixed tiny Report scopedPDU never overflows v3_c (SNMP_MSG_BUF_SIZE)
356 }
357
358 return build_message(msg_id, auth, false, s_v3.v3_c, sc.len, resp, pc_resp_cap);
359}
360
361// ---------------------------------------------------------------------------
362// Message processing
363// ---------------------------------------------------------------------------
364
365size_t pc_snmp_v3_process(const uint8_t *req, size_t req_len, uint8_t *resp, size_t pc_resp_cap)
366{
367 BerDec d;
368 pc_ber_dec_init(&d, req, req_len);
369 uint8_t tag;
370 size_t l;
371 if (!pc_ber_read_header(&d, &tag, &l) || tag != (uint8_t)SnmpTag::BER_SEQUENCE)
372 {
373 return 0;
374 }
375 long version;
376 if (!pc_ber_read_integer(&d, &version) || version != (int)SnmpVersion::SNMP_V3)
377 {
378 return 0;
379 }
380
381 // msgGlobalData
382 if (!pc_ber_read_header(&d, &tag, &l) || tag != (uint8_t)SnmpTag::BER_SEQUENCE)
383 {
384 return 0;
385 }
386 long msg_id;
387 long msg_max;
388 long sec_model;
389 if (!pc_ber_read_integer(&d, &msg_id) || !pc_ber_read_integer(&d, &msg_max))
390 {
391 return 0;
392 }
393 (void)msg_max;
394 size_t flags_len;
395 if (!pc_ber_read_header(&d, &tag, &flags_len) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING || flags_len < 1)
396 {
397 return 0;
398 }
399 uint8_t flags = d.buf[d.pos];
400 d.pos += flags_len;
401 if (!pc_ber_read_integer(&d, &sec_model) || sec_model != 3) // USM only
402 {
403 return 0;
404 }
405 bool req_auth = flags & 0x01;
406 bool req_priv = flags & 0x02;
407 if (req_priv && !req_auth)
408 {
409 return 0;
410 }
411
412 // msgSecurityParameters: OCTET STRING wrapping a SEQUENCE.
413 size_t sec_len;
414 if (!pc_ber_read_header(&d, &tag, &sec_len) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING)
415 {
416 return 0;
417 }
418 const uint8_t *sec = d.buf + d.pos;
419 d.pos += sec_len;
420
421 BerDec sd;
422 pc_ber_dec_init(&sd, sec, sec_len);
423 if (!pc_ber_read_header(&sd, &tag, &l) || tag != (uint8_t)SnmpTag::BER_SEQUENCE)
424 {
425 return 0;
426 }
427 size_t eid_len;
428 if (!pc_ber_read_header(&sd, &tag, &eid_len) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING)
429 {
430 return 0;
431 }
432 const uint8_t *eid = sd.buf + sd.pos;
433 sd.pos += eid_len;
434 long req_boots;
435 long req_time;
436 if (!pc_ber_read_integer(&sd, &req_boots) || !pc_ber_read_integer(&sd, &req_time))
437 {
438 return 0;
439 }
440 size_t uname_len;
441 if (!pc_ber_read_header(&sd, &tag, &uname_len) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING)
442 {
443 return 0;
444 }
445 const uint8_t *uname = sd.buf + sd.pos;
446 sd.pos += uname_len;
447 size_t aparm_len;
448 if (!pc_ber_read_header(&sd, &tag, &aparm_len) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING)
449 {
450 return 0;
451 }
452 const uint8_t *aparm = sd.buf + sd.pos;
453 size_t aparm_off = (size_t)(aparm - req);
454 sd.pos += aparm_len;
455 size_t pparm_len;
456 if (!pc_ber_read_header(&sd, &tag, &pparm_len) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING)
457 {
458 return 0;
459 }
460 const uint8_t *pparm = sd.buf + sd.pos;
461 sd.pos += pparm_len;
462 if (!sd.ok) // GCOVR_EXCL_LINE see below
463 {
464 return 0; // GCOVR_EXCL_LINE redundant: every secParams field read above returns on failure, so sd.ok holds
465 // here
466 }
467
468 const uint8_t *mdata = d.buf + d.pos;
469 size_t mdata_len = req_len - d.pos;
470
471 // Engine discovery: unknown/empty authoritative engine ID.
472 bool engine_match = (eid_len == s_v3.engine_id_len) && (memcmp(eid, s_v3.engine_id, eid_len) == 0);
473 if (!engine_match)
474 {
475 s_v3.stat_unknown_engine++;
476 return build_report(msg_id, false, (int)UsmStat::USM_STAT_UNKNOWN_ENGINE, s_v3.stat_unknown_engine,
477 inner_request_id(mdata, mdata_len, req_priv), resp, pc_resp_cap);
478 }
479
480 if (!req_auth) // non-discovery noAuthNoPriv is not supported
481 {
482 return 0;
483 }
484
485 // Known user?
486 if (!s_v3.auth_set || !(uname_len == strnlen(s_v3.user, SNMP_V3_USER_MAX) && s_v3.user[0] &&
487 memcmp(uname, s_v3.user, uname_len) == 0))
488 {
489 s_v3.stat_unknown_user++;
490 return build_report(msg_id, false, (int)UsmStat::USM_STAT_UNKNOWN_USER, s_v3.stat_unknown_user, 0, resp,
491 pc_resp_cap);
492 }
493
494 // Authenticate: HMAC over the whole message with the auth field zeroed.
495 if (aparm_len != SNMP_V3_AUTH_PARAM_LEN || req_len > sizeof(s_v3.v3_a))
496 {
497 s_v3.stat_wrong_digest++;
498 return build_report(msg_id, false, (int)UsmStat::USM_STAT_WRONG_DIGEST, s_v3.stat_wrong_digest, 0, resp,
499 pc_resp_cap);
500 }
501 memcpy(s_v3.v3_a, req, req_len);
502 memset(s_v3.v3_a + aparm_off, 0, SNMP_V3_AUTH_PARAM_LEN);
503 uint8_t mac[PC_HMAC_SHA256_LEN];
504 pc_hmac_sha256(s_v3.auth_key, SNMP_USM_KEY_LEN, s_v3.v3_a, req_len, mac);
505 if (!ct_eq(mac, aparm, SNMP_V3_AUTH_PARAM_LEN))
506 {
507 s_v3.stat_wrong_digest++;
508 return build_report(msg_id, false, (int)UsmStat::USM_STAT_WRONG_DIGEST, s_v3.stat_wrong_digest, 0, resp,
509 pc_resp_cap);
510 }
511
512 // Timeliness window (RFC 3414 ยง3.2): boots must match, time within +/-150s.
513 uint32_t now = pc_snmp_v3_uptime_s();
514 long dt = (long)now - req_time;
515 if (dt < 0)
516 {
517 dt = -dt;
518 }
519 if ((uint32_t)req_boots != s_v3.boots || dt > 150)
520 {
521 s_v3.stat_not_in_time++;
522 return build_report(msg_id, true, (int)UsmStat::USM_STAT_NOT_IN_TIME, s_v3.stat_not_in_time, 0, resp,
523 pc_resp_cap);
524 }
525
526 // Privacy: decrypt the scopedPDU if the priv flag is set.
527 const uint8_t *scoped;
528 size_t scoped_len;
529 if (req_priv)
530 {
531 if (!s_v3.priv_set || pparm_len != SNMP_V3_PRIV_PARAM_LEN)
532 {
533 s_v3.stat_decrypt++;
534 return build_report(msg_id, true, (int)UsmStat::USM_STAT_DECRYPT, s_v3.stat_decrypt, 0, resp, pc_resp_cap);
535 }
536 BerDec md;
537 pc_ber_dec_init(&md, mdata, mdata_len);
538 if (!pc_ber_read_header(&md, &tag, &l) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING)
539 {
540 return 0;
541 }
542 const uint8_t *ct = md.buf + md.pos;
543 size_t ct_len = l;
544 if (ct_len > sizeof(s_v3.v3_a)) // GCOVR_EXCL_LINE see below
545 {
546 return 0; // GCOVR_EXCL_LINE ct_len <= mdata_len < req_len, and req_len<=sizeof(v3_a) was enforced at the
547 // digest step
548 }
549 uint8_t iv[16];
550 pc_wr32be(iv, (uint32_t)req_boots);
551 pc_wr32be(iv + 4, (uint32_t)req_time);
552 memcpy(iv + 8, pparm, SNMP_V3_PRIV_PARAM_LEN);
553 pc_snmp_aes128_cfb(s_v3.priv_key, iv, ct, s_v3.v3_a, ct_len, false);
554 scoped = s_v3.v3_a;
555 scoped_len = ct_len;
556 }
557 else
558 {
559 scoped = mdata;
560 scoped_len = mdata_len;
561 }
562
563 // Inner PDU -> dispatch (authenticated: writes allowed; v2c-style semantics).
564 const uint8_t *ctxname;
565 const uint8_t *pdu;
566 size_t ctxname_len;
567 size_t pdu_len;
568 if (!parse_scoped(scoped, scoped_len, &ctxname, &ctxname_len, &pdu, &pdu_len))
569 {
570 return 0;
571 }
572 size_t rpdu = pc_snmp_dispatch_pdu(pdu, pdu_len, true, true, s_v3.v3_b, sizeof(s_v3.v3_b));
573 if (rpdu == 0)
574 {
575 return 0;
576 }
577
578 // Response scopedPDU { our engineID, echoed contextName, response PDU }.
579 BerEnc sc;
580 pc_ber_enc_init(&sc, s_v3.v3_c, sizeof(s_v3.v3_c));
581 size_t s = pc_ber_seq_begin(&sc, (uint8_t)SnmpTag::BER_SEQUENCE);
582 pc_ber_put_octet_string(&sc, (uint8_t)SnmpTag::BER_OCTET_STRING, s_v3.engine_id, s_v3.engine_id_len);
583 pc_ber_put_octet_string(&sc, (uint8_t)SnmpTag::BER_OCTET_STRING, ctxname, ctxname_len);
584 pc_ber_put_raw(&sc, s_v3.v3_b, rpdu);
585 pc_ber_seq_end(&sc, s);
586 if (!sc.ok)
587 {
588 return 0;
589 }
590
591 return build_message(msg_id, true, req_priv, s_v3.v3_c, sc.len, resp, pc_resp_cap);
592}
593
594#if PC_ENABLE_SNMP_TRAP
595
596// Shared SNMPv3 USM notification path: authenticated, and encrypted when a
597// privacy password is configured. Reuses the engine ID + localized keys from
598// pc_snmp_v3_init()/pc_snmp_v3_set_user() and the same build_message() as responses.
599// @p pdu_tag selects Trap ((uint8_t)SnmpTag::SNMP_PDU_TRAPV2) vs InformRequest (0xA6); @p request_id
600// is the inner PDU request-id (the inform receiver echoes it in its Response).
601static bool send_v3_notify(const char *dst_ip, uint16_t port, uint8_t pdu_tag, uint32_t request_id,
602 const uint32_t *trap_oid, size_t trap_oid_len, const SnmpVarbind *vbs, size_t n)
603{
604 if (!s_v3.auth_set) // a v3 notification must be authenticated
605 {
606 return false;
607 }
608
609 // Notification PDU into the inner-PDU scratch.
610 BerEnc e;
611 pc_ber_enc_init(&e, s_v3.v3_b, sizeof(s_v3.v3_b));
612 pc_snmp_notify_build_pdu(&e, pdu_tag, request_id, trap_oid, trap_oid_len, pc_snmp_v3_uptime_s() * 100, vbs, n);
613 if (!e.ok)
614 {
615 return false;
616 }
617
618 // scopedPDU { contextEngineID = our engine, contextName = "", PDU }.
619 BerEnc sc;
620 pc_ber_enc_init(&sc, s_v3.v3_c, sizeof(s_v3.v3_c));
621 size_t s = pc_ber_seq_begin(&sc, (uint8_t)SnmpTag::BER_SEQUENCE);
622 pc_ber_put_octet_string(&sc, (uint8_t)SnmpTag::BER_OCTET_STRING, s_v3.engine_id, s_v3.engine_id_len);
623 pc_ber_put_octet_string(&sc, (uint8_t)SnmpTag::BER_OCTET_STRING, nullptr, 0);
624 pc_ber_put_raw(&sc, s_v3.v3_b, e.len);
625 pc_ber_seq_end(&sc, s);
626 if (!sc.ok)
627 {
628 return false;
629 }
630
631 uint8_t out[SNMP_MSG_BUF_SIZE];
632 size_t len = build_message((long)request_id, s_v3.auth_set, s_v3.priv_set, s_v3.v3_c, sc.len, out, sizeof(out));
633 return len && pc_udp_sendto(dst_ip, port, out, len);
634}
635
636bool pc_snmp_trap_v3(const char *dst_ip, uint16_t port, const uint32_t *trap_oid, size_t trap_oid_len,
637 const SnmpVarbind *vbs, size_t n)
638{
639 static uint32_t s_v3_trap_id = 1; // a trap is unconfirmed; the id is informational
640 return send_v3_notify(dst_ip, port, (uint8_t)SnmpTag::SNMP_PDU_TRAPV2, s_v3_trap_id++, trap_oid, trap_oid_len, vbs,
641 n);
642}
643
644// SNMPv3 USM InformRequest (confirmed notification, RFC 3416 4.2.7). Symmetric to
645// pc_snmp_inform_v2c: builds + sends the InformRequest. The caller owns the
646// request_id and, for confirmed delivery, retransmits until the receiver's
647// Response (echoing request_id) arrives.
648bool pc_snmp_inform_v3(const char *dst_ip, uint16_t port, uint32_t request_id, const uint32_t *trap_oid,
649 size_t trap_oid_len, const SnmpVarbind *vbs, size_t n)
650{
651 return send_v3_notify(dst_ip, port, 0xA6 /* InformRequest */, request_id, trap_oid, trap_oid_len, vbs, n);
652}
653#endif // PC_ENABLE_SNMP_TRAP
654
655#endif // PC_ENABLE_SNMP_V3
Fixed-width integer serializers into a raw uint8_t* buffer - one source of truth.
size_t pc_wr32be(uint8_t *p, uint32_t v)
Write v big-endian at p.
Definition endian.h:93
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.
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
#define SNMP_V3_USER_MAX
Maximum SNMPv3 USM user-name length (including null terminator).
#define SNMP_MSG_BUF_SIZE
Static request/response datagram buffers for the SNMP UDP agent.
#define SNMP_V3_ENGINEID_MAX
Maximum SNMPv3 authoritative engine-ID length in bytes (RFC 3411 allows 5..32).
bool pc_udp_sendto(const char *dst_ip, uint16_t dst_port, const uint8_t *data, size_t len)
Send a UDP datagram to an arbitrary destination (outbound client).
Definition udp.cpp:358
Layer 4 (Transport) - connectionless UDP datagram service.