訂正#
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_sigalgs
はhs->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_sigalgs
はtls1_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_sigalgs
は ext_sigalgs_parse_clienthello
14から呼ばれていて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_algorithms
14の定義は以下の通り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_inner
でkExtension.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;
}
kExtension
はtls_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_clienthello
14を調べる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_clienthello
はtls12_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_sigalgs
かkVerifySignatureAlgorithms
を使うようだ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_sigalgs
はSSL_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 にめっちゃ推敲してもらった…けど多分読みづらいところがまだあるんだろうな
おまけ#
探索中に見つけたが本筋とは関係ないリンクとか
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
上位鍵による署名も ed25519 であるにはあるが今回の文脈では誤りであった ↩︎
google が openssl からフォークしてメンテナンスしている TLS 実装 https://github.com/google/boringssl
4194b7e6-be19-4190-8775-625d7fde821eja↩︎http/1,http/2,http/3 を統合したものを作ろうとしている。エラーが出たのは TCP の https の文脈であるが筆者が QUIC の話を入れたりしているのはこういう背景による ↩︎
署名アルゴリズム。この文脈ではサーバーがクライアントに「自分は某というサーバーだよ。ほらこれが公開鍵と証拠の署名ね」とするときにサーバーがする署名の方法。具体的にいうと CertificateVerify で使うやつ。 厳密な定義や使い方は自分で調べてね。 ↩︎
エドワーズ曲線デジタル署名アルゴリズムの一種。短い鍵で 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 ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L4154 ↩︎
なんか安全で高速っていうのをみて「じゃあそれでいいんじゃない?」という安易な発想に基づく ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L405 ↩︎
https://github.com/chromium/chromium/blob/c37978c399439b0626603234bf5c40b9debb578f/net/socket/ssl_client_socket_impl.cc#L750 https://issues.chromium.org/issues/42290575 ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L4139 ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L4090 ↩︎
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_clienthello
やext_sigalgs_parse_clienthello
と対応している ↩︎ ↩︎ ↩︎https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L1045 ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L3118 (
kExtension
先頭) https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L3185 ↩︎https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/include/openssl/tls1.h#L192 ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L3333 ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L3643 ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L3394 ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L485 ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L1037 ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L436 ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L428 ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/extensions.cc#L385 ↩︎
https://github.com/google/boringssl/blob/76968bb3d53982560bcf08bcd0ba3e1865fe15cd/ssl/ssl_privkey.cc#L954 ↩︎
ECDSA あたりかな ↩︎
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, }