From 3a084820f5974b1230f74827c05917e0a47d8979 Mon Sep 17 00:00:00 2001 From: Harald Wolff Date: Mon, 11 Mar 2019 15:07:34 +0100 Subject: [PATCH] WIP USM --- SNMPEngine.cs | 31 ++++ SNMPInterface.cs | 16 +- channel/USMEndpoint.cs | 145 --------------- endpoint/SNMPEndpoint.cs | 99 ++++++++++ {channel => endpoint}/SnmpV1Endpoint.cs | 4 +- {channel => endpoint}/SnmpV2Endpoint.cs | 4 +- endpoint/USMEndpoint.cs | 237 ++++++++++++++++++++++++ ln.snmp.csproj | 11 +- types/SnmpMessage.cs | 2 +- types/SnmpV1Message.cs | 2 +- types/SnmpV2Message.cs | 2 +- types/USMMessage.cs | 13 +- 12 files changed, 399 insertions(+), 167 deletions(-) delete mode 100644 channel/USMEndpoint.cs create mode 100644 endpoint/SNMPEndpoint.cs rename {channel => endpoint}/SnmpV1Endpoint.cs (94%) rename {channel => endpoint}/SnmpV2Endpoint.cs (94%) create mode 100644 endpoint/USMEndpoint.cs diff --git a/SNMPEngine.cs b/SNMPEngine.cs index 0e135bf..4a1b5c6 100644 --- a/SNMPEngine.cs +++ b/SNMPEngine.cs @@ -15,6 +15,7 @@ using System.Net; using System.Threading; using ln.snmp.asn1; using ln.logging; +using ln.snmp.endpoint; namespace ln.snmp { @@ -29,6 +30,8 @@ namespace ln.snmp private bool shutdown = false; private Thread ReceiverThread { get; set; } + private Dictionary currentEndpoints = new Dictionary(); + private int nextMessageID = (int)DateTimeOffset.Now.ToUnixTimeSeconds(); public int NextMessageID => nextMessageID++; @@ -101,6 +104,17 @@ namespace ln.snmp } } + public void SendMessage(IPEndPoint remoteEndpoint,SnmpMessage message) + { + ASN1Value snmpMessage = message; + byte[] snmpMessageBytes = snmpMessage.AsByteArray; + + lock (LocalEndpoint) + { + LocalEndpoint.Send(snmpMessageBytes, snmpMessageBytes.Length, remoteEndpoint); + } + } + public SnmpMessage SNMPRequest(IPEndPoint remoteEndpoint,SnmpMessage snmpMessage,int timeout) { lock (queuedRequests) @@ -136,6 +150,23 @@ namespace ln.snmp } } + public void RegisterEndpoint(SnmpEndpoint intf) + { + lock (currentEndpoints) + { + currentEndpoints.Add(intf.RemoteEndpoint,intf); + } + } + public void UnregisterEndpoint(SnmpEndpoint intf) + { + lock (currentEndpoints) + { + currentEndpoints.Remove(intf.RemoteEndpoint); + } + } + + + public void Dispose() { if (ReceiverThread != null) diff --git a/SNMPInterface.cs b/SNMPInterface.cs index e661f7d..ff1e4e6 100644 --- a/SNMPInterface.cs +++ b/SNMPInterface.cs @@ -3,24 +3,19 @@ using ln.snmp.types; using System.Net; using System.Collections.Generic; using System.Linq; -using ln.snmp.channel; +using ln.snmp.endpoint; using System.Reflection; namespace ln.snmp { public enum SnmpVersion : int { V1 = 0, V2c = 1, V3 = 3 } - public abstract class SNMPInterface + public abstract class SnmpInterface : IDisposable { - public SNMPEngine SNMPEngine { get; } - public IPEndPoint RemoteEndpoint { get; set; } - public abstract SnmpVersion SnmpVersion { get; } - public SNMPInterface(SNMPEngine snmpEngine,IPEndPoint remoteEndpoint) + public SnmpInterface() { - SNMPEngine = snmpEngine; - RemoteEndpoint = remoteEndpoint; } public abstract PDU snmpRequest(PDU pdu); @@ -264,6 +259,9 @@ namespace ln.snmp return snmpWalk(new ObjectIdentifier(objectIdentifier)); } - + public virtual void Dispose() + { + SNMPEngine.UnregisterEndpoint(this); + } } } diff --git a/channel/USMEndpoint.cs b/channel/USMEndpoint.cs deleted file mode 100644 index 7437b65..0000000 --- a/channel/USMEndpoint.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; -using System.Net; -using ln.snmp.types; -using System.Text; -using System.Security.Cryptography; -using ln.snmp.asn1; -using System.Linq; - -namespace ln.snmp.channel -{ - public enum SnmpV3AuthMethod { MD5, SHA } - public enum SnmpV3PrivMethod { DES } - public enum SnmpAuthLevel : int { noAuthNoPriv = 0, authNoPriv = 0x01, authPriv = 0x03 } - - public class USMEndpoint : SNMPInterface - { - public OctetString RemoteEngineID { get; set; } - - public string Username { get; set; } - public string AuthKey { get; set; } - public string PrivKey { get; set; } - - public SnmpV3AuthMethod AuthMethod { get; set; } - public SnmpV3PrivMethod PrivMethod { get; set; } - - public override SnmpVersion SnmpVersion => SnmpVersion.V3; - - public USMEndpoint(SNMPEngine snmpEngine,IPEndPoint remoteEndpoint) - :base(snmpEngine,remoteEndpoint) - { - } - - public SnmpAuthLevel AuthLevel - { - get - { - if (PrivKey != null) - return SnmpAuthLevel.authPriv; - if (AuthKey != null) - return SnmpAuthLevel.authNoPriv; - - return SnmpAuthLevel.noAuthNoPriv; - } - } - - public override PDU snmpRequest(PDU pdu) - { - if ((RemoteEngineID == null)||(RemoteEngineID.Bytes.Length == 0)) - { - QueryEngineID(); - } - - USMMessage request = new USMMessage(); - ScopedPDU scopedPDU = new ScopedPDU(); - - request.MessageID = SNMPEngine.NextMessageID; - request.msgData = scopedPDU; - - scopedPDU.contextEngineID = RemoteEngineID; - scopedPDU.PDU = pdu; - - AuthenticateMessage(request); - - - SnmpMessage reply = SNMPEngine.SNMPRequest(RemoteEndpoint, request, SNMPEngine.Timeout); - USMMessage replyUSM = reply as USMMessage; - - PDU responsePDU = (replyUSM.msgData as ScopedPDU).PDU; - - return responsePDU; - } - - public void QueryEngineID() - { - USMMessage queryMessage = new USMMessage(); - queryMessage.MessageID = SNMPEngine.NextMessageID; - - ScopedPDU scopedPDU = new ScopedPDU(); - - queryMessage.msgData = scopedPDU; - - queryMessage.Dump(); - - - SnmpMessage reply = SNMPEngine.SNMPRequest(RemoteEndpoint, queryMessage, SNMPEngine.Timeout); - USMMessage usmReply = reply as USMMessage; - - RemoteEngineID = usmReply.SecurityParameters.msgAuthoritativeEngineID; - } - - public bool AuthenticateMessage(USMMessage message) - { - message.msgGlobalData.msgFlags.Bytes = new byte[] { (byte)(int)AuthLevel }; - message.SecurityParameters.msgUserName.StringValue = Username; - message.SecurityParameters.msgAuthoritativeEngineID = RemoteEngineID; - message.SecurityParameters.msgAuthenticationParameters.Bytes = new byte[12]; - - byte[] wholeMsg = ((ASN1Value)message).AsByteArray; - - byte[] extendedAuthKey = new byte[64]; - byte[] authKey = Encoding.ASCII.GetBytes(AuthKey); - - //Array.Copy(authKey, extendedAuthKey, 16); - - //byte[] K1 = new byte[64]; - //for (int n = 0; n < 64; n++) - // K1[n] = (byte)(extendedAuthKey[n] ^ IPAD[n]); - - //byte[] K2 = new byte[64]; - //for (int n = 0; n < 64; n++) - //K2[n] = (byte)(extendedAuthKey[n] ^ OPAD[n]); - - HMAC hmac = null; - - switch (AuthMethod) - { - case SnmpV3AuthMethod.MD5: - hmac = HMACMD5.Create(); - break; - case SnmpV3AuthMethod.SHA: - hmac = HMACSHA1.Create(); - break; - } - - hmac.Key = authKey; - byte[] mac = hmac.ComputeHash(wholeMsg); - - message.SecurityParameters.msgAuthenticationParameters.Bytes = mac.Take(12).ToArray(); - - return true; - } - - static byte[] IPAD = __PAD(0x36); - static byte[] OPAD = __PAD(0x5C); - - public static byte[] __PAD(byte pad) - { - byte[] ipad = new byte[0x40]; - for (int n = 0; n < ipad.Length; n++) - ipad[n] = pad; - return ipad; - } - - } -} diff --git a/endpoint/SNMPEndpoint.cs b/endpoint/SNMPEndpoint.cs new file mode 100644 index 0000000..07fb3dc --- /dev/null +++ b/endpoint/SNMPEndpoint.cs @@ -0,0 +1,99 @@ +using System; +using ln.snmp.types; +using System.Net; +using System.Collections.Generic; +using System.Linq; +using ln.snmp.endpoint; +using System.Reflection; +using System.Threading; +using ln.logging; + +namespace ln.snmp.endpoint +{ + public abstract class SnmpEndpoint : SnmpInterface, IDisposable + { + public SNMPEngine SNMPEngine { get; } + public IPEndPoint RemoteEndpoint { get; set; } + + public SnmpEndpoint(SNMPEngine snmpEngine, IPEndPoint remoteEndpoint) + { + SNMPEngine = snmpEngine; + RemoteEndpoint = remoteEndpoint; + SNMPEngine.RegisterEndpoint(this); + } + + public virtual void Send(SnmpMessage message) + { + message = Encrypt(message); + message = ApplyAuthentication(message); + SNMPEngine.SendMessage(RemoteEndpoint, message); + } + + public virtual void Received(SnmpMessage message) + { + message = CheckAuthentication(message); + message = Decrypt(message); + + + + lock (queuedRequests) + { + if (queuedRequests.ContainsKey(message.MessageID)) + { + SnmpMessage queuedMessage = queuedRequests[message.MessageID]; + lock (queuedMessage) + { + queuedRequests[message.MessageID] = message; + Monitor.PulseAll(queuedMessage); + } + } + else + { + Logging.Log(LogLevel.WARNING, "SNMPEndpoint: Received unqueued message id: {0} (0x{0:x8})", message.MessageID); + } + } + } + + public abstract SnmpMessage Encrypt(SnmpMessage message); + public abstract SnmpMessage ApplyAuthentication(SnmpMessage message); + + public abstract SnmpMessage CheckAuthentication(SnmpMessage message); + public abstract SnmpMessage Decrypt(SnmpMessage message); + + /** + * Requests + **/ + private Dictionary queuedRequests = new Dictionary(); + + public SnmpMessage EnqueueRequest(SnmpMessage requestMessage) + { + requestMessage.MessageID = SNMPEngine.NextMessageID; + + lock (queuedRequests) + { + queuedRequests.Add(requestMessage.MessageID, requestMessage); + lock (requestMessage) + { + Send(requestMessage); + + Monitor.Exit(queuedRequests); + Monitor.Wait(requestMessage, SNMPEngine.Timeout); + Monitor.Enter(queuedRequests); + } + + SnmpMessage responseMessage = queuedRequests[requestMessage.MessageID]; + if (responseMessage == requestMessage) + throw new TimeoutException(); + + return responseMessage; + } + } + + + public override void Dispose() + { + SNMPEngine.UnregisterEndpoint(this); + base.Dispose(); + } + } +} diff --git a/channel/SnmpV1Endpoint.cs b/endpoint/SnmpV1Endpoint.cs similarity index 94% rename from channel/SnmpV1Endpoint.cs rename to endpoint/SnmpV1Endpoint.cs index 9930300..89c0f07 100644 --- a/channel/SnmpV1Endpoint.cs +++ b/endpoint/SnmpV1Endpoint.cs @@ -3,9 +3,9 @@ using ln.snmp.types; using System.Net; using System.Collections.Generic; -namespace ln.snmp.channel +namespace ln.snmp.endpoint { - public class SnmpV1Endpoint : SNMPInterface + public class SnmpV1Endpoint : SnmpEndpoint { public OctetString CommunityString { get; set; } diff --git a/channel/SnmpV2Endpoint.cs b/endpoint/SnmpV2Endpoint.cs similarity index 94% rename from channel/SnmpV2Endpoint.cs rename to endpoint/SnmpV2Endpoint.cs index 4fea0c3..9f1cb1f 100644 --- a/channel/SnmpV2Endpoint.cs +++ b/endpoint/SnmpV2Endpoint.cs @@ -3,9 +3,9 @@ using ln.snmp.types; using System.Net; using System.Collections.Generic; -namespace ln.snmp.channel +namespace ln.snmp.endpoint { - public class SnmpV2Endpoint : SNMPInterface + public class SnmpV2Endpoint : SnmpEndpoint { public OctetString CommunityString { get; set; } diff --git a/endpoint/USMEndpoint.cs b/endpoint/USMEndpoint.cs new file mode 100644 index 0000000..6b76275 --- /dev/null +++ b/endpoint/USMEndpoint.cs @@ -0,0 +1,237 @@ +using System; +using System.Net; +using ln.snmp.types; +using System.Text; +using System.Security.Cryptography; +using ln.snmp.asn1; +using System.Linq; +using ln.logging; + +namespace ln.snmp.endpoint +{ + public enum SnmpV3AuthMethod { MD5, SHA } + public enum SnmpV3PrivMethod { DES } + public enum SnmpAuthLevel : int { noAuthNoPriv = 0, authNoPriv = 0x01, authPriv = 0x03 } + + public class USMEndpoint : SnmpEndpoint + { + public OctetString RemoteEngineID { get; set; } + public Integer CacheAuthoritativeEngineBoots { get; set; } + public Integer CacheAuthoritativeEngineTime { get; set; } + + public string Username { get; set; } + + public byte[] AuthKey { get; set; } + public byte[] PrivKey { get; set; } + + public byte[] LocalAuthKey { get; set; } + public byte[] LocalPrivKey { get; set; } + + public SnmpV3AuthMethod AuthMethod { get; set; } + public SnmpV3PrivMethod PrivMethod { get; set; } + + public override SnmpVersion SnmpVersion => SnmpVersion.V3; + + public USMEndpoint(SNMPEngine snmpEngine,IPEndPoint remoteEndpoint) + :base(snmpEngine,remoteEndpoint) + { + } + + public SnmpAuthLevel AuthLevel + { + get + { + if (PrivKey != null) + return SnmpAuthLevel.authPriv; + if (AuthKey != null) + return SnmpAuthLevel.authNoPriv; + + return SnmpAuthLevel.noAuthNoPriv; + } + } + + public override PDU snmpRequest(PDU pdu) + { + if ((RemoteEngineID == null)||(RemoteEngineID.Bytes.Length == 0)) + { + QueryEngineID(); + } + + USMMessage request = new USMMessage(); + ScopedPDU scopedPDU = new ScopedPDU(); + + request.MessageID = SNMPEngine.NextMessageID; + + request.msgData = scopedPDU; + + scopedPDU.contextEngineID = RemoteEngineID; + scopedPDU.PDU = pdu; + + AuthenticateMessage(request); + + USMMessage replyUSM = InternalRequest(request); + + PDU responsePDU = (replyUSM.msgData as ScopedPDU).PDU; + + return responsePDU; + } + + private USMMessage InternalRequest(USMMessage request) + { + SnmpMessage reply = SNMPEngine.SNMPRequest(RemoteEndpoint, request, SNMPEngine.Timeout); + USMMessage usmReply = reply as USMMessage; + + CacheAuthoritativeEngineBoots = usmReply.SecurityParameters.msgAuthoritativeEngineBoots; + CacheAuthoritativeEngineTime = usmReply.SecurityParameters.msgAuthoritativeEngineTime; + + return usmReply; + } + + public void QueryEngineID() + { + USMMessage queryMessage = new USMMessage(); + queryMessage.MessageID = SNMPEngine.NextMessageID; + + ScopedPDU scopedPDU = new ScopedPDU(); + + queryMessage.msgData = scopedPDU; + + queryMessage.Dump(); + + USMMessage usmReply = InternalRequest(queryMessage); + + RemoteEngineID = usmReply.SecurityParameters.msgAuthoritativeEngineID; + //RemoteEngineID.Bytes = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 }; + + LocalizeKeys(); + } + + public bool AuthenticateMessage(USMMessage message) + { + message.msgGlobalData.msgFlags.Bytes = new byte[] { (byte)(int)AuthLevel }; + message.SecurityParameters.msgUserName.StringValue = Username; + message.SecurityParameters.msgAuthoritativeEngineID = RemoteEngineID; + message.SecurityParameters.msgAuthenticationParameters.Bytes = new byte[12]; + message.SecurityParameters.msgAuthoritativeEngineBoots = CacheAuthoritativeEngineBoots != null ? CacheAuthoritativeEngineBoots : new Integer(); + message.SecurityParameters.msgAuthoritativeEngineTime = CacheAuthoritativeEngineTime != null ? CacheAuthoritativeEngineTime : new Integer(); + + + byte[] wholeMsg = ((ASN1Value)message).AsByteArray; + + byte[] extendedAuthKey = new byte[64]; + byte[] authKey = LocalAuthKey; + + Array.Copy(authKey, extendedAuthKey, authKey.Length); + + byte[] K1 = new byte[64]; + for (int n = 0; n < 64; n++) + K1[n] = (byte)(extendedAuthKey[n] ^ IPAD[n]); + + byte[] K2 = new byte[64]; + for (int n = 0; n < 64; n++) + K2[n] = (byte)(extendedAuthKey[n] ^ OPAD[n]); + + using (HashAlgorithm hash = CreateAuthHashAlgorithm()) + { + byte[] b1 = new byte[K1.Length + wholeMsg.Length]; + + Array.Copy(K1, b1, K1.Length); + Array.Copy(wholeMsg, 0, b1, K1.Length, wholeMsg.Length); + + byte[] intermediate = hash.ComputeHash(b1); + byte[] inter2 = new byte[K2.Length + intermediate.Length]; + + Array.Copy(K2, inter2, K2.Length); + Array.Copy(intermediate, 0, inter2, K2.Length, intermediate.Length); + + byte[] mac = hash.ComputeHash(inter2); + + message.SecurityParameters.msgAuthenticationParameters.Bytes = mac.Take(12).ToArray(); + } + + return true; + } + + private byte[] DeriveKey(string passphrase,HashAlgorithm hash) + { + using (hash) + { + byte[] pass = Encoding.ASCII.GetBytes(passphrase); + byte[] block = new byte[64]; + + for (int n = 0; n < 1048576; n += 64) + { + for (int p = 0; p < block.Length; p++) + { + block[p] = pass[(p + n) % pass.Length]; + } + hash.TransformBlock(block, 0, block.Length, block, 0); + } + + hash.TransformFinalBlock(new byte[0], 0, 0); + + byte[] result = hash.Hash; + return result; + } + } + + private HashAlgorithm CreateAuthHashAlgorithm() + { + return AuthMethod == SnmpV3AuthMethod.SHA ? (HashAlgorithm)SHA1.Create() : (HashAlgorithm)MD5.Create(); + } + + private void LocalizeKeys() + { + if ((AuthKey != null) && (RemoteEngineID != null)) + { + using (HashAlgorithm hash = CreateAuthHashAlgorithm()) + { + byte[] engineID = (byte[])RemoteEngineID.Bytes.Clone(); + + hash.TransformBlock(AuthKey, 0, AuthKey.Length, AuthKey, 0); + hash.TransformBlock(engineID, 0, engineID.Length, engineID, 0); + hash.TransformBlock(AuthKey, 0, AuthKey.Length, AuthKey, 0); + + hash.TransformFinalBlock(new byte[0], 0, 0); + + LocalAuthKey = hash.Hash; + + Logging.Log(LogLevel.DEBUG, "Derived LocalAuthKey from AuthKey: {0}", BitConverter.ToString(LocalAuthKey)); + } + } + } + + public string AuthKeyPhrase + { + get => throw new NotSupportedException(); + set + { + AuthKey = DeriveKey( + value, + CreateAuthHashAlgorithm() + ); + + Logging.Log(LogLevel.DEBUG, "Derived AuthKey from Phrase: {0}",BitConverter.ToString(AuthKey)); + + if (RemoteEngineID != null) + LocalizeKeys(); + } + } + public string PrivKeyPhrase { get; set; } + + + + + static byte[] IPAD = __PAD(0x36); + static byte[] OPAD = __PAD(0x5C); + + public static byte[] __PAD(byte pad) + { + byte[] ipad = new byte[0x40]; + for (int n = 0; n < ipad.Length; n++) + ipad[n] = pad; + return ipad; + } + + } +} diff --git a/ln.snmp.csproj b/ln.snmp.csproj index b165862..8c10bb1 100644 --- a/ln.snmp.csproj +++ b/ln.snmp.csproj @@ -44,11 +44,10 @@ - - - - + + + @@ -57,10 +56,12 @@ + + - + diff --git a/types/SnmpMessage.cs b/types/SnmpMessage.cs index 410a0f1..057d91f 100644 --- a/types/SnmpMessage.cs +++ b/types/SnmpMessage.cs @@ -16,7 +16,7 @@ namespace ln.snmp.types public SnmpVersion SnmpVersion { get; } public abstract int MessageID { get; set; } - + public abstract PDU snmpPDU { get; set; } public SnmpMessage(SnmpVersion snmpVersion) : base(new Identifier(IdentifierClass.UNIVERSAL, true, 0x10)) diff --git a/types/SnmpV1Message.cs b/types/SnmpV1Message.cs index 88c933c..c187b6f 100644 --- a/types/SnmpV1Message.cs +++ b/types/SnmpV1Message.cs @@ -14,7 +14,7 @@ namespace ln.snmp.types public class SnmpV1Message : SnmpMessage { public OctetString snmpCommunity { get; set; } - public PDU snmpPDU { get; set; } + public override PDU snmpPDU { get; set; } public SnmpV1Message() :base(SnmpVersion.V1) diff --git a/types/SnmpV2Message.cs b/types/SnmpV2Message.cs index 972a3e5..4499d67 100644 --- a/types/SnmpV2Message.cs +++ b/types/SnmpV2Message.cs @@ -15,7 +15,7 @@ namespace ln.snmp.types public class SnmpV2Message : SnmpMessage { public OctetString snmpCommunity { get; set; } - public PDU snmpPDU { get; set; } + public override PDU snmpPDU { get; set; } public SnmpV2Message() :base(SnmpVersion.V2c) diff --git a/types/USMMessage.cs b/types/USMMessage.cs index 79a78ca..f8cbd35 100644 --- a/types/USMMessage.cs +++ b/types/USMMessage.cs @@ -10,7 +10,7 @@ using System; using ln.snmp.asn1; using System.Linq; -using ln.snmp.channel; +using ln.snmp.endpoint; namespace ln.snmp.types { public class USMMessage : SnmpMessage @@ -22,8 +22,17 @@ namespace ln.snmp.types public MsgGlobalData msgGlobalData { get; set; } public UsmSecurityParameters SecurityParameters { get; set; } + public ScopedPDU ScopedPDU { get; set; } + public OctetString EncryptedPDU { get; set; } + public Variable msgData { get; set; } + public override PDU snmpPDU + { + get => ScopedPDU?.PDU; + set => ScopedPDU.PDU = value; + } + public USMMessage() : base(SnmpVersion.V3) { @@ -50,6 +59,8 @@ namespace ln.snmp.types { msgGlobalData = new MsgGlobalData(value[1]); SecurityParameters = new UsmSecurityParameters(new ASN1Value(value[2].Bytes)); + + // ToDo: Check if value[3] is OctetString (Encrypted) or Sequence (ScopedPDU) msgData = value[3]; } }