詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

中間証明書のないサーバにアクセスする

不特定多数のウェブサイトにアクセスするアプリケーションを書いていると、ときおり SSL 証明書の検証エラーとなる URL に行き当たることがある。が、確認のためブラウザでアクセスしてみると、普通に見れてしまったりもする。そんな事例のひとつ、タイトルの通り中間CA証明書のないサーバについて。

https://incomplete-chain.badssl.com/ というわかりやすい例がある。これを curl してみると:

% docker run -it --rm buildpack-deps:buster bash
root@22f1788d53c7:/# curl --version
curl 7.64.0 (x86_64-pc-linux-gnu) libcurl/7.64.0 OpenSSL/1.1.1d zlib/1.2.11 libidn2/2.0.5 libpsl/0.20.2 (+libidn2/2.0.5) libssh2/1.8.0 nghttp2/1.36.0 librtmp/2.3
Release-Date: 2019-02-06
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets HTTPS-proxy PSL
root@22f1788d53c7:/# curl -sS https://www.example.com/ > /dev/null # 正常系として
root@22f1788d53c7:/# curl -sS https://incomplete-chain.badssl.com/ > /dev/null
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

となるわけだけど、手元のブラウザではたぶんとくにエラーもなく開ける(はず)。

この URL では、サーバはあえて中間証明書を配信していない。にもかかわらずブラウザで見えてしまうのは、badssl.com の説明によると、

  • ブラウザが中間証明書をキャッシュしている
  • または、ブラウザが AIA (= Authority Information Access) fetching に対応している

から、ということらしい。AIA は SSL 証明書の拡張仕様で、サーバの証明書の発行者の証明書へのアクセス方法が記述されるもの。ブラウザで証明書の情報を見てみると確認できるはず。見てみると証明書の URL が http: で記述されていることに最初おどろいたけど、平文で取得した証明書も検証されるので、安全性には問題ないのだった。それを自動的に取得してくるのが AIA fetching(なのだと思う)。AIA fetching は最近のメジャーなブラウザだと Firefox 以外は対応しているのだというがあったが、公式にアナウンスしているものはすぐには見つからなかった。ソースを読まないといけないだろう。

中間証明書をまとめて取得する

で、ブラウザで見られるページはアプリケーションからも取得できたい、というのは普通にありえる需要で、これをどのように実現するのか、という話。

AIA fetching を実装してしまう、というのも正攻法ではあって、そういう実装もある(未検証)。使えるライブラリがない場合や実装したくない場合に、すべてのサイトを尽くすことはできないかもしれないけど、多くをカバーできる方法として、あらかじめ中間証明書を手元に取得しておく、という手をここでは考える。

つまり、ルートCA証明書によって署名されている中間証明書をあらかじめダウンロードしておけばいいわけだ。そして、都合のいいことにそういうリストが作られている。

https://wiki.mozilla.org/CA/Intermediate_Certificates

Mozilla による Common CA Database (CCADB) プロジェクト のリソースのひとつで、Mozilla が信頼しているルート CA によって署名された中間証明書の一覧らしい。このページの権威は自分には確認できなかったけど、どのみちすでに信頼されている手元のルート証明書で検証するので、それっぽいものが網羅されていれば出自はなんでもいいのである。

先述のページから中間証明書を取得し、すでに手元にあるルート証明書で検証できたものだけ保存することにする。Dockerfile にするとこういう感じになる。楽したかったので、apt で入る csvtool というのを使ってみた。

FROM buildpack-deps:buster

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

RUN apt-get update && \
    apt-get install -y csvtool
RUN mkdir -p /opt/intermediate-certs/certs && \
    curl -sL https://ccadb-public.secure.force.com/mozilla/PublicAllIntermediateCertsWithPEMCSV | \
    csvtool namedcol 'PEM Info' - | \
    csvtool drop 1 - | \ # ヘッダを落とす
    csvtool call "printf '%q\n'" - | \ # 改行が含まれているので1行に
    while IFS= read -r e; do \
        pem=$(eval "echo $e" | sed "s/'//"); \ # ' を除く
        if echo "$pem" | openssl verify; then \ # 既存のルート証明書で検証
            echo "$pem" | \
                tee -a /opt/intermediate-certs/ca-bundle.crt \ # 証明書バンドルを作る
                > /opt/intermediate-certs/certs/"$(echo "$pem" | openssl x509 -subject -noout | perl -pe 's/\W/_/g')".pem; \ # 個別の証明書も保存する
        fi; \
    done && \
    c_rehash /opt/intermediate-certs/certs

openssl verify して成功したものだけ保存する。今気づいたけど、中間証明書によって署名されている中間証明書はここで跳ねられてしまうな……。

証明書バンドルは、単にテキストとして並べて記述すればよい(opensslRFC)。取り回しがしやすいように、すべての証明書を1ファイルにまとめたものとそれぞれ別々のファイルにしたものとを作っている。

今回取得した中間証明書をシステムにそのまま組み込んでしまいたいなら、システムのルート(debian なら /etc/ssl/certs)に置いてしまえばいいのだけど、アプリケーション側で制御したいことがほとんどだと思うので、別の場所に置いておくのだいいだろう。

さっそく試してみる。--cacert がポイント。--capath /opt/intermediate-certs/certs:/etc/ssl/certs としてもいい。

% docker run --rm -it $(docker build -q .) bash
root@39401ca59f18:/# curl --version
curl 7.64.0 (x86_64-pc-linux-gnu) libcurl/7.64.0 OpenSSL/1.1.1d zlib/1.2.11 libidn2/2.0.5 libpsl/0.20.2 (+libidn2/2.0.5) libssh2/1.8.0 nghttp2/1.36.0 librtmp/2.3
Release-Date: 2019-02-06
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets HTTPS-proxy PSL
root@39401ca59f18:/# curl --cacert /opt/intermediate-certs/ca-bundle.crt -sS https://www.example.com/ > /dev/n
ull
root@39401ca59f18:/# curl --cacert /opt/intermediate-certs/ca-bundle.crt -sS https://incomplete-chain.badssl.com/ > /dev/null

やったね! 成功です。

ちなみに

ブラウザの確認だけで済ませてしまうと、こういう設定漏れに気づきにくいみたいです。Mackerel のようなサービスを使えば、証明書の期限なども含めて監視できます。わたしも使っています! どうぞご利用下さい。

mackerel.io

f:id:motemen:20200422224025p:plain

参考文献

ghq など、github.com/motemen 下のリポジトリを x-motemen Org に移しています

もうけっこう前のことになりますが、github.com/motemen 下にあったリポジトリの一部を github.com/x-motemen organization に移管しました。コードやバイナリの配布形式によって、ユーザ側で対処する必要があったりなかったりしますが、それぞれのリポジトリにおける指示(あれば)に従ってもらえればと思います。

もともとこれらは id:Songmuid:itchynyid:onk らにアクティブにメンテされているもので、motemen 個人はほぼ完全にノータッチとなっているのが現状です。そういうリポジトリは他にもあるので、今後も随時このような移管をやっていくつもりです。よろしくお願いします。

Organization 移管のメリット

で、実際的な話として、個人リポジトリにコラボレータとして追加することと organization のリポジトリとすることの差なんだけど、与えられる権限が違うっぽい。

とくに個人リポジトリではコラボレータに admin 権限を与えることができず、そうすると webhook の設定ができないので、メンテナがリポジトリを積極的に改善しよう! と思ったときにいきなり出鼻をくじかれることになってしまう(とくに最近だと GitHub Actions 化しようとしてやられる)。

こちらは org 化していないリポジトリ(gore)のコラボレータ管理画面。Write 権限のあるなししかない。

f:id:motemen:20200302133140p:plain

こちらは org 化したリポジトリ(ghq)のコラボレータ管理画面。songmu さんに admin 権限をあげて、自分は Write みたいにできている。

f:id:motemen:20200302133159p:plain

移管の経緯

大きなきっかけは ghq の v1 リリース。メンテナの songmu さんと会ったときに、この際 ghq organization みたいなのを作ってそこに置きます? というような話をしたんだけど、motemen/ghq でひとつの名前として認知されてるからなー、という話もあり、その時はそのままの名前で行くことになったのでした。

OSS(とも思ってないが)を別の名前の下に移す……ということにはなかなかアンビバレントな感情があって、大方の人間と同じく自分もいわゆる承認欲求を燃料にしてこれまで生きてきたので、自分の作ったものが使われたり、言及されることはとても嬉しい。そういう面では、自分の名前から剥がしてしまうことには心理的な抵抗が長いことあった。

しかし幸いなことに自分のソフトウェアが多くの人に使われるようになってくると、自分にとって負の影響も大きくなってくる。つまりイシューや pull request がやってくるようになる。はじめの頃はこういう直接の反応がやってくるのが嬉しいので丁寧に応対しようとするけど、数は多くなくても、一つひとつがその裏に人間の意図や要望のあることであるし、否応なくコミュニケーションを必要とするので、疲れてしまう。そもそも自分は人間とアレコレしたくないからコンピュータを触っているのであった(今どきこういう考えをしている人は少ないのかもしれない)。それなのになせこんなことに……。他人に認められたいのに、他人とのコミュニケーションを拒む、という姿勢自体がはらんでいる歪みだったのかもしれない。

もうひとつ重要なのは、リポジトリを公開した時点で、そのソフトウェアによって解決したかった自分の課題はほぼ満たされているので、それ以上に手を加えるモチベーションがない、ということだった。公開したソフトウェアはそこで硬直してしまっていて、そこに変更を加えることの喜びは、他人にどんな風に使われているか分からないものを変更するコストを上回らない。自分で書いててひでーなとも思うが、そういうことである。こういうのをひっくるめて、自分の書いたものがOSSとも呼べない(というかバザール的でない)な、と思っている。

それでも使われているものは身近な人がメンテナになってくれていて、イシューに回答したりリリースまでやってくれていたりする。それはありがたいことで、コミュニケーションを拒否している罪悪感はいくらか和らぐものの、もうおれの持ち物じゃなくね? という感覚は大きくなっていく。

とまあこんなふうに、自分のものであるという主張には固執しつつ、しかし他者からの要望に応えもしないでいる、ということにだんだんと嫌気が差してきて、冒頭に書いたように org 化の話が出てくるわけです。結局その時は見送ったわけだけど、それでもそのアイデアは頭の中に残っていて、ghq 以外のソフトウェアも他の人にメンテしてもらってるものはひとつの org に移してしまおうと画策はしていた。Apache Incubator に倣って motemen-incubated とか考えてたんだけど、x-motemen という名前を思いついて、試しに organization 作成フォームに入力していたら作れてしまったので、そのまま利用したという流れであった。x- には卒業感があって、いいなあと自画自賛する次第である。

結局自分の名前残っとるやんけ、との指摘はその通りで、しかし自分の中ではもはやあまり重要でもない。

それにしても、移管と直接関係するわけではないけれど、メンテナンスされるソフトウェアの価値というものをひしひしと感じている昨今であり、brew upgrade ghq して新しいバージョンが手元に降ってくることには、自分が最初のユーザであるからこその喜びがあると思う。こればかりは特別な体験です。

テキストを画面に流していくアプリをElectronで作った

この記事は、はてなエンジニア Advent Calendar 2019の12日目の記事です。


任意のテキストを画面に流していきたいことってありませんか? ぼくはあります。定期的にエンジニアみんなの前でスライドを映しつつ話す機会があって、そんなとき Slack で実況的に反応がなされることがあるんだけど、Slack 映しっぱなしにするわけにもいかず、話し終わってあとからコメントに気づく……ってこともまあまあある。そんなとき、画面のスライドに重ねてコメントが流れてくれると自分も聞き手も共有できてうれしい。わけです。

それを達成するための1ステップとして、任意のテキストを画面に次々流してくれるアプリをElectronで作りました。

GitHub - motemen/TextCast

じつは過去のこのエントリたちも、「Slack の発言をリアルタイムにデスクトップに流したい」という欲望からうまれたものです:

使い方

ビルドしたものの配布までは作り込めてないので、git cloneyarn install した上で yarn dev で起動してみてください。 http://localhost:4140 で HTTP サーバが立つので、ここに POST リクエストを送るとテキストを描画できます。

  • curl localhost:4140/post -d text={text} で都度
  • curl localhost:4140/stream -T - でストリーミング

分かりづらいかもしれないけどスクリーンショット。デスクトップの右下からにゅっと出てきて、時間が経つと消える感じ。

f:id:motemen:20191212183358g:plain f:id:motemen:20191212145632g:plain

ニコニコ風にしてみたい気持ちもあるけどそれはみなさんの叡智に期待したい!!!

Slackとの連携

そしてSlackと連携するには! 以前の2つのエントリを組み合わせて……

こうするのじゃ:

slack-stream-json -f -i | \
    jq --raw-output --unbuffered 'select(.type == "message" and (.subtype | not) and .channel.name == "engineer") | "\(.user.name): \(.text)"' | \
    curl -s -T - localhost:4140/stream

#engineer チャンネルの内容がどんどん流れてくる! うおおこれややりたかったことは! やったね。コンテンツ的にスクショはありません。

Electron まわりの話

ウィンドウを透明化する

ウィンドウを透明化(マウスイベントを奪わないで)できんのかな~ってのが最大の障壁だったけど、以下の記事の通りにやってたらできてしまった。

qiita.com

electron-webpack

electron-webpack

Electron よくわからない状態からはじめたので、シュッとはじめたくてこういうやつを使った。TypeScript 化とかもドキュメントにある通りやればすぐできる。 バージョン番号を package.json から取りたくて少しだけ webpack 設定をいじる必要があったのだが、それもちょちょっと書けば実現できたのでたいへん楽だった。


そんなわけで、最近作ったアプリの紹介でした。どうぞご利用ください。 はてなエンジニアアドベントカレンダー2019、明日の担当は id:takuji31 です。よろしくお願いします!!!

はてなで一緒に働きませんか?