Repository navigation
fix: make SNMPv3 keys valid by construction - #64
Merged
Merged
Conversation
ilyam8
marked this pull request as ready for review
October 14, 2026 10:38
Encrypt and Decrypt take an AES-128 key of 32 octets (and encrypt with AES-256), AES salts of 9 and 16 octets and a DES key of 20 octets. InitSecurityKeys leaves an HMAC-SHA user the whole 20-octet localized key as its DES key, and it and a table's Add take keys of another length than the protocol's: an authentication key that is not the hash size, an AES key of 32 octets for AES-128, a DES key of 8 octets.
The USM took keys and salts of any length: HMAC-MD5/SHA-96 cut a key longer than the block, crypto/hmac panicked for a short HMAC-SHA-2 key in FIPS 140-only mode, DES indexed past a key shorter than 16 octets or a salt shorter than 8 and panicked, AES took any key aes.NewCipher takes (an AES-128 key of 32 octets encrypted with AES-256) and any salt, and AES-CFB panicked in FIPS 140-only mode. Localizing an empty passphrase dropped the error and returned an empty key, so a new engine ID gave a user configured with keys and no passphrases the empty keys, and a trap signed with the empty key, which anyone can compute, passed. Guards in the engine and the answer check refused an empty key, SnmpEncodePacket signed with one, and the signer recovered a panic onto stdout, returning no message and no error. One rule, in the internal/usm specs every path reaches: an authentication key is exactly the size of its hash, a privacy key exactly its cipher's key (AES's, or DES's 16 octets of key and pre-IV), and a salt 8 octets. Digest, Verify, Encrypt and Decrypt refuse anything else, and AES-CFB in FIPS 140-only mode, with an error; decryption errors say "decrypting". Localizing an empty passphrase is an error. InitSecurityKeys derives the missing keys from non-empty passphrases and checks each key with the rule. A key in the parameters is localized to AuthoritativeEngineID: a new engine ID re-derives the keys from the passphrases and drops a key given without one, which serves only its engine. The signer's recover, the engine's guard, the answer check's empty-key test and errAuthKeyRequired go. A DES key derived for an HMAC-SHA-family user is now the first 16 octets of the localized key; ciphertexts are unchanged.
Encrypt and Decrypt now wrap every error, the cipher constructors' included, so DES refused in FIPS 140-only mode says "usm: encrypting:" or "usm: decrypting:" like the AES-CFB refusal. CheckKey alone decides whether a privacy protocol has a cipher, and PrivacyKey cuts the key and checks it with the rule, instead of separate no-cipher tests. The discovery doc gives the real reason a request goes out without a key, and UnmarshalTrap's doc says parameters with a protocol but neither key nor passphrase fail every message. Tests: the trap cases check the error words Netdata's labels read; the derived DES key is compared with the localized key's prefix; a privacy key still short after its extension, and a protocol without a cipher, fail; InitSecurityKeys returns a FIPS 140-only derivation error; the validate cases use a key of the right size; the empty-key trap is signed with a key of zeros, which HMAC treats as the empty key.
Adopting another engine ID dropped a key given without its passphrase, so the key's use failed, but a message that uses no key still passed: a keys-only trap user accepted noAuthNoPriv, security model 2 and empty-identity traps from any engine ID, which a failed privacy key derivation had rejected before for most protocol pairs. Keys localize to one engine ID (RFC 3414 section 2.6). Adopting another one derives new keys from the passphrases, so parameters with a protocol whose passphrase is empty now refuse it with an error and stay unchanged: they decode no message naming another engine ID, whatever its security level, model or user, and take no engine ID from discovery. A reply naming another engine ID than a keys-only client's is undecodable, so the request is sent again and ends with that error instead of ErrUnknownEngineID.
stelfrag
approved these changes
Oct 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fifth step of the bug-fix phase: an SNMPv3 key is valid for its protocol by construction.
The USM took keys and salts of any length:
crypto/hmacpanickedfor an HMAC-SHA-2 key under 14 octets in FIPS 140-only mode.
aes.NewCiphertakes, so an AES-128 key of 32 octets encrypted with AES-256, and any salt (9 and16 octets were accepted inbound). AES-CFB panicked in FIPS 140-only mode.
with keys and no passphrases the empty keys, so a trap signed with the empty key, which anyone can compute, passed,
and so did an unauthenticated trap from any engine ID for keys-only DES and no-privacy users.
SnmpEncodePacketsigned with one; the signer recovered a panic onto stdout and returned no message and no error.Now:
internal/usmspecs every path reaches: an authentication key is exactly the size of its hash, aprivacy key exactly its cipher's key (AES's, or DES's 16 octets of key and pre-IV, RFC 3414 section 8.1.1.1), a salt
exactly 8 octets (RFC 3414 section 8.3.2, RFC 3826 section 3.3.2).
Digest,Verify,EncryptandDecryptrefuse anything else, and AES-CFB in FIPS 140-only mode, with an error.
InitSecurityKeysderives the missing keys from non-empty passphrasesand checks every key with the rule, so a table's
Addrefuses a key that does not fit.AuthoritativeEngineID(RFC 3414 section 2.6): a new engine ID re-derivesthe keys from the passphrases (traps from new engines keep working), and parameters with a key given without its
passphrase cannot take another engine ID. Such a user decodes no message naming another engine ID, whatever its
security level, security model or user name, and such a client takes no engine ID from discovery.
they caught, with an error.
Behavior changes:
ErrUnknownEngineID; other privacy protocols: undecodable (a failed key derivation)Passphrases stay accepted at any non-empty length (RFC 3414 section 11.2 asks for 8 octets; Netdata's jobs may use
shorter ones). Netdata uses passphrases only; its trap receiver's error labels keep working ("authentication",
"decrypting").
Tests
passphrases); the USM decrypt golden (DES panics and wrong salts become errors); the engine goldens (the rule's
errors replace the guard's, AES-CFB errors replace recovered panics).
TestDigest(keys of 0 to 100 octets),TestPrivKeyAndSaltLengths,TestUSMKeyLengths,TestUSMEmptyPassphraseKeys,TestSnmpEncodePacketEmptyKey,TestUSMFIPS140OnlyShortSHA2Key, the hand-forgedempty-key trap of
TestUSMTrapUnauthenticated; the RFC 3414 key vectors still match.TestUSMKeysBoundToEngine(every protocol pair; the trap table, a single trap user with its own orthe message's flags, and
SnmpDecodePacket; unauthenticated, security model 2 and empty-identity messages fromanother engine rejected, signed ones from the keys' engine accepted),
TestUSMAdoptEngineID(the rule per parametershape), engine scenarios for a keys-only client with another configured engine ID and a forged Report from another
engine.
Validation
netsnmpplayback pass.GOARCH=386 and the end-to-end suite run in CI.
rule on any path, FIPS 140-only included. Their findings are in the last commit: every cipher error wrapped at one
point (DES refused in FIPS 140-only mode now reads like AES-CFB's refusal), one privacy-key check, tests of the
error words Netdata's trap receiver labels, and two doc corrections.
(unauthenticated traps from other engines to keys-only users). The last commit binds such parameters to their engine
ID instead; the review's own probes pass.