作者: hetao

  • 注册证书时的DN名称解释

    C Country 国家
    L locality 城市
    O organization 组织,公司
    OU organizationalUnit 组织部门
    CN commonName 一般名字
    ST stateOrProvince 省份
    emailAddress 邮箱地址

    Views: 40

  • CMP中的数据加密问题

    CMP协义中私钥和证书是要加密传输的,但是CMP所用的传输协议并不要求加密,所以CMP专门定义敏感信息的加密方式。

    CMP V2

    CMP V2中使用EncryptedValue结构来加密,通常是使用CA的RSA公钥来加密内容,这就导致所有的CA证书都是用RSA签名的,因为用其它签名算法会导致不能使用CMP或其它兼容性问题。
    EncryptedValue数据结构,由PKCS#11 v2.11版本12.11节定义。PKCS#11 v2.11中只提到了可以用一个密码对私钥进行包装,并没有提到密码的来源。在PKCS#11 v2.40中定义了CKM_RSA_AES_KEY_WRAP和CKM_ECDH_AES_KEY_WRAP两种包装机制,分别使用RSA公钥和ECDH派生密钥再结合AES来包装私钥。对于CKM_ECDH_AES_KEY_WRAP机制现实中应该很少有工具实现,而且CMPV2发布时还没有定义CKM_ECDH_AES_KEY_WRAP。这就导致CA证书只能使用RSA签名,带来了不少兼容性问题。

    PKCS#11 3.1:
    https://docs.oasis-open.org/pkcs11/pkcs11-spec/v3.1/os/pkcs11-spec-v3.1-os.html
    PKCS#11 2.11:
    https://www.cryptsoft.com/pkcs11doc/v211/

    CMP V3

    CMP V3中使用EnvelopedData结构加密,Enveloped-data结构是在CMS标准中定义的。
    RFC 3370定义了Ephemeral-Static Diffie-Hellman,Static-Static Diffie-Hellman,RSA算法的用于CMS的EnvelopedData
    RFC 5753定义了ephemeral-static ECDH, ephemeral-static ECDH,1-Pass ECMQV算法的用于CMS的EnvelopedData
    RFC 8418定义了X25519,X448算法的用于CMS的EnvelopedData
    RFC 5652定义Cryptographic Message Syntax,其中定义了Enveloped-data数据结构。CMS由PKCS #7 1。5演进而来.
    CMP V3中指定了3种方法:
    * recipient’s certificate with an algorithm identifier and a public
    key that supports key transport and where any given key usage
    extension allows keyEncipherment: The content-encryption key will
    be protected using the key transport key management technique, as
    specified in Section 6.2.1 of CMS [RFC5652].

    • recipient’s certificate with an algorithm identifier and a public
      key that supports key agreement and where any given key usage
      extension allows keyAgreement: The content-encryption key will be
      protected using the key agreement key management technique, as
      specified in Section 6.2.2 of CMS [RFC5652].

    • a password or shared secret: The content-encryption key will be
      protected using the password-based key management technique, as
      specified in Section 6.2.4 of CMS [RFC5652].
      翻译过来分别是具有keyEncipherment标志位证书,具体keyAgreement标志位的证书,密码加密。
      目前据我所知nist,secp,brainpool系列的曲线支持digitalSignature和keyAgreement,SM2支持所有三种用途,ed25519/448仅支持digitalSignature,x25519/448仅支持keyAgreement,cruve25519这种把digitalSignature和keyAgreement分开的设计使其不适合用于CA证书。通过ECIES可以让支持keyAgreement的ECC都支持keyEncipherment,其实SM2加密时也是一种特殊的ECIES(X9.63标准)。目前支持ECIES的加密程序库并不多,只有bouncy castle,crypto++支持,也就是用ECC算法实现keyEncipherment还不太现实。

    其实所有的加密方案都不是由CMP定义的,面是直接使用了其它标准中的机制。

    参考:
    https://webstore.ansi.org/preview-pages/ASCX9/preview_ANSI+X9.63-2001.pdf
    https://webstore.ansi.org/preview-pages/ASCX9/preview_ANSI+X9.63-2011+(R2017).pdf
    https://csrc.nist.gov/CSRC/media/Events/Key-Management-Workshop-2000/documents/x963_overview.pdf

    Views: 22

  • CMP V2协议(RFC4210)

    CMP(Certificate Management Protocol)是由IETF开发的协议,用于证书管理,包括证书申请、更新、撤销等。CMP协议的标准由IETF的RFC 4210定义。CMP协议消息的格式基于ASN.1(Abstract Syntax Notation One)描述,并使用DER(Distinguished Encoding Rules)进行编码。
    RFC4210定义的是CMP V2,最新的CMP V3(RFC9480)也已经发布了。
    以下是关于CMP协议格式的详细说明:

    1. CMP协议消息的基本结构

    CMP协议消息的基本结构由以下几个部分组成:

    1.1. PKIMessage

    PKIMessage是CMP协议的基本消息类型,包含了所有CMP消息的通用结构。其ASN.1描述如下:

    “`asn.1
    PKIMessage ::= SEQUENCE {
    header PKIHeader,
    body PKIBody,
    protection [0] PKIProtection OPTIONAL,
    extraCerts [1] SEQUENCE SIZE (1..MAX) OF CMPCertificate OPTIONAL
    }

    <pre><code class="line-numbers">- **header**:PKIHeader,包含消息的元数据,如消息类型、发送者和接收者信息等。
    – **body**:PKIBody,包含实际的消息内容,如证书请求、证书响应等。
    – **protection**:PKIProtection,可选字段,用于保护消息的完整性和真实性。
    – **extraCerts**:SEQUENCE OF CMPCertificate,可选字段,包含额外的证书。

    ### 1.2. PKIHeader

    PKIHeader包含消息的元数据,其ASN.1描述如下:

    “`asn.1
    PKIHeader ::= SEQUENCE {
    pvno INTEGER { cmp1999(1), cmp2000(2) },
    sender GeneralName,
    recipient GeneralName,
    messageTime [0] GeneralizedTime OPTIONAL,
    protectionAlg [1] AlgorithmIdentifier OPTIONAL,
    senderKID [2] KeyIdentifier OPTIONAL,
    recipKID [3] KeyIdentifier OPTIONAL,
    transactionID [4] OCTET STRING OPTIONAL,
    senderNonce [5] OCTET STRING OPTIONAL,
    recipNonce [6] OCTET STRING OPTIONAL,
    freeText [7] PKIFreeText OPTIONAL,
    generalInfo [8] SEQUENCE SIZE (1..MAX) OF InfoTypeAndValue OPTIONAL
    }

    • pvno:协议版本号。
    • sender:发送者的标识。
    • recipient:接收者的标识。
    • messageTime:消息的时间戳(可选)。
    • protectionAlg:用于保护消息的算法标识符(可选)。
    • senderKID:发送者的密钥标识符(可选)。
    • recipKID:接收者的密钥标识符(可选)。
    • transactionID:事务ID(可选)。
    • senderNonce:发送者的随机数(可选)。
    • recipNonce:接收者的随机数(可选)。
    • freeText:自由文本(可选)。
    • generalInfo:一般信息(可选)。

    1.3. PKIBody

    PKIBody包含实际的消息内容,其ASN.1描述如下:

    “`asn.1
    PKIBody ::= CHOICE {
    ir [0] CertReqMessages, — Initialization Request
    ip [1] CertRepMessage, — Initialization Response
    cr [2] CertReqMessages, — Certification Request
    cp [3] CertRepMessage, — Certification Response
    p10cr [4] CertificationRequest, — PKCS #10 Certificate Request
    popdecc [5] POPODecKeyChallContent, — Proof-of-Possession Challenge
    popdecr [6] POPODecKeyRespContent, — Proof-of-Possession Response
    kur [7] CertReqMessages, — Key Update Request
    kup [8] CertRepMessage, — Key Update Response
    krr [9] CertReqMessages, — Key Recovery Request
    krp [10] KeyRecRepContent, — Key Recovery Response
    rr [11] RevReqContent, — Revocation Request
    rp [12] RevRepContent, — Revocation Response
    ccr [13] CertReqMessages, — Cross-Certification Request
    ccp [14] CertRepMessage, — Cross-Certification Response
    ckuann [15] CAKeyUpdAnnContent, — CA Key Update Announcement
    cann [16] CertAnnContent, — Certificate Announcement
    rann [17] RevAnnContent, — Revocation Announcement
    crlann [18] CRLAnnContent, — CRL Announcement
    pkiconf [19] PKIConfirmContent, — Confirmation
    nested [20] NestedMessageContent, — Nested Message
    genm [21] GenMsgContent, — General Message
    genp [22] GenRepContent, — General Response
    error [23] ErrorMsgContent, — Error Message
    certConf [24] CertConfirmContent, — Certificate Confirmation
    pollReq [25] PollReqContent, — Polling Request
    pollRep [26] PollRepContent — Polling Response
    }

    <pre><code class="line-numbers">- **ir**:初始化请求(Initialization Request)。
    – **ip**:初始化响应(Initialization Response)。
    – **cr**:认证请求(Certification Request)。
    – **cp**:认证响应(Certification Response)。
    – **p10cr**:PKCS #10证书请求(PKCS #10 Certificate Request)。
    – **popdecc**:证明密钥拥有权挑战(Proof-of-Possession Challenge)。
    – **popdecr**:证明密钥拥有权响应(Proof-of-Possession Response)。
    – **kur**:密钥更新请求(Key Update Request)。
    – **kup**:密钥更新响应(Key Update Response)。
    – **krr**:密钥恢复请求(Key Recovery Request)。
    – **krp**:密钥恢复响应(Key Recovery Response)。
    – **rr**:撤销请求(Revocation Request)。
    – **rp**:撤销响应(Revocation Response)。
    – **ccr**:交叉认证请求(Cross-Certification Request)。
    – **ccp**:交叉认证响应(Cross-Certification Response)。
    – **ckuann**:CA密钥更新公告(CA Key Update Announcement)。
    – **cann**:证书公告(Certificate Announcement)。
    – **rann**:撤销公告(Revocation Announcement)。
    – **crlann**:CRL公告(CRL Announcement)。
    – **pkiconf**:确认(Confirmation)。
    – **nested**:嵌套消息(Nested Message)。
    – **genm**:一般消息(General Message)。
    – **genp**:一般响应(General Response)。
    – **error**:错误消息(Error Message)。
    – **certConf**:证书确认(Certificate Confirmation)。
    – **pollReq**:轮询请求(Polling Request)。
    – **pollRep**:轮询响应(Polling Response)。

    ## 2. CMP协议消息的保护

    CMP协议消息可以通过PKIProtection字段进行保护,确保消息的完整性和真实性。PKIProtection字段的ASN.1描述如下:

    “`asn.1
    PKIProtection ::= BIT STRING

    • PKIProtection:包含消息的保护信息,通常是消息的签名或MAC(消息认证码)。

    3. CMP协议消息的编码

    CMP协议消息使用ASN.1描述,并通过DER(Distinguished Encoding Rules)进行编码。DER是一种二进制编码规则,确保消息的唯一编码。

    4. 示例:CMP协议消息

    以下是一个简单的CMP协议消息示例,展示了初始化请求(Initialization Request)的结构:

    “`asn.1
    PKIMessage ::= SEQUENCE {
    header PKIHeader {
    pvno cmp2000,
    sender GeneralName { directoryName { rdnSequence { commonName "Client" } } },
    recipient GeneralName { directoryName { rdnSequence { commonName "CA" } } },
    messageTime GeneralizedTime "20230101000000Z",
    senderNonce OCTET STRING "1234567890abcdef",
    transactionID OCTET STRING "abcdef1234567890"
    },
    body PKIBody {
    ir CertReqMessages {
    CertReqMsg {
    certReq CertRequest {
    certReqId INTEGER 1,
    certTemplate CertTemplate {
    version Version v3,
    subject Name { rdnSequence { commonName "Client" } },
    publicKey SubjectPublicKeyInfo { algorithm { algorithm rsaEncryption }, subjectPublicKey BIT STRING "…" }
    }
    },
    popo ProofOfPossession { raVerified },
    regInfo SEQUENCE { }
    }
    }
    },
    protection PKIProtection "…"
    }

    <pre><code class="line-numbers">## PKI管理模型

    ### PKI实体

    – 最终实体(End Entities,EE)

    最终实体就是subject字段所代表的主体。后面最终实体都用EE表示。

    – 证书颁发机构(Certification Authority,CA)

    CA是issuer字段所代表的主体,CA可以是第三方主体,也可以是与EE相同的主体,可以是离线的也可以是在线的。root CA是EE需要直接信任的CA。

    – 注册机构(Registration Authority,RA)

    许多环境需要独立于CA的注册机构。
    注册机构可能执行的功能因情况而异,但可能包括个人身份验证、令牌分发、撤销报告、名称分配、密钥生成、密钥对存档等。
    RA是可选的,当没有RA时CA完成RA的所有功能。

    RA本身也是一个EE,RA其实就是具有可以签名私钥(签名CSR)的经过认证的EE,CA把哪体EE识别为RA取决于具体实现。一个RA可以属于多个CA。

    ### PKI管理操作

    管理操作的流程图
    “`ascii
    +—+ cert. publish +————+ j
    | | <——————— | End Entity | <——-
    | C | g +————+ “out-of-band”
    | e | | ^ loading
    | r | | | initial
    | t | a | | b registration/
    | | | | certification
    | / | | | key pair recovery
    | | | | key pair update
    | C | | | certificate update
    | R | PKI “USERS” V | revocation request
    | L | ——————-+-+—–+-+——+-+——————-
    | | PKI MANAGEMENT | ^ | ^
    | | ENTITIES a | | b a | | b
    | R | V | | |
    | e | g +——+ d | |
    | p | <———— | RA | <—–+ | |
    | o | cert. | | —-+ | | |
    | s | publish +——+ c | | | |
    | i | | | | |
    | t | V | V |
    | o | g +————+ i
    | r | <————————| CA |——->
    | y | h +————+ “out-of-band”
    | | cert. publish | ^ publication
    | | CRL publish | |
    +—+ | | cross-certification
    e | | f cross-certificate
    | | update
    | |
    V |
    +——+
    | CA-2 |
    +——+

    PKI管理操作包括:

    1. CA建立

      生成自签名证书或者中间CA证书,生成初始CRL,生成证书序列号文件,生成证书数据库

    2. EE初始化

      导入CA证书,获取CA的其它信息

    3. Certification(创建新证书的各种操作)

      3.1. initial registration/certification

      生成密钥对,注册或生成自己的证书请求

      3.2. key pair update

      更新密钥对并颁发新的证书

      3.3. certificate update

      证书过期时如果环境没有任何其它的变化则会refreshed证书

      3.4. CA key pair update

      与EE一样,CA密钥对也需要进行更新,但是会使用不同的机制

      3.5. cross-certification request

      一个CA向另一个CA请求交叉证书

      3.6. cross-certificate update

      同普通证书update

    4. Certificate/CRL discovery operations

      后面具体讲

    5. Recovery operations

      主要是key pair recovery,如果EE丢失了自己的私钥并且CA备份了EE的私钥的情况下可以请求CA恢复私钥

    6. Revocation operations

      证书吊销

    7. PSE operations

      PSE是啥RFC4210中也没说,我想指的应该是发布证书的时候确保提供一个安全渠道。

    Assumptions and Restrictions

    rfc4210中是叫这个标题,我也不知道啥意思

    1. Initiation of Registration/Certification

      第一条PKI消息的产生意味着触发Initiation of Registration/Certification,这可以发生在任何地方,包括EE,RA,CA。

    2. End Entity Message Origin Authentication

      EE与RA或CA通信时需要经过身份认证,也可以不认证。本规范中是通过secret value(用于生成保护字段的MAC,同时用于初始化请求的身份验证,华为的文档中叫秘密值)和reference value(就是senderKID的referenceNum,华为的文档中叫参考值)来认证的,secret value和reference value由CA生成并通过带外途径发送给EE。

    3. Location of Key Generation

      密钥生成的位置等同于PKI消息中首次出现密钥的地方,这可以是EE,RA,CA。

    4. Confirmation of Successful Certification

      CA生成证书后,可以让EE明确接收或不接受证书。

    Mandatory Schemes

    1. Centralized Scheme

      即中心化方案

      • Initiation of Registration/Certification由CA完成
      • 不需要End Entity Message Origin Authentication
      • Location of Key Generation生成在CA
      • 不需要Confirmation of Successful Certification
    2. Basic Authenticated Scheme
      • Initiation of Registration/Certification由EE完成
      • 需要End Entity Message Origin Authentication
      • Location of Key Generation生成在EE
      • 需要Confirmation of Successful Certification

    Proof-of-Possession (POP) of Private Key

    EE持有的私钥,需要向CA/RA证明自己是对应公钥的私钥持有者,这包括用自己的私钥对证书请求进行签名,向CA/RA发送私钥,使用私钥加密一个值。对于Key Agreement Keys,例如DH密钥,可以通过生成一个共享密钥来证明。
    POP证明有很多种,包括包含私钥,直接法(质询响应),间接法(公钥加密证书),后面还有更多说明。

    Root CA Key Update

    更新root CA的密钥是一个比较麻烦的事,因为需要所有EE,RA都要同步处理。
    需要执行的操作包括:

    1. Generate a new key pair;

    2. Create a certificate containing the old CA public key signed with
      the new private key (the “old with new” certificate);

    3. Create a certificate containing the new CA public key signed with
      the old private key (the “new with old” certificate);

    4. Create a certificate containing the new CA public key signed with
      the new private key (the “new with new” certificate);

    5. Publish these new certificates via the repository and/or other
      means (perhaps using a CAKeyUpdAnn message);

    6. Export the new CA public key so that end entities may acquire it
      using the “out-of-band” mechanism (if required).

    The old CA private key is then no longer required. However, the old
    CA public key will remain in use for some time. The old CA public
    key is no longer required (other than for non-repudiation) when all
    end entities of this CA have securely acquired the new CA public key.
    The “old with new” certificate must have a validity period starting
    at the generation time of the old key pair and ending at the expiry
    date of the old public key.

    The “new with old” certificate must have a validity period starting
    at the generation time of the new key pair and ending at the time by
    which all end entities of this CA will securely possess the new CA
    public key (at the latest, the expiry date of the old public key).

    The “new with new” certificate must have a validity period starting
    at the generation time of the new key pair and ending at or before
    the time by which the CA will next update its key pair.

    数据结构

    总体PKI消息

        PKIMessage ::= SEQUENCE {
             header           PKIHeader,
             body             PKIBody,
             protection   [0] PKIProtection OPTIONAL,
             extraCerts   [1] SEQUENCE SIZE (1..MAX) OF CMPCertificate
                              OPTIONAL
        }
        PKIMessages ::= SEQUENCE SIZE (1..MAX) OF PKIMessage
    
    protection 就是消息的签名或者完整性验证
    extraCerts 如果颁发证书的CA不是rootCA,则需要一个证书链来验证证书的有效性,所以这就是新证书的证书链(如果有的话)。在证书请求中还可以用于包含当前用于签名验证的证书。Root CA Key Update消息中则用于发布证书。所以这个字段在不同场景下用途也不一样。
    

    PKI消息头

        PKIHeader ::= SEQUENCE {
            pvno                INTEGER     { cmp1999(1), cmp2000(2) },
            sender              GeneralName,
            recipient           GeneralName,
            messageTime     [0] GeneralizedTime         OPTIONAL,
            protectionAlg   [1] AlgorithmIdentifier     OPTIONAL,
            senderKID       [2] KeyIdentifier           OPTIONAL,
            recipKID        [3] KeyIdentifier           OPTIONAL,
            transactionID   [4] OCTET STRING            OPTIONAL,
            senderNonce     [5] OCTET STRING            OPTIONAL,
            recipNonce      [6] OCTET STRING            OPTIONAL,
            freeText        [7] PKIFreeText             OPTIONAL,
            generalInfo     [8] SEQUENCE SIZE (1..MAX) OF
                                InfoTypeAndValue     OPTIONAL
        }
        PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String
    

    pvno 就是cmp协议的版本号,对于rfc4210来说始终是2
    sender 发送者的DN名称
    recipient 接收者的DN名称
    messageTime 消息发送时间
    protectionAlg protection字段的算法标识符
    senderKID,recipKID DH算法的密钥标识符,有多对密钥时才需要,在初始化请求中senderKID就是用于身份认证的referenceNum(参考值)
    transactionID 事务ID,也就是会话ID,一般由发起会话者生成,后面保持不变
    senderNonce,recipNonce 每个报文生成一个随机数(为什么不用递增序列?),用于防重放攻击
    freeText 包含人类可读信息(可以由多个),回复消息时使用与第一个Text相同的语言。
    generalInfo 向对方发送附加的机器可处理信息,这是一个generalInfo类型的数组,一共定义了以下generalInfo扩展:

    • ImplicitConfirm
      implicitConfirm OBJECT IDENTIFIER ::= {id-it 13}
      ImplicitConfirmValue ::= NULL
      

      如果存在这个generalInfo扩展则不发送证书确认

    • ConfirmWaitTime

      confirmWaitTime OBJECT IDENTIFIER ::= {id-it 14}
      ConfirmWaitTimeValue ::= GeneralizedTime
      

      证书确认的超时时间,如果超时则撤销证书并删除会话。

    PKI消息体

            PKIBody ::= CHOICE {
              ir       [0]  CertReqMessages,       --Initialization Req
              ip       [1]  CertRepMessage,        --Initialization Resp
              cr       [2]  CertReqMessages,       --Certification Req
              cp       [3]  CertRepMessage,        --Certification Resp
              p10cr    [4]  CertificationRequest,  --PKCS #10 Cert.  Req.
              popdecc  [5]  POPODecKeyChallContent --pop Challenge
              popdecr  [6]  POPODecKeyRespContent, --pop Response
              kur      [7]  CertReqMessages,       --Key Update Request
              kup      [8]  CertRepMessage,        --Key Update Response
              krr      [9]  CertReqMessages,       --Key Recovery Req
              krp      [10] KeyRecRepContent,      --Key Recovery Resp
              rr       [11] RevReqContent,         --Revocation Request
              rp       [12] RevRepContent,         --Revocation Response
              ccr      [13] CertReqMessages,       --Cross-Cert.  Request
              ccp      [14] CertRepMessage,        --Cross-Cert.  Resp
              ckuann   [15] CAKeyUpdAnnContent,    --CA Key Update Ann.
              cann     [16] CertAnnContent,        --Certificate Ann.
              rann     [17] RevAnnContent,         --Revocation Ann.
              crlann   [18] CRLAnnContent,         --CRL Announcement
              pkiconf  [19] PKIConfirmContent,     --Confirmation
              nested   [20] NestedMessageContent,  --Nested Message
              genm     [21] GenMsgContent,         --General Message
              genp     [22] GenRepContent,         --General Response
              error    [23] ErrorMsgContent,       --Error Message
              certConf [24] CertConfirmContent,    --Certificate confirm
              pollReq  [25] PollReqContent,        --Polling request
              pollRep  [26] PollRepContent         --Polling response
              }
    
    

    一共27种报文,还是比较多的。
    具体描述在下在面。

    Common Data Structures

    在说明具本的PKIBody前要先说一下通用的数据结构

    • 发送证书

    当需要包含证书内容时使用CertTemplate语法,CA/RA可能修改证书请求的内容,EE也可以选择拒绝CA签置的证书(如果内容被修改)。

    • Encrypted Values

    通常是私钥和证书(接收新证书),需要使用EncryptedValue数据结构,要求双方事先有能解密秘密数据的密钥。

    • 状态码和故障信息

    状态码

            PKIStatus ::= INTEGER {
                accepted               (0),
                grantedWithMods        (1),
                rejection              (2),
                waiting                (3),
                revocationWarning      (4),
                revocationNotification (5),
                keyUpdateWarning       (6)
            }
    

    故障

            PKIFailureInfo ::= BIT STRING {
                badAlg              (0),
                badMessageCheck     (1),
                badRequest          (2),
                badTime             (3),
                badCertId           (4),
                badDataFormat       (5),
                wrongAuthority      (6),
                incorrectData       (7),
                missingTimeStamp    (8),
                badPOP              (9),
                certRevoked         (10),
                certConfirmed       (11),
                wrongIntegrity      (12),
                badRecipientNonce   (13),
                timeNotAvailable    (14),
                unacceptedPolicy    (15),
                unacceptedExtension (16),
                addInfoNotAvailable (17),
                badSenderNonce      (18),
                badCertTemplate     (19),
                signerNotTrusted    (20),
                transactionIdInUse  (21),
                unsupportedVersion  (22),
                notAuthorized       (23),
                systemUnavail       (24),
                systemFailure       (25),
                duplicateCertReq    (26)
            }
            PKIStatusInfo ::= SEQUENCE {
                status        PKIStatus,
                statusString  PKIFreeText     OPTIONAL,
                failInfo      PKIFailureInfo  OPTIONAL
            }
    
    • 证书标识

    用于标只特定的证书,使用CertId数据结构

    • root CA
    OOBCert ::= Certificate
    # or
    OOBCertHash ::= SEQUENCE {
        hashAlg     [0] AlgorithmIdentifier     OPTIONAL,
        certId      [1] CertId                  OPTIONAL,
        hashVal         BIT STRING
    }
    

    对于Certificat要求:

    • 证书是自签名的,即可以使用SubjectPublicKeyInfo中的公钥验证签名
    • subject和issuer相同
    • 如果subject为空则要求SubjectAltName和issuerAltNames相同
    • key identifiers for subject and issuer相同

    OOBCertHash用途是通过带外方式获取root CA时对root CA进行验证。

    • Archive Options

    请求者希望PKI使用PKIArchiveOptions结构存档私钥

    • Publication Information

    请求者希望PKI通过PKIPublicationInfo结构发布证书

    • Proof-of-Possession Structures

      如果请求的是一个签名证书,私钥的所有权证明是通过POPOSigningKey结构完成的。

      参考附录 C and [CRMF] for POPOSigningKey 语法,

          POPOSigningKey ::= SEQUENCE {
              poposkInput           [0] POPOSigningKeyInput OPTIONAL,
              algorithmIdentifier   AlgorithmIdentifier,
              signature             BIT STRING
              #这个签名是用私钥对poposkInput的DER编码的签名,如果EE能用公钥验证这个签名就完成了私钥所有权证明
          }
          POPOSigningKeyInput ::= SEQUENCE {
              authInfo            CHOICE {
                  sender              [0] GeneralName,
                  publicKeyMAC        PKMACValue
              },
              publicKey           SubjectPublicKeyInfo
          }
      

      另一方面, 如果请求的是一个加密证书, 那么私钥的所有权证明可以用下面三种方法中的一个(包含私钥,直接法,间接法)

    • Inclusion of the Private Key

      在CertRequest请求的POPOPrivKey的thisMessage,或者通过PKIArchiveOptions字段包含私钥,这取决于是否需要私钥存档

      POPOPrivKey ::= CHOICE {
          thisMessage       [0] BIT STRING,
      -- **********
      -- * the type of "thisMessage" is given as BIT STRING in
      -- * [CRMF]; it should be "EncryptedValue" (in accordance
      -- * with Section 5.2.2, "Encrypted Values", of this specification).
      -- * Therefore, this document makes the behavioral clarification
      -- * of specifying that the contents of "thisMessage" MUST be encoded
      -- * as an EncryptedValue and then wrapped in a BIT STRING.  This
      -- * allows the necessary conveyance and protection of the
      -- * private key while maintaining bits-on-the-wire compatibility
      -- * with [CRMF].
      -- **********
      subsequentMessage [1] SubsequentMessage,
      dhMAC             [2] BIT STRING }
      
    • Indirect Method

      返回公钥加密的证书,只有用EE的私钥才能解密。

    • Challenge-Response Protocol(direct method)

      通过Challenge-Response Protocol证明EE私钥的POP

    • PoP选项总结

      下面列出了代表各种POP技术的选项. Using “SK” for “signing key”, “EK” for “encryption key”,and “KAK” for “key agreement key”, 总结如下:

      RAVerified;
      SKPOP;
      EKPOPThisMessage;
      KAKPOPThisMessage;
      KAKPOPThisMessageDHMAC;
      EKPOPEncryptedCert;
      KAKPOPEncryptedCert;
      EKPOPChallengeResp; and
      KAKPOPChallengeResp.
      

      考虑到这一系列选项,自然会问EE如何知道CA/RA支持什么选项/技术(即,在请求证书时可以使用哪些选项)。以下指南应该为EE实现者澄清这种情况。

      • RAVerified

        这是RA向CA请求是用的,EE不需要考虑

      • SKPOP

        如果EE有一个签名密钥对,则这是唯一可用选项

      • EKPOPThisMessage and KAKPOPThisMessage

        如果决定向CA/RA公开私钥,则只有这里的方法可以做到,根据密钥类型选择EKPOPThisMessage 还是 KAKPOPThisMessage

      • KAKPOPThisMessageDHMAC

        仅当CA有可用于此目的的DH证书,以及EE已经有此证书的副本时,EE才能使用此方法。如果这两个条件都成立,那么这种技术就得到了明确的支持。

      • EKPOPEncryptedCert, KAKPOPEncryptedCert, EKPOPChallengeResp, KAKPOPChallengeResp

        EE在请求消息中选择其中一个(在subsequentMessage字段中),具体取决于偏好和密钥对类型。EE在此处没有做POP;它只是向CA/RA指示要使用哪种方法。因此,如果CA/RA回复“badPOP”错误,则EE可以使用在后续消息中选择的其他POP方法重新请求。但是,请注意,本规范鼓励使用EncryptedCert选项,并且还指出,当涉及RA并进行POP验证时,通常会使用质询响应。间接方法需要发送的消息更少,也比较简单是EE尽可能使用这种方法,如果是向RA提交POP证明则会使用直接方法(质询响应)

    特定操作的数据结构

    一共定义了27个操作类型,怪不得CMP是最复杂的证书协议

    • Initialization Request

      一个Initialization请求包含CertReqMessages数据结构作为PKI Body,该数据结构指定要请求的证书(CRMF格式(RFC4211),类似于PKCS#10),通常会包含SubjectPublicKeyInfo、KeyId和Validity字段。此消息用于首次初始化PKI实体时。
      因为是首次请求证书需要带外方式获取身份验证信息,使用MSG_MAC_ALG进行身份验证,MAC信息是用共享的Secret加了盐的。senderKID就是参考值。
      一个请求可以包含多个证书,证书模板(要请求的证书)放在CertTemplate字段中。

      CertReqMessages在Rfc4211中定义,很复杂,以下是ASN.1代码:

      -- Core definitions for this module
      
      CertReqMessages ::= SEQUENCE SIZE (1..MAX) OF CertReqMsg
      
      CertReqMsg ::= SEQUENCE {
      certReq   CertRequest,
      popo       ProofOfPossession  OPTIONAL,
      -- content depends upon key type
      regInfo   SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue OPTIONAL }
      
      CertRequest ::= SEQUENCE {
      certReqId     INTEGER,          -- ID for matching request and reply
      certTemplate  CertTemplate,  -- Selected fields of cert to be issued
      controls      Controls OPTIONAL }   -- Attributes affecting issuance
      
      CertTemplate ::= SEQUENCE {
      version      [0] Version               OPTIONAL,
      serialNumber [1] INTEGER               OPTIONAL,
      signingAlg   [2] AlgorithmIdentifier   OPTIONAL,
      issuer       [3] Name                  OPTIONAL,
      validity     [4] OptionalValidity      OPTIONAL,
      subject      [5] Name                  OPTIONAL,
      publicKey    [6] SubjectPublicKeyInfo  OPTIONAL,
      issuerUID    [7] UniqueIdentifier      OPTIONAL,
      subjectUID   [8] UniqueIdentifier      OPTIONAL,
      extensions   [9] Extensions            OPTIONAL }
      
      OptionalValidity ::= SEQUENCE {
      notBefore  [0] Time OPTIONAL,
      notAfter   [1] Time OPTIONAL } -- at least one MUST be present
      
      Controls  ::= SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue
      AttributeTypeAndValue ::= SEQUENCE {
      type         OBJECT IDENTIFIER,
      value        ANY DEFINED BY type }
      
      ProofOfPossession ::= CHOICE {
      raVerified        [0] NULL,
      -- used if the RA has already verified that the requester is in
      -- possession of the private key
      signature         [1] POPOSigningKey,
      keyEncipherment   [2] POPOPrivKey,
      keyAgreement      [3] POPOPrivKey }
      
      POPOSigningKey ::= SEQUENCE {
      poposkInput           [0] POPOSigningKeyInput OPTIONAL,
      algorithmIdentifier   AlgorithmIdentifier,
      signature             BIT STRING }
      -- The signature (using "algorithmIdentifier") is on the
      -- DER-encoded value of poposkInput.  NOTE: If the CertReqMsg
      -- certReq CertTemplate contains the subject and publicKey values,
      -- then poposkInput MUST be omitted and the signature MUST be
      -- computed over the DER-encoded value of CertReqMsg certReq.  If
      -- the CertReqMsg certReq CertTemplate does not contain both the
      -- public key and subject values (i.e., if it contains only one
      -- of these, or neither), then poposkInput MUST be present and
      -- MUST be signed.
      
      
      POPOSigningKeyInput ::= SEQUENCE {
      authInfo            CHOICE {
          sender              [0] GeneralName,
          -- used only if an authenticated identity has been
          -- established for the sender (e.g., a DN from a
          -- previously-issued and currently-valid certificate)
          publicKeyMAC        PKMACValue },
          -- used if no authenticated GeneralName currently exists for
          -- the sender; publicKeyMAC contains a password-based MAC
          -- on the DER-encoded value of publicKey
      publicKey           SubjectPublicKeyInfo }  -- from CertTemplate
      
      PKMACValue ::= SEQUENCE {
      algId  AlgorithmIdentifier,
      -- algorithm value shall be PasswordBasedMac {1 2 840 113533 7 66 13}
      -- parameter value is PBMParameter
      value  BIT STRING }
      
      PBMParameter ::= SEQUENCE {
      salt                OCTET STRING,
      owf                 AlgorithmIdentifier,
      -- AlgId for a One-Way Function (SHA-1 recommended)
      iterationCount      INTEGER,
      -- number of times the OWF is applied
      mac                 AlgorithmIdentifier
      -- the MAC AlgId (e.g., DES-MAC, Triple-DES-MAC [PKCS11],
      }   -- or HMAC [HMAC, RFC2202])
      
      POPOPrivKey ::= CHOICE {
      thisMessage       [0] BIT STRING,         -- Deprecated
      -- possession is proven in this message (which contains the private
      -- key itself (encrypted for the CA))
      subsequentMessage [1] SubsequentMessage,
      -- possession will be proven in a subsequent message
      dhMAC             [2] BIT STRING,         -- Deprecated
      agreeMAC          [3] PKMACValue,
      encryptedKey      [4] EnvelopedData }
      
      -- for keyAgreement (only), possession is proven in this message
      -- (which contains a MAC (over the DER-encoded value of the
      -- certReq parameter in CertReqMsg, which MUST include both subject
      -- and publicKey) based on a key derived from the end entity's
      -- private DH key and the CA's public DH key);
      
      SubsequentMessage ::= INTEGER {
      encrCert (0),
      -- requests that resulting certificate be encrypted for the
      -- end entity (following which, POP will be proven in a
      -- confirmation message)
      challengeResp (1) }
      -- requests that CA engage in challenge-response exchange with
      -- end entity in order to prove private key possession
      
      -- Object identifier assignments --
      
      -- Registration Controls in CRMF
      id-regCtrl OBJECT IDENTIFIER ::= { id-pkip 1 }
      
      
      id-regCtrl-regToken OBJECT IDENTIFIER ::= { id-regCtrl 1 }
      --with syntax:
      RegToken ::= UTF8String
      
      id-regCtrl-authenticator OBJECT IDENTIFIER ::= { id-regCtrl 2 }
      --with syntax:
      Authenticator ::= UTF8String
      
      id-regCtrl-pkiPublicationInfo OBJECT IDENTIFIER ::= { id-regCtrl 3 }
      --with syntax:
      
      PKIPublicationInfo ::= SEQUENCE {
      action     INTEGER {
                  dontPublish (0),
                  pleasePublish (1) },
      pubInfos  SEQUENCE SIZE (1..MAX) OF SinglePubInfo OPTIONAL }
      -- pubInfos MUST NOT be present if action is "dontPublish"
      -- (if action is "pleasePublish" and pubInfos is omitted,
      -- "dontCare" is assumed)
      
      SinglePubInfo ::= SEQUENCE {
      pubMethod    INTEGER {
          dontCare    (0),
          x500        (1),
          web         (2),
          ldap        (3) },
      pubLocation  GeneralName OPTIONAL }
      
      id-regCtrl-pkiArchiveOptions     OBJECT IDENTIFIER ::= { id-regCtrl 4 }
      --with syntax:
      PKIArchiveOptions ::= CHOICE {
      encryptedPrivKey     [0] EncryptedKey,
      -- the actual value of the private key
      keyGenParameters     [1] KeyGenParameters,
      -- parameters that allow the private key to be re-generated
      archiveRemGenPrivKey [2] BOOLEAN }
      -- set to TRUE if sender wishes receiver to archive the private
      -- key of a key pair that the receiver generates in response to
      -- this request; set to FALSE if no archival is desired.
      
      EncryptedKey ::= CHOICE {
      encryptedValue        EncryptedValue,   -- Deprecated
      envelopedData     [0] EnvelopedData }
      -- The encrypted private key MUST be placed in the envelopedData
      -- encryptedContentInfo encryptedContent OCTET STRING.
      
      EncryptedValue ::= SEQUENCE {
      intendedAlg   [0] AlgorithmIdentifier  OPTIONAL,
      -- the intended algorithm for which the value will be used
      symmAlg       [1] AlgorithmIdentifier  OPTIONAL,
      -- the symmetric algorithm used to encrypt the value
      encSymmKey    [2] BIT STRING           OPTIONAL,
      -- the (encrypted) symmetric key used to encrypt the value
      keyAlg        [3] AlgorithmIdentifier  OPTIONAL,
      -- algorithm used to encrypt the symmetric key
      valueHint     [4] OCTET STRING         OPTIONAL,
      -- a brief description or identifier of the encValue content
      -- (may be meaningful only to the sending entity, and used only
      -- if EncryptedValue might be re-examined by the sending entity
      -- in the future)
      encValue       BIT STRING }
      -- the encrypted value itself
      -- When EncryptedValue is used to carry a private key (as opposed to
      -- a certificate), implementations MUST support the encValue field
      -- containing an encrypted PrivateKeyInfo as defined in [PKCS11],
      -- section 12.11.  If encValue contains some other format/encoding
      -- for the private key, the first octet of valueHint MAY be used
      -- to indicate the format/encoding (but note that the possible values
      -- of this octet are not specified at this time).  In all cases, the
      -- intendedAlg field MUST be used to indicate at least the OID of
      -- the intended algorithm of the private key, unless this information
      -- is known a priori to both sender and receiver by some other means.
      
      KeyGenParameters ::= OCTET STRING
      
      id-regCtrl-oldCertID          OBJECT IDENTIFIER ::= { id-regCtrl 5 }
      --with syntax:
      OldCertId ::= CertId
      
      CertId ::= SEQUENCE {
      issuer           GeneralName,
      serialNumber     INTEGER }
      
      id-regCtrl-protocolEncrKey    OBJECT IDENTIFIER ::= { id-regCtrl 6 }
      --with syntax:
      ProtocolEncrKey ::= SubjectPublicKeyInfo
      
      -- Registration Info in CRMF
      id-regInfo OBJECT IDENTIFIER ::= { id-pkip 2 }
      
      id-regInfo-utf8Pairs    OBJECT IDENTIFIER ::= { id-regInfo 1 }
      --with syntax
      UTF8Pairs ::= UTF8String
      
      
      id-regInfo-certReq       OBJECT IDENTIFIER ::= { id-regInfo 2 }
      --with syntax
      CertReq ::= CertRequest
      
      -- id-ct-encKeyWithID is a new content type used for CMS objects.
      -- it contains both a private key and an identifier for key escrow
      -- agents to check against recovery requestors.
      
      id-ct-encKeyWithID OBJECT IDENTIFIER ::= {id-ct 21}
      
      EncKeyWithID ::= SEQUENCE {
      privateKey           PrivateKeyInfo,
      identifier CHOICE {
          string             UTF8String,
          generalName        GeneralName
      } OPTIONAL
      }
      
      PrivateKeyInfo ::= SEQUENCE {
      version                   INTEGER,
      privateKeyAlgorithm       AlgorithmIdentifier,
      privateKey                OCTET STRING,
      attributes                [0] IMPLICIT Attributes OPTIONAL
      }
      
      Attributes ::= SET OF Attribute
      
    • Initialization Response

      初始化请求的响应,返回请求的证书
      CertRepMessage数据结构作为PKIBody,该数据结构为每个请求的证书提供一个PKIStatusInfo字段、一个主题证书,可能还有一个私钥(通常使用会话密钥加密,会话密钥本身使用ProtocolEncrey加密。
      CertRepMessage的asn.1代码

          CertRepMessage ::= SEQUENCE {
              caPubs       [1] SEQUENCE SIZE (1..MAX) OF CMPCertificate
                              OPTIONAL,
              response         SEQUENCE OF CertResponse
          }
      
          CertResponse ::= SEQUENCE {
              certReqId           INTEGER,
              -- to match this response with corresponding request (a value
              -- of -1 is to be used if certReqId is not specified in the
              -- corresponding request)
              status              PKIStatusInfo,
              certifiedKeyPair    CertifiedKeyPair    OPTIONAL,
              rspInfo             OCTET STRING        OPTIONAL
              -- analogous to the id-regInfo-utf8Pairs string defined
              -- for regInfo in CertReqMsg [CRMF]
          }
      
          CertifiedKeyPair ::= SEQUENCE {
              certOrEncCert       CertOrEncCert,
              privateKey      [0] EncryptedValue      OPTIONAL,
              -- see [CRMF] for comment on encoding
              publicationInfo [1] PKIPublicationInfo  OPTIONAL
          }
      
          CertOrEncCert ::= CHOICE {
              certificate     [0] CMPCertificate,
              encryptedCert   [1] EncryptedValue
          }
      
          KeyRecRepContent ::= SEQUENCE {
              status                  PKIStatusInfo,
              newSigCert          [0] CMPCertificate OPTIONAL,
              caCerts             [1] SEQUENCE SIZE (1..MAX) OF
                                                  CMPCertificate OPTIONAL,
              keyPairHist         [2] SEQUENCE SIZE (1..MAX) OF
                                                  CertifiedKeyPair OPTIONAL
          }
      
          RevReqContent ::= SEQUENCE OF RevDetails
      
          RevDetails ::= SEQUENCE {
              certDetails         CertTemplate,
              -- allows requester to specify as much as they can about
              -- the cert. for which revocation is requested
              -- (e.g., for cases in which serialNumber is not available)
              crlEntryDetails     Extensions       OPTIONAL
              -- requested crlEntryExtensions
          }
      
          RevRepContent ::= SEQUENCE {
              status       SEQUENCE SIZE (1..MAX) OF PKIStatusInfo,
              -- in same order as was sent in RevReqContent
              revCerts [0] SEQUENCE SIZE (1..MAX) OF CertId
                                                  OPTIONAL,
              -- IDs for which revocation was requested
              -- (same order as status)
              crls     [1] SEQUENCE SIZE (1..MAX) OF CertificateList
                                                  OPTIONAL
              -- the resulting CRLs (there may be more than one)
          }
      
          CAKeyUpdAnnContent ::= SEQUENCE {
              oldWithNew   CMPCertificate, -- old pub signed with new priv
              newWithOld   CMPCertificate, -- new pub signed with old priv
              newWithNew   CMPCertificate  -- new pub signed with new priv
          }
      
          CertAnnContent ::= CMPCertificate
      
          RevAnnContent ::= SEQUENCE {
              status              PKIStatus,
              certId              CertId,
              willBeRevokedAt     GeneralizedTime,
              badSinceDate        GeneralizedTime,
              crlDetails          Extensions  OPTIONAL
              -- extra CRL details (e.g., crl number, reason, location, etc.)
          }
      
          CRLAnnContent ::= SEQUENCE OF CertificateList
      
          CertConfirmContent ::= SEQUENCE OF CertStatus
      
          CertStatus ::= SEQUENCE {
              certHash    OCTET STRING,
              -- the hash of the certificate, using the same hash algorithm
              -- as is used to create and verify the certificate signature
              certReqId   INTEGER,
              -- to match this confirmation with the corresponding req/rep
              statusInfo  PKIStatusInfo OPTIONAL
          }
          CMPCertificate ::= CHOICE {
          x509v3PKCert        Certificate
          }
      
    • Certification Request(证书请求)

      现有PKI EE获得其它证书(用已有证书提供身份验证),报文格式同Initialization Request。
      PKI主体可以是CertificationRequest(此结构由[PKCS10]中给出的ASN.1结构CertificationRequest完全指定,用于支持PKCS #10证书请求)。当需要与遗留系统进行互操作时,签名密钥对的证书请求可能需要此结构,但如果不是绝对必要,强烈建议不要使用此结构(为什么不建议呢?)。

    • Certification Response

      报文格式同Initialization Response

    • Key Update Request Content

      仍然使用CertReqMessages PKI Body,但是要包括SubjectPublicKeyInfo、KeyId和Validity字段,此消息用于请求对现有(未吊销和未过期)证书的更新(因此,有时称为“Certificate Update”操作)。更新是包含新subject公钥或当前subject公钥的替换证书(尽管后一种做法可能不适用于某些环境)。

    • Key Update Response Content

      报文格式同Initialization Response

    • Key Recovery Request Content

      报文格式同Initialization Request,但SubjectPublicKeyInfo和KeyId是必选字段可用于提供需要证书的签名公钥

    • Key Recovery Response Content

      使用以下结构作为Key Recovery Request Content的响应

          KeyRecRepContent ::= SEQUENCE {
              status          PKIStatusInfo,
              newSigCert  [0] Certificate                   OPTIONAL,
              caCerts     [1] SEQUENCE SIZE (1..MAX) OF
                                          Certificate      OPTIONAL,
              keyPairHist [2] SEQUENCE SIZE (1..MAX) OF
                                          CertifiedKeyPair OPTIONAL
          }
      
    • Revocation Request Content

      请求吊销证书
      PKI Body结构:

          RevReqContent ::= SEQUENCE OF RevDetails
      
      
          RevReqContent ::= SEQUENCE OF RevDetails
      
      
          RevDetails ::= SEQUENCE {
              certDetails         CertTemplate,
              crlEntryDetails     Extensions       OPTIONAL
          }
      
    • Revocation Response Content

      撤销响应是对上述消息的响应。如果生成,则发送给撤销请求者。(可向请求撤销的证书主体发送单独的撤销公告消息。)

          RevRepContent ::= SEQUENCE {
              status        SEQUENCE SIZE (1..MAX) OF PKIStatusInfo,
              revCerts  [0] SEQUENCE SIZE (1..MAX) OF CertId OPTIONAL,
              crls      [1] SEQUENCE SIZE (1..MAX) OF CertificateList
                          OPTIONAL
          }
      
    • Cross Certification Request Content

      请求交叉认证证书
      交叉认证请求使用与普通认证请求相同的语法(CertReqMessages),但有一个限制,即密钥对必须由请求CA生成,并且私钥不得发送到响应CA。下级CA也可以使用此请求来获取由父CA签名的证书。

    • Cross Certification Response Content

      交叉认证响应使用与正常认证响应相同的语法(CertRepMessage),但限制不能发送加密的私钥。

    • CA Key Update Announcement Content

      当CA更新其自己的密钥对时,可以使用以下数据结构来宣布此事件。

        CAKeyUpdAnnContent ::= SEQUENCE {
           oldWithNew         Certificate,
           newWithOld         Certificate,
           newWithNew         Certificate
        }
    
    • Certificate Announcement

      此结构可用于宣布证书的存在。 请注意,此消息旨在用于没有其它证书发布方法的情况;例如,如果X.500(LDAP)是发布证书的方法,则不打算使用该方法。

      CertAnnContent ::= Certificate
      
    • Revocation Announcement

      当CA已撤销或即将撤销特定证书时,它可能会发布此事件的公告。

              RevAnnContent ::= SEQUENCE {
                  status              PKIStatus,
                  certId              CertId,
                  willBeRevokedAt     GeneralizedTime,
                  badSinceDate        GeneralizedTime,
                  crlDetails          Extensions  OPTIONAL
              }
      

      CA可以使用此类公告警告(或通知)主体其证书即将(或已)被吊销。这通常用于撤销请求并非来自相关主体的情况。
      willBeRevokedAt字段包含将新条目添加到相关CRL的时间。

      • CRL Announcement
        当CA发布新的CRL(或一组CRL)时,可以使用以下数据结构来宣布此事件。
      CRLAnnContent ::= SEQUENCE OF CertificateList
      
    • PKI Confirmation Content

      此数据结构在协议交换中用作最终PKI消息。它的内容在所有情况下都是相同的——实际上没有内容,因为PKI头包含所有必需的信息。

      PKIConfirmContent ::= NULL
      

      不建议将此消息用于证书确认;应改用certConf。在收到证书响应的PKIConfirm后,接收方可以将其视Certificate Confirmation Content,并接受所有证书。

    • Certificate Confirmation Content

      客户端使用此数据结构向CA/RA发送确认以接受或拒绝证书。

          CertConfirmContent ::= SEQUENCE OF CertStatus
      
          CertStatus ::= SEQUENCE {
          certHash    OCTET STRING,
          certReqId   INTEGER,
          statusInfo  PKIStatusInfo OPTIONAL
          }
      

      对于任何特定的CertStatus,省略statusInfo字段表示接受指定的证书。或者,可以在statusInfo字段中提供明确的状态详细信息(关于接受或拒绝),可能用于CA/RA的审计目的。
      在CertConfirmContent中,省略与前一响应消息中提供的证书对应的CertStatus结构表示拒绝证书。因此,空CertConfirmContent(零长度序列)可用于指示拒绝所有提供的证书。有关占有证明的certHash字段的讨论,请参见第5.2.8节第(2)项。

    • PKI General Message Content

          InfoTypeAndValue ::= SEQUENCE {
              infoType               OBJECT IDENTIFIER,
              infoValue              ANY DEFINED BY infoType  OPTIONAL
          }
          -- where {id-it} = {id-pkix 4} = {1 3 6 1 5 5 7 4}
          GenMsgContent ::= SEQUENCE OF InfoTypeAndValue
      

      没有归类的估计都放这了吧,这部分又包含了13种报文结构

      • CA Protocol Encryption Certificate

        用于EE向CA获取证书用于保护CMP会话

        GenMsg:    {id-it 1}, < absent >
        GenRep:    {id-it 1}, Certificate | < absent >
        

        这里面的absent应该表示的是只有表示infoType的oid,没有infoValue(猜测)’

      • Signing Key Pair Types

        获取CA支持的签名算法

        GenMsg:    {id-it 2}, < absent >
        GenRep:    {id-it 2}, SEQUENCE SIZE (1..MAX) OF
                AlgorithmIdentifier
        
      • Encryption/Key Agreement Key Pair Types

        获取CA的加密或KAK算法列表

        GenMsg:    {id-it 3}, < absent >
        GenRep:    {id-it 3}, SEQUENCE SIZE (1..MAX) OF
                                AlgorithmIdentifier
        
      • Preferred Symmetric Algorithm

        获取CA的首选对称加密算法

        GenMsg:    {id-it 4}, < absent >
        GenRep:    {id-it 4}, AlgorithmIdentifier
        
      • Updated CA Key Pair
        CA可用此消息宣布CA密钥对更新

        GenMsg:    {id-it 5}, CAKeyUpdAnnContent
        
      • CRL
        EE用此报文获取最新的CRL

        GenMsg:    {id-it 6}, < absent >
        GenRep:    {id-it 6}, CertificateList
        
      • Unsupported Object Identifiers

        服务器使用它从客户端提交的列表中返回它不识别或不支持的对象标识符列表。
        GenRep: {id-it 7}, SEQUENCE SIZE (1..MAX) OF OBJECT IDENTIFIER

      • Key Pair Parameters

        这可由EE用于请求域参数以用于生成特定公钥算法的密钥对。例如,它可以用于请求适当的P、Q和G以生成DH/DSA密钥,或者请求一组众所周知的椭圆曲线。

        GenMsg:    {id-it 10}, OBJECT IDENTIFIER -- (Algorithm object-id)
        GenRep:    {id-it 11}, AlgorithmIdentifier | < absent >
        

        GenRep中缺少infoValue表示不支持GenMsg中指定的算法。
        EEs必须确保参数是可接受的,并且GenRep消息经过身份验证(以避免替代攻击)。
        跟openssl genpkey的-genparam选项是不是有点类似

      • Revocation Passphrase

        这可由EE用于向CA/RA发送密码短语,以便认证稍后的撤销请求(在适当的签名私钥不再可用于认证请求的情况下)。有关此机制使用的更多详细信息,请参见附录B。

        GenMsg:    {id-it 12}, EncryptedValue
        GenRep:    {id-it 12}, < absent >
        
      • ImplicitConfirm

        不发送确认消息,这个前面通用部分已经说过

      • ConfirmWaitTime

        确认超时时间,前面通用部分已经说过

      • Original PKIMessage

        RA如果修改了EE的请求,再向CA发关请求时携带的原始PKIMessage(EE向RA发送消息)

      • Supported Language Tags

        这可用于确定在后续消息中使用的适当语言标记。发送方发送其支持的语言列表(按顺序,从最优先到最不优先);接收者返回它想要使用的那个。(注意:每个UTF8String必须包含一个语言标记。)如果不支持任何提供的标记,则必须返回错误。

        GenMsg:    {id-it 16}, SEQUENCE SIZE (1..MAX) OF UTF8String
        GenRep:    {id-it 16}, SEQUENCE SIZE (1) OF UTF8String
        
    • PKI General Response Content

      对General Message的响应

          InfoTypeAndValue ::= SEQUENCE {
              infoType               OBJECT IDENTIFIER,
              infoValue              ANY DEFINED BY infoType  OPTIONAL
          }
          -- where {id-it} = {id-pkix 4} = {1 3 6 1 5 5 7 4}
          GenMsgContent ::= SEQUENCE OF InfoTypeAndValue
      
    • Error Message Content
      EE、CA或RA可以使用此数据结构来传递错误信息

          ErrorMsgContent ::= SEQUENCE {
              pKIStatusInfo          PKIStatusInfo,
              errorCode              INTEGER           OPTIONAL,
              errorDetails           PKIFreeText       OPTIONAL
          }
      

      此消息可在PKI交易期间的任何时间生成。如果客户端发送此请求,服务器必须使用PKIConfirm响应进行响应,如果标头的任何部分无效,则必须使用另一个ErrorMsg进行响应。双方必须将此消息视为会话的结束(如果会话正在进行)。
      如果需要对消息进行保护,则客户端必须使用与Initialization Request相同的技术(即签名或MAC)对其进行保护。CA必须始终使用签名密钥对其进行签名。

    • Polling Request and Response
      这对消息用于处理客户端需要轮询服务器以确定未完成的ir、cr或kur会话的状态(即,当收到“waiting”PKIStatus时)的场景。

          PollReqContent ::= SEQUENCE OF SEQUENCE {
              certReqId    INTEGER }
      
          PollRepContent ::= SEQUENCE OF SEQUENCE {
              certReqId    INTEGER,
              checkAfter   INTEGER,  -- time in seconds
              reason       PKIFreeText OPTIONAL }
      

      以下子句描述何时使用轮询消息以及如何使用它们。假定在事务期间可以发送多个certConf消息。对于包含已颁发证书的CertStatus的每个ip、cp或kup,将发送一个响应。

      1. 如果EE收到对ip、cp或kup消息的响应,将为所有已颁发的证书发送certConf,并在确认之后为所有待定证书发送pollReq。
      2. 如果CA收到pollReq,如果一个或多个挂起证书就绪,CA/RA将返回ip、cp或kup的响应;否则,它将返回一个pollRep。
      3. 如果EE收到一个pollRep,它将在发送另一个pollReq之前等待至少与checkAfter值相同的时间。
      4. 如果在pollReq响应期间接收到ip、cp或kup,会按initial response相同的方法处理
                            START
                                |
                                v
                            Send ir
                                | ip
                                v
                            Check status
                            of returned <------------------------+
                            certs                             |
                                |                               |
        

        +————————>|<——————+ |
        | | | |
        | (issued) v (waiting) | |
        Add to <----------- Check CertResponse ------> Add to |
        conf list for each certificate pending list |
        / |
        / |
        (conf list) / (empty conf list) |
        / ip |
        / +—————-+
        (empty pending list) / | pRep
        END <---- Send certConf Send pReq------------>Wait
        | ^ ^ |
        | | | |
        +—————–+ +—————+
        (pending list)

      在以下交互中,EE在一个请求中注册两个证书。

      Step  End Entity                       PKI
      --------------------------------------------------------------------
      1   Format ir
      2                    -> ir      ->
      3                                    Handle ir
      4                                    Manual intervention is
                                          required for both certs.
      5                    <- ip      <-
      6   Process ip
      7   Format pReq
      8                    -> pReq     ->
      9                                    Check status of cert requests
      10                                   Certificates not ready
      11                                   Format pRep
      12                   <- pRep     <-
      13  Wait
      14  Format pReq
      15                   -> pReq     ->
      16                                   Check status of cert requests
      17                                   One certificate is ready
      18                                   Format ip
      19                   <- ip       <-
      20  Handle ip
      21  Format certConf
      22                   -> certConf ->
      23                                   Handle certConf
      24                                   Format ack
      25                   <- pkiConf   <-
      26  Format pReq
      27                   -> pReq     ->
      28                                   Check status of certificate
      29                                   Certificate is ready
      30                                   Format ip
      31                   <- ip       <-
      31  Handle ip
      32  Format certConf
      33                   -> certConf ->
      34                                   Handle certConf
      35                                   Format ack
      36                   <- pkiConf  <-
      

    强制性的PKI功能

    这些功能是EE,RA,CA必须实现的

    • Root CA初始化

      Root CA证书必须是自签名证书,为了支持EE通过带外方式获取Root CA证书还需要生成证书指纺,用OOBCertHash结构表示

    • Root CA密钥更新

      CA密钥(与所有其他密钥一样)具有有限的生存期,必须定期更新。CA可颁发证书NewWithNew、NewWithOld和OldWithNew(见第4.4.1节),以帮助持有当前自签名CA证书(OldWithOld)的现有终端实体安全地过渡到新自签名CA证书(NewWithNew),并帮助持有NewWithNew的新终端实体安全地获取OldWithOld,以验证现有数据。

    • 下级CA(中间CA)的初始化

      从PKI管理协议的角度来看,下级CA的初始化与终端实体的初始化相同。唯一的区别是下级CA还必须生成初始吊销列表.

    • CRL生成

      新成立的CA在颁发任何证书之前必须生成每个 CRL 的“空”版本,这些版本将定期生成。

    • PKI信息请求

      当PKI实体(CA、RA或EE)希望获取有关CA当前状态的信息时,它可以向该CA发送获取此类信息的请求。请求是通过一般消息中的PKI General Message Content消息实现的,响应是通过PKI General Response Content。

    • 交叉认证

      请求者CA是将成为交叉证书subject的CA;响应者CA将成为交叉证书的issuer。

    • EE初始化

      包括获取PKI信息(Root CA证书和其它信息),和Root CA的带外验证(指纹)

    • 证书请求(为什么不是初始化请求)

      已初始化的EE可随时(出于任何目的)请求额外的证书。此请求将使用证书请求(cr)消息发出。如果EE已经拥有签名密钥对(具有相应的验签证书),则该cr消息通常将受到EE数字签名的保护。CA在CertRepMessage中返回新证书(如果请求成功)。

    • 密钥更新

      当密钥对到期时,相关EE可请求密钥更新;也就是说,它可以请求CA为新密钥对颁发新证书(或者,在某些情况下,为同一密钥对颁发新证书)。使用密钥更新请求(kur)消息发出请求(在某些环境中称为“Certificate Update”操作)。如果EE已经拥有签名密钥对(具有相应的验证证书),则该消息通常将受到EE数字签名的保护。CA在密钥更新响应(kup)消息中返回新证书(如果请求成功),该消息在语法上与CertRepMessage相同。

    Views: 17

  • 开源CA服务器

    • 支持cmp的CA服务器

    ejbca

    • 支持secp的CA服务器

    ejbca(java开发),Smallstep商业版(go开发),Dogtag PKI(java开发),OpenXPKI(perl开发)

    • 支持EST的CA服务器

    OpenXPKI,ejbca商业版,Dogtag PKI

    • 支持ACME的CA服务器

    OpenXPKI,ejbca社区版,Dogtag PKI,Smallstep社区版

    华为防火墙只支持CMP协议,支持CMP的CA服务器太少了

    Dogtag PKI似乎不提供Web介面,支持ACME,SECP,EST协议,基于nss库(https://wiki.mozilla.org/NSS)
    OpenXPKI提供完整的Web介面,不同的角色都可以使用Web进行操作,而且100%开源,基于OpenSSL,如果不考虑CMP支持是个不错的选择。
    EJBCA提供了管理员和RA角色的Web界面,可以支持所有证书协议(商业版),社会版本只支持CMP和SCEP两种协议,基于bouncycastle。bouncycstle支持的算法:https://www.bouncycastle.org/documentation/specification_interoperability/#algorithms-and-key-types
    Smallstep目前只有管理员Web介面,因为是go开发比较轻量,如果要求比较简单,只需要ACME协议可以考虑smallstep。使用go自带的crypto/tls提供加密算法。

    下载地址:
    https://github.com/Keyfactor/ejbca-ce
    https://github.com/smallstep/certificates
    https://github.com/dogtagpki/pki
    https://github.com/openxpki/openxpki

    Views: 21

  • 交叉证书

    证书4要素

    pubkey(公钥) ,有一个其一一对应的私钥,由证书拥有者保存。
    subject(名字),可以理解为证书的名字,你可以理解为 网站的域名。
    issuer(颁发者的名字),即上级证书的subject。
    signature(签名) ,上级证书使用私钥对当前证书进行签名的值。

    证书签名

    假设 有 3 级 证书

    image.png
    首先 ,R1 CA证书 是由 R1证书签发,而www.taobao.com是由R1 CA证书签发。

    签发过程核心就是签名,比如 签发R1 CA证书时,拿R1证书的私钥对R1 CA证书进行数字签名操作,签名的值添加在R1 CA证书中。同理签发www.taobao.com这张证书,是用 R1 CA证书的私钥对该证书进行数字签名,签名值添加在www.taobao.com的证书中。

    被数字签名的值,是该证书的HASH。举个例子当 使用 R1证书加签R1 CA时,其签名值是 R1_privatekey_sign( HASH(R1 CA) )。

    数字证书校验

    由于 私钥签名的数据,可以使用公钥校验,假设我们信任R1 + R1 CA证书,我们收到www.taobao.com这个证书,如何校验?

    首先,构造证书链,即 拿 server的证书然后和本地(操作系统或者浏览器内置)的可信证书进行上级证书的查找,一般通issuer字段查找(还有通过keyid的方式,这里不讨论),比如 www.taobao.com的证书的issuer是R1 CA,那么在本地仓库里面找R1 CA,接着找R1 CA的上级证书,直到找到最高一级的证书,即R1证书。

    第二步,光靠名字来找上级是不够的,因为名字可以伪造,我们还需要对证书的签名值进行校验,我们知道,私钥签名的数据可以使用公钥来校验,所以,首先拿R1 CA证书的的公钥,去解开 www.taobao.com签名值,然后校验其值是否是www.taobao.com的hash,同理可以验证证书链中R1是否是R1 CA的上级证书。 签名校验成功,表明了2点
    1:上下级关系正确
    2:证书被未篡改,如果证书被篡改,使用上级证书的公钥解开当前证书后,会发现其值和当前证书的HASH值不一样

    交叉证书

    如果一个CA机构有如下2个受信的链:

    image.png

    那么又 R1 CA签发的证书只能由R1+R1 CA进行验证;视同 R3 CA签发的证书只能由R3+R3 CA进行签发。

    但是有些客户端只有R1和R1 CA而没有R3以及R3 CA,而有些客户端只有R3和R3 CA而没有R1 以及 R1 CA ,我们能不能让证书有2条可以用来验证的链呢? 比如 www.taobao.com->…->R1 是一条证书链, www.taobao.com->…->R3 也算一条证书链,这样假设有些客户端只信任R1或者R3也能同时验证同一个证书。

    为了达到这个目的,我们需要做这么一个操作,就是 使用 R1 对 R3进行签名生成一张叫做 R1-R3 的证书(即使用R1的私钥对R3进行签名)。

    image.png

    注意,R1-R3 拥有和 R3一模一样的 subject+公钥,和R3唯一的区别就是 R3是自签名证书,其issuer是R3自己,而 R1-R3 是中间证书,其issuer是R1。

    接着,使用 R1-R3 来对 签发 www.taobao.com ,即 www.taobao.com 的 issuer 是R3(上面说了,issuer就是个字符串,和上级证书的subject一样,而R1-R3 这张证书的subject就是R3),这张 R1-R3 就是所谓的交叉证书corss-certificate

    image.png

    www.taobao.com 这样的证书,如何能让只信任R1的客户端进行验证:

    1、构造证书链
    服务器发送 www.taobao.com + R1 R3,客户端构造证书链,自然,通过找 www.taobao.com 的issuer:R3,在server发送的证书中找到了 R1 R3这张证书 ,通过 R1 R3 的issuer:R1,在本地找到了R1。
    2、验证证书链
    略,通上节一致,通过上级证书的公钥验证当前证书的签名值。

    www.taobao.com 这样的证书,如何能让只信任R3的客户端进行验证:

    1、构造证书链
    服务器发送 www.taobao.com ,客户端构造证书链,自然,通过找 www.taobao.com 的issuer:R3,在本地找到了受信的R3 ,R3是根证书,证书链构造完成。

    2、验证证书链
    虽然,实际上 www.taobao.com 的证书是由 R1 R3这张证书签名的,但是上面说过,R1 R3这张证书的公钥和R3一样,也就意味着使用R1 R3的公钥,能够验证 www.taobao.com的签名值。

    结尾

    实际上,使用R1-R3www.taobao.com签名的效果,和使用R3www.taobao.com签名的效果是一模一样的,因为R1-R3R3拥有相同的subject以及公钥,自然拥有相同的私钥。

    说的通俗一点,就是CA机构通过欺骗的方式,签了一张交叉证书,这张证书能够让客户端的证书链引导到R1。

    交叉证书有什么作用?

    1、兼容老设备。假设,老的客户端只支持R1根证书,但是我们的证书是R3是签名的,那需server发送交叉证书,来“引导”客户端使用R1校验。

    2、CA机构合并,假设R1 收购了R3,R1为了“收回最终校验权”,可以生成这么一个交叉证书,那么使用R3签名的证书,最后都被R1来校验。

    由以上可知交叉证书的关键是把Root CA2的自签名证书改为用另一机构的RootCA1来签名,RootCA1成为了RootCA2的上级CA。

    转自:https://developer.aliyun.com/article/703706

    Views: 12

  • OpenGPG证书

    OpenGPG是由RFC4880定义的,相对X.509来说是一个简易的,离线使用的公钥证书。

    GPG中使用user id来代表签名的实体,相当于X.509中的DN。
    生成GPG证书时会生成一个主证书,相当于自签名的Root CA和若干由主证书签名的子证书。主证书只用于对子证书进行签名,实际加密时使用子证书。
    GPG定义了以下证书用途(相当于KeyUsage)
    Encryption:加密(E)。
    Signing:签名(S)。
    Certification:认证其他子密钥或 uid(C)。
    Authentication:身份认证,例如用于 SSH 登录(A)。
    GPG可以支持对称加密和信封加密,信封加密时用证书生成监时对称密钥。
    GPG证书也可以对别人的证书进行签名,提供共识认证。
    目前GPG主要用于文件的加密和签名。

    参考:
    https://datatracker.ietf.org/doc/html/rfc4880

    Views: 17

  • X.509公钥证书

    X.509公钥证书

    X.509最初由ITU-T用于X500目录服务中,对X.500实体的身份进行验证,所以X.509使用了一些X500中的元素。
    后来IETF PKIX工作组对X.509进行了扩展和补充,使更好的用于互联网环境中。目前X.509已经是独立的标准,最新版本为X.509 V3,而且X.509是TLS中唯一受支持的证书标准。关于X.509 V3的定义可参考rfc5280。

    1. X.509证书的语法格式和序列化

    X.509证书的格式格式定义和序列化都使用了ASN.1标准。X.509的语义描述使用ASN.1代码,而ASN.1的序列化编码有BER,DER,PEM。BER和DER都是二进制编码,BER编码有些地方存在歧义或者对编码结果没有严格约束,而DER则是BER的strict版本,确保一个ASN.1代码序列化后的二进制是唯一的,而不会出现不同实现结果不一样的情况。X.509证书的序列化就是用的DER格式,而PEM编码则是到DER进一步进行Base64处理。
    理解ASN.1后再看RFC5280标准文档就容易多了。

    1. 密码算法

      X.509证书涉及到了非对称加密,Hash,对称加密等加密算法,学习X.509证书需要有这方面的知识储备。

    2. X.509的ASN.1定义

    Certificate  ::=  SEQUENCE  {
        tbsCertificate       TBSCertificate,
        signatureAlgorithm   AlgorithmIdentifier,
        signature            BIT STRING  }
    TBSCertificate  ::=  SEQUENCE  {
        version         [0]  Version DEFAULT v1,
        serialNumber         CertificateSerialNumber,
        signature            AlgorithmIdentifier,
        issuer               Name,
        validity             Validity,
        subject              Name,
        subjectPublicKeyInfo SubjectPublicKeyInfo,
        issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                            -- If present, version MUST be v2 or v3
        subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                            -- If present, version MUST be v2 or v3
        extensions      [3]  Extensions OPTIONAL
                            -- If present, version MUST be v3 --  }
    
    Version  ::=  INTEGER  {  v1(0), v2(1), v3(2)  }
    
    CertificateSerialNumber  ::=  INTEGER
    
    Validity ::= SEQUENCE {
        notBefore      Time,
        notAfter       Time  }
    
    Time ::= CHOICE {
        utcTime        UTCTime,
        generalTime    GeneralizedTime }
    
    UniqueIdentifier  ::=  BIT STRING
    
    SubjectPublicKeyInfo  ::=  SEQUENCE  {
        algorithm            AlgorithmIdentifier,
        subjectPublicKey     BIT STRING  }
    
    Extensions  ::=  SEQUENCE SIZE (1..MAX) OF Extension
    
    Extension  ::=  SEQUENCE  {
        extnID      OBJECT IDENTIFIER,
        critical    BOOLEAN DEFAULT FALSE,
        extnValue   OCTET STRING
                    -- contains the DER encoding of an ASN.1 value
                    -- corresponding to the extension type identified
                    -- by extnID
        }
    

    这个ASN.1代码是RFC5280定义的,描述了X.509证书的基本字段(省略了扩展字段)
    下面是CRL的定义

    CertificateList  ::=  SEQUENCE  {
        tbsCertList          TBSCertList,
        signatureAlgorithm   AlgorithmIdentifier,
        signature            BIT STRING  }
    
    TBSCertList  ::=  SEQUENCE  {
        version                 Version OPTIONAL,
                                    -- if present, MUST be v2
        signature               AlgorithmIdentifier,
        issuer                  Name,
        thisUpdate              Time,
        nextUpdate              Time OPTIONAL,
        revokedCertificates     SEQUENCE OF SEQUENCE  {
            userCertificate         CertificateSerialNumber,
            revocationDate          Time,
            crlEntryExtensions      Extensions OPTIONAL
                                    -- if present, version MUST be v2
                                }  OPTIONAL,
        crlExtensions           [0] Extensions OPTIONAL }
                                    -- if present, version MUST be v2
    
    -- Version, Time, CertificateSerialNumber, and Extensions were
    -- defined earlier for use in the certificate structure
    
    AlgorithmIdentifier  ::=  SEQUENCE  {
        algorithm               OBJECT IDENTIFIER,
        parameters              ANY DEFINED BY algorithm OPTIONAL  }
                                    -- contains a value of the type
                                    -- registered for use with the
                                    -- algorithm object identifier value
    

    只要理解上面各个字段的函义,证书的定义就完全掌握了。
    其中tbsCertList包含了证书主体,除了签名以外的内容全在这里面。

    1. 字段详解

    4.1. version字段

    0表示Version1,1表示Version2,2表示Version3

    4.2. serialNumber字段

    签发证书时的序列号,每次递增,用正整数表示。同一个CA签发的证书不能重复。

    4.3. signature

    这个定义的是签名算法。

    4.4. issuer

    证书发行方的DN,自签名证书这一项与subject相同

    4.5. validity

    证书书的有效期,ASN.1类型定义是UTC Time

    4.6. subject

    证书主题的DN,主题代表证书所代表的实体,也就是这个证书的持有人。

    4.7. subjectPublicKeyInfo

    主题的公钥信息,包含算法类型和公钥的Bit流数据

    4.8. issuerUniqueID和subjectUniqueID

    这是X.509 V2中增加的字段,issuer和subject的值是用唯一标识符表示的,RFC5280中并没有说UniqueID的长度和生成规则,而且这两个字段已很少使用。

    4.9. extensions

    这个就是在TLS X.509证书中常说的V3扩展,是在X.509V3中新增的字段,也是最复杂的一个字段。在RFC5280中的4.2节专门禅述了这个字段。
    ASN.1定义

    Extension  ::=  SEQUENCE  {
        extnID      OBJECT IDENTIFIER,
        critical    BOOLEAN DEFAULT FALSE,
        extnValue   OCTET STRING
                    -- contains the DER encoding of an ASN.1 value
                    -- corresponding to the extension type identified
                    -- by extnID
        }
    

    extnID 是一个oid,标识扩展的类型
    critical 标识该扩展是否是关键的
    extnValue 扩展的值,按照ASN.1格式定义

    这里只说在X.509中定义的标准扩展。

    4.9.1. Authority Key Identifier

    签发当前证书所用公钥的指纹,一个issuer可能有多个公钥。指纹是用160-bit SHA-1计算的。在ASN.1中用属性名authorityKeyIdentifier表示。
    自签名证书这一项可忽略

    4.9.2. Subject Key Identifier

    主题公钥标识符,就是证书自己的公钥指纹了

    4.9.3 Key Usage

    Key Usage是一个bit开关字段,指定证书的用途。CA证书中KeyUsage是必选的。
    ASN.1定义如下:

          id-ce-keyUsage OBJECT IDENTIFIER ::=  { id-ce 15 }
    
          KeyUsage ::= BIT STRING {
               digitalSignature        (0),
               nonRepudiation          (1), -- recent editions of X.509 have
                                    -- renamed this bit to contentCommitment
               keyEncipherment         (2),
               dataEncipherment        (3),
               keyAgreement            (4),
               keyCertSign             (5),
               cRLSign                 (6),
               encipherOnly            (7),
               decipherOnly            (8) }
    

    digitalSignature 数据签名和验证(不包括证书和CRL)
    nonRepudiation 不可否认性,签名和验证数据(不包括证书和CRL)时提供不可否认性(具有法律效力)
    keyEncipherment 密钥加密
    dataEncipherment 使用RSA公钥进行数据加密,很少使用
    keyAgreement 密钥协商,如DH协议
    keyCertSign 证书签名和验证,CA证书这一位必须为1
    cRLSign CRL(证书吊销列表)签名和验证
    encipherOnly 密钥协商时加密数据,需要与keyAgreement一起使用
    decipherOnly 密钥协商时解密数据,需要与keyAgreement一起使用

    4.9.4. Certificate Policies

    证书策略指示证书的使用规范

    ASN.1定义

       id-ce-certificatePolicies OBJECT IDENTIFIER ::=  { id-ce 32 }
    
       anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 }
    
       certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation
    
       PolicyInformation ::= SEQUENCE {
            policyIdentifier   CertPolicyId,
            policyQualifiers   SEQUENCE SIZE (1..MAX) OF
                                    PolicyQualifierInfo OPTIONAL }
    
       CertPolicyId ::= OBJECT IDENTIFIER
    
       PolicyQualifierInfo ::= SEQUENCE {
            policyQualifierId  PolicyQualifierId,
            qualifier          ANY DEFINED BY policyQualifierId }
    
       -- policyQualifierIds for Internet policy qualifiers
    
       id-qt          OBJECT IDENTIFIER ::=  { id-pkix 2 }
       id-qt-cps      OBJECT IDENTIFIER ::=  { id-qt 1 }
       id-qt-unotice  OBJECT IDENTIFIER ::=  { id-qt 2 }
    
       PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
    
       Qualifier ::= CHOICE {
            cPSuri           CPSuri,
            userNotice       UserNotice }
    
       CPSuri ::= IA5String
    
       UserNotice ::= SEQUENCE {
            noticeRef        NoticeReference OPTIONAL,
            explicitText     DisplayText OPTIONAL }
    
       NoticeReference ::= SEQUENCE {
            organization     DisplayText,
            noticeNumbers    SEQUENCE OF INTEGER }
    
       DisplayText ::= CHOICE {
            ia5String        IA5String      (SIZE (1..200)),
            visibleString    VisibleString  (SIZE (1..200)),
            bmpString        BMPString      (SIZE (1..200)),
            utf8String       UTF8String     (SIZE (1..200)) }
    

    由以上ASN.1代码可看出CertPolicyId都是由OID表示的。
    定义很长,但实际上关键的就两个cPSuri和userNotice。cPSuri是一个URL,指向证书的使用规范,可以是html或pdf。userNotice是通知文本,相当于证书的使用公告或通知。X.509定义的证书策略就这两个,但是CA/Browser Forum还定义了以下TLS证书策略。

    策略OID 描述
    2.23.140.1.1 Extended Validation Certificate Policy
    2.23.140.1.2.1 Domain Validation Certificates Policy
    2.23.140.1.2.2 Organization Validation Certificates Policy

    这三项分别是EV,DV,OV证书策略,在TLS证书中至少要有一个。
    完整的策略可以参考这里:
    https://cabforum.org/resources/object-registry/
    https://www.globalsign.com/en/repository/GlobalSign-CP-v7.4-final.pdf

    4.9.5. Policy Mappings

    当不同组织的CA交叉认证的时候,因为两个组织有不同的CP(Certificate Policies)定义,但是有些CP又是类似的,这时候就可以使用Policy Mappings把一个组织中的CP映射到另一个组织。
    ASN.1定义

       id-ce-policyMappings OBJECT IDENTIFIER ::=  { id-ce 33 }
    
    
       id-ce-policyMappings OBJECT IDENTIFIER ::=  { id-ce 33 }
    
    
       PolicyMappings ::= SEQUENCE SIZE (1..MAX) OF SEQUENCE {
            issuerDomainPolicy      CertPolicyId,
            subjectDomainPolicy     CertPolicyId }
    

    issuerDomainPolicy 外部组织的CP
    subjectDomainPolicy 本组织的CP
    其中CertPolicyId就是策略的OID值。

    4.9.6. Subject Alternative Name

    Subject Alternative Name,顾名思义就是用来替代subject的DN的,如果存在Subject Alternative Name时subject的DN允许为空,但是Subject Alternative Name要标记critical。
    ASN.1定义

       id-ce-subjectAltName OBJECT IDENTIFIER ::=  { id-ce 17 }
    
       SubjectAltName ::= GeneralNames
    
       GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
    
       GeneralName ::= CHOICE {
            otherName                       [0]     OtherName,
            rfc822Name                      [1]     IA5String,
            dNSName                         [2]     IA5String,
            x400Address                     [3]     ORAddress,
            directoryName                   [4]     Name,
            ediPartyName                    [5]     EDIPartyName,
            uniformResourceIdentifier       [6]     IA5String,
            iPAddress                       [7]     OCTET STRING,
            registeredID                    [8]     OBJECT IDENTIFIER }
    
       OtherName ::= SEQUENCE {
            type-id    OBJECT IDENTIFIER,
            value      [0] EXPLICIT ANY DEFINED BY type-id }
    
       EDIPartyName ::= SEQUENCE {
            nameAssigner            [0]     DirectoryString OPTIONAL,
            partyName               [1]     DirectoryString }
    
    

    由ASN.1代码可见SubjectAltName所允许的值非常宽泛,可以是域名,ip地址,DN,邮件地址或者其它的任何名字,甚至还可以是通配符”*”。在TLS证书中通常是域名或IP地址。

    4.9.7. Subject Directory Attributes

    Subject的一个或多个Directory属性,属于非关键字段,这个应该主要用于LDAP/X500了。
    ASN.1定义

    “`ASN.1
    id-ce-subjectDirectoryAttributes OBJECT IDENTIFIER ::= { id-ce 9 }

    id-ce-subjectDirectoryAttributes OBJECT IDENTIFIER ::= { id-ce 9 }

    SubjectDirectoryAttributes ::= SEQUENCE SIZE (1..MAX) OF Attribute

    <pre><code class="line-numbers">4.9.8. Basic Constraints

    标识当前证书是否为CA以及自当前证书开始允许的证书链长度(pathLenConstraint),只为证书为CA时证书链长度约束才有效。
    ASN.1定义
    “`ASN1
    BasicConstraints ::= SEQUENCE {
    cA BOOLEAN DEFAULT FALSE,
    pathLenConstraint INTEGER (0..MAX) OPTIONAL }

    cA 指示是否为CA
    pathLenConstraint 自当前证书开始,允许的证书链长度

    4.9.9. Name Constraints

    用于限制CA签发证书时subject和Subject Alternative Name属性的值,这样就能限制CA只能为指定的域名签发证书。
    只有CA才有NameConstraints字段,而且自签名CA没有这个字段。

    “`ASN.1
    id-ce-nameConstraints OBJECT IDENTIFIER ::= { id-ce 30 }

    <pre><code> NameConstraints ::= SEQUENCE {
    permittedSubtrees [0] GeneralSubtrees OPTIONAL,
    excludedSubtrees [1] GeneralSubtrees OPTIONAL }

    GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree
    GeneralSubtree ::= SEQUENCE {
    base GeneralName,
    minimum [0] BaseDistance DEFAULT 0,
    maximum [1] BaseDistance OPTIONAL }

    BaseDistance ::= INTEGER (0..MAX)
    </code></pre>

    <pre><code class="line-numbers">4.9.10. Policy Constraints

    在证书链中限制证书策略和策略映射,用于CA证书。

    ASN.1定义
    “`ASN1

    id-ce-policyConstraints OBJECT IDENTIFIER ::= { id-ce 36 }

    PolicyConstraints ::= SEQUENCE {
    requireExplicitPolicy [0] SkipCerts OPTIONAL,
    inhibitPolicyMapping [1] SkipCerts OPTIONAL }

    SkipCerts ::= INTEGER (0..MAX)

    requireExplicitPolicy 指示证书链中多少个证书以后必须声明政书策略
    inhibitPolicyMapping 指示证书链中多少个证书以后不能指定策略映射

    4.9.11. Extended Key Usage

    这个字段是对Key Usage的扩展,增加了更多Key Usage定义。
    不同于Key Usage用状态字来定义,Extended Key Usage是用OID来定义的。

       id-ce-extKeyUsage OBJECT IDENTIFIER ::= { id-ce 37 }
    
       ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
    
       KeyPurposeId ::= OBJECT IDENTIFIER
    
       anyExtendedKeyUsage OBJECT IDENTIFIER ::= { id-ce-extKeyUsage 0 }
    
       id-kp OBJECT IDENTIFIER ::= { id-pkix 3 }
    
       id-kp-serverAuth             OBJECT IDENTIFIER ::= { id-kp 1 }
       -- TLS WWW server authentication
       -- Key usage bits that may be consistent: digitalSignature,
       -- keyEncipherment or keyAgreement
    
       id-kp-clientAuth             OBJECT IDENTIFIER ::= { id-kp 2 }
       -- TLS WWW client authentication
       -- Key usage bits that may be consistent: digitalSignature
       -- and/or keyAgreement
    
       id-kp-codeSigning             OBJECT IDENTIFIER ::= { id-kp 3 }
       -- Signing of downloadable executable code
       -- Key usage bits that may be consistent: digitalSignature
    
       id-kp-emailProtection         OBJECT IDENTIFIER ::= { id-kp 4 }
       -- Email protection
       -- Key usage bits that may be consistent: digitalSignature,
       -- nonRepudiation, and/or (keyEncipherment or keyAgreement)
    
       id-kp-timeStamping            OBJECT IDENTIFIER ::= { id-kp 8 }
       -- Binding the hash of an object to a time
       -- Key usage bits that may be consistent: digitalSignature
       -- and/or nonRepudiation
    
       id-kp-OCSPSigning            OBJECT IDENTIFIER ::= { id-kp 9 }
       -- Signing OCSP responses
       -- Key usage bits that may be consistent: digitalSignature
       -- and/or nonRepudiation
    

    对于TLS服务器证书必须包含id-kp-serverAuth

    4.9.12. CRL Distribution Points

    指定CRL文件的位置,这是个非关键字段。

       id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::=  { id-ce 31 }
    
       CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
    
       DistributionPoint ::= SEQUENCE {
            distributionPoint       [0]     DistributionPointName OPTIONAL,
            reasons                 [1]     ReasonFlags OPTIONAL,
            cRLIssuer               [2]     GeneralNames OPTIONAL }
    
       DistributionPointName ::= CHOICE {
            fullName                [0]     GeneralNames,
            nameRelativeToCRLIssuer [1]     RelativeDistinguishedName }
    
       ReasonFlags ::= BIT STRING {
            unused                  (0),
            keyCompromise           (1),
            cACompromise            (2),
            affiliationChanged      (3),
            superseded              (4),
            cessationOfOperation    (5),
            certificateHold         (6),
            privilegeWithdrawn      (7),
            aACompromise            (8) }
    

    distributionPoint http或ldap url,用于下载crl文件,可以有多个
    reasons 证书吊销的原因
    cRLIssuer CRL文件的签发者的DN,CRL和证书签发人不一样时才需要
    4.9.13. Inhibit anyPolicy

    限制anyPolicy的生效,也算一种策略约束

       Conforming CAs MUST mark this extension as critical.
    
       id-ce-inhibitAnyPolicy OBJECT IDENTIFIER ::=  { id-ce 54 }
    
       InhibitAnyPolicy ::= SkipCerts
    
       SkipCerts ::= INTEGER (0..MAX)
    

    SkipCerts 指定证书链中多少个证书以后anyPolicy策略不再生效

    4.9.14. Freshest CRL (a.k.a. Delta CRL Distribution Point)

    增量CRL分发点,语法跟CRL Distribution Points一样

       id-ce-freshestCRL OBJECT IDENTIFIER ::=  { id-ce 46 }
    
       FreshestCRL ::= CRLDistributionPoints
    

    4.9.15. Authority Information Access

    授权信息访问,就是签发人(CA)的可访问的在线信息

       id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 }
    
       AuthorityInfoAccessSyntax  ::=
               SEQUENCE SIZE (1..MAX) OF AccessDescription
    
       AccessDescription  ::=  SEQUENCE {
               accessMethod          OBJECT IDENTIFIER,
               accessLocation        GeneralName  }
    
       id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
    
       id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 }
    
       id-ad-ocsp OBJECT IDENTIFIER ::= { id-ad 1 }
    

    id-ad-caIssuers 指示证书链中各个上级CA的链接,用于对证书链进行验证。这个字段可以多次出现。
    id-ad-ocsp CA的ocsp(Online Certificate Status Protocol)链接,维护证书的吊销状态

    4.9.16. Subject Information Access

    主题信息访问,就是证书持有人的可访问的在线信息,当subject是CA时这个扩展才存在

       id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
    
       id-ad-caRepository OBJECT IDENTIFIER ::= { id-ad 5 }
    
       id-ad-timeStamping OBJECT IDENTIFIER ::= { id-ad 3 }
    

    id-ad-caRepository 证书仓库,就是CA签发证书后会放在这个仓库中,可以从这里下载证书
    id-ad-timeStamping TSP(Time-Stamp Protocol)协议服务地址,TSP用于对时间进行签名,时间的签名信息是从TSP在线获取的。

    1. 签名

    5.1. signatureAlgorithm

    签名算法,与TBSCertificate.signature的值相同

    5.2. signature

    证书的具体签名信息

    1. 名词解释

    6.1. DN

    一个DN(distinguished name)是由多个RDN(relative distinguished name)组成的,DN中的一个字段就是一个RDN。在X500/LDAP中广泛使用。
    这是LDAP/X.500用到的基本RDN字段
    String X.500 AttributeType
    —— ——————————————–
    CN commonName (2.5.4.3)
    L localityName (2.5.4.7)
    ST stateOrProvinceName (2.5.4.8)
    O organizationName (2.5.4.10)
    OU organizationalUnitName (2.5.4.11)
    C countryName (2.5.4.6)
    STREET streetAddress (2.5.4.9)
    DC domainComponent (0.9.2342.19200300.100.1.25)
    UID userId (0.9.2342.19200300.100.1.1)
    这些字段是在X500中定义的,参考RFC4514和RFC4519。
    DN验证的时候忽略大小写和多余的空白符,DN必须支持UTF8。
    这是X.509中用到的基本RDN字段,就是说这些字段是必须实现的。
    * country,
    * organization,
    * organizational unit,
    * distinguished name qualifier,
    * state or province name,
    * common name (e.g., “Susan Housley”), and
    * serial number.

    必须实现的附加字段

    * locality,
    * title,
    * surname,
    * given name,
    * initials,
    * pseudonym, and
    * generation qualifier (e.g., "Jr.", "3rd", or "IV").
    

    X.509中还必须实现domainComponent字段,domainComponent是LDAP中的DC(域给件),不是DNS中的域名,详细介绍可以看LDAP/X.500。
    因为有些系统使用DN名作为认证方法,所以DN的比较也是很重要的。
    RFC5280中分为了三种情况,
    – DN1和DN2的RDN数量相同,则DN1中的每个字段都能在DN2中找到相同的字段则认为DN1与DN2相同
    – DN1和DN2的RDN数量相同,顺序也相同,并且则DN1中的每个字段都能在DN2中找到相同的字段则认为DN1与DN2相同
    – DN1和DN2的RDN数量不同,则把较长的DN1从尾部把多余的RDN去掉,然后按前面的方法进行比较。DN的定义是大的单位在前面,小的单位在后面,所以这样等于DN1是否属于DN2的下属单位。

    对于两个不同的实体DN不能相同,但是一个实体可以有多个DN。
    在LDAP中DN是用于对目录名(DirectoryName)进行标识的。

    6.2. Subject

    代表持有证书的实体,可以是最实体,也可以是CA。

    6.3. Issuer

    代表签发证书的实体,一定是CA

    参考:

    RFC5280

    Views: 9

  • ASN.1中的IMPLICIT和EXPLICIT关键字

    TBSCertificate  ::=  SEQUENCE  {
         version         [0]  Version DEFAULT v1,
         serialNumber         CertificateSerialNumber,
         signature            AlgorithmIdentifier,
         issuer               Name,
         validity             Validity,
         subject              Name,
         subjectPublicKeyInfo SubjectPublicKeyInfo,
         issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                              -- If present, version MUST be v2 or v3
         subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                              -- If present, version MUST be v2 or v3
         extensions      [3]  Extensions OPTIONAL
                              -- If present, version MUST be v3 --  
    }
    

    以上编码方括号中指定的是自定义tag,那为什么要指定自定义tag呢?
    在一个Sequence复合数据结构中,各个子字段的函义完全是由在sequence中出现的顺序决定的。如果所有字段都是固定的,这没有什么问题。但是如果出现可选字段,顺序就会打乱。所以对于可选字段就不能单纯依靠字段顺序进行解析,这时候自定义tag就派上用场了,字段的函义由tag来决定。
    ASN.1为自定义tag当义了两种编码方式。在IMPLICIT编码方式中自定义tag会替换原始的tag,优点是不会增加额外的存储空间,但是原始字段的tag就丢掉了,这样要确定字段的数据类型就需要联系上下文来推断。另一种是EXPLICIT编码,这种方式会创建一个squence然后原始字段会成为这个squence的唯一字段,自定义的tag是这个squence的tag,缺点是新创建的squence会额外占用两个字节。
    两种编码的具体示例可以在这里找到:
    https://letsencrypt.org/zh-cn/docs/a-warm-welcome-to-asn1-and-der/#explicit-%E4%B8%8E-implicit
    其中EXPLICIT编码是默认的编码方式。

    Views: 30

  • ASN.1中多字节标签的表示

    在ASN.1(Abstract Syntax Notation One)中,标签(Tag)用于标识数据类型和结构。标签由一个标识符(Tag Identifier)和一个长度(Length)组成。对于简单的数据类型,标签通常是单字节的,但对于某些复杂的数据类型,标签可能需要使用多字节来表示。以下是关于ASN.1中多字节标签的详细说明:

    1. ASN.1标签概述

    1.1 标签的组成

    ASN.1标签由以下部分组成:
    标签类(Class):表示标签的类型,包括通用(Universal)、应用(Application)、特定语境(Context-Specific)和私有(Private)。
    标签号(Tag Number):表示具体的数据类型或结构。
    构造类型(Constructed Type):指示数据是原始类型(Primitive)还是构造类型(Constructed)。

    1.2 标签的格式

    标签的格式如下:

    +----------------+----------------+----------------+
    | Class (2 bits) | P/C (1 bit)    | Tag Number     |
    +----------------+----------------+----------------+
    
    • Class:标签类,占2位。
    • P/C:构造类型,占1位。0表示原始类型,1表示构造类型。
    • Tag Number:标签号,占5位或更多。

    2. 单字节标签

    2.1 单字节标签的表示

    对于标签号在0到30之间的标签,可以使用单字节表示:

    +----------------+----------------+----------------+
    | Class (2 bits) | P/C (1 bit)    | Tag Number (5 bits) |
    +----------------+----------------+----------------+
    

    例如:
    INTEGER:标签类为通用(00),构造类型为原始(0),标签号为2(00010)。
    SEQUENCE:标签类为通用(00),构造类型为构造(1),标签号为16(10000)。

    3. 多字节标签

    3.1 多字节标签的表示

    对于标签号大于30的标签,需要使用多字节表示。第一个字节的标签号部分设置为31(11111),后续字节用于表示实际的标签号。每个后续字节的最高位(MSB)用于指示是否有更多的字节,剩余7位用于表示标签号的一部分。

    3.2 多字节标签的格式

    多字节标签的格式如下:

    +----------------+----------------+----------------+
    | Class (2 bits) | P/C (1 bit)    | 11111          |
    +----------------+----------------+----------------+
    | 1xxxxxxx       | 1xxxxxxx       | 0xxxxxxx       |
    +----------------+----------------+----------------+
    
    • Class:标签类,占2位。
    • P/C:构造类型,占1位。
    • 11111:表示标签号大于30。
    • 1xxxxxxx:后续字节的最高位为1,表示还有更多字节。
    • 0xxxxxxx:最后一个字节的最高位为0,表示这是最后一个字节。

    3.3 示例

    假设标签号为201:
    第一个字节:标签类为通用(00),构造类型为构造(1),标签号部分为11111(表示多字节标签)。
    后续字节:201的二进制表示为11001001。将其分成7位一组,得到两个字节:0000001 1001001。
    – 第一个后续字节:最高位为1,表示还有更多字节,剩余部分为0000001(01)。
    – 第二个后续字节:最高位为0,表示这是最后一个字节,剩余部分为1001001(49)。

    最终的多字节标签表示为:

    +----------------+----------------+----------------+
    | 00 | 1 | 11111 | 1 0000001     | 0 1001001      |
    +----------------+----------------+----------------+
    

    即:

    1F 81 49
    

    4. 编码示例

    4.1 示例:标签号为201的SEQUENCE

    假设我们有一个标签号为201的SEQUENCE,标签类为通用(00),构造类型为构造(1),标签号为201。编码结果如下:

    +----------------+----------------+----------------+
    | 00 | 1 | 11111 | 1 0000001     | 0 1001001      |
    +----------------+----------------+----------------+
    

    即:

    1F 81 49
    

    结论

    在ASN.1中,多字节标签用于表示标签号大于30的标签。多字节标签的第一个字节的标签号部分设置为31(11111),后续字节用于表示实际的标签号。每个后续字节的最高位用于指示是否有更多的字节,剩余7位用于表示标签号的一部分。通过了解多字节标签的表示方法,我们可以更好地设计和编码ASN.1数据结构,确保数据的准确性和可读性。

    Views: 17

  • ASN.1语言

    ASN.1是一种数据格式描述语言,如同XML也是一种描述语言,广泛用于IETF文档中描述协议和证书格式,所以要想看懂IETF文档要先看懂ASN.1语言。

    以下内容转自:https://letsencrypt.org/zh-cn/docs/a-warm-welcome-to-asn1-and-der/#%E5%8D%95%E4%B8%80%E5%AD%97%E6%AE%B5%E4%B8%8E%E5%A4%8D%E5%90%88%E5%AD%97%E6%AE%B5

    本文以轻松的笔触介绍了 HTTPS 证书背后的数据结构和格式, 只要稍有计算机科学知识并对数字证书略知一二就不难理解。

    HTTPS 证书是一种文件,与任何其他文件一样。 它的具体格式是由 RFC 5280 规定的, 该标准采用了一门名为 ASN.1 的语言,专门用于定义文件格式,也可以说是定义数据结构。 譬如,C 语言是这样定义数据结构的:

    struct point {
      int x, y;
      char label[10];
    };
    

    Go 语言则是这样定义的:

    type point struct {
      x, y int
      label string
    }
    

    而写成 ASN.1 就会是这样:

    Point ::= SEQUENCE {
      x INTEGER,
      y INTEGER,
      label UTF8String
    }
    

    与 Go 和 C 的语法相比,ASN.1 的优势在于它不依赖某一门特定的编程语言, 任何语言都可以根据上述 ASN.1 定义实现 Point 结构。不仅如此,借助工具还可以将 ASN.1 定义自动转换成你想使用的语言。 在 ASN.1 中,一系列相关的定义称为一个“模块”。

    ASN.1 的另一特点在于它有各种序列化格式,也就是说数据可以从内存导出成一串字节(或存入文件中),也可以由一串字节反过来导入内存。 因此任何一台设备生成的证书其他设备都能正常读取,无需顾忌 CPU 和操作系统的差异。

    还有一些语言也能起到与 ASN.1 同样的作用。 例如,Protocol Buffers 也提供了一门定义类型的语言,以及负责将各类对象编码的序列化格式Thrift 同样具备类似的语言和序列化格式。 Protocol Buffers 和 Thrift 完全可以胜任定义 HTTPS 证书的职责,只不过 ASN.1 早在 1984 年就已诞生,对于数字证书(1988 年)和 HTTPS(1994 年)技术来说具备先天优势。

    多年来 ASN.1 标准几经修订,各修订版本通常以发布年份加以区分。 本文介绍 ASN.1 旨在帮助读者准确理解 RFC 5280 及其他 HTTPS 证书相关标准,所以我们将着重探讨 1988 年版,并简要提及后续版本引入的功能。 各版本的标准文档可以直接在国际电信联盟(ITU)的网站上下载,不过部分文件只对 ITU 成员开放。 相关标准包括 X.680(定义了 ASN.1 语言)和 X.690(定义了 DER 和 BER 序列化格式), 其前身分别是 X.208X.209

    ASN.1 最主要的序列化格式称为 Distinguished Encoding Rules (DER)。 它是对 Basic Encoding Rules (BER) 格式加以严格规范化而成的一种变体。 例如,在 DER 格式中,即使是集合类型(SET OF)的元素也必须有序排列。

    DER 格式的证书通常会进一步转换成 PEM 格式,这一过程采用 Base64 编码将二进制字节转化为一连串的字母、数字、“+”和“/”符号,并在头尾加入“—–BEGIN CERTIFICATE—–”与“—–END CERTIFICATE—–”两行文字以示分隔。 PEM 格式的优点在于方便复制与粘贴。

    本文将首先介绍 ASN.1 使用的类型与语法,然后再介绍 ASN.1 定义的对象是如何编码的。 读者不妨随时翻阅前后章节,毕竟 ASN.1 语言的部分功能直接决定了其编码方式。 本文倾向使用更为通行的术语,如用“字节”而非“八位组”,用“数值”而非“内容”。 “序列化”与“编码”则作为同义词使用。

    类型

    INTEGER

    就是老生常谈的整数。 可以是正数,也可以是负数。 ASN.1 的 INTEGER 类型最为特别之处在于大小不限。 int64 不够大? 小事一桩。 这对于表示 RSA 模数之类远超 int64 范围的整数(比如 22048)尤为有用。 严格说来 DER 格式的整数范围是有上限的,但上限极高:任何 DER 字段的长度必须能够用 126 个字节来表示。 因此,DER 能表示的最大的 INTEGER 数值是 256(2**1008)−1。 真正无限大的 INTEGER 必须用 BER 编码,因为 BER 对字段长度没有限制。

    字符串

    ASN.1 有很多字符串类型:BMPString、GeneralString、GraphicString、IA5String、ISO646String、NumericString、PrintableString、TeletexString、T61String、UniversalString、UTF8String、VideotexString,还有 VisibleString。 对于 HTTPS 证书来说基本只需要关心 PrintableString、UTF8String 和 IA5String。 ASN.1 模块定义字段的时候也会定义其应当使用的字符串类型。 例如

    CPSuri ::= IA5String
    

    PrintableString 只能存储 ASCII 中的部分字符,包括字母、数字、空格以及一些特定的符号:' () + , - . / : = ?。 注意其中不包括 *@。 虽然允许使用的字符有限,但在存储空间上并无优势。

    某些字段,例如 RFC 5280 中的 DirectoryString,有多种字符串类型可供选择。 由于 DER 编码会将字符串的类型信息一并存储,使用 PrintableString 这类字符串时务必确保其内容符合类型要求

    IA5String 源自 International Alphabet No. 5,限制较为宽松,所有 ASCII 字符几乎都能使用,在证书中常用于邮箱地址、DNS 域名、网址之类的字段。 需要注意的是 IA5 编码中有部分数值的含义与 US-ASCII 并不相同。

    TeletexString、BMPString 和 UniversalString 对 HTTPS 证书来说都已过时,建议不再使用。但一些历史悠久的证书可能在其过时之前就已颁布,因此还能发现它们的身影,需要予以解析。

    与 C 和 C++ 语言中的字符串不同,ASN.1 的字符串不以空字符结尾。 事实上,字符串中出现空字符也是完全符合规范的。 如果两套系统采取不同方式读取同一 ASN.1 字符串,就可能出现安全漏洞。 例如,过去攻击者只要持有 evil.com 域名,就能诱使某些证书颁发机构为“example.com\0.evil.com”颁发证书。 当时的证书验证软件会认为这一证书对 example.com 也是有效的。 在 C 和 C++ 中处理 ASN.1 字符串必须加倍小心,避免产生安全漏洞。

    日期和时间

    时间类型也有不少:UTCTime、GeneralizedTime、DATE、TIME-OF-DAY、DATE-TIME 以及 DURATION。 但在 HTTPS 证书中只需关心 UTCTime 和 GeneralizedTime。

    UTCTime 采用 YYMMDDhhmm[ss] 的格式表示日期和时间,末尾还可以加上时区,或者加上“Z”表示协调世界时(UTC)。 例如,UTCTime 的 820102120000Z 和 820102070000-0500 表示同一时刻:1982 年 1 月 2 日纽约(UTC−5)的早晨七点,即协调世界时的正午十二点。

    因为 UTCTime 没有写明是 20 世纪还是 21 世纪,RFC 5280 补充规定了其范围是 1950 至 2050 年。 RFC 5280 还规定时区必须为“Z”,且秒数不能省略。

    GeneralizedTime 用四位数字表示年份,从而得以支持 2050 年以后的日期。 它还允许秒数出现小数(而且句点和逗号竟然都可以作为小数点)。 RFC 5280 又禁止了秒数出现小数,并要求时区为“Z”。

    OBJECT IDENTIFIER

    Object identifier (OID) 是一种全球唯一的层状标识符,由一串整数组成。 它可以用来标识任何东西,但通常用于指代标准、算法、证书扩展、组织机构或政策文件。 例如 1.2.840.113549 表示的是 RSA 安全公司, 因此该公司有权分配以这一串数字开头的 OID,比如 RFC 8017 定义的 1.2.840.113549.1.1.11 表示 sha256WithRSAEncryption。

    类似地,1.3.6.1.4.1.11129 指的是 Google 公司,Google 则在 RFC 6962 中将 1.3.6.1.4.1.11129.2.4.2 分配给了证书透明化系统中的 SCT 列表扩展,因为该系统最初便是由 Google 研发的。

    同一前缀下的所有 OID 在英文中又称为 OID arc。 OID 越短,占据的空间就越小,价值也就越高,对于大量使用 OID 的格式尤为如此。 OID 前缀 2.5 分配给了 Directory Services,指的是一系列的标准文档,在 HTTPS 证书中处于核心地位的 X.509 标准也位列其中。 证书中有很多字段用的都是这个简短的前缀。 例如,2.5.4.6 表示国家名称(countryName),2.5.4.10 则表示机构名称(organizationName)。 这些 OID 在绝大多数证书中至少都会出现一次,自然越短越好。

    标准文件中的 OID 常用英文词汇表示以便阅读,有时还会拼接形成新的 OID。 以 RFC 8017 为例

       pkcs-1    OBJECT IDENTIFIER ::= {
           iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 1
       }
       ...
    
       sha256WithRSAEncryption      OBJECT IDENTIFIER ::= { pkcs-1 11 }
    

    NULL

    NULL 就是 NULL,懂了吧?

    SEQUENCE 和 SEQUENCE OF

    别被名字给骗了,这是两种截然不同的类型。 SEQUENCE 相当于大多数编程语言中的 struct 类型, 用于存放若干不同类型的固定字段。 参见下文的证书示例

    SEQUENCE OF 表示的则是任意数量同一类型的字段, 相当于编程语言中的数组或列表。 例如

       RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
    

    可以是 0 个、1 个乃至 7000 个 RelativeDistinguishedName,并且按某种特定顺序排列。

    不过 SEQUENCE 和 SEQUENCE OF 还是有相似之处的:二者的编码完全相同! 这一点留到探讨编码时再详细介绍。

    SET 和 SET OF

    这两种类型与 SEQUENCE 和 SEQUENCE OF 基本相同,只是强调其中元素的顺序无关紧要。 但是在编码前必须先予以排序。 例如

    RelativeDistinguishedName ::=
      SET SIZE (1..MAX) OF AttributeTypeAndValue
    

    注意:本例使用了 SIZE 关键字要求 RelativeDistinguishedName 中至少应有一个元素,但通常 SET 和 SET OF 都可以为空。

    BIT STRING 和 OCTET STRING

    前者表示一段任意的二进制位,而后者表示一段任意的字节。 这两种类型可以存放无特殊结构的数据,比如密码学中的 nonce 和散列值。 它们也可以当作 C 语言中的 void 指针或 Go 语言中的空接口(interface{})使用,表示数据虽然存在内部结构,但该结构只在别处定义,与 ASN.1 的类型系统无关。 例如,证书的数字签名就是用 BIT STRING 定义的:

    Certificate  ::=  SEQUENCE  {
         tbsCertificate       TBSCertificate,
         signatureAlgorithm   AlgorithmIdentifier,
         signature            BIT STRING  }
    

    新版的 ASN.1 语言还支持详细规定 BIT STRING 的内容,OCTET STRING 亦然。

    CHOICE 和 ANY

    CHOICE 表示可以使用其定义中列出的任意一种类型。 例如,下方的 Time 要么是 UTCTime,要么是 GeneralizedTime:

    Time ::= CHOICE {
         utcTime        UTCTime,
         generalTime    GeneralizedTime }
    

    ANY 表示允许使用任意类型。 但实际中通常还有一些 ASN.1 语法难以表达的限制。 例如

       AttributeTypeAndValue ::= SEQUENCE {
         type     AttributeType,
         value    AttributeValue }
    
       AttributeType ::= OBJECT IDENTIFIER
    
       AttributeValue ::= ANY -- DEFINED BY AttributeType
    

    ANY 在协议的扩展机制中扮演了重要的角色。它为将来新增字段留出了余地,协议的主体规范发布后不必反复修订,新字段只需注册新的类型(用 OID 表示),并随之定义具体的字段格式即可。

    需要注意的是 ANY 属于 ASN.1 从 1988 年遗留下来的类型。 1994 年 ANY 就已经被废弃,取而代之的是 Information Object Class,这是一种规范化的扩展定义方式,弥补了 ANY 的不足。 现如今这一改变已经相当彻底,2015 年版的 ASN.1 标准甚至连 ANY 的字样都没有出现。 但是如果回过头看 1994 年版还是可以找到有关这一变动的探讨。 此处介绍这种旧式语法是因为 RFC 5280 仍在使用, RFC 5912 则把 RFC 5280 及相关标准中的类型都转换成了 2002 年的 ASN.1 格式。

    其他语法

    -- 开头的文字是注释。 SEQUENCE 和 SET 类型的字段标注 OPTIONAL 表示可以省略,或者标注 DEFAULT foo 表示省略后的默认值为 foo。 具有长度的类型(字符串、OCTET STRING、BIT STRING、SET OF、SEQUENCE OF)可以通过 SIZE 参数限定长度必须在某一范围内或恰为某个数。

    类型定义结尾可以使用花括号限定该类型只能取某些值。 例如此处定义 Version 字段只能取三个值中的一个,并对这三个值分别赋予了含义:

    Version ::= INTEGER { v1(0), v2(1), v3(2) }
    

    这种方法也常用于标识 OID 的名称。注意此时没有用逗号,因此表示的是一个整体,而非可供选择的多个值。 以 RFC 5280 为例

    id-pkix  OBJECT IDENTIFIER  ::=
             { iso(1) identified-organization(3) dod(6) internet(1)
                        security(5) mechanisms(5) pkix(7) }
    

    接下来还会出现 [数字]、IMPLICIT、EXPLICIT、UNIVERSAL、APPLICATION 等语法要素, 这些语法关系到编码细节,我们将在下文探讨。

    编码

    ASN.1 有很多种编码:BER、DER、PER、XER 等等。 BER (Basic Encoding Rules) 是一种很灵活的编码, DER (Distinguished Encoding Rules) 则在 BER 的基础上加入了规范化的规则,从而保证同样的信息编码方式也是唯一的。 PER (Packed Encoding Rules) 编码更为紧凑,适合对存储空间或传输速度要求较高的场景。 XER (XML Encoding Rules) 则在需要使用 XML 的情况下能够派上用场。

    HTTPS 证书通常采用 DER 编码。 用 BER 也是可以的,但是数字签名必须根据 DER 编码计算,和证书的实际编码方式无关,所以用 BER 相当于自讨苦吃。 本文以介绍 BER 为主,并随之说明 DER 在其基础上施加的额外规定。

    阅读本章时建议在另一窗口打开这个实际证书解码示例,方便对照。

    类型–长度–数据

    与 Protocol Buffers 和 Thrift 一样,BER 的编码形式称为“类型–长度–数据”(type–length–value,常缩写为 TLV)。 也就是说,如果按顺序读取 BER 编码的每个字节,首先读取到的是类型信息,在 ASN.1 中称为“标签”。 标签可以有一个或多个字节,表示存储的是哪种数据,例如 INTEGER、UTF8String 等等。

    类型 长度 数据
    02 03 01 00 01

    接下来出现的是长度,表示数据究竟有多少个字节。 再接下来自然就是数据本身了。 例如,以十六进制表示的字节 02 03 01 00 01 表示一个 INTEGER(02 就是 INTEGER 类型对应的标签),长度是 03,后面的 3 个字节 01 00 01 则是数据。

    与类型–长度–数据的编码方式不同,JSON、CSV、XML 等格式通过分隔符实现编码,也就是事先并不知道数据的长度,出现特定的分隔符(比如 JSON 中的 } 和 XML 中的 </some-tag>)便表示数据结束。

    标签

    标签通常只有一个字节。 如果使用多个字节还可以编码任意大小的标签,但通常没有这个必要。

    以下是一些常见的标签:

    标签(十进制) 标签(十六进制) 对应类型
    2 02 INTEGER
    3 03 BIT STRING
    4 04 OCTET STRING
    5 05 NULL
    6 06 OBJECT IDENTIFIER
    12 0C UTF8String
    16 10(和 30)* SEQUENCE 和 SEQUENCE OF
    17 11(和 31)* SET 和 SET OF
    19 13 PrintableString
    22 16 IA5String
    23 17 UTCTime
    24 18 GeneralizedTime

    这些标签都属于“通用”标签(universal tags),由 ASN.1 核心规范定义,在所有 ASN.1 模块中都有着相同的含义。此外还有一些无关紧要的通用标签,这里略去不表。

    这些标签的值都小于 31(0x1F),其实是有原因的。第 8、7、6 位(也就是标签字节的最高三位)有着特殊含义,所以大于 31 的通用标签只能用多字节标签表示。 有一小部分通用标签的值大于 31,但是数量很少。

    标 * 的两个标签在编码中一定是 0x30 和 0x31,因为第 6 位表示该字段是单一字段还是复合字段。 这两种类型必然是复合字段,所以第 6 位只能是 1。 详见单一字段与复合字段的说明。

    标签类别

    通用标签把“好用”的数字都用光了,但这并不妨碍我们定义自己的标签。 除了通用标签外还有三种标签:程序内部标签、特定语境标签和私有标签。 标签类别可通过第 7 位和第 8 位区分:

    类别 第 8 位 第 7 位
    通用(Universal) 0 0
    程序内部(Application) 0 1
    特定语境(Context-specific) 1 0
    私有(Private) 1 1

    标准规范中使用的大多是通用标签,因为通用标签已经涵盖了所有常用数据结构。 例如,证书序列号就是用朴实无华的 INTEGER 类型编码的,标签值为 0x02。 但有时标准中也需要定义特定语境标签来区分 SET 和 SEQUENCE 中的可省略元素,或者区分 CHOICE 中同类型的选项。 以下述定义为例:

    Point ::= SEQUENCE {
      x INTEGER OPTIONAL,
      y INTEGER OPTIONAL
    }
    

    OPTIONAL 字段如果省略,在编码中就完全不存在,这样一来只有 x 坐标和只有 y 坐标的 Point 就无法区分。 例如,一个只声明 x 坐标为 9 的 Point 编码如下(30 是 SEQUENCE 的标签):

    30 03 02 01 09
    

    这是一个长度为 3(字节)的 SEQUENCE,包含一个长度为 1 的 INTEGER 元素,且其数值为 9。 但一个 y 坐标为 9 的 Point 也得这么编码,于是就出现了歧义。

    编码指令

    为避免出现歧义,标准规范需要通过编码指令为每个字段分配互不相同的标签。 既然通用标签不能随意改变含义,我们只好改用其他类别,比如程序内部标签:

    Point ::= SEQUENCE {
      x [APPLICATION 0] INTEGER OPTIONAL,
      y [APPLICATION 1] INTEGER OPTIONAL
    }
    

    不过这种情形下最常用的还是特定语境标签,表示方法就是方括号内只有一个数字:

    Point ::= SEQUENCE {
      x [0] INTEGER OPTIONAL,
      y [1] INTEGER OPTIONAL
    }
    

    这样一来,只声明 x 坐标为 9 的 Point 编码时便不再用 INTEGER 的通用标签,而是将标签的第 8 和第 7 位分别改为 1 和 0,表示特定语境标签,其他低位设为 0,得到以下编码:

    30 03 80 01 09
    

    y 坐标为 9 的 Point 编码方式如出一辙,只不过低位要设为 1:

    30 03 81 01 09
    

    x 和 y 坐标都为 9 的 Point 则可以编码如下:

    30 06 80 01 09 81 01 09
    

    长度

    类型–长度–数据中的长度指的一定是数据的字节数,数据中嵌套的所有字段也包含在内。 因此,只含有一个元素的 SEQUENCE 长度也不是 1,而是该元素编码后有多少个字节。

    长度也有两种编码方式:短编码和长编码。 短编码就是一个字节,取值范围是 0 到 127。

    长编码则至少有两个字节。第一个字节的第 8 位必须为 1, 其余 7 位表示这个长度字段还有几个字节。 接下来就是一个多字节整数,给出了长度的具体数值。

    可想而知,这样得到的长度值可以非常大。 长度最大时第一个字节是 254(255 是保留值,供将来扩展使用),表示这个长度字段内还有足足 126 个字节。 如果这 126 个字节都是 255,那么实际数据的长度会达到 21008−1 字节(超过 10294 GB)。

    此外,同一长度值的长编码并不唯一,比如一个字节的数字可以拿两个字节表示,短编码就能表示的数字也可以用长编码。 所以 DER 规定必须采用最短的编码方式。

    安全警示:不要轻信长度字段的值! 举例来说,待解码的数据流究竟有没有该字段显示的那么长还是需要核实的。

    不定长编码

    在 BER 中,字符串、SEQUENCE、SEQUENCE OF、SET 和 SET OF 类型的字段即使事先不知道长度也可以直接编码,通过数据流输出就可以采用这种方式。 具体方法是将长度字段设为一个字节 0x80,数据中连续存放若干个编码后的字段,最后以两个字节 00 00(可以认为是一个标签和长度都为 0 的字段)结尾。 例如,UTF8String 的不定长编码就是若干个 UTF8String 拼接在一起,然后在末尾加上 00 00

    不定长编码还可以任意嵌套。 例如,在 UTF8String 的不定长编码中,待拼接的每一段 UTF8String 本身也可以选用定长或不定长编码。

    作为长度的 0x80 字节并不存在歧义,因为它既不是短编码,也不是长编码。 它的第 8 位是 1,按理说应该是长编码,其余 7 位表示还有多少个字节。 但这 7 位都是 0,说明长度要用 0 个字节来表示,这是不允许的。

    DER 禁止使用不定长编码, 所以必须使用定长编码,提前写明数据的实际长度。

    单一字段与复合字段

    标签中第一个字节的第 6 位表示该字段是单一(primitive)字段还是复合(constructed)字段。 单一字段中存储的就是数据本身,比如 UTF8String 的数据就是经过 UTF-8 编码的字符串。 复合字段存储的则是若干其他字段,经过编码后拼接在一起。 例如“不定长编码”一节中提到的不定长 UTF8String 就会有多个 UTF8String 字段(各有标签和长度)编码后连在一起,形成复合字段。 复合字段的长度便是拼接后各字段的总字节数。 复合字段可以采用定长或不定长编码, 而单一字段只能用定长编码,因为其数据中没有其他字段,也就无法表明数据该在哪里结束。

    INTEGER、OBJECT IDENTIFIER 和 NULL 类型必须是单一字段, 而 SEQUENCE、SEQUENCE OF、SET 和 SET OF 类型必须是复合字段(因为它们的作用本来就是存放多个元素)。 BIT STRING、OCTET STRING、UTCTime、GeneralizedTime 还有各种字符串类型既可以是单一字段,也可以是复合字段,在 BER 中编码者可以自行决定, 但在 DER 中凡是单一、复合均可的类型都必须用单一字段。

    EXPLICIT 与 IMPLICIT

    前面提到的 [1][APPLICATION 8]编码指令还可以加上 EXPLICIT 或 IMPLICIT 关键字。以 RFC5280 为例:

    TBSCertificate  ::=  SEQUENCE  {
         version         [0]  Version DEFAULT v1,
         serialNumber         CertificateSerialNumber,
         signature            AlgorithmIdentifier,
         issuer               Name,
         validity             Validity,
         subject              Name,
         subjectPublicKeyInfo SubjectPublicKeyInfo,
         issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                              -- If present, version MUST be v2 or v3
         subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                              -- If present, version MUST be v2 or v3
         extensions      [3]  Extensions OPTIONAL
                              -- If present, version MUST be v3 --  }
    

    这两个关键字定义的是标签的编码方式,与标签值是明确写出还是自动分配无关。事实上使用这两个关键字时都必须在方括号中写明标签的值。 IMPLICIT 字段的编码方式与其原始类型相同,只是标签的值和类别由方括号中的内容决定。 EXPLICIT 字段则需要先将数据按原始类型编码,再将编码结果套入另一字段中。 外层字段的标签值和类别再由方括号中的内容决定,而且一定是复合字段

    例如,若在 ASN.1 的编码指令中加上 IMPLICIT:

    [5] IMPLICIT UTF8String
    

    那么字符串“hi”会被编码为:

    85 02 68 69
    

    但如果在 ASN.1 的编码指令中加上 EXPLICIT:

    [5] EXPLICIT UTF8String
    

    则字符串“hi”会被编码为:

    A5 04 0C 02 68 69
    

    如果 IMPLICIT 和 EXPLICIT 关键字都没有出现,默认编码为 EXPLICIT,除非该模块开头指定了“EXPLICIT TAGS”、“IMPLICIT TAGS”或“AUTOMATIC TAGS”。 例如 RFC 5280 定义了两个模块,一个默认编码为 EXPLICIT,另一个则导入了前一模块,并将默认编码设为 IMPLICIT。 IMPLICIT 编码比 EXPLICIT 编码更短。

    AUTOMATIC TAGS 的作用与 IMPLICIT TAGS 相同,只是标签的数值([0][1] 等等)会在必要时自动分配,比如 SEQUENCE 中存在可省略元素的情况。

    各种类型的具体编码

    我们接下来结合具体示例看看各种类型究竟是如何编码的。

    INTEGER 的编码

    整数的编码由一个或多个字节组成,采用补码格式,最左侧字节的最高位(即第 8 位)为符号位。 BER 标准中是这样写的:

    计算二进制补码的数值时,首先将其各字节的每一位标号,最后一个字节的最低位序号为 0,从这一位开始到第一个字节的最高位序号依次递增, 第 N 位表示的数值为 2N。 整个二进制补码的值即为所有等于 1 的二进制位表示的数值的总和(除了最高位),再减去最高位表示的数值(如果最高位为 1)。

    例如,这个字节(以二进制表示)等于十进制数字 50:

    00110010(即十进制的 25 + 24 + 21 = 50)

    这个字节(以二进制表示)则等于十进制数字 −100:

    10011100(即十进制的 24 + 23 + 22 − 27 = −100)

    这五个字节(以二进制表示)等于十进制数字 −549755813887:

    10000000 00000000 00000000 00000000 00000001(即十进制的 20 − 239

    BER 和 DER 都规定整数必须以最短的方式编码。 这一规则是通过以下条件体现的:

    ……第一个字节的每一位和第二个字节的最高位:
    
    1.  既不能同时为 1;
    2.  也不能同时为 0。
    

    第二项条件的大意就是说,如果编码开头存在零字节,那么把它们删去也不会影响数值。 第二个字节的最高位也很重要,因为有些数值的编码开头必须有零字节。 例如,十进制的 255 需要用两个字节编码:

    00000000 11111111

    因为一个字节 11111111 表示的是 −1(最高位是符号位)。

    第一项条件则最好结合实例来看。 比如十进制数字 −128 的编码为:

    10000000(即十进制的 −27 = −128)

    但是似乎也可以这么编码:

    11111111 10000000(还是十进制的 −128,不过是错误的编码)

    不难验证,−215 + 214 + 213 + 212 + 211 + 210 + 29 + 28 + 27 = −27 = −128。 注意,1 在单字节的 10000000 中是符号位,但在第二个字节中只代表 27

    这种变换具有普遍性:BER(或 DER)编码的任何一个负数都可以在开头加上 11111111 并保持数值不变。 这称为符号扩展。 反过来说,如果一个负数的编码开头是 11111111,将该字节删去同样不会影响数值。 这就是 BER 与 DER 要求使用最短编码的原因。

    INTEGER 采用补码表示对于证书发行影响重大。RFC 5280 规定证书序列号必须是正数, 但最高位必然是符号位,所以 DER 中 8 个字节只能编码 63 位的序列号, 64 位的正数需要 9 个字节才能编码(尽管第一个字节为 0)。

    值为 263+1(恰好是 64 位的正数)的 INTEGER 编码如下:

    02 09 00 80 00 00 00 00 00 00 01
    

    字符串的编码

    字符串的编码就是其字节序列。 IA5String 和 PrintableString 因为都是 ASCII 的子集,只是范围有所不同,所以它们的编码除了标签以外完全一样。

    PrintableString 的“hi”编码为:

    13 02 68 69
    

    而 IA5String 的“hi”编码为:

    16 02 68 69
    

    UTF8String 也类似,但它能表示的字符更多。 例如,表情“😎”(U+1F60E Smiling Face With Sunglasses)的编码是:

    0c 04 f0 9f 98 8e
    

    日期和时间的编码

    出人意料的是,UTCTime 和 GeneralizedTime 的编码方式其实和字符串一样。 正如上文“类型”一章所述,UTCTime 表示时间的格式是 YYMMDDhhmmss, GeneralizedTime 则在其基础上将 YY 改成了四位年份 YYYY。 二者最后都可以加上时区,或者加一个 Z 表示协调世界时(UTC)。

    例如,太平洋标准时间(PST,即 UTC−8)的 2019 年 12 月 15 日 19:02:10 用 UTCTime 表示为 191215190210-0800, 用 BER 编码就是:

    17 11 31 39 31 32 31 35 31 39 30 32 31 30 2d 30 38 30 30
    

    BER 编码中,UTCTime 和 GeneralizedTime 的秒数都可以省略,并且可以使用各种时区。 但在 DER 编码(及 RFC 5280)中,秒数则不能省略,且不能出现小数,时区也只能用 Z,表示协调世界时。

    上述时间如果用 DER 编码则是:

    17 0d 31 39 31 32 31 36 30 33 30 32 31 30 5a
    

    OBJECT IDENTIFIER 的编码

    上文所述,OID 的实质就是一串整数, 而且至少由两个整数组成。 第一个数必须是 0、1、2 三者之一, 如果是 0 或 1,则第二个数必须小于 40。 因此,前两个数 X 和 Y 可以直接用 40×X+Y 来表示,不会产生歧义。

    以 2.999.3 的编码为例,首先要将前两个数合并成 1079(即 40×2+999),得到 1079.3。

    完成合并后再用 Base 128 编码,左侧为高位字节。 也就是说,每个字节的最高位设为 1,但最后一个字节最高位设为 0,表示一个整数到此结束。各字节的其余七位从高到低依次相连表示数值。 例如数字 3 就用一个字节 0x03 表示, 而 129 则需要两个字节 0x81 0x01。 每个数字都如此转换成字节后,拼接在一起就形成了 OID 的编码。

    无论是在 BER 还是 DER 中,OID 都必须用最短的方式编码。 所以其中每个数字编码时开头都不能出现 0x80 字节。

    例如,OID 1.2.840.113549.1.1.11(代表 sha256WithRSAEncryption)的编码是:

    06 09 2a 86 48 86 f7 0d 01 01 0b
    

    NULL 的编码

    NULL 字段的长度永远为 0,所以它的编码只有标签和长度:

    05 00
    

    SEQUENCE 的编码

    首先需要注意的是,SEQUENCE 必然是复合字段,因为它的作用就是容纳其他对象。 换句话说,SEQUENCE 中的数据就是其元素各自编码后按定义中的顺序拼接在一起组成的。 因此,SEQUENCE 标签的第 6 位(单一/复合字段位)一定是 1。 虽然 SEQUENCE 本来的标签是 0x10,但在编码中一定会以 0x30 的形式出现。

    SEQUENCE 的定义中标为 OPTIONAL 的字段可以省略,省略后就会直接从编码中消失。 读取 SEQUENCE 的内容时,解码器可以根据标签和已读入的元素确定正在读取的是哪个字段。 如果存在歧义,比如有同类型的元素,则在 ASN.1 模块中必须借助编码指令为这些元素分配不同的标签。

    标有 DEFAULT 的字段与 OPTIONAL 类似。 如果该字段取默认值,在 BER 编码中就可以予以省略, 而在 DER 编码中则必须省略。

    例如,RFC 5280 中的 AlgorithmIdentifier 就是 SEQUENCE 类型:

       AlgorithmIdentifier  ::=  SEQUENCE  {
            algorithm               OBJECT IDENTIFIER,
            parameters              ANY DEFINED BY algorithm OPTIONAL  }
    

    algorithm 为 1.2.840.113549.1.1.11 的 AlgorithmIdentifier 编码如下。 RFC 8017 建议这种算法对应的 parameters 字段应为 NULL

    30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00
    

    SEQUENCE OF 的编码

    SEQUENCE OF 的编码方式与 SEQUENCE 完全相同, 甚至连标签都一样! 解码时也只有查阅 ASN.1 模块才能确定一个字段究竟是 SEQUENCE 还是 SEQUENCE OF。

    例如以下是一个 SEQUENCE OF 对象的编码,其中包含 7、8、9 三个 INTEGER。

    30 09 02 01 07 02 01 08 02 01 09
    

    SET 的编码

    与 SEQUENCE 一样,SET 也一定是复合字段,其数据总是由若干个字段的编码组成的。 它的标签是 0x11, 但因为单一/复合字段位(第 6 位)必须为 1,所以在编码中实际上是 0x31。

    SET 的编码方式也和 SEQUENCE 类似,OPTIONAL 和 DEFAULT 字段如果省略或取默认值就不会出现, 任何由类型相同导致的歧义都需要在 ASN.1 模块中解决,并且取默认值的 DEFAULT 字段在 DER 编码中必须省略。

    BER 编码对 SET 中元素的顺序没有要求, 但在 DER 中各元素必须按标签值升序排列。

    SET OF 的编码

    SET OF 的编码方式和 SET 一样,标签也是 0x31。 DER 编码同样规定 SET OF 中的字段要按升序排列。 因为 SET OF 中所有元素的类型都一样,仅靠标签排序是不够的。 所以 SET OF 中的元素以编码后的字节序为准,编码较短的就先在最后补齐零字节再作排序。

    BIT STRING 的编码

    一个 N 位的 BIT STRING 用 N/8 个字节(向上取整)来编码,开头还有一个字节的前缀,当 N 不是 8 的倍数时可以指明最后有几个二进制位是无用的。 例如,编码 18 个二进制位 011011100101110111 至少需要三个字节, 但三个字节其实足以存放 24 个二进制位, 有 6 位是用不上的。 这 6 位就放在末尾,于是最终编码为:

    03 04 06 6e 5d c0
    

    在 BER 中这些无用的二进制位可以取任意值,所以最后一个字节也可以是 c1、c2、c3 等等。 DER 则要求无用的位都必须为 0。

    OCTET STRING 的编码

    OCTET STRING 的编码就是其中包含的所有字节。 例如,包含四个字节 03 02 06 A0 的 OCTET STRING 的编码为:

    04 04 03 02 06 A0
    

    CHOICE 和 ANY 的编码

    CHOICE 和 ANY 的编码与其最终表示的实际类型一致,但也可以通过编码指令更改。 假如一份 ASN.1 相关规范要求某一个 CHOICE 字段必须是 INTEGER 或 UTCTime,而现在该字段恰好属于 INTEGER,那就按 INTEGER 编码即可。

    但实际应用中 CHOICE 往往都有编码指令。 比如在 RFC 5280 的下列定义中,rfc822Name 和 dNSName 的类型都是 IA5String,只有靠编码指令才能区分。

       GeneralName ::= CHOICE {
            otherName                       [0]     OtherName,
            rfc822Name                      [1]     IA5String,
            dNSName                         [2]     IA5String,
            x400Address                     [3]     ORAddress,
            directoryName                   [4]     Name,
            ediPartyName                    [5]     EDIPartyName,
            uniformResourceIdentifier       [6]     IA5String,
            iPAddress                       [7]     OCTET STRING,
            registeredID                    [8]     OBJECT IDENTIFIER }
    

    举例来说,如果一个 GeneralName 里包含的是 rfc822Name,值为 a@example.com,则编码如下(回忆一下,[1] 表示标签值为 1,类别为特定语境标签,即第 8 位为 1,并且标签编码方式为 IMPLICIT):

    81 0d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d
    

    如果 GeneralName 里包含的是 dNSName,值为“example.com”,编码则是:

    82 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d
    

    安全性

    解码 BER 和 DER 格式时必须格外小心,尤其是在 C、C++ 等非内存安全的编程语言中。 各种解码器的安全漏洞可谓罄竹难书, 而解析用户输入本身就是安全问题的一大来源。 ASN.1 编码与漏洞近乎如影随形, 毕竟这种格式颇为复杂,各种不定长度的字段不计其数, 连长度字段本身都没有固定长度! 另一方面,ASN.1 格式的数据又往往来自潜在的攻击者, 所以如果要靠数字证书验证用户身份,不能光考虑如何解码正确的证书,还得应对五花八门的恶意输入,以免 ASN.1 代码中存在漏洞而被攻陷。

    面对这些问题,最好的解决办法就是尽可能使用内存安全的编程语言, 并且无论能否使用这类语言,都应当借助现成的 ASN.1 编译器生成解析程序,而非闭门造车,自行编写解码器。

    致谢

    首先我要向 A Layman’s Guide to a Subset of ASN.1, DER, and BER 致以诚挚的敬意,本文中的大部分知识我都是从这份材料中学到的。 我还要感谢另一篇佳作 A warm welcome to DNS 的作者,其文风也奠定了本文的笔法基调。

    一点题外话

    你可曾注意到 PEM 编码的证书开头都是“MII”? 例如:

    -----BEGIN CERTIFICATE-----
    
    MIIFajCCBFKgAwIBAgISA6HJW9qjaoJoMn8iU8vTuiQ2MA0GCSqGSIb3DQEBCwUA
    ...
    

    现在你已经能解释原因了! 证书本身是一个 SEQUENCE 结构,所以第一个字节是 0x30。 接下来是长度字段, 绝大多数证书都不止 127 个字节,所以需要采用长编码, 也就是说第一个字节是 0x80 + N,意味着后面还有 N 个字节表示长度。 N 一般都是 2,因为大多数证书的长度都在 128 至 65535 个字节之间,只需要两个字节表示。

    于是我们可以得知 DER 格式的证书前两个字节是 0x30 0x82。 但 PEM 采用了 Base64 编码,将每 3 个二进制字节转换成 4 个 ASCII 字符。 换句话说,Base64 是将 24 个二进制位写成 4 个 ASCII 字符,每个字符表示 6 个二进制位。 我们已经知道证书的前 16 位是什么了, 要证明(几乎)所有证书开头都是“MII”,还需确定接下来的两个二进制位。 这两位正是长度字节的最高位, 它们会是 1 吗? 那这个证书就非得超过 16383 个字节不可! 由此我们可以断定 PEM 证书开头的几个字符都是相同的。 你不妨也试试看:

    xxd -r -p <<<308200 | base64
    

    本文以轻松的笔触介绍了 HTTPS 证书背后的数据结构和格式, 只要稍有计算机科学知识并对数字证书略知一二就不难理解。

    HTTPS 证书是一种文件,与任何其他文件一样。 它的具体格式是由 RFC 5280 规定的, 该标准采用了一门名为 ASN.1 的语言,专门用于定义文件格式,也可以说是定义数据结构。 譬如,C 语言是这样定义数据结构的:

    struct point {
      int x, y;
      char label[10];
    };
    

    Go 语言则是这样定义的:

    type point struct {
      x, y int
      label string
    }
    

    而写成 ASN.1 就会是这样:

    Point ::= SEQUENCE {
      x INTEGER,
      y INTEGER,
      label UTF8String
    }
    

    与 Go 和 C 的语法相比,ASN.1 的优势在于它不依赖某一门特定的编程语言, 任何语言都可以根据上述 ASN.1 定义实现 Point 结构。不仅如此,借助工具还可以将 ASN.1 定义自动转换成你想使用的语言。 在 ASN.1 中,一系列相关的定义称为一个“模块”。

    ASN.1 的另一特点在于它有各种序列化格式,也就是说数据可以从内存导出成一串字节(或存入文件中),也可以由一串字节反过来导入内存。 因此任何一台设备生成的证书其他设备都能正常读取,无需顾忌 CPU 和操作系统的差异。

    还有一些语言也能起到与 ASN.1 同样的作用。 例如,Protocol Buffers 也提供了一门定义类型的语言,以及负责将各类对象编码的序列化格式Thrift 同样具备类似的语言和序列化格式。 Protocol Buffers 和 Thrift 完全可以胜任定义 HTTPS 证书的职责,只不过 ASN.1 早在 1984 年就已诞生,对于数字证书(1988 年)和 HTTPS(1994 年)技术来说具备先天优势。

    多年来 ASN.1 标准几经修订,各修订版本通常以发布年份加以区分。 本文介绍 ASN.1 旨在帮助读者准确理解 RFC 5280 及其他 HTTPS 证书相关标准,所以我们将着重探讨 1988 年版,并简要提及后续版本引入的功能。 各版本的标准文档可以直接在国际电信联盟(ITU)的网站上下载,不过部分文件只对 ITU 成员开放。 相关标准包括 X.680(定义了 ASN.1 语言)和 X.690(定义了 DER 和 BER 序列化格式), 其前身分别是 X.208X.209

    ASN.1 最主要的序列化格式称为 Distinguished Encoding Rules (DER)。 它是对 Basic Encoding Rules (BER) 格式加以严格规范化而成的一种变体。 例如,在 DER 格式中,即使是集合类型(SET OF)的元素也必须有序排列。

    DER 格式的证书通常会进一步转换成 PEM 格式,这一过程采用 Base64 编码将二进制字节转化为一连串的字母、数字、“+”和“/”符号,并在头尾加入“—–BEGIN CERTIFICATE—–”与“—–END CERTIFICATE—–”两行文字以示分隔。 PEM 格式的优点在于方便复制与粘贴。

    本文将首先介绍 ASN.1 使用的类型与语法,然后再介绍 ASN.1 定义的对象是如何编码的。 读者不妨随时翻阅前后章节,毕竟 ASN.1 语言的部分功能直接决定了其编码方式。 本文倾向使用更为通行的术语,如用“字节”而非“八位组”,用“数值”而非“内容”。 “序列化”与“编码”则作为同义词使用。

    类型

    INTEGER

    就是老生常谈的整数。 可以是正数,也可以是负数。 ASN.1 的 INTEGER 类型最为特别之处在于大小不限。 int64 不够大? 小事一桩。 这对于表示 RSA 模数之类远超 int64 范围的整数(比如 22048)尤为有用。 严格说来 DER 格式的整数范围是有上限的,但上限极高:任何 DER 字段的长度必须能够用 126 个字节来表示。 因此,DER 能表示的最大的 INTEGER 数值是 256(2**1008)−1。 真正无限大的 INTEGER 必须用 BER 编码,因为 BER 对字段长度没有限制。

    字符串

    ASN.1 有很多字符串类型:BMPString、GeneralString、GraphicString、IA5String、ISO646String、NumericString、PrintableString、TeletexString、T61String、UniversalString、UTF8String、VideotexString,还有 VisibleString。 对于 HTTPS 证书来说基本只需要关心 PrintableString、UTF8String 和 IA5String。 ASN.1 模块定义字段的时候也会定义其应当使用的字符串类型。 例如

    CPSuri ::= IA5String
    

    PrintableString 只能存储 ASCII 中的部分字符,包括字母、数字、空格以及一些特定的符号:' () + , - . / : = ?。 注意其中不包括 *@。 虽然允许使用的字符有限,但在存储空间上并无优势。

    某些字段,例如 RFC 5280 中的 DirectoryString,有多种字符串类型可供选择。 由于 DER 编码会将字符串的类型信息一并存储,使用 PrintableString 这类字符串时务必确保其内容符合类型要求

    IA5String 源自 International Alphabet No. 5,限制较为宽松,所有 ASCII 字符几乎都能使用,在证书中常用于邮箱地址、DNS 域名、网址之类的字段。 需要注意的是 IA5 编码中有部分数值的含义与 US-ASCII 并不相同。

    TeletexString、BMPString 和 UniversalString 对 HTTPS 证书来说都已过时,建议不再使用。但一些历史悠久的证书可能在其过时之前就已颁布,因此还能发现它们的身影,需要予以解析。

    与 C 和 C++ 语言中的字符串不同,ASN.1 的字符串不以空字符结尾。 事实上,字符串中出现空字符也是完全符合规范的。 如果两套系统采取不同方式读取同一 ASN.1 字符串,就可能出现安全漏洞。 例如,过去攻击者只要持有 evil.com 域名,就能诱使某些证书颁发机构为“example.com\0.evil.com”颁发证书。 当时的证书验证软件会认为这一证书对 example.com 也是有效的。 在 C 和 C++ 中处理 ASN.1 字符串必须加倍小心,避免产生安全漏洞。

    日期和时间

    时间类型也有不少:UTCTime、GeneralizedTime、DATE、TIME-OF-DAY、DATE-TIME 以及 DURATION。 但在 HTTPS 证书中只需关心 UTCTime 和 GeneralizedTime。

    UTCTime 采用 YYMMDDhhmm[ss] 的格式表示日期和时间,末尾还可以加上时区,或者加上“Z”表示协调世界时(UTC)。 例如,UTCTime 的 820102120000Z 和 820102070000-0500 表示同一时刻:1982 年 1 月 2 日纽约(UTC−5)的早晨七点,即协调世界时的正午十二点。

    因为 UTCTime 没有写明是 20 世纪还是 21 世纪,RFC 5280 补充规定了其范围是 1950 至 2050 年。 RFC 5280 还规定时区必须为“Z”,且秒数不能省略。

    GeneralizedTime 用四位数字表示年份,从而得以支持 2050 年以后的日期。 它还允许秒数出现小数(而且句点和逗号竟然都可以作为小数点)。 RFC 5280 又禁止了秒数出现小数,并要求时区为“Z”。

    OBJECT IDENTIFIER

    Object identifier (OID) 是一种全球唯一的层状标识符,由一串整数组成。 它可以用来标识任何东西,但通常用于指代标准、算法、证书扩展、组织机构或政策文件。 例如 1.2.840.113549 表示的是 RSA 安全公司, 因此该公司有权分配以这一串数字开头的 OID,比如 RFC 8017 定义的 1.2.840.113549.1.1.11 表示 sha256WithRSAEncryption。

    类似地,1.3.6.1.4.1.11129 指的是 Google 公司,Google 则在 RFC 6962 中将 1.3.6.1.4.1.11129.2.4.2 分配给了证书透明化系统中的 SCT 列表扩展,因为该系统最初便是由 Google 研发的。

    同一前缀下的所有 OID 在英文中又称为 OID arc。 OID 越短,占据的空间就越小,价值也就越高,对于大量使用 OID 的格式尤为如此。 OID 前缀 2.5 分配给了 Directory Services,指的是一系列的标准文档,在 HTTPS 证书中处于核心地位的 X.509 标准也位列其中。 证书中有很多字段用的都是这个简短的前缀。 例如,2.5.4.6 表示国家名称(countryName),2.5.4.10 则表示机构名称(organizationName)。 这些 OID 在绝大多数证书中至少都会出现一次,自然越短越好。

    标准文件中的 OID 常用英文词汇表示以便阅读,有时还会拼接形成新的 OID。 以 RFC 8017 为例

       pkcs-1    OBJECT IDENTIFIER ::= {
           iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 1
       }
       ...
    
       sha256WithRSAEncryption      OBJECT IDENTIFIER ::= { pkcs-1 11 }
    

    NULL

    NULL 就是 NULL,懂了吧?

    SEQUENCE 和 SEQUENCE OF

    别被名字给骗了,这是两种截然不同的类型。 SEQUENCE 相当于大多数编程语言中的 struct 类型, 用于存放若干不同类型的固定字段。 参见下文的证书示例

    SEQUENCE OF 表示的则是任意数量同一类型的字段, 相当于编程语言中的数组或列表。 例如

       RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
    

    可以是 0 个、1 个乃至 7000 个 RelativeDistinguishedName,并且按某种特定顺序排列。

    不过 SEQUENCE 和 SEQUENCE OF 还是有相似之处的:二者的编码完全相同! 这一点留到探讨编码时再详细介绍。

    SET 和 SET OF

    这两种类型与 SEQUENCE 和 SEQUENCE OF 基本相同,只是强调其中元素的顺序无关紧要。 但是在编码前必须先予以排序。 例如

    RelativeDistinguishedName ::=
      SET SIZE (1..MAX) OF AttributeTypeAndValue
    

    注意:本例使用了 SIZE 关键字要求 RelativeDistinguishedName 中至少应有一个元素,但通常 SET 和 SET OF 都可以为空。

    BIT STRING 和 OCTET STRING

    前者表示一段任意的二进制位,而后者表示一段任意的字节。 这两种类型可以存放无特殊结构的数据,比如密码学中的 nonce 和散列值。 它们也可以当作 C 语言中的 void 指针或 Go 语言中的空接口(interface{})使用,表示数据虽然存在内部结构,但该结构只在别处定义,与 ASN.1 的类型系统无关。 例如,证书的数字签名就是用 BIT STRING 定义的:

    Certificate  ::=  SEQUENCE  {
         tbsCertificate       TBSCertificate,
         signatureAlgorithm   AlgorithmIdentifier,
         signature            BIT STRING  }
    

    新版的 ASN.1 语言还支持详细规定 BIT STRING 的内容,OCTET STRING 亦然。

    CHOICE 和 ANY

    CHOICE 表示可以使用其定义中列出的任意一种类型。 例如,下方的 Time 要么是 UTCTime,要么是 GeneralizedTime:

    Time ::= CHOICE {
         utcTime        UTCTime,
         generalTime    GeneralizedTime }
    

    ANY 表示允许使用任意类型。 但实际中通常还有一些 ASN.1 语法难以表达的限制。 例如

       AttributeTypeAndValue ::= SEQUENCE {
         type     AttributeType,
         value    AttributeValue }
    
       AttributeType ::= OBJECT IDENTIFIER
    
       AttributeValue ::= ANY -- DEFINED BY AttributeType
    

    ANY 在协议的扩展机制中扮演了重要的角色。它为将来新增字段留出了余地,协议的主体规范发布后不必反复修订,新字段只需注册新的类型(用 OID 表示),并随之定义具体的字段格式即可。

    需要注意的是 ANY 属于 ASN.1 从 1988 年遗留下来的类型。 1994 年 ANY 就已经被废弃,取而代之的是 Information Object Class,这是一种规范化的扩展定义方式,弥补了 ANY 的不足。 现如今这一改变已经相当彻底,2015 年版的 ASN.1 标准甚至连 ANY 的字样都没有出现。 但是如果回过头看 1994 年版还是可以找到有关这一变动的探讨。 此处介绍这种旧式语法是因为 RFC 5280 仍在使用, RFC 5912 则把 RFC 5280 及相关标准中的类型都转换成了 2002 年的 ASN.1 格式。

    其他语法

    -- 开头的文字是注释。 SEQUENCE 和 SET 类型的字段标注 OPTIONAL 表示可以省略,或者标注 DEFAULT foo 表示省略后的默认值为 foo。 具有长度的类型(字符串、OCTET STRING、BIT STRING、SET OF、SEQUENCE OF)可以通过 SIZE 参数限定长度必须在某一范围内或恰为某个数。

    类型定义结尾可以使用花括号限定该类型只能取某些值。 例如此处定义 Version 字段只能取三个值中的一个,并对这三个值分别赋予了含义:

    Version ::= INTEGER { v1(0), v2(1), v3(2) }
    

    这种方法也常用于标识 OID 的名称。注意此时没有用逗号,因此表示的是一个整体,而非可供选择的多个值。 以 RFC 5280 为例

    id-pkix  OBJECT IDENTIFIER  ::=
             { iso(1) identified-organization(3) dod(6) internet(1)
                        security(5) mechanisms(5) pkix(7) }
    

    接下来还会出现 [数字]、IMPLICIT、EXPLICIT、UNIVERSAL、APPLICATION 等语法要素, 这些语法关系到编码细节,我们将在下文探讨。

    编码

    ASN.1 有很多种编码:BER、DER、PER、XER 等等。 BER (Basic Encoding Rules) 是一种很灵活的编码, DER (Distinguished Encoding Rules) 则在 BER 的基础上加入了规范化的规则,从而保证同样的信息编码方式也是唯一的。 PER (Packed Encoding Rules) 编码更为紧凑,适合对存储空间或传输速度要求较高的场景。 XER (XML Encoding Rules) 则在需要使用 XML 的情况下能够派上用场。

    HTTPS 证书通常采用 DER 编码。 用 BER 也是可以的,但是数字签名必须根据 DER 编码计算,和证书的实际编码方式无关,所以用 BER 相当于自讨苦吃。 本文以介绍 BER 为主,并随之说明 DER 在其基础上施加的额外规定。

    阅读本章时建议在另一窗口打开这个实际证书解码示例,方便对照。

    类型–长度–数据

    与 Protocol Buffers 和 Thrift 一样,BER 的编码形式称为“类型–长度–数据”(type–length–value,常缩写为 TLV)。 也就是说,如果按顺序读取 BER 编码的每个字节,首先读取到的是类型信息,在 ASN.1 中称为“标签”。 标签可以有一个或多个字节,表示存储的是哪种数据,例如 INTEGER、UTF8String 等等。

    类型 长度 数据
    02 03 01 00 01

    接下来出现的是长度,表示数据究竟有多少个字节。 再接下来自然就是数据本身了。 例如,以十六进制表示的字节 02 03 01 00 01 表示一个 INTEGER(02 就是 INTEGER 类型对应的标签),长度是 03,后面的 3 个字节 01 00 01 则是数据。

    与类型–长度–数据的编码方式不同,JSON、CSV、XML 等格式通过分隔符实现编码,也就是事先并不知道数据的长度,出现特定的分隔符(比如 JSON 中的 } 和 XML 中的 </some-tag>)便表示数据结束。

    标签

    标签通常只有一个字节。 如果使用多个字节还可以编码任意大小的标签,但通常没有这个必要。

    以下是一些常见的标签:

    标签(十进制) 标签(十六进制) 对应类型
    2 02 INTEGER
    3 03 BIT STRING
    4 04 OCTET STRING
    5 05 NULL
    6 06 OBJECT IDENTIFIER
    12 0C UTF8String
    16 10(和 30)* SEQUENCE 和 SEQUENCE OF
    17 11(和 31)* SET 和 SET OF
    19 13 PrintableString
    22 16 IA5String
    23 17 UTCTime
    24 18 GeneralizedTime

    这些标签都属于“通用”标签(universal tags),由 ASN.1 核心规范定义,在所有 ASN.1 模块中都有着相同的含义。此外还有一些无关紧要的通用标签,这里略去不表。

    这些标签的值都小于 31(0x1F),其实是有原因的。第 8、7、6 位(也就是标签字节的最高三位)有着特殊含义,所以大于 31 的通用标签只能用多字节标签表示。 有一小部分通用标签的值大于 31,但是数量很少。

    标 * 的两个标签在编码中一定是 0x30 和 0x31,因为第 6 位表示该字段是单一字段还是复合字段。 这两种类型必然是复合字段,所以第 6 位只能是 1。 详见单一字段与复合字段的说明。

    标签类别

    通用标签把“好用”的数字都用光了,但这并不妨碍我们定义自己的标签。 除了通用标签外还有三种标签:程序内部标签、特定语境标签和私有标签。 标签类别可通过第 7 位和第 8 位区分:

    类别 第 8 位 第 7 位
    通用(Universal) 0 0
    程序内部(Application) 0 1
    特定语境(Context-specific) 1 0
    私有(Private) 1 1

    标准规范中使用的大多是通用标签,因为通用标签已经涵盖了所有常用数据结构。 例如,证书序列号就是用朴实无华的 INTEGER 类型编码的,标签值为 0x02。 但有时标准中也需要定义特定语境标签来区分 SET 和 SEQUENCE 中的可省略元素,或者区分 CHOICE 中同类型的选项。 以下述定义为例:

    Point ::= SEQUENCE {
      x INTEGER OPTIONAL,
      y INTEGER OPTIONAL
    }
    

    OPTIONAL 字段如果省略,在编码中就完全不存在,这样一来只有 x 坐标和只有 y 坐标的 Point 就无法区分。 例如,一个只声明 x 坐标为 9 的 Point 编码如下(30 是 SEQUENCE 的标签):

    30 03 02 01 09
    

    这是一个长度为 3(字节)的 SEQUENCE,包含一个长度为 1 的 INTEGER 元素,且其数值为 9。 但一个 y 坐标为 9 的 Point 也得这么编码,于是就出现了歧义。

    编码指令

    为避免出现歧义,标准规范需要通过编码指令为每个字段分配互不相同的标签。 既然通用标签不能随意改变含义,我们只好改用其他类别,比如程序内部标签:

    Point ::= SEQUENCE {
      x [APPLICATION 0] INTEGER OPTIONAL,
      y [APPLICATION 1] INTEGER OPTIONAL
    }
    

    不过这种情形下最常用的还是特定语境标签,表示方法就是方括号内只有一个数字:

    Point ::= SEQUENCE {
      x [0] INTEGER OPTIONAL,
      y [1] INTEGER OPTIONAL
    }
    

    这样一来,只声明 x 坐标为 9 的 Point 编码时便不再用 INTEGER 的通用标签,而是将标签的第 8 和第 7 位分别改为 1 和 0,表示特定语境标签,其他低位设为 0,得到以下编码:

    30 03 80 01 09
    

    y 坐标为 9 的 Point 编码方式如出一辙,只不过低位要设为 1:

    30 03 81 01 09
    

    x 和 y 坐标都为 9 的 Point 则可以编码如下:

    30 06 80 01 09 81 01 09
    

    长度

    类型–长度–数据中的长度指的一定是数据的字节数,数据中嵌套的所有字段也包含在内。 因此,只含有一个元素的 SEQUENCE 长度也不是 1,而是该元素编码后有多少个字节。

    长度也有两种编码方式:短编码和长编码。 短编码就是一个字节,取值范围是 0 到 127。

    长编码则至少有两个字节。第一个字节的第 8 位必须为 1, 其余 7 位表示这个长度字段还有几个字节。 接下来就是一个多字节整数,给出了长度的具体数值。

    可想而知,这样得到的长度值可以非常大。 长度最大时第一个字节是 254(255 是保留值,供将来扩展使用),表示这个长度字段内还有足足 126 个字节。 如果这 126 个字节都是 255,那么实际数据的长度会达到 21008−1 字节(超过 10294 GB)。

    此外,同一长度值的长编码并不唯一,比如一个字节的数字可以拿两个字节表示,短编码就能表示的数字也可以用长编码。 所以 DER 规定必须采用最短的编码方式。

    安全警示:不要轻信长度字段的值! 举例来说,待解码的数据流究竟有没有该字段显示的那么长还是需要核实的。

    不定长编码

    在 BER 中,字符串、SEQUENCE、SEQUENCE OF、SET 和 SET OF 类型的字段即使事先不知道长度也可以直接编码,通过数据流输出就可以采用这种方式。 具体方法是将长度字段设为一个字节 0x80,数据中连续存放若干个编码后的字段,最后以两个字节 00 00(可以认为是一个标签和长度都为 0 的字段)结尾。 例如,UTF8String 的不定长编码就是若干个 UTF8String 拼接在一起,然后在末尾加上 00 00

    不定长编码还可以任意嵌套。 例如,在 UTF8String 的不定长编码中,待拼接的每一段 UTF8String 本身也可以选用定长或不定长编码。

    作为长度的 0x80 字节并不存在歧义,因为它既不是短编码,也不是长编码。 它的第 8 位是 1,按理说应该是长编码,其余 7 位表示还有多少个字节。 但这 7 位都是 0,说明长度要用 0 个字节来表示,这是不允许的。

    DER 禁止使用不定长编码, 所以必须使用定长编码,提前写明数据的实际长度。

    单一字段与复合字段

    标签中第一个字节的第 6 位表示该字段是单一(primitive)字段还是复合(constructed)字段。 单一字段中存储的就是数据本身,比如 UTF8String 的数据就是经过 UTF-8 编码的字符串。 复合字段存储的则是若干其他字段,经过编码后拼接在一起。 例如“不定长编码”一节中提到的不定长 UTF8String 就会有多个 UTF8String 字段(各有标签和长度)编码后连在一起,形成复合字段。 复合字段的长度便是拼接后各字段的总字节数。 复合字段可以采用定长或不定长编码, 而单一字段只能用定长编码,因为其数据中没有其他字段,也就无法表明数据该在哪里结束。

    INTEGER、OBJECT IDENTIFIER 和 NULL 类型必须是单一字段, 而 SEQUENCE、SEQUENCE OF、SET 和 SET OF 类型必须是复合字段(因为它们的作用本来就是存放多个元素)。 BIT STRING、OCTET STRING、UTCTime、GeneralizedTime 还有各种字符串类型既可以是单一字段,也可以是复合字段,在 BER 中编码者可以自行决定, 但在 DER 中凡是单一、复合均可的类型都必须用单一字段。

    EXPLICIT 与 IMPLICIT

    前面提到的 [1][APPLICATION 8]编码指令还可以加上 EXPLICIT 或 IMPLICIT 关键字。以 RFC5280 为例:

    TBSCertificate  ::=  SEQUENCE  {
         version         [0]  Version DEFAULT v1,
         serialNumber         CertificateSerialNumber,
         signature            AlgorithmIdentifier,
         issuer               Name,
         validity             Validity,
         subject              Name,
         subjectPublicKeyInfo SubjectPublicKeyInfo,
         issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                              -- If present, version MUST be v2 or v3
         subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                              -- If present, version MUST be v2 or v3
         extensions      [3]  Extensions OPTIONAL
                              -- If present, version MUST be v3 --  }
    

    这两个关键字定义的是标签的编码方式,与标签值是明确写出还是自动分配无关。事实上使用这两个关键字时都必须在方括号中写明标签的值。 IMPLICIT 字段的编码方式与其原始类型相同,只是标签的值和类别由方括号中的内容决定。 EXPLICIT 字段则需要先将数据按原始类型编码,再将编码结果套入另一字段中。 外层字段的标签值和类别再由方括号中的内容决定,而且一定是复合字段

    例如,若在 ASN.1 的编码指令中加上 IMPLICIT:

    [5] IMPLICIT UTF8String
    

    那么字符串“hi”会被编码为:

    85 02 68 69
    

    但如果在 ASN.1 的编码指令中加上 EXPLICIT:

    [5] EXPLICIT UTF8String
    

    则字符串“hi”会被编码为:

    A5 04 0C 02 68 69
    

    如果 IMPLICIT 和 EXPLICIT 关键字都没有出现,默认编码为 EXPLICIT,除非该模块开头指定了“EXPLICIT TAGS”、“IMPLICIT TAGS”或“AUTOMATIC TAGS”。 例如 RFC 5280 定义了两个模块,一个默认编码为 EXPLICIT,另一个则导入了前一模块,并将默认编码设为 IMPLICIT。 IMPLICIT 编码比 EXPLICIT 编码更短。

    AUTOMATIC TAGS 的作用与 IMPLICIT TAGS 相同,只是标签的数值([0][1] 等等)会在必要时自动分配,比如 SEQUENCE 中存在可省略元素的情况。

    各种类型的具体编码

    我们接下来结合具体示例看看各种类型究竟是如何编码的。

    INTEGER 的编码

    整数的编码由一个或多个字节组成,采用补码格式,最左侧字节的最高位(即第 8 位)为符号位。 BER 标准中是这样写的:

    计算二进制补码的数值时,首先将其各字节的每一位标号,最后一个字节的最低位序号为 0,从这一位开始到第一个字节的最高位序号依次递增, 第 N 位表示的数值为 2N。 整个二进制补码的值即为所有等于 1 的二进制位表示的数值的总和(除了最高位),再减去最高位表示的数值(如果最高位为 1)。

    例如,这个字节(以二进制表示)等于十进制数字 50:

    00110010(即十进制的 25 + 24 + 21 = 50)

    这个字节(以二进制表示)则等于十进制数字 −100:

    10011100(即十进制的 24 + 23 + 22 − 27 = −100)

    这五个字节(以二进制表示)等于十进制数字 −549755813887:

    10000000 00000000 00000000 00000000 00000001(即十进制的 20 − 239

    BER 和 DER 都规定整数必须以最短的方式编码。 这一规则是通过以下条件体现的:

    ……第一个字节的每一位和第二个字节的最高位:
    
    1.  既不能同时为 1;
    2.  也不能同时为 0。
    

    第二项条件的大意就是说,如果编码开头存在零字节,那么把它们删去也不会影响数值。 第二个字节的最高位也很重要,因为有些数值的编码开头必须有零字节。 例如,十进制的 255 需要用两个字节编码:

    00000000 11111111

    因为一个字节 11111111 表示的是 −1(最高位是符号位)。

    第一项条件则最好结合实例来看。 比如十进制数字 −128 的编码为:

    10000000(即十进制的 −27 = −128)

    但是似乎也可以这么编码:

    11111111 10000000(还是十进制的 −128,不过是错误的编码)

    不难验证,−215 + 214 + 213 + 212 + 211 + 210 + 29 + 28 + 27 = −27 = −128。 注意,1 在单字节的 10000000 中是符号位,但在第二个字节中只代表 27

    这种变换具有普遍性:BER(或 DER)编码的任何一个负数都可以在开头加上 11111111 并保持数值不变。 这称为符号扩展。 反过来说,如果一个负数的编码开头是 11111111,将该字节删去同样不会影响数值。 这就是 BER 与 DER 要求使用最短编码的原因。

    INTEGER 采用补码表示对于证书发行影响重大。RFC 5280 规定证书序列号必须是正数, 但最高位必然是符号位,所以 DER 中 8 个字节只能编码 63 位的序列号, 64 位的正数需要 9 个字节才能编码(尽管第一个字节为 0)。

    值为 263+1(恰好是 64 位的正数)的 INTEGER 编码如下:

    02 09 00 80 00 00 00 00 00 00 01
    

    字符串的编码

    字符串的编码就是其字节序列。 IA5String 和 PrintableString 因为都是 ASCII 的子集,只是范围有所不同,所以它们的编码除了标签以外完全一样。

    PrintableString 的“hi”编码为:

    13 02 68 69
    

    而 IA5String 的“hi”编码为:

    16 02 68 69
    

    UTF8String 也类似,但它能表示的字符更多。 例如,表情“😎”(U+1F60E Smiling Face With Sunglasses)的编码是:

    0c 04 f0 9f 98 8e
    

    日期和时间的编码

    出人意料的是,UTCTime 和 GeneralizedTime 的编码方式其实和字符串一样。 正如上文“类型”一章所述,UTCTime 表示时间的格式是 YYMMDDhhmmss, GeneralizedTime 则在其基础上将 YY 改成了四位年份 YYYY。 二者最后都可以加上时区,或者加一个 Z 表示协调世界时(UTC)。

    例如,太平洋标准时间(PST,即 UTC−8)的 2019 年 12 月 15 日 19:02:10 用 UTCTime 表示为 191215190210-0800, 用 BER 编码就是:

    17 11 31 39 31 32 31 35 31 39 30 32 31 30 2d 30 38 30 30
    

    BER 编码中,UTCTime 和 GeneralizedTime 的秒数都可以省略,并且可以使用各种时区。 但在 DER 编码(及 RFC 5280)中,秒数则不能省略,且不能出现小数,时区也只能用 Z,表示协调世界时。

    上述时间如果用 DER 编码则是:

    17 0d 31 39 31 32 31 36 30 33 30 32 31 30 5a
    

    OBJECT IDENTIFIER 的编码

    上文所述,OID 的实质就是一串整数, 而且至少由两个整数组成。 第一个数必须是 0、1、2 三者之一, 如果是 0 或 1,则第二个数必须小于 40。 因此,前两个数 X 和 Y 可以直接用 40×X+Y 来表示,不会产生歧义。

    以 2.999.3 的编码为例,首先要将前两个数合并成 1079(即 40×2+999),得到 1079.3。

    完成合并后再用 Base 128 编码,左侧为高位字节。 也就是说,每个字节的最高位设为 1,但最后一个字节最高位设为 0,表示一个整数到此结束。各字节的其余七位从高到低依次相连表示数值。 例如数字 3 就用一个字节 0x03 表示, 而 129 则需要两个字节 0x81 0x01。 每个数字都如此转换成字节后,拼接在一起就形成了 OID 的编码。

    无论是在 BER 还是 DER 中,OID 都必须用最短的方式编码。 所以其中每个数字编码时开头都不能出现 0x80 字节。

    例如,OID 1.2.840.113549.1.1.11(代表 sha256WithRSAEncryption)的编码是:

    06 09 2a 86 48 86 f7 0d 01 01 0b
    

    NULL 的编码

    NULL 字段的长度永远为 0,所以它的编码只有标签和长度:

    05 00
    

    SEQUENCE 的编码

    首先需要注意的是,SEQUENCE 必然是复合字段,因为它的作用就是容纳其他对象。 换句话说,SEQUENCE 中的数据就是其元素各自编码后按定义中的顺序拼接在一起组成的。 因此,SEQUENCE 标签的第 6 位(单一/复合字段位)一定是 1。 虽然 SEQUENCE 本来的标签是 0x10,但在编码中一定会以 0x30 的形式出现。

    SEQUENCE 的定义中标为 OPTIONAL 的字段可以省略,省略后就会直接从编码中消失。 读取 SEQUENCE 的内容时,解码器可以根据标签和已读入的元素确定正在读取的是哪个字段。 如果存在歧义,比如有同类型的元素,则在 ASN.1 模块中必须借助编码指令为这些元素分配不同的标签。

    标有 DEFAULT 的字段与 OPTIONAL 类似。 如果该字段取默认值,在 BER 编码中就可以予以省略, 而在 DER 编码中则必须省略。

    例如,RFC 5280 中的 AlgorithmIdentifier 就是 SEQUENCE 类型:

       AlgorithmIdentifier  ::=  SEQUENCE  {
            algorithm               OBJECT IDENTIFIER,
            parameters              ANY DEFINED BY algorithm OPTIONAL  }
    

    algorithm 为 1.2.840.113549.1.1.11 的 AlgorithmIdentifier 编码如下。 RFC 8017 建议这种算法对应的 parameters 字段应为 NULL

    30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00
    

    SEQUENCE OF 的编码

    SEQUENCE OF 的编码方式与 SEQUENCE 完全相同, 甚至连标签都一样! 解码时也只有查阅 ASN.1 模块才能确定一个字段究竟是 SEQUENCE 还是 SEQUENCE OF。

    例如以下是一个 SEQUENCE OF 对象的编码,其中包含 7、8、9 三个 INTEGER。

    30 09 02 01 07 02 01 08 02 01 09
    

    SET 的编码

    与 SEQUENCE 一样,SET 也一定是复合字段,其数据总是由若干个字段的编码组成的。 它的标签是 0x11, 但因为单一/复合字段位(第 6 位)必须为 1,所以在编码中实际上是 0x31。

    SET 的编码方式也和 SEQUENCE 类似,OPTIONAL 和 DEFAULT 字段如果省略或取默认值就不会出现, 任何由类型相同导致的歧义都需要在 ASN.1 模块中解决,并且取默认值的 DEFAULT 字段在 DER 编码中必须省略。

    BER 编码对 SET 中元素的顺序没有要求, 但在 DER 中各元素必须按标签值升序排列。

    SET OF 的编码

    SET OF 的编码方式和 SET 一样,标签也是 0x31。 DER 编码同样规定 SET OF 中的字段要按升序排列。 因为 SET OF 中所有元素的类型都一样,仅靠标签排序是不够的。 所以 SET OF 中的元素以编码后的字节序为准,编码较短的就先在最后补齐零字节再作排序。

    BIT STRING 的编码

    一个 N 位的 BIT STRING 用 N/8 个字节(向上取整)来编码,开头还有一个字节的前缀,当 N 不是 8 的倍数时可以指明最后有几个二进制位是无用的。 例如,编码 18 个二进制位 011011100101110111 至少需要三个字节, 但三个字节其实足以存放 24 个二进制位, 有 6 位是用不上的。 这 6 位就放在末尾,于是最终编码为:

    03 04 06 6e 5d c0
    

    在 BER 中这些无用的二进制位可以取任意值,所以最后一个字节也可以是 c1、c2、c3 等等。 DER 则要求无用的位都必须为 0。

    OCTET STRING 的编码

    OCTET STRING 的编码就是其中包含的所有字节。 例如,包含四个字节 03 02 06 A0 的 OCTET STRING 的编码为:

    04 04 03 02 06 A0
    

    CHOICE 和 ANY 的编码

    CHOICE 和 ANY 的编码与其最终表示的实际类型一致,但也可以通过编码指令更改。 假如一份 ASN.1 相关规范要求某一个 CHOICE 字段必须是 INTEGER 或 UTCTime,而现在该字段恰好属于 INTEGER,那就按 INTEGER 编码即可。

    但实际应用中 CHOICE 往往都有编码指令。 比如在 RFC 5280 的下列定义中,rfc822Name 和 dNSName 的类型都是 IA5String,只有靠编码指令才能区分。

       GeneralName ::= CHOICE {
            otherName                       [0]     OtherName,
            rfc822Name                      [1]     IA5String,
            dNSName                         [2]     IA5String,
            x400Address                     [3]     ORAddress,
            directoryName                   [4]     Name,
            ediPartyName                    [5]     EDIPartyName,
            uniformResourceIdentifier       [6]     IA5String,
            iPAddress                       [7]     OCTET STRING,
            registeredID                    [8]     OBJECT IDENTIFIER }
    

    举例来说,如果一个 GeneralName 里包含的是 rfc822Name,值为 a@example.com,则编码如下(回忆一下,[1] 表示标签值为 1,类别为特定语境标签,即第 8 位为 1,并且标签编码方式为 IMPLICIT):

    81 0d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d
    

    如果 GeneralName 里包含的是 dNSName,值为“example.com”,编码则是:

    82 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d
    

    安全性

    解码 BER 和 DER 格式时必须格外小心,尤其是在 C、C++ 等非内存安全的编程语言中。 各种解码器的安全漏洞可谓罄竹难书, 而解析用户输入本身就是安全问题的一大来源。 ASN.1 编码与漏洞近乎如影随形, 毕竟这种格式颇为复杂,各种不定长度的字段不计其数, 连长度字段本身都没有固定长度! 另一方面,ASN.1 格式的数据又往往来自潜在的攻击者, 所以如果要靠数字证书验证用户身份,不能光考虑如何解码正确的证书,还得应对五花八门的恶意输入,以免 ASN.1 代码中存在漏洞而被攻陷。

    面对这些问题,最好的解决办法就是尽可能使用内存安全的编程语言, 并且无论能否使用这类语言,都应当借助现成的 ASN.1 编译器生成解析程序,而非闭门造车,自行编写解码器。

    致谢

    首先我要向 A Layman’s Guide to a Subset of ASN.1, DER, and BER 致以诚挚的敬意,本文中的大部分知识我都是从这份材料中学到的。 我还要感谢另一篇佳作 A warm welcome to DNS 的作者,其文风也奠定了本文的笔法基调。

    一点题外话

    你可曾注意到 PEM 编码的证书开头都是“MII”? 例如:

    -----BEGIN CERTIFICATE-----
    
    MIIFajCCBFKgAwIBAgISA6HJW9qjaoJoMn8iU8vTuiQ2MA0GCSqGSIb3DQEBCwUA
    ...
    

    现在你已经能解释原因了! 证书本身是一个 SEQUENCE 结构,所以第一个字节是 0x30。 接下来是长度字段, 绝大多数证书都不止 127 个字节,所以需要采用长编码, 也就是说第一个字节是 0x80 + N,意味着后面还有 N 个字节表示长度。 N 一般都是 2,因为大多数证书的长度都在 128 至 65535 个字节之间,只需要两个字节表示。

    于是我们可以得知 DER 格式的证书前两个字节是 0x30 0x82。 但 PEM 采用了 Base64 编码,将每 3 个二进制字节转换成 4 个 ASCII 字符。 换句话说,Base64 是将 24 个二进制位写成 4 个 ASCII 字符,每个字符表示 6 个二进制位。 我们已经知道证书的前 16 位是什么了, 要证明(几乎)所有证书开头都是“MII”,还需确定接下来的两个二进制位。 这两位正是长度字节的最高位, 它们会是 1 吗? 那这个证书就非得超过 16383 个字节不可! 由此我们可以断定 PEM 证书开头的几个字符都是相同的。 你不妨也试试看:

    xxd -r -p <<<308200 | base64
    

    Views: 12