DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 snmp_v3.cpp
6 * @brief SNMPv3 USM: message framing, engine discovery, timeliness, auth, privacy.
7 */
8
10
11#if DETWS_ENABLE_SNMP_V3
12
17#include <string.h>
18
19#if defined(ARDUINO)
20#include <Arduino.h>
21static uint32_t snmp_v3_uptime_s()
22{
23 return (uint32_t)(millis() / 1000ULL);
24}
25#else
26static uint32_t snmp_v3_uptime_s()
27{
28 return 0; // host build: tests drive boots/time via the discovery handshake
29}
30#endif
31
32// usmStats... subtree: 1.3.6.1.6.3.15.1.1.<n>.0
33static const uint32_t kUsmStatsBase[] = {1, 3, 6, 1, 6, 3, 15, 1, 1};
34enum class UsmStat : uint8_t
35{
36 USM_STAT_NOT_IN_TIME = 2,
37 USM_STAT_UNKNOWN_USER = 3,
38 USM_STAT_UNKNOWN_ENGINE = 4,
39 USM_STAT_WRONG_DIGEST = 5,
40 USM_STAT_DECRYPT = 6,
41};
42
43// ---------------------------------------------------------------------------
44// Engine / user state
45// ---------------------------------------------------------------------------
46
47// All SNMPv3 USM engine state, owned by one instance (internal linkage): the engine id/boots,
48// the configured user + localized auth/priv keys, the USM stats counters, and the per-request
49// working buffers (staggered lifetimes; see snmp_v3_process), grouped so it is one named owner,
50// unreachable cross-TU. Single-threaded (the lwIP callback or a test, never reentrant).
51struct SnmpV3Ctx
52{
53 uint8_t engine_id[SNMP_V3_ENGINEID_MAX] = {0x80, 0x00, 0xC0, 0xDE, 0x05, 0x01, 0x02, 0x03, 0x04};
54 size_t engine_id_len = 9;
55 uint32_t boots = 1;
56
57 char user[SNMP_V3_USER_MAX] = "";
58 uint8_t auth_key[SNMP_USM_KEY_LEN];
59 uint8_t priv_key[SNMP_USM_KEY_LEN];
60 bool auth_set = false;
61 bool priv_set = false;
62 uint32_t salt_ctr = 0;
63
64 // USM stats counters (reported in discovery / error Reports)
65 uint32_t stat_unknown_engine = 0;
66 uint32_t stat_unknown_user = 0;
67 uint32_t stat_wrong_digest = 0;
68 uint32_t stat_not_in_time = 0;
69 uint32_t stat_decrypt = 0;
70
71 // Working buffers; lifetimes staggered so they never alias within a single request.
72 uint8_t v3_a[SNMP_MSG_BUF_SIZE]; // auth-verify copy / decrypted scopedPDU
73 uint8_t v3_b[SNMP_MSG_BUF_SIZE]; // inner response PDU
74 uint8_t v3_c[SNMP_MSG_BUF_SIZE]; // outgoing scopedPDU
75 uint8_t v3_d[SNMP_MSG_BUF_SIZE]; // privacy ciphertext
76 uint8_t v3_sec[256]; // msgSecurityParameters scratch
77};
78static SnmpV3Ctx s_v3;
79
80// ---------------------------------------------------------------------------
81// Configuration
82// ---------------------------------------------------------------------------
83
84void snmp_v3_init(const uint8_t *engine_id, size_t engine_id_len)
85{
86 if (engine_id && engine_id_len >= 5 && engine_id_len <= SNMP_V3_ENGINEID_MAX)
87 {
88 memcpy(s_v3.engine_id, engine_id, engine_id_len);
89 s_v3.engine_id_len = engine_id_len;
90 }
91 s_v3.user[0] = '\0';
92 s_v3.auth_set = false;
93 s_v3.priv_set = false;
94}
95
96void snmp_v3_set_user(const char *user, const char *auth_pass, const char *priv_pass)
97{
98 strncpy(s_v3.user, user ? user : "", sizeof(s_v3.user) - 1);
99 s_v3.user[sizeof(s_v3.user) - 1] = '\0';
100 s_v3.auth_set = auth_pass && auth_pass[0];
101 s_v3.priv_set = priv_pass && priv_pass[0];
102 if (s_v3.auth_set)
103 snmp_usm_localize_key(auth_pass, s_v3.engine_id, s_v3.engine_id_len, s_v3.auth_key);
104 if (s_v3.priv_set)
105 snmp_usm_localize_key(priv_pass, s_v3.engine_id, s_v3.engine_id_len, s_v3.priv_key);
106}
107
108void snmp_v3_set_boots(uint32_t boots)
109{
110 s_v3.boots = boots;
111}
112uint32_t snmp_v3_get_boots()
113{
114 return s_v3.boots;
115}
116
117// ---------------------------------------------------------------------------
118// Helpers
119// ---------------------------------------------------------------------------
120
121static bool ct_eq(const uint8_t *a, const uint8_t *b, size_t n)
122{
123 uint8_t r = 0;
124 for (size_t i = 0; i < n; i++)
125 r |= (uint8_t)(a[i] ^ b[i]);
126 return r == 0;
127}
128
129static void put_be32(uint8_t *p, uint32_t v)
130{
131 p[0] = (uint8_t)(v >> 24);
132 p[1] = (uint8_t)(v >> 16);
133 p[2] = (uint8_t)(v >> 8);
134 p[3] = (uint8_t)v;
135}
136
137// Split a scopedPDU SEQUENCE into its contextName and inner PDU TLV.
138static bool parse_scoped(const uint8_t *buf, size_t len, const uint8_t **ctxname, size_t *ctxname_len,
139 const uint8_t **pdu, size_t *pdu_len)
140{
141 BerDec d;
142 ber_dec_init(&d, buf, len);
143 uint8_t tag;
144 size_t l;
145 if (!ber_read_header(&d, &tag, &l) || tag != (uint8_t)SnmpTag::BER_SEQUENCE)
146 return false;
147 if (!ber_read_header(&d, &tag, &l) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING) // contextEngineID
148 return false;
149 d.pos += l;
150 if (!ber_read_header(&d, &tag, &l) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING) // contextName
151 return false;
152 *ctxname = d.buf + d.pos;
153 *ctxname_len = l;
154 d.pos += l;
155 size_t pdu_start = d.pos;
156 if (!ber_read_header(&d, &tag, &l)) // PDU header (kept)
157 return false;
158 *pdu = buf + pdu_start;
159 *pdu_len = (size_t)(d.pos - pdu_start) + l;
160 return d.ok;
161}
162
163// Best-effort read of the inner request-id from a plaintext probe (for Reports).
164static long inner_request_id(const uint8_t *mdata, size_t mdata_len, bool priv)
165{
166 if (priv)
167 return 0;
168 const uint8_t *ctxname;
169 const uint8_t *pdu;
170 size_t ctxname_len;
171 size_t pdu_len;
172 if (!parse_scoped(mdata, mdata_len, &ctxname, &ctxname_len, &pdu, &pdu_len))
173 return 0;
174 BerDec d;
175 ber_dec_init(&d, pdu, pdu_len);
176 uint8_t tag;
177 size_t l;
178 long rid;
179 if (!ber_read_header(&d, &tag, &l) || !ber_read_integer(&d, &rid))
180 return 0;
181 return rid;
182}
183
184// Build a complete v3 message wrapping the already-built scopedPDU bytes.
185static size_t build_message(long msg_id, bool auth, bool priv, const uint8_t *scoped, size_t scoped_len, uint8_t *resp,
186 size_t resp_cap)
187{
188 uint32_t now = snmp_v3_uptime_s();
189 const uint8_t *data_ptr = scoped;
190 size_t data_len = scoped_len;
191 uint8_t salt[SNMP_V3_PRIV_PARAM_LEN];
192
193 if (priv)
194 {
195 put_be32(salt, s_v3.boots);
196 put_be32(salt + 4, ++s_v3.salt_ctr);
197 uint8_t iv[16];
198 put_be32(iv, s_v3.boots);
199 put_be32(iv + 4, now);
200 memcpy(iv + 8, salt, SNMP_V3_PRIV_PARAM_LEN);
201 if (scoped_len > sizeof(s_v3.v3_d))
202 return 0; // GCOVR_EXCL_LINE scoped is built in v3_c, the same size (SNMP_MSG_BUF_SIZE) as v3_d
203 snmp_aes128_cfb(s_v3.priv_key, iv, scoped, s_v3.v3_d, scoped_len, true);
204 data_ptr = s_v3.v3_d;
205 }
206
207 // msgSecurityParameters (a SEQUENCE, later wrapped in an OCTET STRING).
208 BerEnc se;
209 ber_enc_init(&se, s_v3.v3_sec, sizeof(s_v3.v3_sec));
210 size_t ss = ber_seq_begin(&se, (uint8_t)SnmpTag::BER_SEQUENCE);
211 ber_put_octet_string(&se, (uint8_t)SnmpTag::BER_OCTET_STRING, s_v3.engine_id, s_v3.engine_id_len);
212 ber_put_integer(&se, (long)s_v3.boots);
213 ber_put_integer(&se, (long)now);
214 ber_put_octet_string(&se, (uint8_t)SnmpTag::BER_OCTET_STRING, (const uint8_t *)s_v3.user,
215 strnlen(s_v3.user, SNMP_V3_USER_MAX));
216 size_t auth_off = 0;
217 if (auth)
218 {
219 auth_off = se.len + 2; // value follows tag + 1-byte length (24 < 128)
220 uint8_t zeros[SNMP_V3_AUTH_PARAM_LEN];
221 memset(zeros, 0, sizeof(zeros));
222 ber_put_octet_string(&se, (uint8_t)SnmpTag::BER_OCTET_STRING, zeros, SNMP_V3_AUTH_PARAM_LEN);
223 }
224 else
225 {
226 ber_put_octet_string(&se, (uint8_t)SnmpTag::BER_OCTET_STRING, nullptr, 0);
227 }
228 if (priv)
229 ber_put_octet_string(&se, (uint8_t)SnmpTag::BER_OCTET_STRING, salt, SNMP_V3_PRIV_PARAM_LEN);
230 else
231 ber_put_octet_string(&se, (uint8_t)SnmpTag::BER_OCTET_STRING, nullptr, 0);
232 ber_seq_end(&se, ss);
233 if (!se.ok)
234 return 0; // GCOVR_EXCL_LINE secParams (<=~120B: 32B engineID + 32B user + 24B auth + ints) never overflow
235 // v3_sec[256]
236 size_t sec_len = se.len;
237
238 // Full message.
239 BerEnc e;
240 ber_enc_init(&e, resp, resp_cap);
241 size_t msg = ber_seq_begin(&e, (uint8_t)SnmpTag::BER_SEQUENCE);
242 ber_put_integer(&e, (int)SnmpVersion::SNMP_V3);
243 size_t hdr = ber_seq_begin(&e, (uint8_t)SnmpTag::BER_SEQUENCE);
244 ber_put_integer(&e, msg_id);
245 ber_put_integer(&e, 65507); // msgMaxSize
246 uint8_t fl = (uint8_t)((auth ? 0x01 : 0) | (priv ? 0x02 : 0));
247 ber_put_octet_string(&e, (uint8_t)SnmpTag::BER_OCTET_STRING, &fl, 1);
248 ber_put_integer(&e, 3); // msgSecurityModel = USM
249 ber_seq_end(&e, hdr);
250 ber_put_octet_string(&e, (uint8_t)SnmpTag::BER_OCTET_STRING, s_v3.v3_sec, sec_len);
251 size_t sec_value_pos = e.len - sec_len;
252 if (priv)
253 ber_put_octet_string(&e, (uint8_t)SnmpTag::BER_OCTET_STRING, data_ptr, data_len);
254 else
255 ber_put_raw(&e, data_ptr, data_len);
256 ber_seq_end(&e, msg);
257 if (!e.ok)
258 return 0;
259 size_t total = e.len;
260
261 if (auth)
262 {
263 uint8_t mac[SSH_HMAC_SHA256_LEN];
264 ssh_hmac_sha256(s_v3.auth_key, SNMP_USM_KEY_LEN, resp, total, mac);
265 memcpy(resp + sec_value_pos + auth_off, mac, SNMP_V3_AUTH_PARAM_LEN);
266 }
267 return total;
268}
269
270// Build a Report PDU (usmStats<stat>.0 = Counter32) and wrap it in a v3 message.
271static size_t build_report(long msg_id, bool auth, uint32_t stat, uint32_t count, long request_id, uint8_t *resp,
272 size_t resp_cap)
273{
274 uint32_t oid[11];
275 for (int i = 0; i < 9; i++)
276 oid[i] = kUsmStatsBase[i];
277 oid[9] = stat;
278 oid[10] = 0;
279
280 BerEnc e;
281 ber_enc_init(&e, s_v3.v3_b, sizeof(s_v3.v3_b));
282 size_t pdu = ber_seq_begin(&e, (uint8_t)SnmpTag::SNMP_PDU_REPORT);
283 ber_put_integer(&e, request_id);
284 ber_put_integer(&e, 0);
285 ber_put_integer(&e, 0);
286 size_t vbl = ber_seq_begin(&e, (uint8_t)SnmpTag::BER_SEQUENCE);
287 size_t vb = ber_seq_begin(&e, (uint8_t)SnmpTag::BER_SEQUENCE);
288 ber_put_oid(&e, oid, 11);
289 ber_put_uint(&e, (uint8_t)SnmpTag::SNMP_COUNTER32, count);
290 ber_seq_end(&e, vb);
291 ber_seq_end(&e, vbl);
292 ber_seq_end(&e, pdu);
293 if (!e.ok)
294 return 0; // GCOVR_EXCL_LINE the Report is one fixed usmStats varbind, far under v3_b (SNMP_MSG_BUF_SIZE)
295
296 BerEnc sc;
297 ber_enc_init(&sc, s_v3.v3_c, sizeof(s_v3.v3_c));
298 size_t s = ber_seq_begin(&sc, (uint8_t)SnmpTag::BER_SEQUENCE);
299 ber_put_octet_string(&sc, (uint8_t)SnmpTag::BER_OCTET_STRING, s_v3.engine_id, s_v3.engine_id_len);
300 ber_put_octet_string(&sc, (uint8_t)SnmpTag::BER_OCTET_STRING, nullptr, 0);
301 ber_put_raw(&sc, s_v3.v3_b, e.len);
302 ber_seq_end(&sc, s);
303 if (!sc.ok)
304 return 0; // GCOVR_EXCL_LINE fixed tiny Report scopedPDU never overflows v3_c (SNMP_MSG_BUF_SIZE)
305
306 return build_message(msg_id, auth, false, s_v3.v3_c, sc.len, resp, resp_cap);
307}
308
309// ---------------------------------------------------------------------------
310// Message processing
311// ---------------------------------------------------------------------------
312
313size_t snmp_v3_process(const uint8_t *req, size_t req_len, uint8_t *resp, size_t resp_cap)
314{
315 BerDec d;
316 ber_dec_init(&d, req, req_len);
317 uint8_t tag;
318 size_t l;
319 if (!ber_read_header(&d, &tag, &l) || tag != (uint8_t)SnmpTag::BER_SEQUENCE)
320 return 0;
321 long version;
322 if (!ber_read_integer(&d, &version) || version != (int)SnmpVersion::SNMP_V3)
323 return 0;
324
325 // msgGlobalData
326 if (!ber_read_header(&d, &tag, &l) || tag != (uint8_t)SnmpTag::BER_SEQUENCE)
327 return 0;
328 long msg_id;
329 long msg_max;
330 long sec_model;
331 if (!ber_read_integer(&d, &msg_id) || !ber_read_integer(&d, &msg_max))
332 return 0;
333 (void)msg_max;
334 size_t flags_len;
335 if (!ber_read_header(&d, &tag, &flags_len) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING || flags_len < 1)
336 return 0;
337 uint8_t flags = d.buf[d.pos];
338 d.pos += flags_len;
339 if (!ber_read_integer(&d, &sec_model) || sec_model != 3) // USM only
340 return 0;
341 bool req_auth = flags & 0x01;
342 bool req_priv = flags & 0x02;
343 if (req_priv && !req_auth)
344 return 0;
345
346 // msgSecurityParameters: OCTET STRING wrapping a SEQUENCE.
347 size_t sec_len;
348 if (!ber_read_header(&d, &tag, &sec_len) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING)
349 return 0;
350 const uint8_t *sec = d.buf + d.pos;
351 d.pos += sec_len;
352
353 BerDec sd;
354 ber_dec_init(&sd, sec, sec_len);
355 if (!ber_read_header(&sd, &tag, &l) || tag != (uint8_t)SnmpTag::BER_SEQUENCE)
356 return 0;
357 size_t eid_len;
358 if (!ber_read_header(&sd, &tag, &eid_len) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING)
359 return 0;
360 const uint8_t *eid = sd.buf + sd.pos;
361 sd.pos += eid_len;
362 long req_boots;
363 long req_time;
364 if (!ber_read_integer(&sd, &req_boots) || !ber_read_integer(&sd, &req_time))
365 return 0;
366 size_t uname_len;
367 if (!ber_read_header(&sd, &tag, &uname_len) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING)
368 return 0;
369 const uint8_t *uname = sd.buf + sd.pos;
370 sd.pos += uname_len;
371 size_t aparm_len;
372 if (!ber_read_header(&sd, &tag, &aparm_len) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING)
373 return 0;
374 const uint8_t *aparm = sd.buf + sd.pos;
375 size_t aparm_off = (size_t)(aparm - req);
376 sd.pos += aparm_len;
377 size_t pparm_len;
378 if (!ber_read_header(&sd, &tag, &pparm_len) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING)
379 return 0;
380 const uint8_t *pparm = sd.buf + sd.pos;
381 sd.pos += pparm_len;
382 if (!sd.ok)
383 return 0; // GCOVR_EXCL_LINE redundant: every secParams field read above returns on failure, so sd.ok holds
384 // here
385
386 const uint8_t *mdata = d.buf + d.pos;
387 size_t mdata_len = req_len - d.pos;
388
389 // Engine discovery: unknown/empty authoritative engine ID.
390 bool engine_match = (eid_len == s_v3.engine_id_len) && (memcmp(eid, s_v3.engine_id, eid_len) == 0);
391 if (!engine_match)
392 {
393 s_v3.stat_unknown_engine++;
394 return build_report(msg_id, false, (int)UsmStat::USM_STAT_UNKNOWN_ENGINE, s_v3.stat_unknown_engine,
395 inner_request_id(mdata, mdata_len, req_priv), resp, resp_cap);
396 }
397
398 if (!req_auth) // non-discovery noAuthNoPriv is not supported
399 return 0;
400
401 // Known user?
402 if (!s_v3.auth_set || !(uname_len == strnlen(s_v3.user, SNMP_V3_USER_MAX) && s_v3.user[0] &&
403 memcmp(uname, s_v3.user, uname_len) == 0))
404 {
405 s_v3.stat_unknown_user++;
406 return build_report(msg_id, false, (int)UsmStat::USM_STAT_UNKNOWN_USER, s_v3.stat_unknown_user, 0, resp,
407 resp_cap);
408 }
409
410 // Authenticate: HMAC over the whole message with the auth field zeroed.
411 if (aparm_len != SNMP_V3_AUTH_PARAM_LEN || req_len > sizeof(s_v3.v3_a))
412 {
413 s_v3.stat_wrong_digest++;
414 return build_report(msg_id, false, (int)UsmStat::USM_STAT_WRONG_DIGEST, s_v3.stat_wrong_digest, 0, resp,
415 resp_cap);
416 }
417 memcpy(s_v3.v3_a, req, req_len);
418 memset(s_v3.v3_a + aparm_off, 0, SNMP_V3_AUTH_PARAM_LEN);
419 uint8_t mac[SSH_HMAC_SHA256_LEN];
420 ssh_hmac_sha256(s_v3.auth_key, SNMP_USM_KEY_LEN, s_v3.v3_a, req_len, mac);
421 if (!ct_eq(mac, aparm, SNMP_V3_AUTH_PARAM_LEN))
422 {
423 s_v3.stat_wrong_digest++;
424 return build_report(msg_id, false, (int)UsmStat::USM_STAT_WRONG_DIGEST, s_v3.stat_wrong_digest, 0, resp,
425 resp_cap);
426 }
427
428 // Timeliness window (RFC 3414 §3.2): boots must match, time within +/-150s.
429 uint32_t now = snmp_v3_uptime_s();
430 long dt = (long)now - req_time;
431 if (dt < 0)
432 dt = -dt;
433 if ((uint32_t)req_boots != s_v3.boots || dt > 150)
434 {
435 s_v3.stat_not_in_time++;
436 return build_report(msg_id, true, (int)UsmStat::USM_STAT_NOT_IN_TIME, s_v3.stat_not_in_time, 0, resp, resp_cap);
437 }
438
439 // Privacy: decrypt the scopedPDU if the priv flag is set.
440 const uint8_t *scoped;
441 size_t scoped_len;
442 if (req_priv)
443 {
444 if (!s_v3.priv_set || pparm_len != SNMP_V3_PRIV_PARAM_LEN)
445 {
446 s_v3.stat_decrypt++;
447 return build_report(msg_id, true, (int)UsmStat::USM_STAT_DECRYPT, s_v3.stat_decrypt, 0, resp, resp_cap);
448 }
449 BerDec md;
450 ber_dec_init(&md, mdata, mdata_len);
451 if (!ber_read_header(&md, &tag, &l) || tag != (uint8_t)SnmpTag::BER_OCTET_STRING)
452 return 0;
453 const uint8_t *ct = md.buf + md.pos;
454 size_t ct_len = l;
455 if (ct_len > sizeof(s_v3.v3_a))
456 return 0; // GCOVR_EXCL_LINE ct_len <= mdata_len < req_len, and req_len<=sizeof(v3_a) was enforced at the
457 // digest step
458 uint8_t iv[16];
459 put_be32(iv, (uint32_t)req_boots);
460 put_be32(iv + 4, (uint32_t)req_time);
461 memcpy(iv + 8, pparm, SNMP_V3_PRIV_PARAM_LEN);
462 snmp_aes128_cfb(s_v3.priv_key, iv, ct, s_v3.v3_a, ct_len, false);
463 scoped = s_v3.v3_a;
464 scoped_len = ct_len;
465 }
466 else
467 {
468 scoped = mdata;
469 scoped_len = mdata_len;
470 }
471
472 // Inner PDU -> dispatch (authenticated: writes allowed; v2c-style semantics).
473 const uint8_t *ctxname;
474 const uint8_t *pdu;
475 size_t ctxname_len;
476 size_t pdu_len;
477 if (!parse_scoped(scoped, scoped_len, &ctxname, &ctxname_len, &pdu, &pdu_len))
478 return 0;
479 size_t rpdu = snmp_dispatch_pdu(pdu, pdu_len, true, true, s_v3.v3_b, sizeof(s_v3.v3_b));
480 if (rpdu == 0)
481 return 0;
482
483 // Response scopedPDU { our engineID, echoed contextName, response PDU }.
484 BerEnc sc;
485 ber_enc_init(&sc, s_v3.v3_c, sizeof(s_v3.v3_c));
486 size_t s = ber_seq_begin(&sc, (uint8_t)SnmpTag::BER_SEQUENCE);
487 ber_put_octet_string(&sc, (uint8_t)SnmpTag::BER_OCTET_STRING, s_v3.engine_id, s_v3.engine_id_len);
488 ber_put_octet_string(&sc, (uint8_t)SnmpTag::BER_OCTET_STRING, ctxname, ctxname_len);
489 ber_put_raw(&sc, s_v3.v3_b, rpdu);
490 ber_seq_end(&sc, s);
491 if (!sc.ok)
492 return 0;
493
494 return build_message(msg_id, true, req_priv, s_v3.v3_c, sc.len, resp, resp_cap);
495}
496
497#if DETWS_ENABLE_SNMP_TRAP
500
501// Shared SNMPv3 USM notification path: authenticated, and encrypted when a
502// privacy password is configured. Reuses the engine ID + localized keys from
503// snmp_v3_init()/snmp_v3_set_user() and the same build_message() as responses.
504// @p pdu_tag selects Trap ((uint8_t)SnmpTag::SNMP_PDU_TRAPV2) vs InformRequest (0xA6); @p request_id
505// is the inner PDU request-id (the inform receiver echoes it in its Response).
506static bool send_v3_notify(const char *dst_ip, uint16_t port, uint8_t pdu_tag, uint32_t request_id,
507 const uint32_t *trap_oid, size_t trap_oid_len, const SnmpVarbind *vbs, size_t n)
508{
509 if (!s_v3.auth_set) // a v3 notification must be authenticated
510 return false;
511
512 // Notification PDU into the inner-PDU scratch.
513 BerEnc e;
514 ber_enc_init(&e, s_v3.v3_b, sizeof(s_v3.v3_b));
515 snmp_notify_build_pdu(&e, pdu_tag, request_id, trap_oid, trap_oid_len, snmp_v3_uptime_s() * 100, vbs, n);
516 if (!e.ok)
517 return false;
518
519 // scopedPDU { contextEngineID = our engine, contextName = "", PDU }.
520 BerEnc sc;
521 ber_enc_init(&sc, s_v3.v3_c, sizeof(s_v3.v3_c));
522 size_t s = ber_seq_begin(&sc, (uint8_t)SnmpTag::BER_SEQUENCE);
523 ber_put_octet_string(&sc, (uint8_t)SnmpTag::BER_OCTET_STRING, s_v3.engine_id, s_v3.engine_id_len);
524 ber_put_octet_string(&sc, (uint8_t)SnmpTag::BER_OCTET_STRING, nullptr, 0);
525 ber_put_raw(&sc, s_v3.v3_b, e.len);
526 ber_seq_end(&sc, s);
527 if (!sc.ok)
528 return false;
529
530 uint8_t out[SNMP_MSG_BUF_SIZE];
531 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));
532 return len && det_udp_sendto(dst_ip, port, out, len);
533}
534
535bool snmp_trap_v3(const char *dst_ip, uint16_t port, const uint32_t *trap_oid, size_t trap_oid_len,
536 const SnmpVarbind *vbs, size_t n)
537{
538 static uint32_t s_v3_trap_id = 1; // a trap is unconfirmed; the id is informational
539 return send_v3_notify(dst_ip, port, (uint8_t)SnmpTag::SNMP_PDU_TRAPV2, s_v3_trap_id++, trap_oid, trap_oid_len, vbs,
540 n);
541}
542
543// SNMPv3 USM InformRequest (confirmed notification, RFC 3416 4.2.7). Symmetric to
544// snmp_inform_v2c: builds + sends the InformRequest. The caller owns the
545// request_id and, for confirmed delivery, retransmits until the receiver's
546// Response (echoing request_id) arrives.
547bool snmp_inform_v3(const char *dst_ip, uint16_t port, uint32_t request_id, const uint32_t *trap_oid,
548 size_t trap_oid_len, const SnmpVarbind *vbs, size_t n)
549{
550 return send_v3_notify(dst_ip, port, 0xA6 /* InformRequest */, request_id, trap_oid, trap_oid_len, vbs, n);
551}
552#endif // DETWS_ENABLE_SNMP_TRAP
553
554#endif // DETWS_ENABLE_SNMP_V3
#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).
void put_be32(uint8_t *p, uint32_t v)
Definition ghash.h:43
Zero-heap SNMP v1/v2c agent: PDU processing + a fixed MIB table.
Zero-heap ASN.1 BER encoder/decoder for the SNMP agent (DETWS_ENABLE_SNMP).
USM cryptographic primitives for SNMPv3 (DETWS_ENABLE_SNMP_V3).
Outbound SNMP notifications - Trap and InformRequest (DETWS_ENABLE_SNMP_TRAP).
SNMPv3 User-based Security Model (USM) layer (DETWS_ENABLE_SNMP_V3).
void ssh_hmac_sha256(const uint8_t *key, size_t key_len, const uint8_t *data, size_t len, uint8_t mac[SSH_HMAC_SHA256_LEN])
Compute HMAC-SHA2-256 over a single contiguous buffer.
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1).
#define SSH_HMAC_SHA256_LEN
HMAC-SHA2-256 output length in bytes.
bool det_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:196
Layer 4 (Transport) - connectionless UDP datagram service.