Diffie-Hellman USM key change¶
RFC 2786 defines SNMP-USM-DH-OBJECTS-MIB, which changes a USM key by
agreeing a new one over the wire. Neither the old key nor the new one is ever
transmitted, and – unlike the RFC 3414 usmUserAuthKeyChange this engine
has always supported – the old key is not needed to perform the change. Only
write access to the row is. That is what makes provisioning an agent remotely
practical rather than a chicken-and-egg problem.
How the exchange works¶
The agent publishes Diffie-Hellman parameters in usmDHParameters and, for
each row of usmDHUserKeyTable, a public value. A manager reads both, offers
a public value of its own, and both sides arrive at a shared secret neither
sent. The new key is the right-most bits of that secret – as many as the row’s
authentication or privacy protocol needs.
The key that results is already localized to the agent: it is derived from a secret only that agent could have computed, so it is used directly rather than being run through RFC 3414 key localization.
Changing a key¶
dh_key_change() runs the whole exchange:
"""
Change a USM authentication key by Diffie-Hellman agreement
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rotate an agent's authentication key without either key crossing the wire and
without needing the old one, per :RFC:`2786`.
The agent must implement SNMP-USM-DH-OBJECTS-MIB, and the credentials used must
have write access to the row. Net-SNMP implements it when built with the
``snmp-usm-dh-objects-mib`` module, which distribution packages usually leave out.
"""
import asyncio
from pysnmp.hlapi.asyncio import (
ContextData,
SnmpEngine,
UdpTransportTarget,
UsmUserData,
dh_key_change,
get_cmd,
usmHMACSHAAuthProtocol,
usmKeyTypeLocalized,
)
from pysnmp.proto.rfc1902 import OctetString
from pysnmp.smi.rfc1902 import ObjectIdentity, ObjectType
async def run():
"""Rotate the key, then use the new one."""
current = UsmUserData(
"usr-sha-none", "authkey1", authProtocol=usmHMACSHAAuthProtocol
)
target = await UdpTransportTarget.create(("demo.pysnmp.com", 161))
result = await dh_key_change(
SnmpEngine(),
current,
target,
ContextData(),
"auth",
)
# The agent re-keyed when the SET committed, so `current` is now stale. The
# derived key is already localized to this agent, and USM needs to be told
# which agent that is -- both come back from the exchange.
rekeyed = UsmUserData(
"usr-sha-none",
result.key,
authProtocol=usmHMACSHAAuthProtocol,
authKeyType=usmKeyTypeLocalized,
securityEngineId=OctetString(result.securityEngineId),
)
errorIndication, errorStatus, errorIndex, varBinds = await get_cmd(
SnmpEngine(),
rekeyed,
await UdpTransportTarget.create(("demo.pysnmp.com", 161)),
ContextData(),
ObjectType(ObjectIdentity("SNMPv2-MIB", "sysDescr", 0)),
)
if errorIndication:
print(errorIndication)
elif errorStatus:
print(
f"{errorStatus.prettyPrint()} at {errorIndex and varBinds[int(errorIndex) - 1][0] or '?'}"
)
else:
for varBind in varBinds:
print(" = ".join([x.prettyPrint() for x in varBind]))
asyncio.run(run())
The synchronous facade offers the same call under
pysnmp.hlapi.dh_key_change, blocking until the exchange completes.
Using the result¶
The agent installs the new key when the SET commits, so the credentials that
ran the change stop working the moment it returns. Build fresh credentials from
the result, which carries both halves of what
UsmUserData needs:
key– the new localized key, passed withauthKeyType=usmKeyTypeLocalized(orprivKeyTypefor a privacy key).securityEngineId– the agent’s authoritative engine ID. A localized key is bound to one engine and USM will not accept it without being told which, so this is not optional. The exchange learns it from the row index, which saves a separate discovery.
Anything that fails raises DHKeyChangeError rather than
returning an error indication, because a key change that half happened leaves
the caller unable to reach the agent.
When the SET is the step that fails¶
Everything the result needs is derived before the SET, so a parameter or key length that cannot produce a key is refused while the agent is still untouched.
The SET itself is the step that cannot be taken back, and its failure is
ambiguous. SNMP over UDP gives no way to tell a refused SET from one that
committed and whose response was lost, and RFC 2786 defines no recovery for
that. A transport target with retries makes it likelier to surface as
wrongValue than as a timeout, because a successful SET makes the agent
publish a fresh public value, so the retransmitted copy no longer matches the
half it echoes.
So the exception carries what is needed to find out:
try:
result = dh_key_change(engine, current, target, ContextData())
except DHKeyChangeError as exc:
if exc.candidate is not None:
# The agent may have re-keyed to this. Try it before concluding
# that the old credentials are still live.
...
raise
Pass a transport target with retries=0 if a timeout is the clearer signal
for your recovery path; the exchange does not override the retry policy it is
given.
Finding the row¶
usmDHUserKeyTable is indexed by engine ID and user name. By default the
table is walked to find the row for the user being changed, so callers do not
need to know the engine ID in advance. An agent that does not permit walking
the table can be addressed directly by passing securityEngineId.
What is supported¶
The manager side of the key change is implemented: all four DHKeyChange
columns, for authentication and privacy keys, for a named user or for the
agent’s own key. pysnmp.proto.secmod.rfc2786 also provides the kickstart
key derivation (PBKDF2 with the salts the MIB fixes) for reading a
usmDHKickstartTable.
The agent side is not implemented – pysnmp does not serve these objects – and
no agent-side implementation of usmDHKickstartTable is known to exist to
interoperate with.
None of this needs a cryptographic library: the agreement is modular
exponentiation, and the one derivation the RFC names is PBKDF2 from
hashlib.
Interoperability¶
The exchange is tested end to end against Net-SNMP in CI, under the v3-dh
profile of the Net-SNMP integration workflow. That profile runs an agent built
with the snmp-usm-dh-objects-mib module, which is not in Net-SNMP’s default
module set and which distribution packages – Debian’s snmpd among them –
leave out. An agent without it answers noSuchObject for the whole
1.3.6.1.3.101 subtree.
The test asserts more than that the exchange completed: it authenticates with the derived key afterwards, which is the only way to observe that the key the agent installed is the key this engine computed.
API reference¶
- async pysnmp.hlapi.asyncio.dh_key_change(snmpEngine: Any, authData: Any, transportTarget: Any, contextData: Any, keyType: str = 'auth', *, own: bool = False, userName: bytes | str | None = None, securityEngineId: bytes | None = None, keyLength: int | None = None, **options: Any) DHKeyChangeResult¶
Change a USM key by Diffie-Hellman agreement (RFC 2786).
Neither the old key nor the new one crosses the wire, and the old one is not needed to run this – only write access to the row. That is what
usmUserAuthKeyChangecannot offer, and what makes provisioning an agent remotely practical.The agent installs the new key when the SET commits, so the credentials used for this call stop working the moment it returns. Build fresh
UsmUserDatafrom the returned key, passingauthKeyType=usmKeyTypeLocalized.- Parameters:
snmpEngine (SnmpEngine) – Class instance representing SNMP engine.
authData (UsmUserData) – The credentials to run the exchange under. Their user name names the row unless userName overrides it, and their protocols say how long the new key is unless keyLength overrides it.
transportTarget (TransportTarget) – Class instance representing transport type along with SNMP peer address.
contextData (ContextData) – Class instance representing SNMP ContextEngineId and ContextName values.
keyType (str, optional) –
"auth"for the authentication key,"priv"for the privacy key.own (bool, optional) – Use the
usmDHUserOwn*column, which is how an agent changes its own key rather than the named user’s.userName (bytes or str, optional) – The USM user whose row to change, when it is not the one authenticating.
securityEngineId (bytes, optional) – The agent’s authoritative engine ID. Given, the row is addressed directly; omitted, the table is walked to find it.
keyLength (int, optional) – Octets of key to derive, when the protocol in authData is not the one the row uses.
**options – Passed through to the underlying commands.
- Returns:
DHKeyChangeResult – The new localized key, the instance that was set and the parameters used.
- Raises:
DHKeyChangeError – If any step fails. Everything is derived before the SET, so the agent’s key is unchanged unless the SET itself was the step that failed – and that case carries a candidate result, because a failed SET may still have been applied. A transport target with retries turns a lost response into a wrongValue on the retransmission rather than a timeout; pass a target with
retries=0if a timeout is the clearer signal for your recovery path.
Examples
>>> import asyncio >>> from pysnmp.hlapi.asyncio import * >>> from pysnmp.proto.rfc1902 import OctetString >>> >>> async def run(): ... result = await dh_key_change( ... SnmpEngine(), ... UsmUserData('user', 'authkey1'), ... await UdpTransportTarget.create(('localhost', 161)), ... ContextData(), ... ) ... return UsmUserData( ... 'user', ... result.key, ... authKeyType=usmKeyTypeLocalized, ... securityEngineId=OctetString(result.securityEngineId), ... ) >>> >>> rekeyed = asyncio.run(run())
- class pysnmp.hlapi.asyncio.DHKeyChangeResult(key: bytes, securityEngineId: bytes, instance: tuple[int, ...], parameters: DHParameters)¶
What a completed key change produced.
- securityEngineId¶
The agent’s authoritative engine ID. Returned because a localized key is only usable with the engine ID it is bound to, and this exchange has already learned it – pass both to
UsmUserData.- Type:
- parameters¶
The agreement parameters the agent published.
Create new instance of DHKeyChangeResult(key, securityEngineId, instance, parameters)
- class pysnmp.proto.secmod.rfc2786.DHParameters(prime: int, base: int, privateValueLength: int | None = None)¶
The prime, base and optional private-value length of an agreement.
Frozen and made only of integers, so importing this module costs nothing and the well-known groups below can be module constants.
- class pysnmp.hlapi.asyncio.DHKeyChangeError(message: str, candidate: DHKeyChangeResult | None = None)¶
A Diffie-Hellman key change did not complete.
Raised rather than returned, because a key change that half happened leaves the caller unable to talk to the agent, and a return value is too easy to ignore.
- candidate¶
Set when the SET was the step that failed, and then the failure is ambiguous: the agent may have applied the change and lost the response. RFC 2786 defines no recovery for that, so the way to find out is to try this key. None for every other step, where nothing was changed.
- Type:
DHKeyChangeResult or None
Record the message and, for a failed SET, the key that may be live.