ASN.1 DER 格式編碼與解碼


在工作時常會遇到的幾個資料,certificate(X.509 DER),signed image(PKCS#7 DER), snmp packet
這些資料都用了 ASN.1 的 DER 格式。雖然有很多工具能把這些值解出來,但還是希望能靠著hexdump和小算盤就解出資料,有時也想要對資料做一些處理
那就只好硬吞spec,看它到底是如何編碼的。不過因為英文不好,spec看不太懂,有時候反而覺得程式碼看起來還比較輕切,於是一邊看spec,搭配tcl的ASN package來了解編碼方式
這邊紀錄一些常看到的資料類型,了解怎麼編碼解碼之後,就可以編輯certificate,SNMP 封包,當然直接使用現成的程式碼移植擴展到TCL是最方便的,但我不是每次都能順利地把
程式碼轉成TCL用的格式,在還沒編譯出TCL的dll時,就是純用TCL硬上

參考資料
ITU-T X.680
ITU-T X.690
http://www.obj-sys.com/asn1tutorial/asn1only.html




DER編碼把每一筆資料分三部分 Identifier octets, Length octets, Contents octets


Identifier octets


Identifier octets 存放的值就是ASN.1 的tag,tag可以分成4個Class,這可以用前2 bits來編碼,再用 bit6 把tag分成Primitive(0)或 Constructed(1)

Class Bit 8 Bit 7
UNIVERSAL 0 0
Application 0 1
Context-specific 1 0
Private 1 1

8 7 6 5 4 3 2 1
Class P/C Tag Number

Tag class 使用原則(T-REC-X.680-200811-I -> G.2.12 Tagged)


1.UNIVERSAL : Universal class tags are used only within this Recommendation | International Standard.
應該就是指X.680裡提到的那些 type, 例如: Boolean(0x01), Integer(0x02), Bitstring(0x03) ...

2.Application : A frequently encountered style for the use of tags is to assign an application class tag precisely once in the
entire specification, using it to identify a type that finds wide, scattered, use within the specification.

例如: RFC2578 SMIv2 定義了一些snmp用到的type, IpAddress(0x40), Unsigned32(0x42), TimeTicks(0x43) ...
可以抓snmp封包驗證

3.Context-specific : Context-specific tagging is frequently applied in an algorithmic manner to all components of a SET, SEQUENCE, or CHOICE.

例如: RFC1157 SNMP 定義的SNMP PDU(protocol data unit), GetRequest(0xA0), GetNextRequest(0xA1), GetResponse(0xA2) ...

4.Private : Private class tagging should normally not be used in internationally standardized specifications (although this
cannot be prohibited).
還沒看過這類的Tag

Tag 編碼方式


這個 byte 被保留了 3 bits 用來區分類型,只剩 5 bits 來表示 tag number,通常應該夠用(32種),不過這套編碼都預留好了不夠用時的編碼方式,所以31 (111112)這個值保留做其他用途,
所以1個byte 能表示的 tag number只有0~30
當tag number >= 31 時的編碼方式 : 第一個 byte 前3 bit (8~6) 依照 class, primitive 編碼, bit5~bit1 全部填 1 (111112 被保留在這邊使用了)
接下來就看tag number大小來決定,先轉成二進位,在前方(MSB)補0,直到bitstring為7的整數倍,再以 7 bits為一組拆開,若是最後一組 bit 8填0,否則填1

例如: 建一組 Tag, calss=Application 且為 primitive, tag number = 200
第一 byte => 0101111 => 0x5F
第一 byte => 01011111 => 0x5F
200=0xC8 => 11001000 => 0000001 1001000 => 10000001 01001000 => 0x8148
會被編成 0x5F8148


tcllib有 asn package 可以處理編碼和解碼

package require asn

# 用來看ASN.1編碼的結果,以下範例都當作有這個proc以及以載入asn package
proc hexdump {data} {
 binary scan $data H* hex 
 puts "[string toupper [regsub -all .. $hex "& "] ]\n"
}

# tag encoding
# syntax:  ::asn::asnTag tagnumber ?class? ?tagstyle?
# class: U,A,C,P 預設值為U
# tagstyle: P,C  預設值為P

puts "Test 1: encoding UNIVERSAL Primitive 2"
hexdump [::asn::asnTag 2 U P]

puts "Test 2: encoding Application Primitive 200"
hexdump [::asn::asnTag 200 A  P]

puts "Test 3: encoding Context Constructed 999"
hexdump [::asn::asnTag 999 C C]

# tag decoding
# syntax ::asn::asnPeekTag data_var tag_var tagtype_var constr_var 
# 這個指令的參數給的是變數名稱,執行後變數值會被更新,類似C 指標的概念

puts "Test 4: decoding 0x02"
set data [binary format H* 02]
::asn::asnPeekTag data tag class constr
puts "tag=$tag, class=$class, constr=$constr\n"

puts "Test 5: decoding 0x5F8148"
set data [binary format H* 5F8148]
::asn::asnPeekTag data tag class constr
puts "tag=$tag, class=$class, constr=$constr\n"

puts "Test 6: decoding 0xBF8767"
set data [binary format H* BF8767]
::asn::asnPeekTag data tag class constr
puts "tag=$tag, class=$class, constr=$constr\n"



Length octets

用來表示資料(Contents octets)長度,不包含Identifier octets 和 Length octets

Length 編碼方式


先將 Length 可以分成2種格式
1.definite form
2.indefinite form

格式選用原則
1. primitive 類的使用 definite form
2. constructed 類並且所有資料是立即可用的可任選一種
3. constructed 類並且不是所有資料是立即可用的使用indefinite form


definite form 又可分成2種編碼方式,可自由選擇
1. short form : 最簡單的方式,資料難容幾個byte就填多少,範圍0~127
2. long form : 用多個 bytes 來描述,第一個 byte bit8 (MSB)填1,bit7~bit1 用來描述後面用幾個 bytes 來描述長度,但不能全為 1 (0xFF),0xFF 先保留未來或許會用到
例如: length=201,
201=0xC9 , 1 byte,所以第一 byte 是 0x81 => 最後就是編成 0x81c9

indefinite form: 固定為 0x80,因為這種格試是用在資料長度未定的方式,為了知道資料何時結束,當最後一筆資料出現時最後要補上End-of-contents
End-of-contents: tag:0x00 length:0x00 value:Absent
目前還沒遇過這種類型的資料,tcl的asn package也只支援definite form編碼

package require asn
# asn package 裡 length 的 encoding/decoding 通常不需要使用者去管它
# 在這個package裡 encoding/decoding時會順便處理掉

# length encoding
# syntax: ::asn::asnLength len
puts "Test 1 length=100"
hexdump [::asn::asnLength 100]

puts "Test 2 length=999"
hexdump [::asn::asnLength 999]

# length decoding
# syntax: ::asn::asnGetLength data_var length_var
# 這個指令解碼完後會將data_var裡length的部分移除,將取得的結果存到length_var
# 這個package解碼的指令大多數都會將原資料改變

puts "Test 3 decoding 0x64"
set data [binary format H* 64]
::asn::asnGetLength data leng
puts length=$leng\n

puts "Test 4 decoding 0x8203E7"
set data [binary format H* 8203E7]
::asn::asnGetLength data leng
puts length=$leng\n




Contents octets

Contents octets: 資料內容,根據不同類型有不同編碼方式,這邊只記錄常用和tcl asn package有的幾種類型
Type Class Constructed Tag number Tag encoding
Boolean UNIVERSAL 0 1 0x01
Integer UNIVERSAL 0 2 0x02
Bitstring UNIVERSAL 0 3 0x03
Octetstring UNIVERSAL 0 4 0x04
Null UNIVERSAL 0 5 0x05
Object identifier UNIVERSAL 0 6 0x06
Sequence UNIVERSAL 1 16 0x30
Sequence of UNIVERSAL 1 16 0x30
Set UNIVERSAL 0 17 0x31
Set of UNIVERSAL 0 17 0x31
UTCTime UNIVERSAL 0 23 0x17



Boolean: Tag: 0x01 Length: 1 byte
value 為 False 時 0x00, True 時為 0xFF

package require asn

# encoding 指令會自動標上tag和length,不需使用者另外處理
# decoding 指令會將原資料的一組(tag,length,content)移除
# 這裡的解碼範例都是假設以之資料類型,一般應該使用asnPeekTag 解出tag再套用相對應的指令

puts "Test 1: encoding Boolean 0"
hexdump [::asn::asnBoolean 0 ]

puts "Test 2: encoding Boolean 1"
hexdump [::asn::asnBoolean 1 ]

puts "Test 3: decoding 0x0101000101FF"
set data [binary format H* 0101000101FF]
puts -nonewline "before decoding data ="; hexdump $data

puts "decoding 1st"
::asn::asnGetBoolean data bool
puts "bool=$bool\n"
puts -nonewline "after decoding data ="; hexdump $data

puts "decoding 2nd"
::asn::asnGetBoolean data bool
puts "bool=$bool\n"
puts -nonewline "after decoding data ="; hexdump $data



Integer: Tag: 0x01 0x02 Length: n byte (n>0)
value: 儲存有號整數,以2的補數表示,並且以最少的bytes數表示,最少 bytes 也就是說,當資料是多 bytes 時,第 1 byte 和第 2 byte 的 MSB
不會同時為 0(正數) 或同時為 1(負數)

package require asn
# 2 bytes 整數令人懷念的2個值
puts "Test 1: encoding Integer 32767"
hexdump [::asn::asnInteger 32767 ]

puts "Test 2: encoding Integer -32768"
hexdump [::asn::asnInteger -32768 ]

# 當數值超過 8 bytes 時,要用 bignum
package require math::bignum
puts "Test 3: encoding bigInteger 9223372036854775808 (0x008000000000000000)"
set val [::math::bignum::fromstr 9223372036854775808]
hexdump [::asn::asnBigInteger $val ]

puts "Test 4: decoding 0x02027FFF"
set data [binary format H* 02027FFF]
::asn::asnGetInteger data val
puts val=$val\n

puts "Test 5: decoding 0x02028000"
set data [binary format H* 02028000]
::asn::asnGetInteger data val
puts val=$val\n

# asn package 0.8.3 解bignum有bug,要更新到0.8.4
puts "Test 6: decoding 0x0209008000000000000000"
set data [binary format H* 0209008000000000000000]
::asn::asnGetBigInteger data val
# 會存成bignum格式
puts val=$val
# 要另外轉換回來
set val [::math::bignum::tostr $val]
puts val=$val



Bitstring: Tag: 0x03 Length: n byte (n>0)
value: 有兩種編碼方式,一次寫完所有 bits 和分批送 bits 兩種,這邊只記錄第一種
一開始先調整長度,讓長度變成整數 bytes ,做法是在尾端(LSB)補 0,共有8總可能(0~7)
然後在前端補上 1 byte,說明補了幾個 0
例如: 0101
先補0 => 01010000=>0x50
補了4個0
在前面補1 byte => 0x0450

package require asn
# 這範例可看出同樣是0x50,但是資料第一byte不同,所表示的bitstring也不同

puts "Test 1: encoding BitString 0101"
hexdump [::asn::asnBitString 0101]

puts "Test 2: encoding BitString 01010000"
hexdump [::asn::asnBitString 01010000]

puts "Test 3: decoding 0x03020450"
set data [binary format H* 03020450]
::asn::asnGetBitString data val
puts val=$val\n

puts "Test 4: decoding 0x03020050"
set data [binary format H* 03020050]
::asn::asnGetBitString data val
puts val=$val\n



Octetstring: Tag: 0x04 Length: n byte (n>=0)
value: 直接填入即可

package require asn
puts "Test 1: encoding OctetString \"5566\""
hexdump [::asn::asnOctetString 5566]

puts "Test 2: encoding OctetString 0x55 0x66"
hexdump [::asn::asnOctetString \x55\x66]

# 這個type可以存任意值,若要binary data存取,解碼完可用binary scan做進一步運算
puts "Test 3: decoding 0x040435353636"
set data [binary format H* 040435353636]
::asn::asnGetOctetString data val
puts val=$val\n

puts "Test 4: decoding 0x04025566"
set data [binary format H* 04025566]
::asn::asnGetOctetString data val
puts val=$val
binary scan $val H* valhex
puts "val(hex)=$valhex"


Null: Tag: 0x05 Length: 1 byte
value: NULL,這個 type 只有 type 和 length,length 值為 0x00
目前只在snmp封包看過這種type,snmpget的vbind value就是Null

package require asn

# Null 的編碼解碼只是補上或移除 0x0500
puts "Test 1: encoding Null"
hexdump [::asn::asnNull]

puts "Test 2: decoding 0x0500"
puts "before decoding data length = [string length $data]"
::asn::asnGetNull data
puts "after decoding data length = [string length $data]"



Object identifier: Tag: 0x06 Length: n byte
value:把 oid 的每一個數值轉成 binary 格式,有兩個規則
第一,oid前兩碼混在一起算 X.Y. ... X,Y編成 (X*40)+Y. oid 前2碼範圍 X:0~2, Y:0~39
第二,因為值有可能超過 1 byte,所以把 bit 8 (MSB)拿來當 flag , bit8 == 1 表示這個值還有下一個 byte,一直串下去到足夠表示這個值為止,最後的 byte bit8 為 0
同樣以最短編碼為原則,所以不會出現 0x80 這種值
例如: 1.3.6.1.4.1.4491
1*40+3=43,前兩碼編成0x2B
接著 6.1.4.1 編成 0x06 0x01 0x04 0x01
最後 4491 = 0x118B = b 00010001 10001011 每個byte的MSB要當flag,每7 bit 就要進位
=>00 0100011 0001011 (7bits一組分類前面是00可以省掉,不然就要補0)(轉二進位看就是7 bits一組切一切就好,用算的就是 mod 128 和/ 128 一直算)
=> 10100011 00001011=0xA30B
所以 1.3.6.1.4.1.4491 會編成 0x2B06010401A30B

package require asn
# 這兩個處理oid的指令不處理 "." ,使用者要自己加入或移除
puts "Test 1: encoding oid 1.3.6.1.4.1.4491"
hexdump [::asn::asnObjectIdentifier [split 1.3.6.1.4.1.4491 .]]
                         
puts "Test 2: decoding 0x06072B06010401A30B"
set data [binary format H* 06072B06010401A30B]
::asn::asnGetObjectIdentifier data oid
puts oid=[join $oid .]



Sequence / Sequence of : Tag: 0x30 Length: n byte
value: Sequence 和 Sequence of 的值是 asn.1 定義的這些值,並且排列是有順序的
兩者不同的是Sequence包含的值可以有多種type,Sequence of包含的值只有一種type,在der編碼下似乎不用特別去區分
package require asn

#encoding時不會檢查內容是否符合der格式,所以每以筆資料都要先處理過
puts "Test 1: encoding Sequence { integet 100 , bitstring 01010101}"
hexdump [::asn::asnSequence [::asn::asnInteger 100] [::asn::asnBitString 01010101]]

#decoding後,sequence裡的每一筆資料都要在另外解碼
puts "Test 2: decoding 0x300702016403020055"
set data [binary format H* 300702016403020055]
::asn::asnGetSequence data val
binary scan $val H* valhex
puts val(hex)=$valhex



Set / Set of : Tag: 0x31 Length: n byte
value: 和Sequence 和 Sequence of 類似,Set / Set of 是用來包沒有順序的值

package require asn
#基本上和sequence的差別只有tag不同

puts "Test 1: encoding Set { integet 100 , bitstring 01010101}"
hexdump [::asn::asnSet [::asn::asnInteger 100] [::asn::asnBitString 01010101]]

puts "Test 2: decoding 0x310702016403020055"
set data [binary format H* 310702016403020055]
::asn::asnGetSet data val
binary scan $val H* valhex
puts val(hex)=$valhex




UTCTime: Tag: 0x17 Length: 13 byte
value:用來標示時間,格式是 YYMMDD000000Z" , 年月日時分秒,24小時制,0:0:0記成000000而不是240000
年的編碼規定沒找到,在openssl裡是小於50的加100,也就是範圍是1950~2049
package require asn
puts "Test 1: encoding UTCTime 140719000000Z"
hexdump [::asn::asnUTCTime 140719000000Z]

puts "Test 2: decoding UTCTime 0x170D3134303731393030303030305A"
set data [binary format H* 170D3134303731393030303030305A]
::asn::asnGetUTCTime data val
puts val=$val\n

# UTCTime encoding/decoding command不檢查內容格式是否正確,使用者須自行判斷
puts "Test 3: encoding UTCTime aAbBcCdDeEfFZ"
hexdump [::asn::asnUTCTime aAbBcCdDeEfFZ]



4 則留言:

  1. "第一 byte => 0101111 => 0x5F" 似乎少打一個 1 --> 01011111

    回覆刪除
  2. Integer: Tag: 0x01 Length: n byte (n>0)
    Integer的Tag應該是0x02?

    回覆刪除