zkat’s diary

技術ブログ

ACMEでサーバー証明書を発行するプロセスを理解する

この記事の概要

  • Automatic Certificate Management Environment(ACME)で証明書を発行するプロセスを理解する為のメモ書きです。
  • ACMEサーバーを実際に立ち上げ手を動かすことで理解を深めたいと思います。
  • この記事では、ACMEのアカウントを作成するところまで書きます。

ACMEについて

  • ACMEは、RFC 8555で定められた証明書発行に関する仕組みです。
  • 様々な認証局で、アドホックに行われていたサーバー証明書発行手順を標準化する目的で作られています。
  • Let's EncryptなどのサービスがACMEを利用しています。

RFC 8555 - Automatic Certificate Management Environment (ACME)

進め方

RFCの22ページ目(7.1 Resources)に、次の通りフローが記載されていましたので、これに沿って進めてみます。

The following table illustrates a typical sequence of requests required to establish a new account with the server, prove control of an identifier, issue a certificate, and fetch an updated certificate some time after issuance. The "->" is a mnemonic for a Location header field pointing to a created resource.

Action Request Response
Get Directory GET directory 200
Get nonce HEAD newNonce 200
Create account POST newAccount 201 -> account
Submit order POST newOrder 201 -> order
Fetch challenges POST-as-GET order's authorization urls 200
Respond to challenges POST authorization challenge urls 200
Poll for status POST-as-GET order 200
Finalize order POST order's finalize url 200
Poll for status POST-as-GET order 200
Download certificate POST-as-GET order's certificate url 200

準備

ACMEに対応した認証局システムを準備します。 Let's Encryptより、ACMEプロトコルのテストベッドとして、Pebbleというシステムが提供されているので、 それを動かしてみます。

github.com

$ go get -u github.com/letsencrypt/pebble/...
$ cd $GOPATH/src/github.com/letsencrypt/pebble && go install ./...

pebbleを起動しておきます。

$ pebble -config ./test/config/pebble-config.json
Pebble 2020/10/10 15:16:39 Starting Pebble ACME server
省略
Pebble 2020/10/10 15:16:39 Listening on: 0.0.0.0:14000
Pebble 2020/10/10 15:16:39 ACME directory available at: https://0.0.0.0:14000/dir

やってみる

Get Directory

まずは、Get Directoryを行うことで、ACMEで行う各種操作のためのURLを取得します。 DirectoryのURL自体は、事前にクライアントに設定しておく必要があります。 Pebbleであれば、起動時にURLが出力されていました。

$ curl --insecure https://localhost:14000/dir
{
   "keyChange": "https://0.0.0.0:14000/rollover-account-key",
   "newAccount": "https://0.0.0.0:14000/sign-me-up",
   "newNonce": "https://0.0.0.0:14000/nonce-plz",
   "newOrder": "https://0.0.0.0:14000/order-plz",
   "revokeCert": "https://0.0.0.0:14000/revoke-cert",
   "meta": {
      "externalAccountRequired": false,
      "termsOfService": "data:text/plain,Do%20what%20thou%20wilt"
   },
}

応答として以下の6つとmetaというフィールドが定義されています。

  • newNonce
  • newAccount
  • newOrder
  • newAuthz
  • revokeCert
  • keyChange

newAuthzについては、ACMEサーバー側がpre-authorizationという仕組みに対応しない場合は、省略されます。 上記応答を見ると、pebbleでは対応していないようです。

更に、metaフィールドの中身については、次のフィールドがあります。

  • termsOfService
  • website
  • caaIdentities
  • externalAccountRequired

上記4つのmetaフィールドの使用例は以下の通りです。

"meta": {
  "termsOfService": "https://example.com/acme/terms/2017-5-30",
  "website": "https://www.example.com/",
  "caaIdentities": ["example.com"],
  "externalAccountRequired": false
}

Get nonce

ACMEクライアントが、サーバーにPOSTでアクセスする際にはnonceを送信する必要があります。 既存のnonceが有効でなくなってしまったり、初めてサーバーにアクセスする際に、nonceを取得する必要があります。

先ほどのDirectoryのnewNonceのURLに対してHEADメソッドで要求すると、nonceを取得できます。 nonceはReplay-Nonceフィールドに記載されています。

$ curl --insecure --head https://localhost:14000/nonce-plz
HTTP/2 200
cache-control: public, max-age=0, no-cache
link: <https://localhost:14000/dir>;rel="index"
replay-nonce: wUoLKrGFi1i8rmbHn40Wyg
date: Sat, 10 Oct 2020 14:20:54 GMT

取得したnonceは、リクエストボディのJWSの中に記載します。 具体的には以下のような形で使用します。

POST /acme/new-account HTTP/1.1
Host: example.com
Content-Type: application/jose+json

{"protected": base64url(
  {"alg": "ES256",
   "jwk": {...},
   "nonce": "6S8IqOGY7eL2lsGoTZYifg",
   "url": "https://example.com/acme/new-account" }), 
 "payload": base64url(
  {"termsOfServiceAgreed": true,
   "contact": ["mailto:cert-admin@example.org",
               "mailto:admin@example.org"]}),
 "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" 
}

Create account

ディレクトリのnewAccountにて取得したURLに対して、以下のようにJWSメッセージをPOSTすることでアカウントを作成します。

$ cat createaccount_message | jq .
{
  "protected": "eyJhbGciOiJSUzI1NiIsIm5vbmNlIjoiWjBMeVpkUWtlNzlURndmdDRYMExJdyIsInVybCI6Imh0dHBzOi8vbG9jYWxob3N0OjE0MDAwL3NpZ24tbWUtdXAiLCJqd2siOnsibiI6IndRcFJQVDZPN1RNSkExWURtazdVLVhsSnZ6TElMSTl3U0ZKSmQ2Z1ZmTzBuOVRUU2VGbHlMYXBVMWZYeU4xU2xJM3dhWm9pTElSSnN3ME9OMUd3NkFLVDhvd1BVZ0lPWTFVLXZRTHF2OTVKOXJVcUlnRGdMQ3dsQmNiVzZJUV9Dckdlb2RuR3VnWFZKYXRoTmt4NmJIS2xOT2ZHUmpMTTJFOWZvbWhReDVIWkRvUTV2R3NtUUVsX01SOUVkd042SzNnVGx5YzBTb1FxU0dwOTJEZ19XODdhelg0eF9hU0pNUDFrLXJSU0V5anc2cVVyVUlKcGdZazdYY3BiS01ZTGR0Si1QdmtlM2x6c0pHdGV3YnRRSWhmZzczZF90WU1Kd3ZfMHlZQ3kySGRaekVkRU43RjZZQjczaFBjQVB1MUdhMjNjVzRZNUFHZFJfSlNRbDFEWTRuUSIsImUiOiJBUUFCIiwia3R5IjoiUlNBIn19",
  "signature": "VrYli0NVuAY4YeX8X97fBPHP4a6KQv0fLph4WB6e4J1tu-aCaZtrWpB2vICCjfjQH9rjnjTO5cdNjZAJSn_C-rYH2WIuIcfqH4k73aU6YPJnLHKB8CBTOLUWGtS3bSlOCuA7sKT294lpcc7VOE30yRbit65OxaPW7m45junVtOQ4SdZveP-11Kiw0QPlhFXgcjT4y4n23g4AOw3JBtjPirUYUnAiF3L15wqMWSIr8tUrbj4rNCO37esxvOKQO8gXpI6udSGZ2D9u1jQhSEocFP0tEGG4YMG5b_D_H56YPWDeJxpZx-zqU7wwnV2wYv5R3aiRvV4K6m_ewrdRr3OHAg",
  "payload": "eyJ0ZXJtc09mU2VydmljZUFncmVlZCI6dHJ1ZSwiY29udGFjdCI6WyJtYWlsdG86Y2VydC1hZG1pbjFAZXhhbXBsZS5vcmciXX0"
}

$ curl -v --insecure -H'Content-Type: application/jose+json' -X POST --data-binary @createaccount_message https://localhost:14000/sign-me-up

< HTTP/2 201
< cache-control: public, max-age=0, no-cache
< content-type: application/json; charset=utf-8
< link: <https://localhost:14000/dir>;rel="index"
< location: https://localhost:14000/my-account/1
< replay-nonce: 9IQ52Zb5rNG7J0AwAoV-pA
< content-length: 552
< date: Sun, 11 Oct 2020 17:19:26 GMT
<
{
  "status": "valid",
  "contact": [
    "mailto:cert-admin1@example.org"
  ],
  "orders": "https://localhost:14000/list-orderz/1",
  "key": {
    "kty": "RSA",
    "n": "wQpRPT6O7TMJA1YDmk7U-XlJvzLILI9wSFJJd6gVfO0n9TTSeFlyLapU1fXyN1SlI3waZoiLIRJsw0ON1Gw6AKT8owPUgIOY1U-vQLqv95J9rUqIgDgLCwlBcbW6IQ_CrGeodnGugXVJathNkx6bHKlNOfGRjLM2E9fomhQx5HZDoQ5vGsmQEl_MR9EdwN6K3gTlyc0SoQqSGp92Dg_W87azX4x_aSJMP1k-rRSEyjw6qUrUIJpgYk7XcpbKMYLdtJ-Pvke3lzsJGtewbtQIhfg73d_tYMJwv_0yYCy2HdZzEdEN7F6YB73hPcAPu1Ga23cW4Y5AGdR_JSQl1DY4nQ",      "e": "AQAB"
  }
}

上記JWSのメッセージは、Pythonのauthlibを利用して作成しました。

ちなみにACMEクライアントのJWSメッセージの全てに共通ですが、以下のフィールドは必須となります。

  • alg
  • nonce
  • url

更に、jwkまたは、kidのどちらかが必要です。newAccountの場合は、jwkが必要になります。