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が必要になります。

クライアント証明書による認証があるウェブサイトでCSRF(Cross Site Request Forgery)は発生するか確認する

結論

発生します。

理由

CSRFは、セッション管理の不備を悪用した攻撃だからです。 認証方式として、クライアント証明書を使っていても、その不備を補うことはできません。

CSRF一般の話になりますが、攻撃者は対象のウェブサイトにアクセス権限(クライアント証明書)を持っている必要は ない(正当な権限をもつユーザーにリクエストを強要しているだけ)ためです。1

実際にやってみる

準備1: クライアント証明書とサーバー証明書を発行する

サーバー証明書とクライアント証明書がそれぞれ必要になりますので、 プライベート認証局を作り、そこから発行します。

まず、認証局(ルート)を作成します。
ここで作成した、DevelopmentPurposePrivateRootCA1.crtをブラウザのトラストストアにインストールしておきます。

$ openssl genrsa -des3 -out DevelopmentPurposePrivateRootCA1.key 2048
$ openssl req -x509 -new -nodes \
-key DevelopmentPurposePrivateRootCA1.key \
-sha256 -days 90 -out DevelopmentPurposePrivateRootCA1.crt \
-subj "/C=JP/ST=Tokyo/L=Chiyoda/O=Dummy Company/OU=Dummy Division/CN=DevelopmentPurposePrivateRootCA1"

つづいて、サーバー証明書を先ほどの認証局から発行します。
ここで発行したサーバー証明書(example.jp.crt)と秘密鍵(private-server-rsa-key)は、次に説明するhttpsサーバー(Nginx)の設定の時に使います。
発行先のドメイン(common name)には、example.jpを指定することとします。

# CSRを作成する
$ openssl genrsa 2048 > ./private-server-rsa-key
$ openssl req -new -key ./private-server-rsa-key -sha256 -out ./server-csr
# CSRをもとに、サーバー証明書を発行する
$ cat << __EOF__ > ./signing_config
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = example.jp
__EOF__
$ openssl x509 -req -in ./server-csr -CA DevelopmentPurposePrivateRootCA1.crt \
-CAkey DevelopmentPurposePrivateRootCA1.key \
-CAcreateserial \
-out example.jp.crt -days 90 -sha256 -extfile ./signing_config

同じように、クライアント証明書を発行します。 発行したクライアント証明書(client.crt.p12)をブラウザにimportしておきます。

# クライアント証明書用のCSRをつくる
$ openssl genrsa 2048 > ./private-client-rsa-key
$ openssl req -new -key ./private-client-rsa-key -sha256 -out ./client-csr
# クライアント証明書を発行する
$ cat << __EOF__ > ./signing_config
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = clientAuth
__EOF__
$  openssl x509 -req -in ./client-csr \
-CA DevelopmentPurposePrivateRootCA1.crt \
-CAkey DevelopmentPurposePrivateRootCA1.key \
-CAcreateserial \
-out client.crt -days 90 -sha256 -extfile ./signing_config
$ openssl pkcs12 -export -out client.crt.p12 -inkey ./private-client-rsa-key -in client.crt

証明書の準備、めんどくさいですね。

準備2: Nginxを立ち上げる

以下のような設定にてNginxでhttpsのサーバーを立ち上げます。 裏側のアプリ(CSRFあり)をPythonのFlaskで作ろうとしているので、uWSGIの設定を入れています。 example.jpとして立ち上げたので、アクセス元のブラウザがexample.jpを名前解決できるように、hostsファイルなどに設定を追加しておく必要がありますが、 割愛します。

server {
        server_name example.jp;
        listen 443;

        ssl on;
        ssl_protocols TLSv1.1 TLSv1.2;
        ssl_ciphers HIGH:!aNULL:!MD5;

        ssl_certificate /path/to/example.jp.crt;
        ssl_certificate_key /path/to/private-server-rsa-key;

        ssl_verify_client on;
        ssl_client_certificate /path/to/DevelopmentPurposeRootCA1.crt;

        # Add index.php to the list if you are using PHP
        index index.html index.htm index.nginx-debian.html;

        location / {
                include uwsgi_params;
                uwsgi_pass unix:/tmp/csrfapp.sock;

        }

}

準備3: CSRF脆弱性があるアプリを立ち上げる

なんでもよいですが、CSRF脆弱性があるアプリを立ち上げます。 このアプリは、buy_confirmからbuy_completeに遷移するときに、画面遷移の正当性を確認していないので、
CSRF脆弱性があります。

CSRFを起こす

まず、対象のウェブサイトにログインしておきます。先ほど発行したクライアント証明書を提示します。

f:id:zkat:20200609015401j:plain f:id:zkat:20200609015452j:plain

つづいて、ログイン状態を保持したまま、悪意のあるウェブサイトのボタンをクリックしてみます。 悪意のあるウェブサイトは例えば以下のようになっています。

<html>
当選おめでとうございます!
以下をクリックして、賞金を獲得!
<form action="https://example.jp/buy_complete" method="POST">
    <input type="hidden" name="amount" value=100>
    <input type="submit" value="アクセス">
</form>
</html>

f:id:zkat:20200609015818j:plain

これにアクセスすると、CSRFが発生し、ユーザーの意図しない操作が実行されてしまいます。

f:id:zkat:20200609020016j:plain

防ぎ方

防ぎ方は、クライアント証明書があるか無いかなどは関係なく、様々なウェブサイトで説明されている通りです。 操作を完了する直前の画面で、攻撃者に知られることのない情報を埋め込み、画面遷移した際にその値が保持されているかチェックすることで、防ぐことができます。


  1. ただ、一般的には攻撃者がCSRFを準備する為のフェーズがあるかと思いますが、アクセス権限がないとその作業が容易にできないと思われる為、攻撃者がアクセス権限を持っている状況でないと発生しずらいとは思います。

異なるTLD(Top Level Domain)間でKSK(Key Signing Key)を共用している

概要

DNSには、応答の完全性を保つために使われるDNSSECという技術があります。 DNSSECで用いられる鍵の一種であるKSK(鍵署名鍵)について、複数のTLDトップレベルドメイン)間での共用があることを見つけたのでメモします。

調べ方

まず、トップレベルドメインの一覧を以下から取得します。

www.icann.org

上記一覧をもとに、各TLDに対して、DNSKEYレコードをdigで問い合わせます。これで、KSKの公開鍵を得ることができます。 (KSKの情報だけが取れるように、適当にgrepしています。)

$ dig +rec @適当なフルリゾルバ com -t DNSKEY | grep DNSKEY | grep -v '^;' | grep 257
com.                    1461    IN      DNSKEY  257 3 8 AQPDzldNmMvZFX4NcNJ0uEnKDg7tmv/F3MyQR0lpBmVcNcsIszxNFxsB fKNW9JYCYqpik8366LE7VbIcNRzfp2h9OO8HRl+H+E08zauK8k7evWEm u/6od+2boggPoiEfGNyvNPaSI7FOIroDsnw/taggzHRX1Z7SOiOiPWPN IwSUyWOZ79VmcQ1GLkC6NlYvG3HwYmynQv6oFwGv/KELSw7ZSdrbTQ0H XvZbqMUI7BaMskmvgm1G7oKZ1YiF7O9ioVNc0+7ASbqmZN7Z98EGU/Qh 2K/BgUe8Hs0XVcdPKrtyYnoQHd2ynKPcMMlTEih2/2HDHjRPJ2aywIpK Nnv4oPo/
$ 

DNSKEYレコードの末尾に書かれているデータが、base64エンコードされたKSKの公開鍵です。 全TLDに対してこれを行い同じKSKの公開鍵が書かれていないか確認します。

結果

以下の表の結果となりました。 グループの番号が同一のTLDについては、同一のKSKを使っていました。

グループ A-label U-label TLD MANAGER Technical Contact
1 XIHUAN. XIHUAN. QIHOO 360 TECHNOLOGY CO. LTD. Beijing Tele-info Network Technology Co., Ltd.
1 YUN. YUN. QIHOO 360 TECHNOLOGY CO. LTD. Beijing Tele-info Network Technology Co., Ltd.
1 SHOUJI. SHOUJI. QIHOO 360 TECHNOLOGY CO. LTD. Beijing Tele-info Network Technology Co., Ltd.
1 ANQUAN. ANQUAN. QIHOO 360 TECHNOLOGY CO. LTD. Beijing Tele-info Network Technology Co., Ltd.
1 XN--3DS443G. .在线 TLD REGISTRY LIMITED Beijing Tele-info Network Technology Co., Ltd
1 XN--FIQ228C5HS. .中文网 TLD REGISTRY LIMITED Beijing Tele-info Network Technology Co., Ltd.
1 XN--RHQV96G. .世界 Stable Tone Limited Building 21, 1 Gaolizhang Road, Haidian District
1 XN--NYQY26A. .健康 Stable Tone Limited Building 21, 1 Gaolizhang Road, Haidian District
2 RIO. RIO. Empresa Municipal de Informatica SA - IPLANRIO NIC.br Registry Services - Tech Contact
2 UOL. UOL. UBN INTERNET LTDA. NIC.br Registry Services - Tech Contact
2 GLOBO. GLOBO. Globo Comunicacao e Participacoes S.A NIC.br Registry Services - Tech Contact
2 BOM. BOM. Nucleo de Informacao e Coordenacao do Ponto BR - NIC.br Frederico Augusto de Carvalho Neves
2 FINAL. FINAL. Nucleo de Informacao e Coordenacao do Ponto BR - NIC.br Frederico Augusto de Carvalho Neves
3 XN--XHQ521B. .广东 Guangzhou YU Wei Information Technology Co., Ltd. Guangzhou YU Wei Information Technology Co., Ltd.
3 XN--1QQW23A. .佛山 Guangzhou YU Wei Information Technology Co., Ltd. Guangzhou YU Wei Information Technology Co., Ltd.
4 XN--IO0A7I. .网络 Computer Network Information Center of Chinese Academy of Sciences (China Internet Network Information Center) Computer Network Information Center of Chinese Academy of Sciences (China Internet Network Information Center)
4 XN--55QX5D. .公司 Computer Network Information Center of Chinese Academy of Sciences (China Internet Network Information Center) Computer Network Information Center of Chinese Academy of Sciences (China Internet Network Information Center)
5 XN--FIQS8S. .中国 China Internet Network Information Center (CNNIC) China Internet Network Information Center (CNNIC)
5 XN--FIQZ9S. .中國 China Internet Network Information Center (CNNIC) China Internet Network Information Center (CNNIC)

実害

実質的にはないのかもしれません。 強いて言うと、KSKが危殆化した時の影響が共用している他のTLDにも及ぶことになると思います。

CTログサーバーのAPIをコマンドラインから実行するツールを作った

概要

CTログサーバーのAPIをたたくコマンドラインツールを作りました。
詳細はこちら

github.com

理由

CTログサーバーは、RFC 6962に記載されるAPIを提供しますが、 使いやすいデータ構造で結果を取得することができません。

そこで、crt.sh などのウェブサービスを使うのですが、 こちらだと、オーバースペックであったり、レートリミットにかかったり、コマンドラインとの相性はよくないので、 もう少しプリミティブにCTログサーバーと直接やりとりしたくなり、作りました。

インストール・使い方

pipenvでインストールしてください。
使い方はこんな感じ

$ ./cttool.py monitor https://ct.googleapis.com/testtube
leafcrt 21805860076285651889842223030228467907313529    2020-04-11 09:26:35     2020-07-10 09:26:35     CN=Fake LE Intermediate X1    CN=www.asa2000.com
leafcrt 21863690318697950353276377428808609746848419    2020-04-11 09:26:23     2020-07-10 09:26:23     CN=Fake LE Intermediate X1    CN=test-4.staging.jp.cloudfoxy.com
precert 21802147377670478084527395429361876508605196    2020-04-11 09:26:24     2020-07-10 09:26:24     CN=Fake LE Intermediate X1    CN=test-5.staging.us-w.cloudfoxy.com
leafcrt 21824396527907332124567728734274662635973328    2020-04-11 09:26:43     2020-07-10 09:26:43     CN=Fake LE Intermediate X1    CN=test-5.staging.jp.cloudfoxy.com
leafcrt 21794684193856491727085434479490803112859831    2020-04-11 09:26:24     2020-07-10 09:26:24     CN=Fake LE Intermediate X1    CN=test.stte.rocks
precert 21864516430149817248388230101943631684877583    2020-04-11 09:26:48     2020-07-10 09:26:48     CN=Fake LE Intermediate X1    CN=test-3.staging.us-e.cloudfoxy.com

jsonでも出力できます。

$ ./cttool.py monitor https://ct.googleapis.com/testtube --start 119361290 --end 119361292 --json
[
    {
        "cert_type": "precert",
        "issuer": "CN=Fake LE Intermediate X1",
        "not_valid_after": "2020-07-05 15:29:06",
        "not_valid_before": "2020-04-06 15:29:06",
        "pem": "MIIFEzCCA/ugA...(snip)...",
        "serial": 21863364922683214852029586752934403698349576,
        "subject": "CN=newguiabr.com.br"
    },
    {
        "cert_type": "precert",
        "issuer": "CN=Fake LE Intermediate X1",
        "not_valid_after": "2020-07-05 15:29:03",
        "not_valid_before": "2020-04-06 15:29:03",
        "pem": "MIIEaDCCA1CgA...(snip)...",
        "subject": "CN=test-2.staging.us-e.cloudfoxy.com"
    }
]

CRLSetsには何が書かれているか

概要

ChromiumならびにGoogle Chromeが証明書の失効状態を確認するために使っているCRLSetsについて、 中身に何が書かれているのか確認してみたいと思います。

Chromeが、OCSP(Online Certificate Status Protocol)やCRL(Certificate Revocation List)を主には使用していない理由などは ググると沢山でてくるので、ここでは純粋にCRLSetsに何が書かれているのかに着目してみたいと思います。

CRLsetsの中身をみるためには

Chromium Projectに下記の通り記載されているツールを使います。

The current CRLSet can be fetched and dumped out using the code at https://github.com/agl/crlset-tools

dev.chromium.org

ということで、こちらのgo実装のツールをビルドします。

$ git clone https://github.com/agl/crlset-tools.git
$ cd crlset-tools
$ go build crlset.go

これで、最新版のCRLSetsをダウンロードしてくることができます。

$ ./crlset fetch > crlset.raw

何がかいてあるか

上記ツールのdumpコマンドで見てみます。

$ ./crlset dump ./crlset.raw 

手元では以下のように表示されました。

Sequence: 5785
Parents: 194

006cb226a772c7182d7772383e373f0f229e7dfe3444810a8d6e50905d20d661
  01a657
023c81cce8e7c64fa942d3c15048707d35d9bb5b87f4f544c5bf1bc5643af2fa
  07c7fb087254a95dd56ab78b3c4fb690
026f0a8e207f05f1f172db713dc22d0f43c8ff0d69724aa6fac6a8393df62508
  01e123522e1808cf312623b0a8
  01e707cde1f486c97bfec41758
  01f240410bf98509385f57d41d
  01f240411672b1bca5a37fe50a
  45d3e5c0c2bdaa7a9df7817ab6c1
  45d3e5c49afce8834a6e40e4db10
03cb44b933d7e14551e52ddbfc335a4d57bf65a703667b57ac961de31e3a106d
  329cab52a80a2e3f
  35c18872ba36292e
  5c923b50b4bac6ff
  7f8dcef53b9a951f
(以下略)

この表示に関して、先のツールのGithubのreadmeには次のように書いてありました。

Revocations are grouped by the SHA-256 hash of the issuing certificate's SubjectPublicKeyInfo and listed as serial numbers.

どうやら、失効した証明書のシリアル番号が、Issuer証明書(信頼の連鎖的に上位の証明書)のSubjectPublicKeyInfoの値(sha256したもの)でグルーピングされているみたいです。

そこで、本当にそのようになっているのか確認してみます。 上記のリストに表示されていた、こちらで確かめます。

03cb44b933d7e14551e52ddbfc335a4d57bf65a703667b57ac961de31e3a106d
  329cab52a80a2e3f

まず、329cab52a80a2e3fが証明書のシリアル番号を示しているようなので、https://crt.sh で検索してみますと、次のように見つかります。

f:id:zkat:20200402005914j:plain
crt.sh-serial-number-search
詳細を表示してみると、実際にCRLSetsで失効扱いになっていることをcrt.shでも確認できます。
f:id:zkat:20200402005828j:plain
crt.sh-revocation-status

上記crt.shの検索結果により、上位の証明書がC=NL, O=Staat der Nederlanden, CN=Staat der Nederlanden Organisatie Persoon CA - G3であることが分かりましたので、 この証明書をPEMでダウンロードしてきます。これは、SubjectPublicKeyInfoの値を知りたいからです。詳細は割愛しますが、crt.shから持ってこれます。

証明書中の何バイト目から、SubjectPublicKeyInfoが始まるか確認したいので、ASN.1形式にダンプしてみます。

すると、265バイト目から814バイト目までがそれであることが分かります。

 265  546:     SEQUENCE {
 269   13:       SEQUENCE {
 271    9:         OBJECT IDENTIFIER rsaEncryption (1 2 840 113549 1 1 1)
 282    0:         NULL
         :         }
 284  527:       BIT STRING
         :         30 82 02 0A 02 82 02 01 00 BE 62 E8 19 3A 7A 2E
         :         BE F7 A1 C2 75 2D C4 04 91 81 70 DF 59 9F 7B CD
         :         45 17 B1 E0 84 63 6F 00 16 78 05 48 E9 24 77 F5
         :         1E 98 36 22 FF 94 E7 93 42 4B E7 5A C4 5B D5 CA
         :         56 48 50 82 2C 4C 58 EB 95 33 8F 0C 56 50 3F D0
         :         C3 9E 58 C7 FE AE C6 82 78 6D 88 C8 DF 75 1D 41
         :         9B 94 CB 1D 6F 6D FF 7B 77 82 9A 75 72 E9 8B 19
         :         30 6E CC 0B 32 E1 7A A3 E3 F8 BD DF 0A 97 46 44
         :         7E 81 48 EC AC B7 2E DE 99 D5 5D 6C DC 2D 83 D2
         :         C2 07 F8 4B F1 03 5E 37 B3 A8 A6 C2 44 45 CF 57
         :         DF 28 49 77 E0 3D 90 00 50 5C 21 4B 78 85 12 16
         :         F3 C7 A6 CB 44 02 0D 8F EB 42 33 89 BD F1 AE 06
         :         B9 D2 41 70 CB 54 34 06 87 FB C0 33 69 CB CF D8
         :         6B F6 9C FB B0 AF 8A C4 93 9A E0 F6 2A 4A 9C 20
         :         C8 F5 75 3B 6E 48 40 D5 3C 33 6D 8E 3D AB 24 43
         :         90 E1 69 6E F0 6A 15 D9 56 3A 01 84 92 6A 23 09
         :         79 09 55 93 89 D3 A1 F7 F3 78 27 D8 24 7D D2 E3
         :         F1 49 BD F4 10 29 7F 24 CB 49 97 57 CC 79 BD 14
         :         EB C9 07 66 E8 5C 92 EC 3A 89 E9 37 05 AD A9 DE
         :         BD 08 39 46 A9 A5 D5 9C 0A 54 2B 05 8D BC 0F 29
         :         BA 5F BE E8 9E 84 AE 8A C7 94 84 AE 4A 52 C3 50
         :         B9 43 C8 09 3A 2B 6C DD A8 AF 4E 8D 82 27 51 8D
         :         81 90 91 64 CA 0C D3 90 E8 E0 D2 86 BA DE 4B 25
         :         9F 53 5E 90 DF ED C9 70 C5 16 F1 59 07 52 10 43
         :         6E 11 AD 73 18 A9 73 7C 75 7F F0 5A EC 43 0C 28
         :         FE 96 64 32 32 37 F4 8F E4 33 71 52 48 1A 68 26
         :         D5 E6 4B 03 FF 70 41 64 72 A7 39 69 6F 2C A1 62
         :         9D 52 84 FC F3 B3 80 B2 40 1F 43 AA 9C C0 A2 19
         :         B7 F5 34 74 13 03 7A DC 06 93 36 5C 8E ED CE 33
         :         ED A2 15 BB 67 3B 6D 88 09 75 DD F4 27 A3 02 81
         :         6D 1D E9 ED DD EF E8 ED 1A DC 9F E2 38 4E 09 52
         :         B4 17 ED 95 97 91 EB 79 92 06 C5 36 05 0C 0F 85
         :         2A CA 4E 06 C7 BB C5 DE A7 02 03 01 00 01
         :       }
 815  261:     [3] {
 819  257:       SEQUENCE {

そこで、証明書をhexdumpして得られた265バイト目から814バイト目までのバイナリデータをSHA256にかけてみます。

$ echo -en "\x30\x82\x02\x22\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d(省略)\xc5\xde\xa7\x02\x03\x01\x00\x01" | openssl sha256
(stdin)= 03cb44b933d7e14551e52ddbfc335a4d57bf65a703667b57ac961de31e3a106d

結果のハッシュが先の値に一致しましたので、IssuerのSPKIsのハッシュでグルーピングされていることが分かりました。

その他

crlsetのdumpコマンドで表示できるのは、CRLSetsファイルの一部のみであるようです。 生のCRLSetsファイルの構造がどのようになっているかは、体系的な説明を見つけることができなかったのですが、 例えば以下のソースコードのコメントに記載があり、参考になりました。

github.com

// CRLSet format:
//
// uint16le header_len
// byte[header_len] header_bytes
// repeated {
//   byte[32] parent_spki_sha256
//   uint32le num_serials
//   [num_serials] {
//     uint8 serial_length;
//     byte[serial_length] serial;
//   }
(省略)

MerkleTreeLeafをパースしようとしているけどうまくいかない

概要

Certificate Transparencyで用いられるデータ構造の一つであるMerkleTreeLeafを分解しようとしたのですが、 うまくいかないことがあったのでメモ(雑記)します。解決してません。

MerkleTreeLeafについて

CTログサーバーへget-entriesすることで取得できるデータ構造です。
応答データのjsonの、leaf_inputキーの値としてBase64エンコードされてデータが入っています。

tools.ietf.org

パースしてみる

適当なログサーバー(Testtube)から適当に拾ってきたデータを用いてみます。
base64をデコードしてhexdumpしてみます。 MTとかLETは、適当に自分が短縮した表現ですが、それぞれMercleLeafTypeと、LogEntryTypeを表しています。

00000000  00 00 00 00 01 48 8c b0  85 72 00 01 ba 9a 47 17  |.....H...r....G.|
          ^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^
         Ver MT       Timestamp           LET     Issuer
00000010  2b d3 21 8c 73 39 09 e9  64 d4 f8 1e 62 60 65 a3  |+.!.s9..d...b`e.|
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                             Key Hash
00000020  cf c8 99 fc 65 cb 8e 96  b4 73 35 8d 00 02 1a 30  |....e....s5....0|
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^
              Rest of Issuer Key Hash           ?????   TBSCertificate
00000030  82 02 16 a0 03 02 01 02  02 04 07 5b cd 15 30 0d  |...........[..0.|
00000040  06 09 2a 86 48 86 f7 0d  01 01 0b 05 00 30 70 31  |..*.H........0p1|
00000050  0b 30 09 06 03 55 04 06  13 02 47 42 31 19 30 17  |.0...U....GB1.0.|
00000060  06 03 55 04 0a 13 10 41  73 63 65 72 74 69 61 20  |..U....Ascertia |
00000070  4c 69 6d 69 74 65 64 31  27 30 25 06 03 55 04 0b  |Limited1'0%..U..|
00000080  13 1e 41 73 63 65 72 74  69 61 20 53 6f 66 74 77  |..Ascertia Softw|
00000090  61 72 65 20 44 69 73 74  72 69 62 75 74 69 6f 6e  |are Distribution|
000000a0  31 1d 30 1b 06 03 55 04  03 13 14 41 44 53 53 20  |1.0...U....ADSS |
000000b0  53 61 6d 70 6c 65 73 20  54 65 73 74 20 43 41 30  |Samples Test CA0|
000000c0  1e 17 0d 31 34 30 39 31  39 30 36 35 32 35 31 5a  |...140919065251Z|
000000d0  17 0d 32 30 30 31 31 35  30 36 35 32 35 31 5a 30  |..200115065251Z0|
000000e0  55 31 12 30 10 06 03 55  04 03 13 09 54 65 73 74  |U1.0...U....Test|
000000f0  20 43 65 72 74 31 0f 30  0d 06 09 2a 86 48 86 f7  | Cert1.0...*.H..|
00000100  0d 01 09 01 16 00 31 12  30 10 06 03 55 04 0b 13  |......1.0...U...|
00000110  09 54 65 73 74 20 55 6e  69 74 31 1a 30 18 06 03  |.Test Unit1.0...|
00000120  55 04 0a 13 11 54 65 73  74 20 4f 72 67 61 6e 69  |U....Test Organi|
00000130  7a 61 74 69 6f 6e 30 81  9f 30 0d 06 09 2a 86 48  |zation0..0...*.H|
00000140  86 f7 0d 01 01 01 05 00  03 81 8d 00 30 81 89 02  |............0...|
略
000001f0  04 16 04 14 fb 22 4b dd  be 5b 72 65 48 12 4b cb  |....."K..[reH.K.|
00000200  7e bb 46 04 18 19 41 bc  30 13 06 03 55 1d 25 04  |~.F...A.0...U.%.|
00000210  0c 30 0a 06 08 2b 06 01  05 05 07 03 01 30 09 06  |.0...+.......0..|
00000220  03 55 1d 13 04 02 30 00  30 1f 06 03 55 1d 23 04  |.U....0.0...U.#.|
00000230  18 30 16 80 14 3a f1 05  c1 0c 07 f9 25 8f 80 72  |.0...:......%..r|
00000240  b7 95 ea 93 50 89 7d f0  17 00 00                 |....P.}....     |

なぜこのように対応づけられると考えるかというと、RFC 6962をはじめとするRFCに以下のように データ構造が定義されているからです。

MerkleTreeLeaf

struct {
    Version version;
    MerkleLeafType leaf_type;
    select (leaf_type) {
        case timestamped_entry: TimestampedEntry;
    }
} MerkleTreeLeaf;

Version

enum { v1(0), (255) }
         Version;

MerkleLeafType

enum { timestamped_entry(0), (255) }
  MerkleLeafType;

TimestampedEntry

struct {
    uint64 timestamp;
    LogEntryType entry_type;
    select(entry_type) {
        case x509_entry: ASN.1Cert;
        case precert_entry: PreCert;
    } signed_entry;
    CtExtensions extensions;
} TimestampedEntry;

LogEntryType

enum { x509_entry(0), precert_entry(1), (65535) } LogEntryType;

PreCert;

struct {
    opaque issuer_key_hash[32];
    TBSCertificate tbs_certificate;
} PreCert;

TBSCertificate

   TBSCertificate  ::=  SEQUENCE  {
        version         [0]  EXPLICIT 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]  EXPLICIT Extensions OPTIONAL
                             -- If present, version MUST be v3
        }

疑問

45バイト目から3バイト存在する、00 02 1aの値は一体何を表しているのかよく分かりませんでした。
状況的には、後続のデータ(TBSCertificate 1 )のサイズを表していそうではあります。

その他

仮に45バイト目以降の3バイトがサイズであったとして、正しく、後続のTBSCertificateの値がとれたとしても、 それをいい感じにパースする仕組みがない気がするので、そもそも、leaf_inputの値を愚直に分解していく必要がない気がしてきました。 LogEntryTypeをみて、extra_dataのほうをパースするのがうまくいくかもしれません。 extra_dataのほうには、おなじみのASN.1形式をDERエンコードした証明書が入っているようなので。

補足

DERのエンコーディング0x00は存在しないので、DER以外の別のものであることが分かります。 調べていると、以下の情報が見つかりました。

“0x00” isn’t a valid tag in DER. What is this? It’s TLS encoding. This is defined in RFC 5246, section 4 (the TLS 1.2 RFC). TLS encoding, like ASN.1, has both a way to define data structures and a way to encode those structures. TLS encoding differs from DER in that there are no tags, and lengths are only encoded when necessary for variable-length arrays.

letsencrypt.org

TLS encodingについて理解すればうまくパースできそうな気がします。


  1. TBSとは、to be signedの略の様で、署名対象のデータを表しているようです。