From c7a75fee40001d4ae99f163e00236065a1c57582 Mon Sep 17 00:00:00 2001 From: ilyam8 Date: Fri, 9 Oct 2026 17:44:26 +0300 Subject: [PATCH] test: pin the SNMPv3 engine under FIPS 140-only mode The engine's behavior when FIPS 140-only mode refuses an algorithm was pinned nowhere: CI's GODEBUG=fips140=only step ran only the digest test. TestEngineV3FIPS140Only (skipped unless enforcement is on; CI's "-run FIPS140Only" step selects it) runs discovery and a Get for noAuth, MD5, SHA-1/AES, SHA-256/DES, SHA-256/AES and SHA-512/AES-256-C users, a second Get after a failed discovery and a preset engine ID, against the fake agent, which runs without enforcement with the key oracle (it stands for the other side). testdata/engine/v3-fips.golden holds the transcripts. Known bugs pinned: a failed key derivation (MD5, SHA-1) leaves the agent's engine ID adopted without keys, boots or time, so the next request skips the discovery and fails to encode; the key derivation error is ignored when the engine ID is known; AES-CFB panics and send recovers it into an error that carries the stack. DES is refused at encoding with the cipher's error. --- engine_v3_fips_test.go | 62 ++++++++++++++++++ engine_v3_test.go | 17 +++-- testdata/engine/v3-fips.golden | 112 +++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 engine_v3_fips_test.go create mode 100644 testdata/engine/v3-fips.golden diff --git a/engine_v3_fips_test.go b/engine_v3_fips_test.go new file mode 100644 index 0000000..9d5bf59 --- /dev/null +++ b/engine_v3_fips_test.go @@ -0,0 +1,62 @@ +// Copyright 2026 Netdata Inc. All rights reserved. Use of this +// source code is governed by a BSD-style license that can be found in the +// LICENSE file. + +package gosnmp + +import ( + "crypto/fips140" + "maps" + "slices" + "testing" + "testing/synctest" +) + +// getFirstOf2 runs two Gets, records the first one's result and returns the +// second's. +func getFirstOf2(x *GoSNMP, a *fakeV3Agent) (*SnmpPacket, error) { + res, err := x.Get([]string{engineOID}) + a.tr.addf("first Get: %s", describeEngineResult(res, err)) + return x.Get([]string{engineOID}) +} + +// v3FIPSScenarios are the scenarios of TestEngineV3FIPS140Only. +func v3FIPSScenarios() map[string]v3Scenario { + const cfbPanic = "AES-CFB panics in FIPS 140-only mode; send recovers it into an error that carries the stack" + const partialState = "a failed key derivation leaves the agent's engine ID adopted without keys, boots or time, so the next request skips the discovery" + return map[string]v3Scenario{ + "discovery/noauth": {user: "codec-noauth"}, + "discovery/md5": {user: "codec-md5", run: getFirstOf2, knownBug: partialState}, + "discovery/sha-aes": {user: "codec-sha-aes", run: getFirstOf2, knownBug: partialState}, + "discovery/sha256-des": {user: "codec-sha256-des"}, + "discovery/sha256-aes": {user: "codec-sha256-aes", knownBug: cfbPanic}, + "discovery/sha512-aes256c": {user: "codec-sha512-aes256c", knownBug: cfbPanic}, + "known-engine/md5": { + user: "codec-md5", setup: func(_ *GoSNMP, sp *UsmSecurityParameters) { sp.AuthoritativeEngineID = agentEngineID }, + knownBug: "the key derivation error is ignored when the engine ID is known", + }, + } +} + +// TestEngineV3FIPS140Only pins, when run with GODEBUG=fips140=only, what the +// request engine does when FIPS 140-only mode refuses an algorithm: MD5 and +// SHA-1 for the key derivation and digests, DES and AES-CFB for privacy. +// The agent runs without enforcement. testdata/engine/v3-fips.golden holds +// the transcripts; regenerate it with +// GODEBUG=fips140=only go test -run TestEngineV3FIPS140Only -update . +func TestEngineV3FIPS140Only(t *testing.T) { + if !fips140.Enforced() { + t.Skip("run with GODEBUG=fips140=only") + } + scenarios := v3FIPSScenarios() + var results []goldenCase + for _, name := range slices.Sorted(maps.Keys(scenarios)) { + sc := scenarios[name] + var dump string + synctest.Test(t, func(t *testing.T) { + dump = runV3Scenario(t, sc) + }) + results = append(results, goldenCase{name: "v3-fips/" + name, dump: dump}) + } + engineGolden.check(t, "v3-fips", results) +} diff --git a/engine_v3_test.go b/engine_v3_test.go index fb2870b..b9beab7 100644 --- a/engine_v3_test.go +++ b/engine_v3_test.go @@ -6,6 +6,7 @@ package gosnmp import ( "bytes" + "crypto/fips140" "fmt" "maps" "slices" @@ -23,6 +24,7 @@ var agentCreds = map[string]agentUser{ "codec-md5": {auth: MD5, authPass: "codec-md5-pass", priv: NoPriv}, "codec-sha-aes": {auth: SHA, authPass: "codec-sha-pass", priv: AES, privPass: "codec-aes-pass"}, "codec-sha256-des": {auth: SHA256, authPass: "codec-sha256-pass", priv: DES, privPass: "codec-des-pass"}, + "codec-sha256-aes": {auth: SHA256, authPass: "codec-sha256-pass", priv: AES, privPass: "codec-aes-pass"}, "codec-sha512-aes256c": {auth: SHA512, authPass: "codec-sha512-pass", priv: AES256C, privPass: "codec-aes256c-pass"}, } @@ -478,12 +480,17 @@ func TestEngineV3Characterization(t *testing.T) { } // runV3Scenario runs sc in the calling synctest bubble and returns its -// transcript. +// transcript. The agent and the key oracle run without FIPS 140-only +// enforcement: they stand for the other side, not the client. func runV3Scenario(t *testing.T, sc v3Scenario) string { tr := newEngineTranscript() - agent := newFakeV3Agent(tr, agentCreds, sysDescr) + var agent *fakeV3Agent + fips140.WithoutEnforcement(func() { agent = newFakeV3Agent(tr, agentCreds, sysDescr) }) agent.script = sc.script - c := newFakeTransport(tr, agent.handle) + c := newFakeTransport(tr, func(n int, data []byte) (replies []agentReply) { + fips140.WithoutEnforcement(func() { replies = agent.handle(n, data) }) + return replies + }) creds, ok := agentCreds[sc.user] if sc.creds != nil { @@ -555,7 +562,9 @@ func describeKeys(sp *UsmSecurityParameters) string { PrivacyPassphrase: sp.PrivacyPassphrase, AuthoritativeEngineID: id, } - if want.InitSecurityKeys() != nil || !bytes.Equal(want.SecretKey, sp.SecretKey) || + var err error + fips140.WithoutEnforcement(func() { err = want.InitSecurityKeys() }) + if err != nil || !bytes.Equal(want.SecretKey, sp.SecretKey) || !bytes.Equal(want.PrivacyKey, sp.PrivacyKey) { continue } diff --git a/testdata/engine/v3-fips.golden b/testdata/engine/v3-fips.golden new file mode 100644 index 0000000..880dec3 --- /dev/null +++ b/testdata/engine/v3-fips.golden @@ -0,0 +1,112 @@ +# Engine characterization results (v3-fips). Generated by go test -update; do not edit. + +== v3-fips/discovery/md5 +0s client: user="codec-md5" authNoPriv+reportable timeout=1s retries=2 +0s deadline +1s +0s hook PreSend +0s write #1: 3 msgID=2001 noAuthNoPriv+reportable user="" engine="" boots=0 time=0 context=""/"" GetRequest id=1001 [] +0s agent: answers #1 with Report id=1001 msgID=2001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] at noAuthNoPriv (unknown engine ID) +0s agent answers #1 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] +0s hook OnRecv +0s hook OnFinish +0s first Get: packet empty, error "crypto/md5: use of MD5 is not allowed in FIPS 140-only mode" +0s deadline +1s +0s result: packet nil, error "marshal: crypto/md5: use of MD5 is not allowed in FIPS 140-only mode" +0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=0 time=0 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" +0s known bug: a failed key derivation leaves the agent's engine ID adopted without keys, boots or time, so the next request skips the discovery + +== v3-fips/discovery/noauth +0s client: user="codec-noauth" noAuthNoPriv+reportable timeout=1s retries=2 +0s deadline +1s +0s hook PreSend +0s write #1: 3 msgID=2001 noAuthNoPriv+reportable user="" engine="" boots=0 time=0 context=""/"" GetRequest id=1001 [] +0s agent: answers #1 with Report id=1001 msgID=2001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] at noAuthNoPriv (unknown engine ID) +0s agent answers #1 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] +0s hook OnRecv +0s hook OnFinish +0s deadline +1s +0s hook PreSend +0s write #2: 3 msgID=2002 noAuthNoPriv+reportable user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" GetRequest id=1002 [.1.3.6.1.2.1.1.1.0] +0s agent: answers #2 with GetResponse id=1002 msgID=2002 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] at noAuthNoPriv (request accepted) +0s agent answers #2 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2002 noAuthNoPriv user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" GetResponse id=1002 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] +0s hook OnRecv +0s hook OnFinish +0s result: packet 3 msgID=2002 noAuthNoPriv GetResponse id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"], no error +0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" + +== v3-fips/discovery/sha-aes +0s client: user="codec-sha-aes" authPriv+reportable timeout=1s retries=2 +0s deadline +1s +0s hook PreSend +0s write #1: 3 msgID=2001 noAuthNoPriv+reportable user="" engine="" boots=0 time=0 context=""/"" GetRequest id=1001 [] +0s agent: answers #1 with Report id=1001 msgID=2001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] at noAuthNoPriv (unknown engine ID) +0s agent answers #1 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] +0s hook OnRecv +0s hook OnFinish +0s first Get: packet empty, error "crypto/sha1: use of SHA-1 is not allowed in FIPS 140-only mode" +0s deadline +1s +0s result: packet nil, error "marshal: crypto/aes: invalid key size 0" +0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=0 time=0 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" +0s known bug: a failed key derivation leaves the agent's engine ID adopted without keys, boots or time, so the next request skips the discovery + +== v3-fips/discovery/sha256-aes +0s client: user="codec-sha256-aes" authPriv+reportable timeout=1s retries=2 +0s deadline +1s +0s hook PreSend +0s write #1: 3 msgID=2001 noAuthNoPriv+reportable user="" engine="" boots=0 time=0 context=""/"" GetRequest id=1001 [] +0s agent: answers #1 with Report id=1001 msgID=2001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] at noAuthNoPriv (unknown engine ID) +0s agent answers #1 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] +0s hook OnRecv +0s hook OnFinish +0s deadline +1s +0s result: packet nil, error "recover: crypto/cipher: use of CFB is not allowed in FIPS 140-only mode Stack: ..." +0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" +0s known bug: AES-CFB panics in FIPS 140-only mode; send recovers it into an error that carries the stack + +== v3-fips/discovery/sha256-des +0s client: user="codec-sha256-des" authPriv+reportable timeout=1s retries=2 +0s deadline +1s +0s hook PreSend +0s write #1: 3 msgID=2001 noAuthNoPriv+reportable user="" engine="" boots=0 time=0 context=""/"" GetRequest id=1001 [] +0s agent: answers #1 with Report id=1001 msgID=2001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] at noAuthNoPriv (unknown engine ID) +0s agent answers #1 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] +0s hook OnRecv +0s hook OnFinish +0s deadline +1s +0s result: packet nil, error "marshal: crypto/des: use of DES is not allowed in FIPS 140-only mode" +0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" + +== v3-fips/discovery/sha512-aes256c +0s client: user="codec-sha512-aes256c" authPriv+reportable timeout=1s retries=2 +0s deadline +1s +0s hook PreSend +0s write #1: 3 msgID=2001 noAuthNoPriv+reportable user="" engine="" boots=0 time=0 context=""/"" GetRequest id=1001 [] +0s agent: answers #1 with Report id=1001 msgID=2001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] at noAuthNoPriv (unknown engine ID) +0s agent answers #1 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] +0s hook OnRecv +0s hook OnFinish +0s deadline +1s +0s result: packet nil, error "recover: crypto/cipher: use of CFB is not allowed in FIPS 140-only mode Stack: ..." +0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" +0s known bug: AES-CFB panics in FIPS 140-only mode; send recovers it into an error that carries the stack + +== v3-fips/known-engine/md5 +0s client: user="codec-md5" authNoPriv+reportable timeout=1s retries=2 +0s deadline +1s +0s result: packet nil, error "marshal: crypto/md5: use of MD5 is not allowed in FIPS 140-only mode" +0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=0 time=0 keys=none context engine="" +0s known bug: the key derivation error is ignored when the engine ID is known