作りました。
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というシステムが提供されているので、 それを動かしてみます。
$ 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を起こす
まず、対象のウェブサイトにログインしておきます。先ほど発行したクライアント証明書を提示します。
つづいて、ログイン状態を保持したまま、悪意のあるウェブサイトのボタンをクリックしてみます。 悪意のあるウェブサイトは例えば以下のようになっています。
<html> 当選おめでとうございます! 以下をクリックして、賞金を獲得! <form action="https://example.jp/buy_complete" method="POST"> <input type="hidden" name="amount" value=100> <input type="submit" value="アクセス"> </form> </html>
これにアクセスすると、CSRFが発生し、ユーザーの意図しない操作が実行されてしまいます。
防ぎ方
防ぎ方は、クライアント証明書があるか無いかなどは関係なく、様々なウェブサイトで説明されている通りです。 操作を完了する直前の画面で、攻撃者に知られることのない情報を埋め込み、画面遷移した際にその値が保持されているかチェックすることで、防ぐことができます。
異なるTLD(Top Level Domain)間でKSK(Key Signing Key)を共用している
概要
DNSには、応答の完全性を保つために使われるDNSSECという技術があります。 DNSSECで用いられる鍵の一種であるKSK(鍵署名鍵)について、複数のTLD(トップレベルドメイン)間での共用があることを見つけたのでメモします。
調べ方
まず、トップレベルドメインの一覧を以下から取得します。
上記一覧をもとに、各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をたたくコマンドラインツールを作りました。
詳細はこちら
理由
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
ということで、こちらの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 で検索してみますと、次のように見つかります。
詳細を表示してみると、実際にCRLSetsで失効扱いになっていることをcrt.shでも確認できます。
上記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ファイルの構造がどのようになっているかは、体系的な説明を見つけることができなかったのですが、 例えば以下のソースコードのコメントに記載があり、参考になりました。
// 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エンコードされてデータが入っています。
パースしてみる
適当なログサーバー(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.
TLS encodingについて理解すればうまくパースできそうな気がします。
-
TBSとは、to be signedの略の様で、署名対象のデータを表しているようです。↩