DIGITAL-SIGN.COM.CN
Home 文档 技术分享 >  你是如何从Let's Encrypt 申请到证书的

你是如何从Let's Encrypt 申请到证书的

本文章不做简单翻译 ACME 协议的搬运工,而是从客户端(acme.sh)与ACME-SERVER直接接口通讯来解析 Let's Encrypt 颁发证书的流程。希望对大家申请 let's encrypt 过程中遇到的问题有所帮助,同时也希望能帮助 PKI 厂商了解 ACME 的流程,以搭建 ACME 服务。

图片来自 NYU (New York University),侵删

第一步: /directory

一般的,ACME 客户端请求 /directory 接口是GET

GET /directory HTTP/1.1
Host: acme-v02.api.letsencrypt.org
User-Agent: acme.sh/2.8.2 (https://github.com/Neilpang/acme.sh)
Accept: */*

拿到的响应如

HTTP/1.1 200 OK
Server: nginx
Content-Type: application/json
Content-Length: 658
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800
Expires: Wed, 24 Jul 2019 10:25:36 GMT
Cache-Control: max-age=0, no-cache, no-store
Pragma: no-cache
Date: Wed, 24 Jul 2019 10:25:36 GMT
Connection: keep-alive

{
  "4bmRla_FAhE": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417",
  "keyChange": "https://acme-v02.api.letsencrypt.org/acme/key-change",
  "meta": {
    "caaIdentities": [
      "letsencrypt.org"
    ],
    "termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf",
    "website": "https://letsencrypt.org"
  },
  "newAccount": "https://acme-v02.api.letsencrypt.org/acme/new-acct",
  "newNonce": "https://acme-v02.api.letsencrypt.org/acme/new-nonce",
  "newOrder": "https://acme-v02.api.letsencrypt.org/acme/new-order",
  "revokeCert": "https://acme-v02.api.letsencrypt.org/acme/revoke-cert"
}

其中

  • newAccount 字段是用于上传 account key 时候调用,
  • newNonce 是业务流程(如申请、撤销) 时候需要预请求获取一次性令牌的一个接口,
  • newOrder是用于下单的接口,revokeCert是用于撤销证书的接口,
  • keyChange是用于更换 KeyPair 的接口。
  • 4bmRla_FAhE 是特殊的随机的字段,可能随意的出现在 json 的头、中、尾部,作用是为了避免开发者集成客户端时候[1],采用 定位法取 json 的数据,导致扩展性变差而引入的属性。

第二步 /acme/new-acct

我们在创建账户时 (如果你是安装完 acme 客户端甩起就走 issue, 会自动平滑的 new account)需要执行如下命令

➜  ~ acme.sh --register-account  --accountemail [email protected]
[2019年 7月24日 星期三 19时28分01秒 CST] Create account key ok.
[2019年 7月24日 星期三 19时28分02秒 CST] Registering account
[2019年 7月24日 星期三 19时28分03秒 CST] Registered
[2019年 7月24日 星期三 19时28分03秒 CST] ACCOUNT_THUMBPRINT='8iMyv8SFPrIub4UN93cLw7910jkmFKo7mPwVNNB0EoY'

通过网络抓包, 我们可以观察到, 除了 /directory 接口外, 其依次调用了如下接口

  • /acme/new-nonce
  • /acme/new-acct

请求 /acme/new-nonce 如下

HEAD /acme/new-nonce HTTP/1.1
Host: acme-v02.api.letsencrypt.org
User-Agent: acme.sh/2.8.2 (https://github.com/Neilpang/acme.sh)
Accept: */*
Content-Type: application/jose+json
Content-Length: 0

需要特别注意, 此请求是 HEAD, 而不是 GET

响应如下

HTTP/1.1 200 OK
Server: nginx
Link: <https://acme-v02.api.letsencrypt.org/directory>;rel="index"
Replay-Nonce: t-giLO9EALhOZ3iB3H-SRSEwxe_LA1k7rZ7aJkHgmnY
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800
Content-Length: 0
Expires: Wed, 24 Jul 2019 11:31:38 GMT
Cache-Control: max-age=0, no-cache, no-store
Pragma: no-cache
Date: Wed, 24 Jul 2019 11:31:38 GMT
Connection: Keep-alive

其中 Replay-Nonce 必须提供, Content-Length 必须为 0, 否则客户端会异常.

请求 /acme/new-acct 如下

POST /acme/new-acct HTTP/1.1
Host: acme-v02.api.letsencrypt.org
User-Agent: acme.sh/2.8.2 (https://github.com/Neilpang/acme.sh)
Accept: */*
Content-Type: application/jose+json
Content-Length: 1181
Expect: 100-continue

{
    "protected": "eyJub25jZSI6ICJ0LWdpTE85RUFMaE9aM2lCM0gtU1JTRXd4ZV9MQTFrN3JaN2FKa0hnbW5ZIiwgInVybCI6ICJodHRwczovL2FjbWUtdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9uZXctYWNjdCIsICJhbGciOiAiUlMyNTYiLCAiandrIjogeyJlIjogIkFRQUIiLCAia3R5IjogIlJTQSIsICJuIjogInV4b2diWk1tM2hYcTVjM3RnSWtxdHJLeWs2OVpXS09sZm1iSG1TRXJkNE5ITmFTeXdmS2V5MmwzLXRBUHl3TF9pSTZJSEVXNFZpWVQ3aGE0MWg2cTZmR3h3dVJjMnY5QUdlZ1YyNFJNd1pYbFp3aWhMQm4zaDUwN1dYUVlJQTVhdVNKUzZOV2tGMEVSc3gyXzA5NU1KQ3JzSDVIdnFiQ3lvY3Jxd2g5N21IeTJ6Qk40SU9Qbi1CRWdiMnhmOF9Bc2xnelFjVlo1VWp6WnhRLXA1bHl6d09wZkRQWFk4LWJzcUZmazktTXUxa3pEdENUYXM2SjM1ZW0yR2hhSFZOODVpX3RBMkxOZmhiejFiTlRvcHQyd1pDRlJ2OWVNaGx4QndfUmhtQ0FrcG1jVm5MLWE2cENuQ1dEU0w4YVd3Mm1HbjVzdVhFNjA3RFdweVhIdm1XSng1USJ9fQ",
    "payload": "eyJjb250YWN0IjogWyJtYWlsdG86IGRvdHNhZmVAemhpaHUuY29tIl0sICJ0ZXJtc09mU2VydmljZUFncmVlZCI6IHRydWV9",
    "signature": "L6aMUAsokgo3BHlxJwvgOAljQZ_bFwUbT2Gc2e3BW1DFXtzmH2qaUg_o-gCyzIqJmc-VAzLG_ipPN2dZRgUl1ARTHHRuniSgItLbSluxoXf138DlW6CJPI-Ma5jYSKoANRnQldAJg_6R07fTfgkzGDp3H2ldZQ-I7QsWmEzp9eRgmpcjIocZJc_AcfbvkK_EPOCn2YSDgDbMUzhwztU3pz5NgQ5tlLO2S_FSQlLlNzv6j4fEU14rR3RQKvl3HmZUAniqXGvsiiiiaYimk815koNei56ZeKTdW0T_lLL1JY3GZbHmK_edzBfKDcaItBdPnnL8FW-bbMB53bMKydpDWw"
}

此处 protected 字段 base64url 还原后即为

{
    "nonce": "t-giLO9EALhOZ3iB3H-SRSEwxe_LA1k7rZ7aJkHgmnY",
    "url": "https://acme-v02.api.letsencrypt.org/acme/new-acct",
    "alg": "RS256",
    "jwk": {
        "e": "AQAB",
        "kty": "RSA",
        "n": "uxogbZMm3hXq5c3tgIkqtrKyk69ZWKOlfmbHmSErd4NHNaSywfKey2l3-tAPywL_iI6IHEW4ViYT7ha41h6q6fGxwuRc2v9AGegV24RMwZXlZwihLBn3h507WXQYIA5auSJS6NWkF0ERsx2_095MJCrsH5HvqbCyocrqwh97mHy2zBN4IOPn-BEgb2xf8_AslgzQcVZ5UjzZxQ-p5lyzwOpfDPXY8-bsqFfk9-Mu1kzDtCTas6J35em2GhaHVN85i_tA2LNfhbz1bNTopt2wZCFRv9eMhlxBw_RhmCAkpmcVnL-a6pCnCWDSL8aWw2mGn5suXE607DWpyXHvmWJx5Q"
    }
}

其中

  • nonce 为上面 /acme/new-nonce所返回的值.
  • url 为本接口地址
  • alg 为签名算法
  • jwk 为账户公钥数据
    • kty[2] 是 key type的意思
    • e 和 n、kty 可以确定一份公钥[3]

在 POST 参数中的 payload 还原后可得

{
    "contact": [
        "mailto: [email protected]"
    ],
    "termsOfServiceAgreed": true
}

由此, protected, payload 和 signature 一起请求给 /acme/new-acct, 服务器存储好公钥与邮箱后, 即返回

HTTP/1.1 200 OK
Server: nginx
Content-Type: application/json
Content-Length: 569
Link: <https://acme-v02.api.letsencrypt.org/directory>;rel="index"
Location: https://acme-v02.api.letsencrypt.org/acme/acct/61907904
Replay-Nonce: gpfqtBSTKs8vrZGuajjItmkzHjJokyuAMNOA2E8LpYs
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800
Expires: Wed, 24 Jul 2019 11:31:41 GMT
Cache-Control: max-age=0, no-cache, no-store
Pragma: no-cache
Date: Wed, 24 Jul 2019 11:31:41 GMT
Connection: Keep-alive

{
  "id": 61907904,
  "key": {
    "kty": "RSA",
    "n": "uxogbZMm3hXq5c3tgIkqtrKyk69ZWKOlfmbHmSErd4NHNaSywfKey2l3-tAPywL_iI6IHEW4ViYT7ha41h6q6fGxwuRc2v9AGegV24RMwZXlZwihLBn3h507WXQYIA5auSJS6NWkF0ERsx2_095MJCrsH5HvqbCyocrqwh97mHy2zBN4IOPn-BEgb2xf8_AslgzQcVZ5UjzZxQ-p5lyzwOpfDPXY8-bsqFfk9-Mu1kzDtCTas6J35em2GhaHVN85i_tA2LNfhbz1bNTopt2wZCFRv9eMhlxBw_RhmCAkpmcVnL-a6pCnCWDSL8aWw2mGn5suXE607DWpyXHvmWJx5Q",
    "e": "AQAB"
  },
  "contact": [
    "mailto: [email protected]"
  ],
  "initialIp": "00e::3::a11::89::4d",
  "createdAt": "2019-07-24T11:27:45Z",
  "status": "valid"
}

至此, 账户创建即完成.

  • [TODO: 待完善账户重复创建的流程]

第三步 /acme/new-order

终于到了下单的流程了.

➜  ~ acme.sh --issue -d www.zhihu.com --nginx
[2019年 7月25日 星期四 00时01分27秒 CST] Single domain='www.zhihu.com'
[2019年 7月25日 星期四 00时01分27秒 CST] Getting domain auth token for each domain
[2019年 7月25日 星期四 00时01分31秒 CST] Getting webroot for domain='www.zhihu.com'
[2019年 7月25日 星期四 00时01分31秒 CST] Verifying: www.zhihu.com
[2019年 7月25日 星期四 00时01分31秒 CST] Nginx mode for domain:www.zhihu.com

除了/directory 外, 请求的 API 为

  • /acme/new-nonce
  • /acme/new-order
  • /acme/authz/*
  • /acme/challenge/*/*

跟 /acme/new-acct 一样的, 申请证书的 /acme/new-order 请求之前也需要获取一次性令牌 (如果 new-acct 和 new-order 是一起的,只需要获取一次). 此处就不在介绍 new-nonce 了.

请求 /acme/new-order的参数为

POST /acme/new-order HTTP/1.1
Host: acme-v02.api.letsencrypt.org
User-Agent: acme.sh/2.8.2 (https://github.com/Neilpang/acme.sh)
Accept: */*
Content-Type: application/jose+json
Content-Length: 734

{
    "protected": "eyJub25jZSI6ICJYMXAydTR0akVyb29yenNDLVNZZFZrN1hvMFZIaktnSUI5OGVxOGlzLUJzIiwgInVybCI6ICJodHRwczovL2FjbWUtdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9uZXctb3JkZXIiLCAiYWxnIjogIlJTMjU2IiwgImtpZCI6ICJodHRwczovL2FjbWUtdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hY2N0LzYxOTI0NzQ0In0",
    "payload": "eyJpZGVudGlmaWVycyI6IFt7InR5cGUiOiJkbnMiLCJ2YWx1ZSI6Ind3dy56aGlodS5jb20ifV19",
    "signature": "bzW7CyT2ln1Ms17vfc8kgDgy9yNlwFWFshEUVrFFBUJVWVROvlBjG6CFDXldCGkPvTJCJ1mRvXoMsk3uQ5FhTNaMpBsv7QVL9N03LuLZax1RE_2eyjj5ATMGq8wzXqgAjzC7ueJfcHYuFbuR33NsDXMWZmpEqKi4cf8h-GBDWnuKKUUN0-N-2aQo_JEbRrk5dMbZ1ACJ2Evy46y7-0jCmrHLgnGK7nPK6fmp__6WfNOK_PzjFj3t65NXqio68w2IqXy4CONqx0miqLdgyZfk-HGCN9c4ACNGEhj_mJ9pUEiABaQJagcOqkIADEESg6tIaw9pj0HjqxxlpHTzgrGwxQ"
}

还原后:

{
    "protected": {
        "nonce": "X1p2u4tjEroorzsC-SYdVk7Xo0VHjKgIB98eq8is-Bs",
        "url": "https://acme-v02.api.letsencrypt.org/acme/new-order",
        "alg": "RS256",
        "kid": "https://acme-v02.api.letsencrypt.org/acme/acct/61924744"
    },
    "payload": {
        "identifiers": [
            {
                "type": "dns",
                "value": "www.zhihu.com"
            }
        ]
    },
    "signature": "bzW7CyT2ln1Ms17vfc8kgDgy9yNlwFWFshEUVrFFBUJVWVROvlBjG6CFDXldCGkPvTJCJ1mRvXoMsk3uQ5FhTNaMpBsv7QVL9N03LuLZax1RE_2eyjj5ATMGq8wzXqgAjzC7ueJfcHYuFbuR33NsDXMWZmpEqKi4cf8h-GBDWnuKKUUN0-N-2aQo_JEbRrk5dMbZ1ACJ2Evy46y7-0jCmrHLgnGK7nPK6fmp__6WfNOK_PzjFj3t65NXqio68w2IqXy4CONqx0miqLdgyZfk-HGCN9c4ACNGEhj_mJ9pUEiABaQJagcOqkIADEESg6tIaw9pj0HjqxxlpHTzgrGwxQ"
}

protected携带的是公钥, signature是用私钥将公钥、payload 取摘要得到的字串。 payload下面一个属性identifiers,其为数组,每一个包含的对象,都有两个属性typevaluetype 一般为dns,若value域名类型为IP时,此type可为ipv4。

/acme/new-order的响应为:

HTTP/1.1 201 Created
Server: nginx
Content-Type: application/json
Content-Length: 372
Boulder-Requester: 61924744
Link: <https://acme-v02.api.letsencrypt.org/directory>;rel="index"
Location: https://acme-v02.api.letsencrypt.org/acme/order/61924744/774083503
Replay-Nonce: DnNRGUwwSO03JDbusnXcv3MaHHD8MB3cUMKLwqMOIaU
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800
Expires: Wed, 24 Jul 2019 16:01:29 GMT
Cache-Control: max-age=0, no-cache, no-store
Pragma: no-cache
Date: Wed, 24 Jul 2019 16:01:29 GMT
Connection: Keep-alive

{
    "status": "pending",
    "expires": "2019-07-31T16:01:29.887180422Z",
    "identifiers": [
        {
            "type": "dns",
            "value": "www.zhihu.com"
        }
    ],
    "authorizations": [
        "https://acme-v02.api.letsencrypt.org/acme/authz/iJTvo1TYkd16sOJgLZQhCtDx7V3siojiH0tdFVzZPtI"
    ],
    "finalize": "https://acme-v02.api.letsencrypt.org/acme/finalize/61924744/774083503"
}
  • http 状态码必须为201,且需要返回 location 头,为订单详情资源
  • status[4]目前为pending ,表示验证中。可能出现的值分别为 pending/ready/processing/valid/invalid
  • expires 表示订单失效时间。有服务商或CA决定
  • identifiers同上为数组,每一个包含的对象,都有两个属性type和value
  • authorizations表示此订单需要依次完成的授权验证资源(Auth-Z)的链接,authorizations不允许为空数组 (必须至少有一个流程)
  • finalize授权验证完成后,调用finalize接口签发证书(包括CSR也是在这一步提交的)

请求/acme/authz/*

POST /acme/authz/iJTvo1TYkd16sOJgLZQhCtDx7V3siojiH0tdFVzZPtI HTTP/1.1
Host: acme-v02.api.letsencrypt.org
User-Agent: acme.sh/2.8.2 (https: //github.com/Neilpang/acme.sh)
Accept: */*
Content-Type: application/jose+json
Content-Length: 711

{
    "protected": "eyJub25jZSI6ICJEbk5SR1V3d1NPMDNKRGJ1c25YY3YzTWFISEQ4TUIzY1VNS0x3cU1PSWFVIiwgInVybCI6ICJodHRwczovL2FjbWUtdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9hdXRoei9pSlR2bzFUWWtkMTZzT0pnTFpRaEN0RHg3VjNzaW9qaUgwdGRGVnpaUHRJIiwgImFsZyI6ICJSUzI1NiIsICJraWQiOiAiaHR0cHM6Ly9hY21lLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2FjbWUvYWNjdC82MTkyNDc0NCJ9",
    "payload": "",
    "signature": "ecIpPr-WmaRcfui_SmDFD9-coaaZkP0zbZltE94wkHfI8myoVz1ojgrDb0vJB3iBc16-s1f099Ags3T1Ms2Pzu7_eLBY6T7V4zjoB7hDGvQrFqkVdHbUYHzA64Bv_ULjt-PXKLvAucBIZx5Vzkd9pFeEiF9jTnrAOtUyN1x0DkKFOyS-hxPD43C9l30U7xDfwbU9bHtRyKgoS79hP3GdQaVTFHkRNi1K8FHRC9kaqmI4pJ49OwW8pVoNPNnTjyag6TY121Mp1W2G-Sm-UYP1rjNv7S9OhfP0SzBUy5C8RKAj156XdUFWGOwxq1qH2O5JG7KfzpWwdTDfPTIyjo21vQ"
}
  • 此接口请求没传递 payload ,遂对请求参数不做展开

响应数据为

HTTP/1.1 200 OK
Server: nginx
Content-Type: application/json
Content-Length: 908
Boulder-Requester: 61924744
Link: <https://acme-v02.api.letsencrypt.org/directory>;rel="index"
Replay-Nonce: jqTnB7tv_0v-HJZ7ZrdT4T8nkUqpoh7hU23VT2BvWuk
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800
Expires: Wed, 24 Jul 2019 16:01:30 GMT
Cache-Control: max-age=0, no-cache, no-store
Pragma: no-cache
Date: Wed, 24 Jul 2019 16:01:30 GMT
Connection: Keep-alive

{
  "identifier": {
    "type": "dns",
    "value": "www.zhihu.com"
  },
  "status": "pending",
  "expires": "2019-07-31T16:01:29Z",
  "challenges": [
    {
      "type": "http-01",
      "status": "pending",
      "url": "https://acme-v02.api.letsencrypt.org/acme/challenge/iJTvo1TYkd16sOJgLZQhCtDx7V3siojiH0tdFVzZPtI/18676978378",
      "token": "NUO_qleY0NGwfTmAX580Tt7oCLpglUh34nmq-6YkU-A"
    },
    {
      "type": "dns-01",
      "status": "pending",
      "url": "https://acme-v02.api.letsencrypt.org/acme/challenge/iJTvo1TYkd16sOJgLZQhCtDx7V3siojiH0tdFVzZPtI/18676978379",
      "token": "m4heiWkRAKPZ2r9_n5kLkS_g6flckRTiVYDqPjQtFxk"
    },
    {
      "type": "tls-alpn-01",
      "status": "pending",
      "url": "https://acme-v02.api.letsencrypt.org/acme/challenge/iJTvo1TYkd16sOJgLZQhCtDx7V3siojiH0tdFVzZPtI/18676978381",
      "token": "-VZddxtXSpiDgwCoWbAF8FfnSVnn9a_b_XwfXxvnUT8"
    }
  ]
}
  • identifier不再赘述,其为identifiers 中的一项
  • status表示的是该 identifier的验证通过与否状态
  • expires 和 order 的失效时间原则上应保持一致
  • challenges为重要字段,其为数组,返回的数目几乎都包含 type =http-01type=dns-01type=tls-alpn-01三条(若申请的域名符号为IP,则没有dns-01),
    • challenge下的type表示验证方式,目前包含http-01dns-01tls-alpn-01 。
    • status表示此种验证方式的状态
    • url为这条challenge手动呼唤 CA 供应商发起域名控制器校验的请求地址
    • token为执行此验证规则的凭据(你申请某域名,跟别人申请同一域名的证书所需要填写/上传的凭据是不同的)

域名所有权验证

到这时,ACME客户端会上传凭据

  • 若选择--webroot选项 (不同acme客户端可能参数不一样,这里是以acme.sh举例),其会在 .well-known/pki-validation/路径下创建一个文件,名为token,内容为token
  • 若选择--dns {dns_provider}选项,其会在 dns_provider调用API,将控制器在你名下的具体域名添加一项 _acme-challenge.你的域名(若通配符域名,则为_acme-challenge.一级域名) ,类型txt,值为token

部署domain auth后,ACME客户端会自动/手动发起Authz Check的操作。请求URL为前面接口返回的challenges中具体类型对应的url字段

按照规定,www.zhihu.com 的ACME申请证书,在此例中,

HTTP Challenge的成功必须满足:

  • http://www.zhihu.com/.well-known/pki-validation/NUO_qleY0NGwfTmAX580Tt7oCLpglUh34nmq-6YkU-A URL可被请求到(返回200)
  • 此 URL 请求返回的内容必须为 NUO_qleY0NGwfTmAX580Tt7oCLpglUh34nmq-6YkU-A.${thumbprint}
  • HTTPS Challenge (即tls-alpn-01)与之类似。

DNS Challenge的成功必须满足:

  • nslookup 查询 _acme-challenge.www.zhihu.com 的类型为 txt 的值的集合(TXT记录可以添加多条)必须有一条等于 -VZddxtXSpiDgwCoWbAF8FfnSVnn9a_b_XwfXxvnUT8

由此可以看出,let's encrypt与传统CA在验证域名所有权上并无二致,只是做了一些具体化(以便一套ACME客户端可以解决所有CA的domain auth)的规定。

在客户端触发challenge请求后,服务器会去执行上面的验证过程。如果不通过,则认为不能确定这台服务器控制这个域名,就无法颁发证书。

  • 这也是为啥在个人PC上以http-01方式运行acme毫无意义的原因。因为基本上公网域名没有穿透到本机,无法验证域名。

部署域名验证文件成功后, 请求提交验证请求

请求的 URL 位于 challenges 下具体的 url

验证通过, Finalize 提交 CSR

Post acme-v02.api.letsencrypt.org

【未完待续】

参考

  1. ^Adding random entries to the directory https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417
  2. ^kty https://tools.ietf.org/html/rfc7517#page-6
  3. ^e、n、kty https://tools.ietf.org/html/rfc7517#page-25
  4. ^order的status描述 https://tools.ietf.org/html/rfc8555#section-7.1.3