メインコンテンツへスキップ

ed25519 のサーバー証明書はブラウザがまだ対応していないという話

·936 文字·5 分
技術記事 TLS C++ コードリーディング

訂正
#

2024/10/07: タイトルと TL;DR、本文中などに技術的に不正確な表現、意図とは異なる表現になっている部分が含まれていたため訂正(✕: ed25519 で署名した →◯: ed25519 鍵で作成された)1

TL;DR
#

ed25519 鍵で作成した証明書をサーバー証明書として使うのはまだ早い

動機
#

BoringSSL2 を使って https サーバー制作中3にブラウザ(Brave(Chromium ベースのブラウザ)4)からテストしようとしたら

2024-10-06 09:56:30 [::1]:56185 method="SSL_read" ssl_error_code=1 ssl_error_desc="SSL"  lib_error=error:100000fd:SSL routines::NO_COMMON_SIGNATURE_ALGORITHMS

というエラーが出た。 多分証明書の署名関連が悪いんだろうとは思いとりあえず調べていったら 必要以上に深堀ってしまったのでまとめておく。

調査概要
#

BoringSSL のエラー箇所を特定したところエラーの原因は サーバー側とクライアント側で signature algorithm 5 の候補が一致しなかったためであった。 その原因はクライアントサイド(Chromium)では ed255196 のサポートが有効化されていないからであった。

調査過程詳細
#

以下コードのようになっている箇所は関数名や変数名などの識別子を表す。

BoringSSL のデバッグ
#

まず NO_COMMON_SIGNATURE_ALGORITHMS の文字列を探しブレークポイントを貼ってデバッグをしてみたら tls1_choose_signature_algorithm のコードで失敗していて ssl_private_key_supports_signature_algorithm で候補が弾かれてしまっているようだ7

for (uint16_t sigalg : sigalgs) {
    if (!ssl_private_key_supports_signature_algorithm(hs, sigalg)) {
      continue;
    }

    for (uint16_t peer_sigalg : peer_sigalgs) {
      if (sigalg == peer_sigalg) {
        *out = sigalg;
        return true;
      }
    }
  }

  OPENSSL_PUT_ERROR(SSL, SSL_R_NO_COMMON_SIGNATURE_ALGORITHMS);
  return false;

唯一 ssl_private_key_supports_signature_algorithm で弾かれないのが ed25519 のときだけであった。

筆者はこれまで quic-go8 を使って実験をしていた。 そのときにはサーバー証明書を ed25519 鍵で作成しており9、今回使ったサーバー証明書もそれを流用したもののため おそらくこのあたりが原因ではあろうと推測した。

以下がブラウザからきた peer_sigalgsの中身をデバッガーで表示したもの、

0x000001f1a338e548 {1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537}

以下が sigalgs の中身をデバッガーで表示したものである。

0x00007ffa28f01210 {ssl.dll!const unsigned short bssl::kSignSignatureAlgorithms[12]} {2055, 1027, 2052, ...}

以下それぞれの値がどこから来たものなのかを探っていく。

BoringSSL のコードリーディング 1
#

デバッガーのsigalgs のところに表示された kSignSignatureAlgorithms の表記から調べるとそれは以下のように署名アルゴリズムの一覧が定義された配列であった。10

static const uint16_t kSignSignatureAlgorithms[] = {
    // List our preferred algorithms first.
    SSL_SIGN_ED25519,
    SSL_SIGN_ECDSA_SECP256R1_SHA256,
    SSL_SIGN_RSA_PSS_RSAE_SHA256,
    SSL_SIGN_RSA_PKCS1_SHA256,

    // If needed, sign larger hashes.
    //
    // TODO(davidben): Determine which of these may be pruned.
    SSL_SIGN_ECDSA_SECP384R1_SHA384,
    SSL_SIGN_RSA_PSS_RSAE_SHA384,
    SSL_SIGN_RSA_PKCS1_SHA384,

    SSL_SIGN_ECDSA_SECP521R1_SHA512,
    SSL_SIGN_RSA_PSS_RSAE_SHA512,
    SSL_SIGN_RSA_PKCS1_SHA512,

    // If the peer supports nothing else, sign with SHA-1.
    SSL_SIGN_ECDSA_SHA1,
    SSL_SIGN_RSA_PKCS1_SHA1,
};

Chromium のコードリーディング
#

peer_algs の値が Chromium のソースを辿ってどうしてそうなっているのかを探る。

TLS というワードから辿って調べていると ここで署名方式が設定されているようだということがわかった。11

  // Disable SHA-1 server signatures.
  // TODO(crbug.com/boringssl/699): Once the default is flipped in BoringSSL, we
  // no longer need to override it.
  static const uint16_t kVerifyPrefs[] = {
      SSL_SIGN_ECDSA_SECP256R1_SHA256, SSL_SIGN_RSA_PSS_RSAE_SHA256,
      SSL_SIGN_RSA_PKCS1_SHA256,       SSL_SIGN_ECDSA_SECP384R1_SHA384,
      SSL_SIGN_RSA_PSS_RSAE_SHA384,    SSL_SIGN_RSA_PKCS1_SHA384,
      SSL_SIGN_RSA_PSS_RSAE_SHA512,    SSL_SIGN_RSA_PKCS1_SHA512,
  };
  if (!SSL_set_verify_algorithm_prefs(ssl_.get(), kVerifyPrefs,
                                      std::size(kVerifyPrefs))) {
    return ERR_UNEXPECTED;
  }

BoringSSL のコードリーディング 2
#

Chromium も同じく boringssl を使用しているため boringssl 内を再度探していく。

まずサーバー側のpeer_sigalgshs->peer_sigalgsから取得されたものである12

  Span<const uint16_t> peer_sigalgs;
  if (cred->type == SSLCredentialType::kDelegated) {
    peer_sigalgs = hs->peer_delegated_credential_sigalgs;
  } else {
    peer_sigalgs = hs->peer_sigalgs;
    if (peer_sigalgs.empty() && version == TLS1_2_VERSION) {
      // If the client didn't specify any signature_algorithms extension, it is
      // interpreted as SHA-1. See
      // http://tools.ietf.org/html/rfc5246#section-7.4.1.4.1
      static const uint16_t kTLS12Default[] = {SSL_SIGN_RSA_PKCS1_SHA1,
                                               SSL_SIGN_ECDSA_SHA1};
      peer_sigalgs = kTLS12Default;
    }
  }

hs->peer_sigalgstls1_parse_peer_sigalgs から来ているようだ13

bool tls1_parse_peer_sigalgs(SSL_HANDSHAKE *hs, const CBS *in_sigalgs) {
  // Extension ignored for inappropriate versions
  if (ssl_protocol_version(hs->ssl) < TLS1_2_VERSION) {
    return true;
  }

  // In all contexts, the signature algorithms list may not be empty. (It may be
  // omitted by clients in TLS 1.2, but then the entire extension is omitted.)
  return CBS_len(in_sigalgs) != 0 &&
         parse_u16_array(in_sigalgs, &hs->peer_sigalgs);
}

tls1_parse_peer_sigalgsext_sigalgs_parse_clienthello14から呼ばれていて15

static bool ext_sigalgs_parse_clienthello(SSL_HANDSHAKE *hs, uint8_t *out_alert,
                                          CBS *contents) {
  hs->peer_sigalgs.Reset();
  if (contents == NULL) {
    return true;
  }

  CBS supported_signature_algorithms;
  if (!CBS_get_u16_length_prefixed(contents, &supported_signature_algorithms) ||
      CBS_len(contents) != 0 ||
      !tls1_parse_peer_sigalgs(hs, &supported_signature_algorithms)) {
    return false;
  }

  return true;
}

ext_sigalgs_parse_clienthello は TLS extension のパース用に kExtension 配列内に設定されているようだ16

  {
    TLSEXT_TYPE_signature_algorithms,
    ext_sigalgs_add_clienthello,
    forbid_parse_serverhello,
    ext_sigalgs_parse_clienthello,
    dont_add_serverhello,
  },

TLSEXT_TYPE_signature_algorithms14の定義は以下の通り17

// ExtensionType values from RFC 5246
#define TLSEXT_TYPE_signature_algorithms 13

この TLSEXT_TYPE_signature_algorithms が入っている kExtension 配列は tls_extension_find で参照されており18

static const struct tls_extension *tls_extension_find(uint32_t *out_index,
                                                      uint16_t value) {
  unsigned i;
  for (i = 0; i < kNumExtensions; i++) {
    if (kExtensions[i].value == value) {
      *out_index = i;
      return &kExtensions[i];
    }
  }

  return NULL;
}

ssl_scan_clienthello_tlsext内で TLS extension パース時に呼ばれているようだ。 19

    unsigned ext_index;
    const struct tls_extension *const ext =
        tls_extension_find(&ext_index, type);
    if (ext == NULL) {
      continue;
    }

    hs->extensions.received |= (1u << ext_index);
    uint8_t alert = SSL_AD_DECODE_ERROR;
    if (!ext->parse_clienthello(hs, &alert, &extension)) {
      *out_alert = alert;
      OPENSSL_PUT_ERROR(SSL, SSL_R_ERROR_PARSING_EXTENSION);
      ERR_add_error_dataf("extension %u", (unsigned)type);
      return false;
    }

で Chromium の側(=client)ではこれの逆側なのでそれを調べていく

書き込み時にはssl_add_clienthello_tlsext_innerkExtension.add_clientheloが呼ばれている20

    if (!kExtensions[i].add_clienthello(hs, &extensions, compressed.get(),
                                        ssl_client_hello_inner)) {
      OPENSSL_PUT_ERROR(SSL, SSL_R_ERROR_ADDING_EXTENSION);
      ERR_add_error_dataf("extension %u", (unsigned)kExtensions[i].value);
      return false;
    }

kExtensiontls_extensionの配列でありtls_extensionの定義は以下である。 21

struct tls_extension {
  uint16_t value;

  bool (*add_clienthello)(const SSL_HANDSHAKE *hs, CBB *out,
                          CBB *out_compressible, ssl_client_hello_type_t type);
  bool (*parse_serverhello)(SSL_HANDSHAKE *hs, uint8_t *out_alert,
                            CBS *contents);

  bool (*parse_clienthello)(SSL_HANDSHAKE *hs, uint8_t *out_alert,
                            CBS *contents);
  bool (*add_serverhello)(SSL_HANDSHAKE *hs, CBB *out);
};

よってext_sigalgs_add_clienthello14を調べる22

static bool ext_sigalgs_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
                                        CBB *out_compressible,
                                        ssl_client_hello_type_t type) {
  if (hs->max_version < TLS1_2_VERSION) {
    return true;
  }

  CBB contents, sigalgs_cbb;
  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_signature_algorithms) ||
      !CBB_add_u16_length_prefixed(out_compressible, &contents) ||
      !CBB_add_u16_length_prefixed(&contents, &sigalgs_cbb) ||
      !tls12_add_verify_sigalgs(hs, &sigalgs_cbb) ||
      !CBB_flush(out_compressible)) {
    return false;
  }

  return true;
}

ext_sigalgs_add_clienthellotls12_add_verify_sigalgsで取得したデータを追加している23

bool tls12_add_verify_sigalgs(const SSL_HANDSHAKE *hs, CBB *out) {
  for (uint16_t sigalg : tls12_get_verify_sigalgs(hs)) {
    if (!CBB_add_u16(out, sigalg)) {
      return false;
    }
  }
  return true;
}

tls12_add_verify_sigalgsはverify_sigalgskVerifySignatureAlgorithmsを使うようだ24

static Span<const uint16_t> tls12_get_verify_sigalgs(const SSL_HANDSHAKE *hs) {
  if (hs->config->verify_sigalgs.empty()) {
    return Span<const uint16_t>(kVerifySignatureAlgorithms);
  }
  return hs->config->verify_sigalgs;
}

kVerifySignatureAlgorithmsのほうにも ED25519 はない…25

// kVerifySignatureAlgorithms is the default list of accepted signature
// algorithms for verifying.
static const uint16_t kVerifySignatureAlgorithms[] = {
    // List our preferred algorithms first.
    SSL_SIGN_ECDSA_SECP256R1_SHA256,
    SSL_SIGN_RSA_PSS_RSAE_SHA256,
    SSL_SIGN_RSA_PKCS1_SHA256,

    // Larger hashes are acceptable.
    SSL_SIGN_ECDSA_SECP384R1_SHA384,
    SSL_SIGN_RSA_PSS_RSAE_SHA384,
    SSL_SIGN_RSA_PKCS1_SHA384,

    SSL_SIGN_RSA_PSS_RSAE_SHA512,
    SSL_SIGN_RSA_PKCS1_SHA512,

    // For now, SHA-1 is still accepted but least preferable.
    SSL_SIGN_RSA_PKCS1_SHA1,
};

だがこちらは 9 個あるのに対してpeer_sigalgsでは 8 個しか来ていなかった、ので多分違う。 そして、verify_sigalgsSSL_set_verify_algorithm_prefsから呼ばれたで設定されており ここで Chromium のソースコードで設定されていたkVerifyPrefsとつながる26

int SSL_set_verify_algorithm_prefs(SSL *ssl, const uint16_t *prefs,
                                   size_t num_prefs) {
  if (!ssl->config) {
    OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
    return 0;
  }

  return set_sigalg_prefs(&ssl->config->verify_sigalgs,
                          MakeConstSpan(prefs, num_prefs));
}

結論
#

結論としてはとりあえず別の署名アルゴリズム27を使ってテストを進めることにした。 quic-go とかは対応してくれているんだけどな ed25519…28

余談
#

chatgpt にめっちゃ推敲してもらった…けど多分読みづらいところがまだあるんだろうな

おまけ
#

探索中に見つけたが本筋とは関係ないリンクとか

なんだこいつ https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/handshake_server.cc#L426

static bool is_probably_jdk11_with_tls13(const SSL_CLIENT_HELLO *client_hello) {

Chromium のソースコードを辿った履歴:

https://github.com/chromium/chromium/blob/main/net/socket/tls_stream_attempt.h#L108 https://github.com/chromium/chromium/blob/c37978c399439b0626603234bf5c40b9debb578f/net/socket/ssl_client_socket.cc#L150 https://github.com/chromium/chromium/blob/c37978c399439b0626603234bf5c40b9debb578f/net/socket/ssl_client_socket_impl.h#L4 https://github.com/chromium/chromium/blob/c37978c399439b0626603234bf5c40b9debb578f/net/socket/ssl_client_socket_impl.cc#L848 SSL_CONTEXT - SSL_CTX_new はここで https://github.com/chromium/chromium/blob/c37978c399439b0626603234bf5c40b9debb578f/net/socket/ssl_client_socket_impl.cc#L187 SSLClientSocketImpl https://github.com/chromium/chromium/blob/c37978c399439b0626603234bf5c40b9debb578f/net/socket/ssl_client_socket_impl.cc#L271 SSLConfig - not SSL_CONTEXT of openssl https://github.com/chromium/chromium/blob/4d6151c6d142dae91e9868946f2464b37db35c34/net/ssl/ssl_config.h#L4 SSL_new 呼び出し https://github.com/chromium/chromium/blob/c37978c399439b0626603234bf5c40b9debb578f/net/socket/ssl_client_socket_impl.cc#L630 openssl の BIO に接続 https://github.com/chromium/chromium/blob/c37978c399439b0626603234bf5c40b9debb578f/net/socket/socket_bio_adapter.cc#L61

耐量子暗号?… https://github.com/chromium/chromium/blob/c37978c399439b0626603234bf5c40b9debb578f/net/socket/ssl_client_socket_impl.cc#L647


  1. 上位鍵による署名も ed25519 であるにはあるが今回の文脈では誤りであった ↩︎

  2. google が openssl からフォークしてメンテナンスしている TLS 実装 https://github.com/google/boringssl

     ↩︎

  3. http/1,http/2,http/3 を統合したものを作ろうとしている。エラーが出たのは TCP の https の文脈であるが筆者が QUIC の話を入れたりしているのはこういう背景による ↩︎

  4. なぜ Brave かというと普段使っているのがそれだからである ↩︎

  5. 署名アルゴリズム。この文脈ではサーバーがクライアントに「自分は某というサーバーだよ。ほらこれが公開鍵と証拠の署名ね」とするときにサーバーがする署名の方法。具体的にいうと CertificateVerify で使うやつ。 厳密な定義や使い方は自分で調べてね。 ↩︎

  6. エドワーズ曲線デジタル署名アルゴリズムの一種。短い鍵で RSA 並の安全性と高速化が図られている https://ja.wikipedia.org/wiki/%E3%82%A8%E3%83%89%E3%83%AF%E3%83%BC%E3%82%BA%E6%9B%B2%E7%B7%9A%E3%83%87%E3%82%B8%E3%82%BF%E3%83%AB%E7%BD%B2%E5%90%8D%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0 ↩︎

  7. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L4154 ↩︎

  8. https://github.com/quic-go/quic-go ↩︎

  9. なんか安全で高速っていうのをみて「じゃあそれでいいんじゃない?」という安易な発想に基づく ↩︎

  10. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L405 ↩︎

  11. https://github.com/chromium/chromium/blob/c37978c399439b0626603234bf5c40b9debb578f/net/socket/ssl_client_socket_impl.cc#L750 https://issues.chromium.org/issues/42290575 ↩︎

  12. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L4139 ↩︎

  13. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L4090 ↩︎

  14. https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.4 で定義されている定数。 https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.4.1 でフォーマットが定義され ext_sigalgs_add_clienthelloext_sigalgs_parse_clienthello と対応している ↩︎ ↩︎ ↩︎

  15. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L1045 ↩︎

  16. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L3118 (kExtension 先頭) https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L3185 ↩︎

  17. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/include/openssl/tls1.h#L192 ↩︎

  18. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L3333 ↩︎

  19. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L3643 ↩︎

  20. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L3394 ↩︎

  21. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L485 ↩︎

  22. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L1037 ↩︎

  23. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L436 ↩︎

  24. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L428 ↩︎

  25. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L385 ↩︎

  26. https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/ssl_privkey.cc#L954 ↩︎

  27. ECDSA あたりかな ↩︎

  28. go の標準ライブラリが以下の通りデフォルトの署名アルゴリズムを使ってるからですね多分 https://github.com/golang/go/blob/2f507985dc24d198b763e5568ebe5c04d788894f/src/crypto/tls/defaults.go#L30

    // defaultSupportedSignatureAlgorithms contains the signature and hash algorithms that
    // the code advertises as supported in a TLS 1.2+ ClientHello and in a TLS 1.2+
    // CertificateRequest. The two fields are merged to match with TLS 1.3.
    // Note that in TLS 1.2, the ECDSA algorithms are not constrained to P-256, etc.
    var defaultSupportedSignatureAlgorithms = []SignatureScheme{
        PSSWithSHA256,
        ECDSAWithP256AndSHA256,
        Ed25519,
        PSSWithSHA384,
        PSSWithSHA512,
        PKCS1WithSHA256,
        PKCS1WithSHA384,
        PKCS1WithSHA512,
        ECDSAWithP384AndSHA384,
        ECDSAWithP521AndSHA512,
        PKCS1WithSHA1,
        ECDSAWithSHA1,
    }
    
     ↩︎