C#でASN.1のObject Identifierのエンコードを行う

#ASN.1について
ASN.1は、SNMPプロトコルで使用されています。
SNMP通信を行うマネージドライブラリをC#で実装することを最終目的とします。
その前段階として、SNMPプロトコルで使用するASN.1のBERのエンコード、デコード処理をC#で実装します。
ここでは、さらに限定してASN.1のBERのObject Identifierのエンコードを実施します。
なぜ最初にObject IdentifierかというとSNMPではObject Identifier(以下、OIDと省略）を検索キーとしてネットワーク上のデバイスの情報収集を行うからです。

#ASN.1のBERについての公式の情報源について
ITU-TのX.690という文書でASN.1のBERについて定義されています。

#OIDのエンコード方法について
例えば「1.3.6.1.2.1.1.5」をエンコードすると、
「06 07 2B 06 01 02 01 01 05」となります。

## 1バイト目「0x06」について
ASN.1では、TAG + Length + Valueでエンコードします。
TAGはValueの種類を表しています。「0x06」はOIDであることを表しています。
この他にも、「0x02」が整数、「0x13」が表示可能文字列などとなっています。
なお、TAGのエンコードルールはITU-T X.690の範囲のようですが、TAG番号自体はSNMPのルールとなっているようです。

## 2バイト目「0x07」について
2バイト目は、ValueのLengthを表しています。ここではValueが7バイトです。
なお、Lengthが128を超える場合は、Lengthを2バイト以上を使って表します。
Lengthが128を超える場合は、1バイト目にLengthを表すのに必要なバイト数、2バイト目以降で実際のLengthを表します。
但し、1バイト目のビット8には1をセットします。

例えば、Length = 200の場合、Lengthは「81 C8」となります。0xC8は10進で200です。
81は、2進数で「1000 0001」です。ビット8の1を取り除くと「0000 0001」でLengthのLengthが1バイトであることを表しています。

もう1つ例を上げると、Lengthが「20000」の場合は「82 4E 20」となります。

## 3バイト目以降「2B 06 01 02 01 01 05」について
3バイト目以降がValueになります。
ここに、OID「1.3.6.1.2.1.1.5」のエンコードされた値が入っています。
OIDをエンコードする場合は1バイト目と、2バイト目について特殊な加工があります。

「1バイト目 x 40 + 2バイト目」で算出した値をまとめて、1ﾊﾞｲﾄ目とします。
つまり、「1 x 40 + 3」= 「43」 = 「0x2B」となります。

OIDの最初のバイトは0, 1, 2のいずれかであることが決まっているためバイト数圧縮のため1ﾊﾞｲﾄにまとめているものと思われます。

OIDの3バイト目以降は基本的には数値をそのままバイトに変換していきます。
ただし、128以上の場合は1ﾊﾞｲﾄではなく複数バイトで表すことになっています。
128進数に分割して複数バイトに設定し、最終バイト以外には2進で「1000 0000」を足します。

例えば、「56789」は「 3 x (128^2) + 59 x (128^1)  + 85 x (128^0)」となりますので、
128進数で表すと、「3 59 85」ですがそれぞれ16進数に直して「03 3B 55」となり更に最終バイト以外には「1000 0000」(0x80）を加算して、「83 BB 55」となります。

#以上をC#のプログラムにします。

まずは今後の拡張を考えて抽象クラスを1つ作成します。
エンコードされたバイトリストを返却する「GetBytes」
TAGを表すバイトを返却する「GetTag」を抽象メソッドとして持ちます。
また、TLVの「Length」をエンコードするメソッドを定義しておきます。

## abstract class AsnValue

```C#
    public abstract class AsnValue
    {
        public abstract List<byte> GetBytes();
        public abstract byte GetTag();

        public static List<byte> GetFieldLengthByte(int length)
        {
            if (length < 128)
            {
                return new byte[] { (byte)length }.ToList();
            }
            else
            {
                List<byte> result = new List<byte>();
                for (; ; )
                {
                    int amari = length % 256;
                    int sho = length / 256;
                    result.Insert(0, (byte)amari);
                    if (sho == 0)
                    {
                        break;
                    }
                    length = sho;

                }
                result.Insert(0, (byte)(result.Count + 0b10000000));
                return result;
            }

        }

    }
```

「0b10000000」は2進数リテラル表記ですが、ビルドエラーになる場合は「0x80」で置き換え可能です。

## class ObjectIdentifierValue : AsnValue

次にAsnValueを継承する「ObjectIdentifierValue」クラスを作成します。
ここまでのObjectIdentifierValueのエンコードの方法の説明と処理内容を照らし合わせていけば
理解出来ると思います。

```C#
    public class ObjectIdentifierValue : AsnValue
    {
        public String Oid {get; set; }

        public override byte GetTag()
        {
            return 0x06;
        }

        public ObjectIdentifierValue(String Oid)
        {
            this.Oid = Oid;
        }

        public override List<byte> GetBytes()
        {
            // OIDをピリオドで区切って数値配列に分けて格納する。 
            int[] values = Oid.Split('.').Select(int.Parse).ToArray();

            List<byte> result = new List<byte>();

            // 1個目、2個目 
            result.AddRange(ConvByte(ParseXY(values[0], values[1])));
            for (int i = 2; i < values.Length; i++)
            {
                result.AddRange(ConvByte(values[i]));
            }
            result.InsertRange(0, GetFieldLengthByte(result.Count));

            result.Insert(0, GetTag());

            return result;
        }


        private List<byte> ConvByte(int param)
        {
            List<byte> result = new List<byte>();
            int value = param;
            int i = 0;

            for (; ; )
            {
                int amari = value % 128;
                int sho = value / 128;
                result.Insert(0, (byte)(amari + (i == 0 ? 0 : 0x80)));
                if (sho == 0)
                {
                    break;
                }
                value = sho;
                i++;

            }
            return result;
        }

        private int ParseXY(int first, int second)
        {
            return (first * 40 + second);
        }

    }
```

#使い方

下記のように画面を作成します。

![TryOidValue.png](https://qiita-image-store.s3.amazonaws.com/0/197947/1e8103e9-27d1-4d55-3841-5e78639a31e3.png)

button1のイベントで下記のように、ObjectIdentifierValueを生成して、GetBytesで取得したByteのリストを
画面に出力するコードでOIDのASN.1によるエンコードがうまくいくか試します。

```C#
        private void button1_Click(object sender, EventArgs e)
        {
            ObjectIdentifierValue oidValue = new ObjectIdentifierValue(txtOriginal.Text);
            txtConverted.Text = BitConverter.ToString(oidValue.GetBytes().ToArray()).Replace("-", " ");
        }
```

button2はついでに、TLVのLengthのエンコードがうまくいっているか試しています。

```C#
        private void button2_Click(object sender, EventArgs e)
        {
            txtLengthBytes.Text = BitConverter.ToString(AsnValue.GetFieldLengthByte(Int32.Parse(txtLength.Text)).ToArray());


        }
```

それぞれ、実行してみます。

![TryOidValueResult.png](https://qiita-image-store.s3.amazonaws.com/0/197947/9aedb2b6-eea3-0492-972f-557ba34fb57d.png)

とりあえずは想定通りのエンコードが出来ていることが分かります。

SNMP通信で使用する場合は、取得したByteリストをByte配列に変換して送信することになります。
もっとも今回作成したのはSNMPで使用するデータの一部分ですのでこれだけではまだSNMP通信はできません。
この他に、SNMPのバージョン番号(v1, v2, v3など）、コミュニティ名(publicなど）、コマンド名（get-Request, get-NextRequest)などのデータをあわせて送信する必要があります。今後、これらのデータの作成方法も公開していく予定です。

#snmpコマンドで答え合わせ

centos7上でsnmpgetnextコマンドでsnmpのパケットを送信し、tcpdumpでパケットを取得して送信されているパケットを確認します。

```shell

snmpgetnext -v1 10.0.75.1 -c public 1.3.6.1.2.1.1.5
```
取得したパケットを確認します。

![SNMP.png](https://qiita-image-store.s3.amazonaws.com/0/197947/f717cfaa-cc06-153a-d245-4660daac4be7.png)

「1.3.6.1.2.1.1.5」の部分のバイト配列を確認すると、
「06 07 2B 06 01 02 01 01 05 05 00」となっています。

最後の「05 00」は、ASN.1のNullValueです。snmpgetnextコマンドではOIDとNullValueをセットでMIB値の取得要求をしますので、最後にNullValueのバイトが付加されています。
最後のNullValueを除くと今回作成したプログラムの実行結果と一致していることが確認できます。

##Windows10のSNMPエージェント機能を有効にして応答メッセージも確認

Windows10では、SNMPを下記の通りまずは有効にします。

![SNMPRESPONSE.png](https://qiita-image-store.s3.amazonaws.com/0/197947/3a4589a6-6a70-b17f-f0d2-bebd35227c43.png)

次にサービスのSNMP Serviceで次の通りの設定を行うことによりSNMP通信に応答するようになります。

![SNMPPublic.png](https://qiita-image-store.s3.amazonaws.com/0/197947/6b84069d-bc00-96e9-0f1f-8be7dd3a7206.png)

Windows10での先ほどのOIDのレスポンスは下記の通り。

![SNMPRESPONSE.png](https://qiita-image-store.s3.amazonaws.com/0/197947/fe2b18c8-2692-eb7e-bc0a-e4a739abe4f8.png)

「1.3.6.1.2.1.1.5」はMIBでsysNameと定義されています。
Windows10からはコンピュータ名が取得できるようです。
なお、応答メッセージのOIDのは、「1.3.6.1.2.1.1.5.0」となっていて最後に「.0」が付加されています。
これはインデックスです。送信メッセージではNullValueになっていたところに、ASN.1のOctetStringでコンピューター名が設定されていることが分かります。


#最後に
今回は、ASN.1で少し手こずる部分についてのエンコードをここでは実施しました。
特にTLVのLengthのエンコード、OIDの大きな数値のエンコードについては、あまり出てこないので
手こずるところです。
今後は、エンコードされたバイト配列から元の情報に戻すデコードについても実装して行く予定です。
SNMPは古典的な技術ですが今でもプリンタやサーバーなどの監視に使用しているところが残っています。
デバイスの種類やその他の情報を手軽に取得する手段としては共通化されているため、今後もしばらくは使用されることもあるかと思います。
.NETは気軽にネットワークツールの開発が出来るツールですので、SNMPプロトコルのマネージドライブラリを作っておいて、気軽にデバイスとのコミュニケーションを取れるようにしたいと考えています。

###続きはこちらです。
[C#でSNMPのGetNextメッセージをエンコードする](https://qiita.com/sukkyxp/items/45d508446810aa217265)