ESP32とオレオレ証明書

とあるシステムを作るのに、ESP32とSinatraを使っている。

いずれ外部に公開するということ、今時はhttpsが普通だということもあって、ESP32からSinatraへのアクセスもhttpsを使うことにした。

まだローカルで試している段階なので、オレオレ証明書(自己署名証明書)を使って試そうとしていたのだが、これが一筋縄では行かなかったのでメモ。

実はESP32でhttpsを使う事例は、ちょっとググればいくらでも出て来る。なので、それ自体はそんなに難しいことではない。何らかの方法でサーバの証明書を手に入れ、それをクライアントに組み込めば良い。

extern esp_http_client_handle_t
initialize_httpc(void)
{
    esp_http_client_handle_t client;

    esp_http_client_config_t config = {
        .url = CONSOLE_HOST,
        .event_handler = _http_event_handle,
        .cert_pem = (char *)pub_key_start
    };
    client = esp_http_client_init(&config);

    return (client);
}

基本はこれだけである。何ならevent_handlerはなくてもいい。urlの設定はないとhttp_client_initがエラーになるので入れてあるが、中身は何でもいい。必要なのは、cert_pemの設定だけである。

ついでに書いておくと、cert_pemについてはコード上に書く必要はなく、main/component.mkに

COMPONENT_EMBED_TXTFILES := pub.key

のように書いておけば、

extern const char pub_key_start[] asm("_binary_pub_key_start");

のように参照出来る。この辺でハマることはない。

また、Sinatraの方は、

class JsonGate < Sinatra::Base
  helpers Sinatra::Cookies
  def self.run!
    certificate_content = File.open("./server.crt").read
    key_content = File.open("./server.key").read

    server_options = {
      :Host => "0.0.0.0",
      :Port => API_PORT,
      :SSLEnable => true,
      :SSLCertificate => OpenSSL::X509::Certificate.new(certificate_content),
      :SSLPrivateKey => OpenSSL::PKey::RSA.new(key_content)
     }
    set :server_settings, server_options

    Rack::Handler::WEBrick.run(self, server_options) do |server|
      [:INT, :TERM].each { |sig| trap(sig) { server.stop } }
      server.threaded = settings.threaded if server.respond_to? :threaded=
        set :running, true
    end
end

のようなことを書いておけばいい。これもそこらじゅうに情報があるので、詳しくはggrksである。

オレオレ証明書の作り方もそこらじゅうにあって、

OpenSSLによるオレオレ認証局が署名した証明書の作成

この辺が詳しくて読みやすい。なお、ChromeではSANがないと文句を言われるので、それについては、

Chromeで使えるオレオレ証明書を作成する方法

に書いてある。もちろん、オレオレ証明書で怒られないためには、適当なCAを作ってそれをChrome(とゆーかブラウザ)に登録しておかなければならない。まぁ、これも難しいことではない。あー、登録してもすんなりと認識してくれないことが多々あるので、一度ブラウザは再起動した方がいいよ。

ということで、これらの証明書を作ってブラウザにオレオレ認証局を登録してやればブラウザでは問題がない。なので同じようにESP32でやればOK…

とは行かない

のだ。ここから先が面倒になる。

まず気をつけなければならないのは、ESP32でESP-IDFを使う場合、SSLはOpenSSLではない。そりゃまぁ当然のことで、こんな小さな機械で動かせるほどOpenSSLは簡単ではない。そこで何が動いているかと言えば、

Mbed TLS

というものが使われている。こいつがクセモノである。

いろいろ調べていると、こいつはSANを理解しない。手元の環境でサーバ立てたりしてテストしている時はDNSに登録とかしないでIPアドレスを直に使うようなことをしていると思う。ところがMbed TLSはSANを理解しないから、SANにIPアドレスを書いても理解してくれない。なので、

CNにIPアドレスを書く

ようにしないと、CNの整合を調べるステップでエラーになってしまう。だから、サーバ証明書を作る時にはそのように指定しておかないとダメである。この辺にいろいろ書いてある。

esp-tls: mbedtls_ssl_handshake returned -0x2700

さらに、こいつはSANを理解しないだけではなくて、

SANが邪魔になる

ようだ。どういうことかと言えば、SANが含まれているサーバ証明書が来ると、CNのチェックでエラーになってしまう(IPアドレスが来ると逆引きが… ということらしい)。ググル用にエラーメッセージを書いておくと、

esp-tls: verification info: ! The certificate Common Name (CN) does not match with the expected CN

ということになってしまう。いや、ちゃんとCNには書いておいたんだけどさ。どうもお気に召さないらしい。

またこれだけではなくて、当然ながらオレオレ認証局も指定しておかなければならない。Mbed TLSにはCAストア的なファイルは存在してないようなので、

extern const unsigned char ca_key_start[] asm("_binary_ca_crt_start");
extern const unsigned char ca_key_end[] asm("_binary_ca_crt_end");

....

esp_tls_init_global_ca_store();
esp_tls_set_global_ca_store(ca_key_start, ca_key_end - ca_key_start);

のようなことをして、オレオレ認証局を教えてやる必要がある。また、esp_http_client_config_tにも、

     esp_http_client_config_t config = {
        .url = CONSOLE_HOST,
        .event_handler = _http_event_handle,
        .use_global_ca_store = true,
        .cert_pem = (char *)pub_key_start
    };

のようにしておかなければならない。

ということで要点をまとめておくと、

  • ESP32はSANを理解しない上に、あると邪魔になる
  • オレオレ認証局はソース上で指定してやる
  • サーバをIPアドレスで指定するならCNにIPアドレスを書いておけ

ということになるようだ。そしてこのような事情があるので、ChromeとESP32(ESP-IDF)が両方満足するようには出来ないっぽい。Chromeが認識しないのはそれまぁいいとしても、Advanced Rest Clientが使えないのは、ちょっと不便。まぁ、証明書切り換えながらテストすりゃいいんだけど。

一連のことは、正規の環境で動かす時には問題にならない。認証局はどこかのまっとうなものを使うだろうし、アクセスする時にはホスト名になるはず。なので、たとえばAWSのIoTなものを使う分には問題にはならない。