SNMP version 3 added authentication and encryption to SNMPv2c, but made relatively few other changes except to nomenclature. The original definitions are in RFC 3410 through RFC 3415; RFC 3410 first appeared as RFC 2570. SNMPv3 introduced the User Security Model, or USM, in which agents allow manager access only if the manager has presented an appropriate key, derived ultimately from a passphrase. The agent’s response can be either digitally signed or encrypted, as desired.
SNMPv3 did make several terminology changes. Any SNMP node – either manager or agent – is now known as an SNMP entity. An entity is divided into two parts; the first is the SNMP engine consisting of the message dispatcher, the message processor, and the security and access-control subsystems. The second part consists of the various SNMP applications, consisting, for agents, primarily of the command responder (responding to Get() and GetNext(), etc). One goal of this architectural division is to separate the applications from the authentication mechanisms. Another goal, however, is to provide a framework in which future new applications can easily be supported.
It is the SNMP engine that must implement all the new security provisions.
RFC 3414 analyzes the the risks of inadequate SNMP security, and identifies two primary and two secondary threats. The primary threats are as follows:
The secondary threats are these:
All but the last, disclosure, can be addressed by appropriate digital signatures and timestamps; encryption is only needed when disclosure is an active concern. Because disclosure is not always a significant concern, and perhaps because when RFC 2574 was written in 1999 the unlicensed export of encryption technology from the United States was illegal, SNMPv3 defines two separate new levels of security:
These correspond to three values for the snmpSecurityLevel textual convention (in which “priv” abbreviates “privacy”):
Encryption (privacy) implies authentication, as only an entity in possession of the secret key could have created the message.
SNMPv3 authentication is based on cryptographic hash functions: functions which take a data string of arbitrary length and return a fixed-length hash, in such a way that
The Internet checksum of 5.4 Error Detection fails as a cryptographic hash, for example, because given a message m and a hash value hʹ, one can calculate hash(m) = h and then append to m a two-byte string based on hʹ−h, yielding a message mʹ for which hash(mʹ) = hʹ.
The two cryptographic hash functions originally supported by SNMPv3 in RFC 3414 are MD5, which produces a 128-bit hash, and SHA-1, which produces a 160-bit hash. Since the publication of RFC 3414 in 2002, vulnerabilities in each of these hash functions have been discovered. The SNMP framework in principle allows easy substitution of new hash functions, but to date new hashes have not been standardized.
If two parties share a secret key k, the basic hash-based way to sign a message m is to append to it the value hash(m⏜k), where m⏜k is the result of appending k to m. An eavesdropper will see m and hash(m⏜k) but this will provide no information about k, and, similarly, an attacker will, given a message m, not be able to generate the value hash(m⏜k) without knowing k.
The SNMP encryption mechanism is also based on shared secret keys; that is, public-key encryption is not used. RFC 3414 describes the use of the Data Encryption Standard cipher, or DES, which uses 56-bit keys. DES is, if anything, even more vulnerable than MD5 and SHA-1, due to the limited key length. Given a plaintext message p and a key k, we can encrypt it with e(p,k); given an encrypted ciphertext message c we can decrypt it with d(c,k), where e() and d() are established functions.
The SNMPv3 engine is the component of an SNMP entity charged with ensuring security. Each SNMPv3 engine – manager or agent – has an snmpEngineID. This is a string that, by default, incorporates the node’s IP or Ethernet address and additional standard information. While it is
In any SNMPv3 exchange, one SNMPv3 engine is designated authoritative. For a Get() or Set() request, or any other request that requires a response, it is always the agent that is authoritative. It is the job of the authoritative engine to validate the message received from the other engine.
For either authentication-only or encrypting security levels, the nonauthoritative engine must present a pair consisting of the following:
The authoritative engine must keep a table of all the userName values it recognizes, and, for each, the appropriate key with which to validate the user.
Any SNMPv3 engine also keeps track of two 32-bit attributes
The pair of these, which we will abbreviate as ⟨Time,Boots⟩, uniquely identifies a point in time for an entity (to the nearest second). They are used to prevent replay attacks; two messages sent more than one second apart will never have the same ⟨Time,Boots⟩ timestamp.
Note that keeping track of snmpEngineBoots implies that the entity have some form of persistent storage.
When a nonauthoritative engine (for our purposes, the manager) wants to send a request to an authoritative engine (the agent), the first step is to find out the agent’s snmpEngineID and then its current ⟨Time,Boots⟩ value. This is done through an initial two-step discovery process. The first request contains an empty varBindList, an empty engineID, a username of “” and a security_model of noAuthNoPriv. The authoritative side sends a response containing its engineID. The second step is to send a request, again with an empty varBindList but now containing a valid ⟨username,key⟩ pair. The value for ⟨Time,Boots⟩ is ⟨0,0⟩. The authoritative engine now responds with a message including its actual ⟨Time,Boots⟩.
After discovering the authoritative engine’s ⟨Time,Boots⟩, the nonauthoritative engine stores the authoritative engine’s snmpEngineBoots and also the difference between the authoritative engine’s snmpEngineTime and its own clock time. It can now use this difference to approximate the authoritative engine’s snmpEngineTime at any point in the future. Every message from the nonauthoritative engine will include its estimate of the authoritative engine’s current ⟨Time,Boots⟩ value. Relative drift between the two engines’ clocks will eventually mean this estimate fails, but, as we shall see, it can be expected to be close enough for quite a while.
The authoritative engine accepts a non-empty request only if all three of the following hold, where Time and Boots are the values submitted by the nonauthoritative engine and snmpEngineTime and snmpEngineBoots are the authoritative engine’s own values:
The maximum allowable clock drift, in other words, is 150 seconds. If the two clocks drift by more than that, the nonauthoritative side must again go through the synchronization process outlined at the end of the previous section.
The actual message signing is based on a shared secret key, authKey.
Users are identified by userName values, and also have human-readable passwords. These passwords must be converted first into keys, in such a way that each ⟨user,agent⟩ pair has its own unique key. We will start with the authentication-only mechanism, which is the same for the MD5 and SHA-1 hashes. We will denote the hash function (either MD5 or SHA-1) by hash().
The first step is to create a digest based on the password. The password is repeated (logically, at least) to fill a buffer 220 bytes in length, and then the hash() function is applied to that megabyte string. The reason for the megabyte extension is to make the password→key conversion process relatively slow, so as to hamper brute-force attacks.
The next step is to take the engineID of the agent for which the user is generating this particular key and take the hash of the concatenation of the digest, engineID and digest again:
k = hash(digest⏜engineID⏜digest)
This is now the ⟨user,agent⟩ shared key. It must be entered (or computed) on the agent, and stored there. For MD5 it is 16 bytes long; for SHA-1 it is 20 bytes long.
An SNMP message mesg is now signed by this key as follows:
The authParameter field – the actual digital signature – is now set to be the first 12 bytes of
hash(k2⏜hash(k1⏜mesg))
This is slightly more complicated than the hash(mesg⏜k) mechanism suggested in Cryptographic Fundamentals, presumably to make it more resistant to potential vulnerabilities in the hash() function.
The receiver can replicate this authParameter calculation and verify that it matches the value transmitted in the packet.
Suppose a user wants to change his or her password, and thus the key k. The manager will need to communicate the new key to the agent in such a way that it is not exposed to eavesdroppers. Here is the mechanism, where, again, hash() is either MD5 or SHA-1 as appropriate; we will use N to denote the length in bytes of the result of hash() (either 16 or 20).
The original key is here denoted oldkey and the new key is newkey.
The manager first chooses a string random of N bytes chosen as randomly as possible. A second N-byte string delta is then calculated as follows:
temp = hash(oldkey⏜random)delta = temp XOR newkey
At this point, random and delta are sent – in the clear – from the manager to the agent. The agent can use random and oldkey to compute temp, and thus newkey = delta XOR temp. An eavesdropper cannot use random to find out anything about temp without knowing oldkey, and cannot get anything useful out of delta unless either newkey or temp is known.
The actual process is to combine random and delta into a single 2N-byte keyChange string.