eSNACC对OBJECT IDENTIFIER的编码和解码
本文剖析asn-oid.h/c,从源代码来学习eSNACC对OBJECT IDENTIFIER的编码和解码。
在研究代码之前,我们先来说明什么是OBJECT IDENTIFIER。
————————————以下一段来自于http://www.eccsdk.com/bbs/read.php?tid=1772————————————————
ASN.1 对象标识符类型
对象标识符(OBJECT IDENTIFIER, OID)类型用层次的形式来表示标准规范.标识符树通过一个点分的十进制符号来定义,这个符号以组织,子部分然后是标准的类型和各自的子标识符开始.
例如:MD5的OID 是 1.2.840.113549.2.5 表示为"iso(1) member-body (2) US (840) rsadsi(113549) digestAlgorithm (2) md5 (5)", 所以当解码程序看到这个OID时,就知道是MD5散列.
OID在公钥算法标准中很流行,它指出证书绑定了哪种散列算法. 同样,也有公钥算法,分组算法,和操作模式的OID. 它们是一种高效且可移植的表示数据包中所选算法的形式.
对OID的编码规则:
1、前两部分如果定义为x.y, 那么它们将合成一个字40*x + y, 其余部分单独作为一个字节进行编码.
2、每个字首先被分割为最少数量的没有头零数字的7位数字.这些数字以big-endian格式进行组织,并且一个接一个地组合成字节. 除了编码的最后一个字节外,其他所有字节的最高位(位8)都为1.
举例: 30331 = 1 * 128^2 + 108 * 128 + 123 分割成7位数字(0x80)后为{1,108,123}设置最高位后变成{129,236,123}.如果该字只有一个7位数字,那么最高为0.
MD5 OID的编码:
1. 将1.2.840.113549.2.5转换成字数组 {42, 840, 113549, 2, 5}.
2. 然后将每个字分割为带有最高位的7位数字,{{0x2A},{0x86,0x48},{0x86,0xF7,0x0D},{0x02},{0x05}}.
3. 最后完整的编码为 0x06 08 2A 86 48 86 F7 0D 02 05.
————————————————————————————————————————————————————————————
有了上面的直观的理解,我们再研究代码就不会困惑了。
在eSNACC中,OBJECT IDENTIFIER实现分为oid和RELATIVE OID。oid要求至少必须由两部分数字组成,因为要编码的一个值为40*x + y,所以如果不满足就会报错。就如同上面例子中的MD5 OID,就有6个部分。而RELATIVE OID就没有这个要求,对她的编码没有做40*x + y的操作,解码也不需要逆处理。但是RELATIVE OID必须和一个oid根相关联。因为RELATIVE OID的定义方式和处理函数都与oid类似,仅仅是少了上面所说的第一个编码规则操作,所以我们只讨论oid,而RELATIVE OID就不展开了。
eSNACC对oid有两种实现:用字节串存放和用链表形式存放,对字节串存放,就是每一个字节存放一个数值;而链表,就是每一个节点元素存放一个值。不过文档说:如果追求更好的性能,应当采用字节串的形式。这两种方式定义如下:
字节串形式,定义为AsnOid,直接从AsnOcts定义过来:
链表形式,定义为OID:
#define NULL_OID_ARCNUM -1
typedef struct OID
{
struct OID *next;
long arcNum;
#if COMPILER || TTBL
struct Value *valueRef;
#endif
} OID;
头文件中还定义了若干函数实现这两者的互相转换。
严重说明:
AsnOid存放的是已经对原始的OBJECT IDENTIFIER编码之后的值!比如上面例子的MD5,字节串是2A 86 48 86 F7 0D 02 05。
而OID存的是OBJECT IDENTIFIER的原始值链。比如上面例子的MD5,链表为1->2->840->113549->2->5。
/**************************************休息一下********************************************
上文我们说过:AsnOid存放的是已经对原始的OBJECT IDENTIFIER编码之后的值。所以我们当我们要对一个AsnOid进行打印输出时,就需要进行前面所说编码算法的逆运算,这个我们可以通过打印例程来验证一下:
* Prints the given OID to the given FILE * in ASN.1 Value Notation.
* Since the internal rep of an OID is 'encoded', this routine
* decodes each individual arc number to print it.
*/
void
PrintAsnOid PARAMS ((f,v, indent),
FILE * f _AND_
AsnOid * v _AND_
unsigned int indent)
{
unsigned int firstArcNum;
unsigned int arcNum;
int i;
fprintf (f,"{");
/**//* un-munge first two arc numbers */
for (arcNum = 0, i=0; (i < (int)(v->octetLen)) && (v->octs[i] & 0x80);i++)
arcNum = (arcNum << 7) + (v->octs[i] & 0x7f);
arcNum = (arcNum << 7) + (v->octs[i] & 0x7f);
i++;
firstArcNum = (unsigned short)(arcNum/40);
if (firstArcNum > 2)
firstArcNum = 2;
fprintf (f,"%u %u", (unsigned int)firstArcNum, arcNum - (firstArcNum * 40));
for (; i < (int)(v->octetLen); )
{
for (arcNum = 0; (i < (int)(v->octetLen)) && (v->octs[i] & 0x80);i++)
arcNum = (arcNum << 7) + (v->octs[i] & 0x7f);
arcNum = (arcNum << 7) + (v->octs[i] & 0x7f);
i++;
fprintf (f," %u", arcNum);
}
fprintf (f,"}");
indent=indent; /**//* referenced to avoid compiler warning. */
} /**/ /* PrintAsnOid */
从代码可以看到,这个算法就是这样的:
首先得到第一个数,因为如果一个编码后的数用了多个字节表示,那么除了最后一个以外,前面的字节的最高位肯定为1.所以就用一个循环来获取一个完整的数。后面嵌套的for也是这个原理。然后对取得的第一个数分拆:num=first*40+second。这样就完成第一步的逆算法并打印出来。
然后遍历这个字节串,每获取一个完整的数就打印出来。由于for循环只是把最高位为1的字节遍历了,所以都需要加上最后一个字节。其实我们发现用多个字节存的数对应前面的字节的128的n次方和最末尾一个字节值的和。
而OID存的是原始值,就让我们通过这个OID -> AsnOid的函数来进一步理解两者的不同:
* given an oid list and a pre-allocated ENC_OID
* (use EncodedOidLen to figure out byte length needed)
* fills the ENC_OID with a BER encoded version
* of the oid.
*/
void
BuildEncodedOid PARAMS ((oid, result),
OID * oid _AND_
AsnOid * result)
{
unsigned long len;
unsigned long headArcNum;
unsigned long tmpArcNum;
char *buf;
int i;
OID *tmpOid;
buf = result->octs;
/**//*
* oid must have at least 2 elmts
*/
if (oid->next == NULL)
return;
/**//*
* munge together first two arcNum
* note first arcnum must be <= 2
* and second must be < 39 if first = 0 or 1
* see (X.209) for ref to this stupidity
*/
//head = first * 40 + second
headArcNum = (oid->arcNum * 40) + oid->next->arcNum;
tmpArcNum = headArcNum;
/**//*
* 计算存放第一个数需要几个字节。每7位要一个字节
*/
for (len = 0; (tmpArcNum >>= 7) != 0; len++)
;
/**//*
* 从高位到低位,把每7位写到缓冲区,因为不是最后一个字节,所以都把最高位设为1
*/
for (i=0; i < (int)len; i++)
*(buf++) = (char)(0x80 | (headArcNum >> ((len-i)*7)));
/**//*
* 将写第一个数的最后7位写到最后一个字节
*/
*(buf++) = (char)(0x7f & headArcNum);
/**//*
* 如果有,就把后面的数写到缓冲区,原理和第一个数相同,里面不再注释
*/
for (tmpOid = oid->next->next; tmpOid != NULL; tmpOid = tmpOid->next)
{
tmpArcNum = tmpOid->arcNum;
for (len = 0; (tmpArcNum >>= 7) != 0; len++)
;
for (i=0; i < (int)len; i++)
*(buf++) = (char)(0x80 | (tmpOid->arcNum >> ((len-i)*7)));
*(buf++) = (char)(0x7f & tmpOid->arcNum);
}
result->octetLen = (buf - result->octs);//根据被写的缓冲区设定字节长度
} /**/ /* BuildEncodedOid */
在上面的函数中,我已经对相应语句做了注释了,所以这里就不复述了。
文件中其他函数都是这个原理,就很简单了。本篇就到此吧。