詩と創作・思索のひろば

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

中間証明書のないサーバにアクセスする: 言語別の実装

前回の記事のつづきです。

中間証明書のないサーバにアクセスする - 詩と創作・思索のひろば

さて、この方法で信頼できる中間証明書を手元に集めることができたので、これをプログラムから利用するにはどうしたらよいのか、ということを見ていく。

実験したいのはこういうことである:

  • 信頼できる中間証明書を手元に置いておくことで、中間証明書がないが本来は信頼できるサイトにアクセスできる。
    • 欠けている中間証明書を含めたチェーンで、サイトの証明書を検証できる。
  • 信頼できる中間証明書を手元に置いても、普通のサイトが正しく取得できる。
    • システムがもともと持っている証明書を壊したりしていない。
  • 信頼できない中間証明書を手元に置いてしまっても、本来信頼できないサイトにはアクセスできない。
    • 雑に中間証明書を持っておいて、実行時にそれらの中間証明書まで含めて検証する、というアプローチが取れるか? という実験。
    • 自己署名ルートによって署名された中間証明書を手元に置いて、その中間証明書によって署名されたサイトにアクセスしても、そのルート証明書を信頼していないので失敗する。
    • ついでに、中間証明書だけで検証してしまう(ルートまでたどらない)方法も検討しておく。

言語は openssl コマンド(言語じゃないけど)、Perl、Go、Node。まあ、よく使ってるやつです。

リポジトリは https://github.com/motemen/sketch-interm-certs 。README に書いてあるとおりに進めれば同じ手順で確認できます。以下、各言語の様子を見ていく。

  • 自己署名ルート以下は cfssl で作っている。これがとにかく楽。
  • 信頼できる中間証明書と、信頼できない中間証明書をまとめて /opt/intermediate-certs/ca-bundle.crt というファイルにしている。
  • システムの信頼する証明書は /etc/ssl/certs ディレクトリに保管されている。

openssl コマンド

https://github.com/motemen/sketch-interm-certs/blob/b896e1f/tests/openssl.sh

openssl s_client でテストする。中間証明書バンドルを -CAfile で指定するが、そうするとシステムの証明書を使わなくなってしまうので、あわせて -CApath /etc/ssl/certs する必要がある。

また、-partial_chain を指定することで中間証明書だけでサイトを検証できるようになる(チェーンをルートまでたどらない)。

Perl

https://github.com/motemen/sketch-interm-certs/blob/b896e1f/tests/perl.t

LWP::UserAgent を利用する。ssl_opts オプションに SSL_ca_fileSSL_ca_path を指定する。openssl の場合と似てますね。内部で OpenSSL 使ってるので当然といえば当然か。

これらはそれぞれ PERL_LWP_SSL_CA_FILEPERL_LWP_SSL_CA_PATH 環境変数で指定できるらしい。けど普通はオープンインターネット向けのエージェント以外にも内部エンドポイント向けのエージェントも作りたいわけで、そうなるとデフォルト値をゆるめに運用はしたくないと思うので、検証はしてない(以下に書いている事情もある)。

my $ua_with_interm_partial_chain = LWP::UserAgent->new(
    ssl_opts => {
        SSL_ca_file => '/opt/intermediate-certs/ca-bundle.crt',
        SSL_ca_path => '/etc/ssl/certs',
    }
);

ただし、気をつけなければならないのは、デフォルトでは -partial_chain が指定されたかのようなふるまいをすること。手元に信頼できないルートによって署名された証明書を持っている場合、その証明書を信頼することになっている。これは openssl s_client と違う挙動。

どういう理由かはよくわからないんだけど、IO::Socket::SSL の中でそういうことをしている

これを回避するには、ssl_opts にコールバックを指定して、OpenSSL の API を呼んでやる。

my $ua_with_interm_full_chain = LWP::UserAgent->new(
    ssl_opts => {
        SSL_ca_file => '/opt/intermediate-certs/ca-bundle.crt',
        SSL_ca_path => '/etc/ssl/certs',
        SSL_create_ctx_callback => sub {
            my $ctx = shift;
            my $param = Net::SSLeay::CTX_get0_param($ctx);
            my $rv = Net::SSLeay::X509_VERIFY_PARAM_get_flags($param);
            Net::SSLeay::X509_VERIFY_PARAM_clear_flags($param, Net::SSLeay::X509_V_FLAG_PARTIAL_CHAIN());
            Net::SSLeay::CTX_set1_param($ctx, $param);
        },
    }
);

Go

https://github.com/motemen/sketch-interm-certs/blob/b896e1f/tests/go_test.go

Go は TLS の実装に OpenSSL を利用していない。ドキュメントを読む限りでは crypt/tls.Config の RootCAs をカスタマイズしてやればよさそう。

func buildTransportWithIntermCertsParialChain() http.RoundTripper {
    rootCAs, err := x509.SystemCertPool()
    if err != nil {
        log.Fatal(err)
    }

    intermPEM, err := ioutil.ReadFile("/opt/intermediate-certs/ca-bundle.crt")
    if err != nil {
        log.Fatal(err)
    }

    rootCAs.AppendCertsFromPEM(intermPEM)

    return &http.Transport{
        TLSClientConfig: &tls.Config{
            RootCAs: rootCAs,
        },
    }
}

ただ、名前のとおりこれはルート証明書を格納するものなので、信頼できない中間証明書をここに入れることはできない。

いちおう中間証明書を与えつつチェーン全部を検証する実装らしきものは書けたけど、専門家の書いたものではないのでこれを信用してはいけない。まあ、参考程度に。

func buildTransportWithIntermCertsFullChain() http.RoundTripper {
    rootCAs, err := x509.SystemCertPool()
    if err != nil {
        log.Fatal(err)
    }

    intermPEM, err := ioutil.ReadFile("/opt/intermediate-certs/ca-bundle.crt")
    if err != nil {
        log.Fatal(err)
    }

    return &http.Transport{
        DialTLS: func(network, addr string) (net.Conn, error) {
            host, _, err := net.SplitHostPort(addr)
            if err != nil {
                return nil, err
            }

            conf := &tls.Config{
                InsecureSkipVerify: true,
                VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {

                    // https://github.com/golang/go/blob/go1.14.4/src/crypto/tls/handshake_client.go#L793
                    certs := make([]*x509.Certificate, len(rawCerts))
                    for i, asn1Data := range rawCerts {
                        cert, err := x509.ParseCertificate(asn1Data)
                        if err != nil {
                            return errors.New("tls: failed to parse certificate from server: " + err.Error())
                        }
                        certs[i] = cert
                    }

                    opts := x509.VerifyOptions{
                        Roots:         rootCAs,
                        CurrentTime:   time.Now(),
                        DNSName:       host,
                        Intermediates: x509.NewCertPool(),
                    }
                    for _, cert := range certs[1:] {
                        opts.Intermediates.AddCert(cert)
                    }

                    opts.Intermediates.AppendCertsFromPEM(intermPEM)
                    _, err = certs[0].Verify(opts)

                    return err
                },
            }

            return tls.Dial(network, addr, conf)
        },
    }
}

Node

https://github.com/motemen/sketch-interm-certs/blob/b896e1f/tests/node.test.js

Node も OpenSSL を利用している。ぜんぜん使ったことはないのだけど、https.Agent に設定を詰めるのがいいみたいだ。こちらは非常にかんたん。

  const agent = new https.Agent({
    ca: [
      fs.readFileSync("/etc/ssl/certs/ca-certificates.crt"),
      fs.readFileSync("/opt/intermediate-certs/ca-bundle.crt")
    ]
  });

ca オプションで証明書のコンテンツを指定する。ドキュメントから明らかじゃないように見えるんだけど、tls.connect の一部のオプションも Agent に指定できるみたい。ca には内容を指定するので、openssl コマンドや Perl で見たようにディレクトリを指定することはできない。

そして Perl の場合と違い、中間証明書を与えてもチェーン全体が検証されるようだ。これが意図通りなのかどうかまではわからない。内部で X509_STORE_add_cert使われているけど、Untrusted objects should not be added in this way.って書いてあるんだよな……。

逆に、自分の見たかぎりでは -partial_chain 相当の挙動を実現することもできなそうだった。

また、NODE_EXTRA_CA_CERTS環境変数を指定することでも ca にそのファイルの中身を追加したかのような挙動を実現できる。

まとめ

いろいろな言語による HTTPS (TLS) 証明書の扱いを見てみました。

冒頭に書いていた「雑に中間証明書を持っておいて、実行時にそれらの中間証明書まで含めて検証する、というアプローチが取れるか?」という疑問に関しては、わりと厳しそう。何にしろ、中間証明書は信頼できるものだけを手元においておいたほうがよいですね。

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