ASN.1如何对对象标识符进行编码?

29

我对ASN.1的基本概念有些困惑。

如果一个类型是OID,那么相应的数字是否实际上会被编码到二进制数据中?

举个例子,在这个定义中:

id-ad-ocsp         OBJECT IDENTIFIER ::= { id-ad 1 }

对应的1.3.6.1.5.5.7.48.1是否会精确地以这种方式编码为二进制?

我之所以问这个问题,是因为我正试图理解DER文件(证书)中看到的特定值,即04020500,我不确定如何解释它。

4个回答

52

是的,OID被编码在二进制数据中。你提到的OID 1.3.6.1.5.5.7.48.1 变成了2b 06 01 05 05 07 30 01 (前两个数字被编码为单个字节,所有其余数字也被编码为单个字节,因为它们都小于128)。

有关OID编码的详细描述可以在这里找到:http://msdn.microsoft.com/en-us/library/bb540809%28v=vs.85%29.aspx

但是,分析ASN.1数据的最佳方法是将其粘贴到在线解码器中,例如:http://lapo.it/asn1js/


1
+1 给 ASN.1 解码器。谢谢。如果我当时知道就好了... :-) - Pete Wilson
但是我怎么知道特定OID的有效值是什么?在我的情况下,对于OID 1.3.6.1.5.5.7.48.1.5,该值(据我了解)为04020500。它有特定的解释吗? - Cratylus
从上下文来看,很难确定04020500是什么。它看起来根本不像OID。 - Codo
4
04020500是一个八位字符串(04),其中两个字节(02)编码了一个NULL(05,长度为00)。 - Omri Barel
04020500(被视为完整的ASN.1块)将是一个2字节的八位字符串= 05:00。04不是构造器。 - Renate
显示剩余2条评论

25

如果您的所有数字都小于或等于127,则它们可以用每个八位字节表示。当您有较大的常见数字时,例如1.2.840.113549.1.1.5 (sha1WithRsaEncryption),则使用可变长度解码。这些示例侧重于解码,但编码正好相反。

1. 前两个“数字”由单个字节表示

您可以通过将第一个字节读入整数来进行解码。

if ($firstByte >= 80) {
    $nodeFirst = 2;
    $nodeSecond = $firstByte - 80;
}
else {
    $nodeFirst = $firstByte / 40;
    $nodeSecond = $firstByte % 40;
}

$oidText = "$nodeFirst.$nodeSecond"

生成值

1.2

2. 后续的字节使用可变长度数量(VLQ)表示,也称为基于128的表示法。

VLQ有两种形式,

短序列 - 如果八位字节以0开头,则只需使用剩余的7位表示。

长序列 - 如果八位字节以1(最高有效位)开头,则将该八位字节的下一个7位与每个后续八位字节的7位组合,直到遇到最高有效位为0的八位字节(这标志着最后一个八位字节)。

值840将用以下两个字节表示:

10000110
01001000

Combine to 00001101001000 and read as int.

针对BER编码的重要资源,http://luca.ntop.org/Teaching/Appunti/asn1.html 很不错

第一个八位字节的值为40*value1+value2。(这是无歧义的,因为value1仅限于0、1和2的值;当value1为0或1时,value2的范围限制在0到39之间;根据X.208标准,n始终至少为2。)

以下的八位组(如果有的话)编码value3,……,valuen。每个值都使用最少的数字和最高有效位先行编码,基数为128,并且除了值的编码最后一个八位组之外,每个八位组中的最高有效位都设置为"1"。例如:RSA数据安全公司对象标识符的BER编码的第一个八位组是40*1+2=42=2a16。840的编码=6*128+4816,是86 48,113549的编码=6*1282+7716*128+d16是86 f7 0d。这导致以下BER编码:
06 06 2a 86 48 86 f7 0d

编辑/免责声明: 根据下面的评论修复了第一个八位组,但尚未测试。我现在将保留此代码片段作为一般参考,但不能保证正确性,不建议盲目复制和粘贴 :)。 对于大于128 VLQ的情况,您通常会使用位移来重新对齐位,而不是一串位。

sub getOid {
    my $bytes = shift;

    #first 2 nodes are 'special';
    use integer;
    my $firstByte = shift @$bytes;
    my $number = unpack "C", $firstByte;

    my $nodeFirst;
    my $nodeSecond;

    if ($number >= 80) {
        $nodeFirst = 2;
        $nodeSecond = $number - 80;
    }
    else {
        $nodeFirst = $number / 40;
        $nodeSecond = $number % 40;
    }


    my @oidDigits = ($nodeFirst, $nodeSecond);

    while (@$bytes) {
        my $num = convertFromVLQ($bytes);
        push @oidDigits, $num;
    }

    return join '.', @oidDigits;
}

sub convertFromVLQ {
    my $bytes = shift;

    my $firstByte = shift @$bytes;
    my $bitString = unpack "B*", $firstByte;

    my $firstBit = substr $bitString, 0, 1;
    my $remainingBits = substr $bitString, 1, 7;

    my $remainingByte = pack "B*", '0' . $remainingBits;
    my $remainingInt = unpack "C", $remainingByte;

    if ($firstBit eq '0') {
        return $remainingInt;
    }
    else {
        my $bitBuilder = $remainingBits;

        my $nextFirstBit = "1";
        while ($nextFirstBit eq "1") {
            my $nextByte = shift @$bytes;
            my $nextBits = unpack "B*", $nextByte;

            $nextFirstBit = substr $nextBits, 0, 1;
            my $nextSevenBits = substr $nextBits, 1, 7;

            $bitBuilder .= $nextSevenBits;
        }

        my $MAX_BITS = 32;
        my $missingBits = $MAX_BITS - (length $bitBuilder);
        my $padding = 0 x $missingBits;
        $bitBuilder = $padding . $bitBuilder;

        my $finalByte = pack "B*", $bitBuilder;
        my $finalNumber = unpack "N", $finalByte;
        return $finalNumber;
    }

}

5
我不确定你的前两个数字解码代码是否正确。考虑来自 T-REC-X.690 的 {2 100 3} 示例。对于这个示例,第一个字节将是180。因此,似乎你的解决方案只在第一个字节小于80时有效。任何大于或等于80的值都意味着第一个数字是2,第二个数字可以通过减去80来找到。 - Peter Friend
“Modulo 40”代码有误。数值2.40+必须进行不同的处理,请参见我的开源OID<->DER转换器https://misc.daniel-marschall.de/asn.1/oid-converter/online.php。 - Daniel Marschall
@PeterFriend,感谢你指出这一点,我更新了代码片段以解决这个问题。 - Despertar

14

OID解码入门 :) :

  • 每个OID组件都被编码成一个或多个字节(八位字节)
  • OID编码只是这些OID组件编码的串联
  • 前两个组件以特殊方式编码(见下文)
  • 如果OID组件的二进制值少于7位,则编码只是一个字节,保持组件值(请注意,最高位,最左边的位始终为0)
  • 否则,如果它有8位及更多位,则该值将“扩展”到多个八位字节中-将二进制表示分成7位块(从右侧开始),如果需要,左侧填充第一个块的零,并通过添加最高(左侧)位1来从这些七位构成八位字节,除了最后一个块,该块的位为0。
  • 前两个组件(X.Y)的编码方式就像是一个值为40*X+Y的单个组件一样进行编码

这是ITU-T建议X.690,第8.19章的改写


1
我刚刚用VBA编写了OID代码,我写了两次,因为微软的文档是错误的。现在我有了一个可工作的版本,我可以证明这是最好的答案。点赞。 - S Meaden
我认为应该读取少于8位。如果你说少于7位,在另一种情况下等于或多于8位,你会排除像64(相当于7位)这样的数字。 - Sebi2020

7

这是一个简单的Python 3实现,用于将上述内容或对象标识符的字符串形式转换为ASN.1 DER或BER格式。请注意,此实现仅供参考,可能需要根据具体需求进行修改。

def encode_variable_length_quantity(v:int) -> list:
    # Break it up in groups of 7 bits starting from the lowest significant bit
    # For all the other groups of 7 bits than lowest one, set the MSB to 1
    m = 0x00
    output = []
    while v >= 0x80:
        output.insert(0, (v & 0x7f) | m)
        v = v >> 7
        m = 0x80
    output.insert(0, v | m)
    return output

def encode_oid_string(oid_str:str) -> tuple:
    a = [int(x) for x in oid_str.split('.')]
    oid = [a[0]*40 + a[1]] # First two items are coded by a1*40+a2
    # A rest is Variable-length_quantity
    for n in a[2:]:
        oid.extend(encode_variable_length_quantity(n))
    oid.insert(0, len(oid)) # Add a Length
    oid.insert(0, 0x06) # Add a Type (0x06 for Object Identifier)
    return tuple(oid)

if __name__ == '__main__':
    oid = encode_oid_string("1.2.840.10045.3.1.7")
    print(oid)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接