X.509证书DN之详解
X.509使用DN(Distinct Name)来唯一标识一个实体,其功能类似我们平常使用的ID,不过不同的是,DN不再是类似 123456 这样得数字标识,而是采用多个字段来标识一个实体,例如”CN=老所,C=CN”,这样做的好处在于方便匹配到诸如LDAP一样的目录服务中。那么,DN的字段是否可以随意增加呢?比如我能否在”CN=老所,O=测试公司”这样一个DN上再增加一个ID属性,变成”CN=老所,O=测试公司,ID=123456″呢?
动手测试。首先采用微软的 XEnroll 组件来进行测试。XEnroll 是微软平台下的 ActiveX 控件,提供证书的加入服务,比如创建 PKCS#10 证书请求。我们可以用 Javascript 来调用这个 ActiveX 控件:
var sOID = '';
var sDN = 'CN="老所",O="测试公司",ID="123456"';
alert(sDN);
var sPKCS10 = oXEnroll.createPKCS10(sDN, sOID);
其中,createPKCS10 函数接受两个参数,第一个是字符串类型的DN,第二个是用以标识该证书请求的用途的OID,这里,我们不在乎该证书的用途,所以就设为空好了。通过 IE 运行该段代码后,我得到了 object Error,看来,不能这样简单地添加DN字段。
于是开始搜索关于 X.509 DN 的信息。原来,X.509 证书里的 DN 属性,都是一些基于 ASN1 编码的对象,也就是说,对于我们熟知的 CN 属性,或者说 commonName 属性,程序是无法从证书里获得的,证书里是不会写诸如 CN=xxx 这样的信息的。所有字段都被表示为该字段类型的 OID 。OID 则是 ASN1 对象的统一表示方法,这种方法采用树状结构,由国际组织统一管理。一个公司如果要增加一个字段类型,并希望被广泛采用,它必须像管理 OID 的组织提出申请,就像我们常用的域名申请一样。
在 www.oid-info.com 这个网站,我们可以查询所有注册过的 OID 信息。这个网站提供两种方式的查询:按树状结构展开各级 OID,或者直接填写诸如”2.3.4″这样的 OID 来进行查询。
假如我们展开 2.5.4 这层 OID,我们就会发现很多我们常用的 DN 字段:
- 2.5.4.3 - commonName
- 2.5.4.4 - surname
- 2.5.4.13 - description
- 2.5.4.10 - organizationName
- ……
既然如此,那我们就采用 OID 形式来添加属性类型吧,比如我使用 OID=2.5.4.888,这是一个未被注册的 OID,可以用来测试:
var sOID = '';
var sDN = 'CN="老所",O="测试公司",2.5.4.888="123456"';
alert(sDN);
var sPKCS10 = oXEnroll.createPKCS10(sDN, sOID);
运行该脚步,成功了,我们从机器里的证书申请库里找到该证书申请,查看其属性:
可以看出,虽然用 OID 添加属性成功了,但这个 OID 并不能被翻译成方便我们阅读的形式,就像我们只能使用 IP 而无法使用域名一样。要让该证书显示 ID= 而不是 2.5.4.888=,我们必须要:
- 向标准组织注册 2.5.4.888 为 ID;
- 通知微软,修改其 Windows 程序,将该 OID 翻译为 ID
显然,这非常不现实,呵呵。
接下来,我们再用 openssl 来测试一下。我选用的是 pyOpenSSL,一个 openssl 的 Python 包装库:
>>> from OpenSSL import crypto >>> req = crypto.X509Req() >>> subject = req.get_subject() >>> setattr(subject, 'CN', 'soloman') >>> setattr(subject, 'ID', '123456') Traceback (most recent call last): File "", line 1, in AttributeError: No such attribute >>>
看来,pyOpenSSL也只认识 CN,不认识 ID。查询了一下 pyopenssl 的源代码:
static int
crypto_X509Name_setattr(crypto_X509NameObj *self, char *name, PyObject *value)
{
int nid;
int result;
char *buffer;
if ((nid = OBJ_txt2nid(name)) == NID_undef)
{
PyErr_SetString(PyExc_AttributeError, "No such attribute");
return -1;
}
......
可以看出,pyOpenSSL 在设置 DN 的时候,首先调用 openssl 的 OBJ_txt2nid()函数来将方便我们记忆的对象名字翻译成 OID, 然后再翻译成 openssl 程序里自己的 NID。
那么继续查询 openssl 的源代码,在 objects.h 文件里我们找到了所有 DN 字段的定义:
......
#define SN_commonName "CN"
#define LN_commonName "commonName"
#define NID_commonName 13
#define OBJ_commonName OBJ_X509,3L
#define SN_countryName "C"
#define LN_countryName "countryName"
#define NID_countryName 14
#define OBJ_countryName OBJ_X509,6L
#define SN_localityName "L"
#define LN_localityName "localityName"
#define NID_localityName 15
#define OBJ_localityName OBJ_X509,7L
......
这里,openssl 为每个常用的 DN 属性定义了4个内容:SN, LN, NID, OBJ。其中 SN 表示 Short Name, LN 表示 Long Name,都是一个 OID 方便我们阅读的名称的两个形式,而 OBJ 则定义了其 OID 信息。
在 openssl/crypto/asn1/a_strnid.c 这个文件里,绑定了 openssl 能够支持并翻译的 DN 字段:
static ASN1_STRING_TABLE tbl_standard[] = {
{NID_commonName, 1, ub_common_name, DIRSTRING_TYPE, 0},
{NID_countryName, 2, 2, B_ASN1_PRINTABLESTRING, STABLE_NO_MASK},
{NID_localityName, 1, ub_locality_name, DIRSTRING_TYPE, 0},
{NID_stateOrProvinceName, 1, ub_state_name, DIRSTRING_TYPE, 0},
{NID_organizationName, 1, ub_organization_name, DIRSTRING_TYPE, 0},
{NID_organizationalUnitName, 1, ub_organization_unit_name, DIRSTRING_TYPE, 0},
{NID_pkcs9_emailAddress, 1, ub_email_address, B_ASN1_IA5STRING, STABLE_NO_MASK},
{NID_pkcs9_unstructuredName, 1, -1, PKCS9STRING_TYPE, 0},
{NID_pkcs9_challengePassword, 1, -1, PKCS9STRING_TYPE, 0},
{NID_pkcs9_unstructuredAddress, 1, -1, DIRSTRING_TYPE, 0},
{NID_givenName, 1, ub_name, DIRSTRING_TYPE, 0},
{NID_surname, 1, ub_name, DIRSTRING_TYPE, 0},
{NID_initials, 1, ub_name, DIRSTRING_TYPE, 0},
{NID_serialNumber, 1, ub_serial_number, B_ASN1_PRINTABLESTRING, STABLE_NO_MASK},
{NID_friendlyName, -1, -1, B_ASN1_BMPSTRING, STABLE_NO_MASK},
{NID_name, 1, ub_name, DIRSTRING_TYPE, 0},
{NID_dnQualifier, -1, -1, B_ASN1_PRINTABLESTRING, STABLE_NO_MASK},
{NID_domainComponent, 1, -1, B_ASN1_IA5STRING, STABLE_NO_MASK},
{NID_ms_csp_name, -1, -1, B_ASN1_BMPSTRING, STABLE_NO_MASK}
};
可以看出,如果要让我们的基于 openssl 的程序支持一个我们自定义的属性,我们得修改 openssl 源代码,添加我们的新字段的”名称-OID” 绑定(可以只有Long Name),然后重新编译 openssl 和我们的程序,使其能使用新的属性。
我们回过头来想想,为什么标准化组织没有为我们提供一个类似 ID 这样的属性呢?这是因为,DN本来就是作为 ID 来使用的,它把一些属性绑定起来,来标识一个实体,如果再有一个通用的 ID 属性,逻辑上就冲突了。当然针对具体的应用,你可以注册一个特殊的 ID 属性,比如 openID ,但这需要实力雄厚的公司来进行注册和推广。
然而实际应用中,当我们想将证书系统与某个特别的应用中的某个 ID 进行绑定,而又不具备向国际组织进行 OID 注册和推广的能力时,我们完全可以利用现有的属性来进行实现,比如,针对这个需求,我可以设计出两个方案:
- 方案一:CN=老所@123456
- 方案二:CN=123456, surname=老, givenName=所
- 方案三:CN=123456, name=老所
- 方案四:CN=老所, description=123456
方案一,通过一定的方式,将用户的姓名和ID组合到CN里,然后在应用中解析出ID,这个方法不是很便于用户书写,用户在填写证书申请资料的时候必须注意符合该规范。
方案二与三的概念就是用CN来记录ID,毕竟,Common Name 嘛,随你如何定义。而用户的姓名由 surname + givenName 或者 name 来指定。该方法通过了 openssl 测试:
>>> req = crypto.X509Req() >>> subject = req.get_subject() >>> setattr(subject, 'surname', 'Lao') >>> setattr(subject, 'givenName', 'Suo') >>> setattr(subject, 'name', 'Lao Suo')
但是,XEnroll 仿佛不支持 surname + givenName 或者 name 属性,其文档上说明是支持 X.500 规范,而不是 X.509 规范。
方案四,则利用了 description 属性来记录和具体应用相关的讯息,比如我们的 ID,该方法通过了 openssll 的测试:
>>> req = crypto.X509Req() >>> subject = req.get_subject() >>> setattr(subject, 'CN', 'Lao Suo') >>> setattr(subject, 'description', '123456')
和 XEnroll 的测试: