Skip to content

test: pin the SNMPv3 request engine against a fake authoritative agent - #37

Merged
ilyam8 merged 3 commits into
masterfrom
engine-v3
Oct 11, 2026
Merged

ilyam8 merged 3 commits into
masterfrom
engine-v3

Conversation

@ilyam8

@ilyam8 ilyam8 commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Summary

Second safety net for the request engine: what it does with an SNMPv3 agent, known bugs included, so the decomposition of sendOneRequest/send/v3.go and the later port of the REPORT resynchronization change are measured against it. Test files only, no library change.

  • engine_v3_agent_test.go: fakeV3Agent, an authoritative SNMPv3 engine on the harness of the previous PR. It reads requests with its own parser, drops what RFC 3412 discards (another version or security model, a msgID or msgMaxSize out of range, privacy without authentication, a scoped PDU that does not decode, so a wrong privacy passphrase gets no answer as with net-snmp's snmpd, and Reports to unreadable requests that are not reportable), and checks the rest with the library's digests and ciphers in RFC 3414 section 3.2 order: the digest over the message as received, decryption when the privacy flag is set. It answers with a GetResponse at the request's security level and in its context, or with a Report at the RFC's level in the default context. Its engine time follows the test clock; it can reboot, change its engine ID and script any answer.
  • engine_v3_test.go -> testdata/engine/v3.golden (90 scenarios): discovery at every security level (no answer, another Report, the unknownUserNames device workaround, preset context engine ID, context name, Reports with another model, another or no context engine ID, an empty msgFlags), a known engine ID, reboots, engine-ID changes (noAuth, authNoPriv and authPriv users, replies from the new engine), the 150 s time window, older engine time and lower boots, resynchronizations that fail again, wrong authentication and privacy passphrases, a privacy protocol mismatch, unknown users, unsupported levels, every Report counter unauthenticated and authenticated, Reports after discovery, forged Reports, and mismatched answers (stale msgID or request ID, unauthenticated, unencrypted, encrypted with another key or to an authNoPriv request, another context, user or security model, an empty msgFlags, Reports with two varbinds).
  • The transcripts show every field a change to the engine can affect: msgID, flags, salts, context, USM parameters, a non-default security model or msgMaxSize, and the engine ID the client's keys are localized to after each scenario.

Known bugs pinned with known bug: notes include: unauthenticated Reports (unknownEngineID, wrongDigest, unknownUserName, unsupportedSecLevel) discarded under an authenticated session, so the caller never sees those errors; a notInTimeWindow or unknownEngineID Report that repeats after the retransmission returned with a nil error; retransmission errors replaced by ErrNotInTimeWindow/ErrUnknownEngineID; the engine time of the last message sent instead of the current one (RFC 3414 section 3.1 step 6 a); the reply's msgID, context, user, security level and engine ID not compared with the request's (RFC 3412 section 7.2 step 12); the context engine ID kept after an engine-ID change; the engine time of an authenticated error Report not stored; an older engine time or lower boots adopted (RFC 3414 section 3.2 step 7 b); replies and Reports with another security model or an empty msgFlags acted on instead of discarded; a Report with two varbinds returned as a success.

Testing

  • go test ./... on darwin/arm64 and GOARCH=386; -race; -shuffle=on; 100 consecutive runs and -cpu 1,8,64 identical; golangci-lint v2.14.0: 0 issues.
  • Two independent review rounds with two reviewers each: one side compared the agent with RFC 3412/3414 and a real net-snmp 5.9.3 snmpd (discovery, Report levels and fields, the time window boundary, reboots, every privacy protocol; 51 crafted requests in the second round), the other ran mutants and a scratch port of the REPORT resynchronization change. Their findings are fixed in the second and third commits.
  • Mutation probes (overlay, whole root package): the author's list 23 of 24 caught (the 24th does not compile, its variant is caught), including the three engine mutants earlier steps could not catch (no re-keying after an engine-ID change, the context engine ID from discovery, response vs connection parameters for the digest check); the reviewers' lists 122 of 130 caught, 2 do not compile (their variants are caught), 6 equivalent.
  • The scratch port of the REPORT resynchronization change flips 37 of the 90 scenarios, each of its changes alone flips specific ones, and variants of the port (skipping the digest for every Report, accepting unauthenticated Reports for some counters only, reusing a salt, storing other Reports) are each visible.
  • Coverage of the root package against the previous PR: sendOneRequest 91.1% -> 99.4%, send 64.0% -> 92.0%, negotiateInitialSecurityParameters 71.4% -> 85.7%, storeSecurityParameters 71.4% -> 85.7%; package 85.2% -> 87.1%.

@ilyam8
ilyam8 added this pull request to stack #38 October 9, 2026 12:00
@ilyam8 ilyam8 changed the title engine v3 test: pin the SNMPv3 request engine against a fake authoritative agent Oct 9, 2026
Base automatically changed from engine-harness to master October 11, 2026 09:34
A fake authoritative SNMPv3 engine answers the client in the engine
harness. It reads each request with its own parser, checks it with the
library's digests and ciphers in the order of RFC 3414 section 3.2
(engine ID, user, security level, digest, time window, decryption) and
answers with a GetResponse at the request's security level or with a
Report at the level the RFC gives it. Its engine time runs on the
bubble's clock; it can reboot or change its engine ID, and scenarios
can script any answer.

testdata/engine/v3.golden pins, known bugs included: discovery and the
request at each security level, a known engine ID with and without
its time, the context engine ID and name, an agent that reboots or
changes its engine ID, requests after 150 s of silence, a wrong
passphrase, unknown users, an unsupported level, every Report counter
for a noAuthNoPriv and an authNoPriv user, and answers that do not
match the request (request ID, msgID, security level, privacy key,
security model, an empty identity). Because the agent verifies request
digests with keys localized to its own engine ID, the net catches a
client that does not re-key after discovery, does not adopt the
context engine ID, or checks a reply with the wrong parameters.

parseV3Message, factored out of splitV3Message, gives the agent and
the transcripts the same independent reading of a message.
The fake SNMPv3 agent now behaves like net-snmp's snmpd where the client's
engine depends on it:

- it drops what RFC 3412 section 7.2 discards before USM (another version or
  security model, a msgMaxSize outside 484..2^31-1, privacy without
  authentication) and a scoped PDU that does not decode, so a wrong privacy
  passphrase gets no answer instead of a decryptionError Report;
- a GetResponse carries the request's context (the agent's engine ID when the
  request has none) and a Report the agent's engine ID and the default context
  (RFC 3412 section 7.1 step 3 d);
- the digest is checked only on minimally encoded messages, the only ones it
  re-serializes exactly.

The transcripts show what mutants of the engine changed without a trace: the
salt of every message, a security model or msgMaxSize that differs from the
library's, the flags an answer was actually sent with, and the engine ID the
client's keys are localized to after each scenario.

New scenarios: a resynchronization answered with notInTimeWindow again and an
unknownEngineID Report that repeats (both returned with a nil error), an
unanswered retransmission after unknownEngineID, an engine-ID change for a
noAuth user (the context engine ID keeps the old engine's), a GetResponse
from the new engine to a request for the old one (accepted and adopted), error
Reports 10 s after discovery (authenticated, and unauthenticated from another
engine ID), a wrong privacy passphrase (AES and DES), discovery Reports with
another security model, another or no context engine ID, replies in another
context, for another user, encrypted to an authNoPriv request, or with an
empty msgFlags (accepted with the request's flags), and an error Report with
two varbinds.

Notes: an authNoPriv reply to an authPriv request is accepted (RFC 3412
section 7.2 step 12 b); discarding an unauthenticated notInTimeWindow Report
conforms to RFC 3414 section 3.2 step 7 and is no longer named a bug; a Report
for another message keeps both IDs stale, as RFC 3412 correlates Reports by
msgID; the time-window note cites RFC 3414 section 3.1 step 6 a.
The fake SNMPv3 agent follows snmpd in the cases the second review compared:

- the digest is checked over the message as received, with the digest field
  zeroed (RFC 3414 section 3.2 step 6), so non-minimal encodings are checked
  instead of dropped;
- it decrypts only when the privacy flag is set, answers decryptionErrors to a
  privacy flag without a ciphertext or an 8-octet salt, and drops a ciphertext
  without the flag;
- it sends no Report to a request that is not reportable and whose PDU it
  cannot read (RFC 3412 section 7.1 step 3 b), and drops a msgID outside
  0..2^31-1.

New scenarios, each pinning a change a mutant or a variant of the REPORT
resync port made without a trace: authenticated Reports with a wrong digest
(discarded), an authPriv client answered from a new engine ID (privacy key
re-derived), every Report counter unauthenticated to an authNoPriv client,
authentic replies with an older engine time or lower boots (adopted), a
Report with another security model after discovery (acted on), a discovery
Report with an empty msgFlags (accepted), and an AES client talking to a DES
user (the unauthenticated decryptionErrors Report is discarded).

The scripted Reports are ones an agent can send: the authenticated error
Report 10 s after discovery is snmpUnknownContexts, and the unknownEngineID
Reports and the late unauthenticated Report come from the other engine ID in
its context.
@ilyam8
ilyam8 merged commit f944e43 into master Oct 11, 2026
21 checks passed
@ilyam8
ilyam8 deleted the engine-v3 branch October 11, 2026 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants