This commit is contained in:
yhirose
2025-12-30 22:37:54 -05:00
parent 4045faffeb
commit 369b93613e
9 changed files with 4078 additions and 1528 deletions

2
.gitignore vendored
View File

@@ -33,8 +33,10 @@ example/*.pem
test/httplib.cc
test/httplib.h
test/test
test/test_mbedtls
test/server_fuzzer
test/test_proxy
test/test_proxy_mbedtls
test/test_split
test/test.xcodeproj/xcuser*
test/test.xcodeproj/*/xcuser*

75
docs/code-cleanup.md Normal file
View File

@@ -0,0 +1,75 @@
# TLS抽象化API導入後のコード整理・簡潔化ガイド
---
## 1. 共通インターフェースの徹底利用
- `tls_ctx_t`, `tls_session_t`, `tls_cert_t` などの抽象型を積極的に使い、API呼び出し部分でのみバックエンド分岐する。
- 例:
```cpp
// 共通API
bool tls_set_server_cert_pem(tls_ctx_t ctx, const char *cert, const char *key, const char *password);
// バックエンドごとの実装は .cc で分岐
```
## 2. 条件付きコンパイルの整理
- `#ifdef CPPHTTPLIB_OPENSSL_SUPPORT` や `#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT` の分岐は、実装部(.ccに集約し、ヘッダでは極力抽象APIのみ宣言。
- 共通部分は `#ifdef` を減らし、実装のみに限定。
## 3. 重複コードの関数化
- 例えば、証明書のロードやエラー処理など、OpenSSL/MbedTLSで似た処理は共通関数にまとめる。
- 例:
```cpp
bool tls_load_ca(tls_ctx_t ctx, const std::string &file, const std::string &dir);
```
## 4. 共通エラーハンドリング
- `TlsError` 構造体や `ErrorCode` enum を全バックエンドで統一利用。
- エラー文字列変換も共通APIでラップ。
## 5. クライアント・サーバの共通化
- `SSLClient`/`SSLServer` のコンストラクタや証明書更新処理は、内部で `tls_*` APIを呼ぶだけにし、分岐を減らす。
## 6. 冗長なパラメータ・状態管理の削減
- 例えば `ctx_` や `session_` の型は `void*` で統一し、型キャストは実装部でのみ行う。
- `host_components_` の分割処理なども共通関数化。
## 7. コメント・ドキュメントの整理
- バックエンドごとの注意点は実装部にコメントを集約し、ヘッダはAPI仕様のみに集中。
---
### 例: 冗長な証明書ロード処理の統一
```cpp
// filepath: /Users/yuji/Projects/cpp-httplib-ssl/httplib.h
// ...existing code...
namespace detail {
namespace tls {
// 共通API
inline bool tls_load_ca(tls_ctx_t ctx, const std::string &file, const std::string &dir) {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
if (!file.empty()) return tls_load_ca_file(ctx, file.c_str());
if (!dir.empty()) return tls_load_ca_dir(ctx, dir.c_str());
#elif defined(CPPHTTPLIB_MBEDTLS_SUPPORT)
if (!file.empty()) return tls_load_ca_file(ctx, file.c_str());
if (!dir.empty()) return tls_load_ca_dir(ctx, dir.c_str());
#endif
return false;
}
} // namespace tls
} // namespace detail
// ...existing code...
```
---
## まとめ
- 抽象APIの利用と実装部での分岐を徹底し、ヘッダはシンプルに。
- 重複処理は関数化し、条件付きコンパイルは最小限に。
- 安全性・正確性・パフォーマンスは維持しつつ、可読性・保守性を向上。
---
ご要望に応じて、具体的なリファクタリング案やコード修正例をさらにご提案できます。どの部分を優先的に整理したいかご指定ください。

View File

@@ -0,0 +1,272 @@
# httplib TLS 抽象化移行ガイド
## 概要
OpenSSL に依存する `httplib.h` の TLS 実装を、バックエンド非依存の `detail::tls` 抽象レイヤーに移行する計画と進捗。
**現在の状態:**
- OpenSSL: **554/554 テスト合格**
- Mbed TLS: **548/548 テスト合格**
- 差分: **6 テスト** (OpenSSL 固有型を直接使用)
---
## 基本チェック項目
各フェーズで確認すること:
- [ ] 既存の公開 API に破壊的変更がない
- [ ] `cd test && make` 通過
- [ ] `cd test && make test_split` 通過
- [ ] メモリリークなし、コンパイル警告なし
- [ ] `make style_check` 通過
---
## フェーズ 1〜4: OpenSSL 抽象化 ✅ 完了
| フェーズ | 内容 | 状態 |
| --- | --- | --- |
| 1 | `namespace detail::tls` 追加、36 個の API 宣言と OpenSSL 実装 | ✅ |
| 2 | `SSLClient`, `SSLSocketStream` を抽象 API に移行 | ✅ |
| 3 | `SSLServer` を抽象 API に移行 | ✅ |
| 4 | 古い `detail::ssl_*` ヘルパーを削除 | ✅ |
---
## フェーズ 5: Mbed TLS バックエンド ✅ 完了
**ビルド:** `cd test && make test_mbedtls`
**参考:** [Mbed TLS GitHub](https://github.com/Mbed-TLS/mbedtls)
### 実装済み機能
- グローバル初期化 (`tls_global_init/cleanup`)
- クライアント/サーバーコンテキスト
- セッション管理、非ブロッキングハンドシェイク
- I/O (`tls_read/write/pending/shutdown`)
- 証明書検証 (DNS 名 + IP アドレス SAN)
- システム証明書 (Windows/macOS)
- HTTPS→HTTPS リダイレクト時の CA 転送
- クライアント証明書認証 (Mutual TLS)
- 証明書イントロスペクション (`tls_get_cert_subject_cn()` 等)
### OpenSSL 専用テスト6 テスト)
以下のテストは OpenSSL 固有型 (`X509*`, `EVP_PKEY*`, `X509_STORE*`, `SSL_CTX*`) を直接使用するため、OpenSSL 専用として残存。各テストには抽象化 API を使用した代替テストが用意されている。
| OpenSSL 専用テスト | 代替テスト(両バックエンド対応) | 備考 |
| --- | --- | --- |
| `UpdateCAStore` | `UpdateCAStoreWithPem` | `X509_STORE*` を直接使用 |
| `MemoryClientCertPresent` | `PemMemoryClientCertPresent` | `X509*`, `EVP_PKEY*` を直接使用 |
| `MemoryClientEncryptedCertPresent` | `PemMemoryClientEncryptedCertPresent` | 同上 |
| `ClientCAListFromX509Store` | `ClientCAListFromPem` | `X509_STORE*` を直接使用 |
| `CustomizeServerSSLCtx` | `CustomizeServerSSLCtxGeneric` | `SSL_CTX` コールバックコンストラクタ |
| `Issue2251_ClientCertFileNotMatchingKey` | なし | OpenSSL 固有の検出タイミング |
**注記:**
- `Issue2251_ClientCertFileNotMatchingKey` は OpenSSL が証明書/鍵不一致をコンテキスト作成時に検出する挙動をテスト。Mbed TLS は接続時まで検出しないため、バックエンド固有の挙動として許容。
- `CustomizeServerSSLCtx``SSLServer(std::function<bool(SSL_CTX&)>)` コンストラクタをテスト。`CustomizeServerSSLCtxGeneric` はバックエンド非依存の `SSLServer(std::function<bool(void*)>)` コンストラクタを使用。
- `CustomizeServerSSLCtxMbedTLS` は mbedTLS 固有の `SSLServer(std::function<bool(mbedtls_ssl_config&)>)` コンストラクタをテスト。
---
## フェーズ 6-8: 将来
| フェーズ | 内容 | 状態 |
| --- | --- | --- |
| 6 | wolfSSL バックエンド | 未着手 |
| 7 | CI マトリックステスト | 未着手 |
| 8 | README.md 更新 | 未着手 |
---
## API 一覧
```cpp
namespace httplib::detail::tls {
// エラー型
enum class ErrorCode : int {
Success = 0, WantRead, WantWrite, PeerClosed, Fatal, SyscallError,
CertVerifyFailed, HostnameMismatch,
};
struct TlsError { ErrorCode code; uint64_t backend_code; int sys_errno; };
// ハンドル
using tls_ctx_t = void*;
using tls_session_t = void*;
using tls_cert_t = void*;
using tls_ca_store_t = void*;
// グローバル
bool tls_global_init();
void tls_global_cleanup();
// コンテキスト
tls_ctx_t tls_create_client_context();
tls_ctx_t tls_create_server_context();
void tls_free_context(tls_ctx_t ctx);
bool tls_set_min_version(tls_ctx_t ctx, int version);
bool tls_load_ca_pem(tls_ctx_t ctx, const char* pem, size_t len);
bool tls_load_ca_file(tls_ctx_t ctx, const char* path);
bool tls_load_ca_dir(tls_ctx_t ctx, const char* path);
bool tls_load_system_certs(tls_ctx_t ctx);
bool tls_set_client_cert_pem(tls_ctx_t, const char* cert, const char* key, const char* pw);
bool tls_set_client_cert_file(tls_ctx_t, const char* cert, const char* key, const char* pw);
bool tls_set_server_cert_pem(tls_ctx_t, const char* cert, const char* key, const char* pw);
bool tls_set_server_cert_file(tls_ctx_t, const char* cert, const char* key, const char* pw);
bool tls_set_client_ca_file(tls_ctx_t ctx, const char* ca_file, const char* ca_dir);
void tls_set_verify_client(tls_ctx_t ctx, bool require);
// セッション
tls_session_t tls_create_session(tls_ctx_t ctx, socket_t sock);
void tls_free_session(tls_session_t session);
bool tls_set_sni(tls_session_t session, const char* hostname);
bool tls_set_hostname(tls_session_t session, const char* hostname);
// ハンドシェイク
TlsError tls_connect(tls_session_t session);
TlsError tls_accept(tls_session_t session);
bool tls_connect_nonblocking(tls_session_t, socket_t, time_t sec, time_t usec, TlsError*);
bool tls_accept_nonblocking(tls_session_t, socket_t, time_t sec, time_t usec, TlsError*);
// I/O
ssize_t tls_read(tls_session_t, void* buf, size_t len, TlsError& err);
ssize_t tls_write(tls_session_t, const void* buf, size_t len, TlsError& err);
int tls_pending(tls_session_t session);
void tls_shutdown(tls_session_t session, bool graceful);
bool tls_is_peer_closed(tls_session_t session, socket_t sock);
// 証明書
tls_cert_t tls_get_peer_cert(tls_session_t session);
void tls_free_cert(tls_cert_t cert);
bool tls_verify_hostname(tls_cert_t cert, const char* hostname);
long tls_get_verify_result(tls_session_t session);
// 証明書イントロスペクション
std::string tls_get_cert_subject_cn(tls_cert_t cert);
std::string tls_get_cert_issuer_name(tls_cert_t cert);
std::vector<TlsSanEntry> tls_get_cert_sans(tls_cert_t cert);
bool tls_get_cert_validity(tls_cert_t cert, std::time_t& not_before, std::time_t& not_after);
std::string tls_get_cert_serial(tls_cert_t cert);
// CA ストア
tls_ca_store_t tls_create_ca_store(const char* pem, size_t len);
void tls_free_ca_store(tls_ca_store_t store);
bool tls_set_ca_store(tls_ctx_t ctx, tls_ca_store_t store);
// エラー
uint64_t tls_peek_error();
uint64_t tls_get_error();
std::string tls_error_string(uint64_t code);
} // namespace
```
---
## バックエンド比較
| API | OpenSSL | Mbed TLS | wolfSSL |
| --- | --- | --- | --- |
| `tls_global_init()` | `OPENSSL_init_ssl()` | `mbedtls_entropy_init()` | `wolfSSL_Init()` |
| `tls_create_client_context()` | `SSL_CTX_new()` | `MbedTlsContext` 構造体 | `wolfSSL_CTX_new()` |
| `tls_create_session()` | `SSL_new()` | `MbedTlsSession` 構造体 | `wolfSSL_new()` |
| `tls_read/write()` | `SSL_read/write()` | `mbedtls_ssl_read/write()` | `wolfSSL_read/write()` |
| `tls_get_peer_cert()` | `SSL_get1_peer_certificate()` | `mbedtls_ssl_get_peer_cert()` | `wolfSSL_get_peer_certificate()` |
| `tls_verify_hostname()` | `X509_check_host()` | 手動実装 (SAN 検証) | `wolfSSL_X509_check_host()` |
| `tls_is_peer_closed()` | `SSL_peek()` | `select()` + `recv(MSG_PEEK)` | `wolfSSL_peek()` |
| `tls_create_ca_store()` | `X509_STORE_new()` + PEM 解析 | `mbedtls_x509_crt` + PEM 解析 | `wolfSSL_X509_STORE_new()` |
| `tls_set_ca_store()` | `SSL_CTX_set_cert_store()` | `mbedtls_ssl_conf_ca_chain()` | `wolfSSL_CTX_set_cert_store()` |
| ErrorCode | OpenSSL | Mbed TLS | wolfSSL |
| --- | --- | --- | --- |
| `WantRead` | `SSL_ERROR_WANT_READ` | `MBEDTLS_ERR_SSL_WANT_READ` | `SSL_ERROR_WANT_READ` |
| `WantWrite` | `SSL_ERROR_WANT_WRITE` | `MBEDTLS_ERR_SSL_WANT_WRITE` | `SSL_ERROR_WANT_WRITE` |
| `PeerClosed` | `SSL_ERROR_ZERO_RETURN` | `MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY` | `SSL_ERROR_ZERO_RETURN` |
---
## 設計メモ
### バックエンド固有 API
共通 API + バックエンド固有 API の方式を採用:
```cpp
// 共通 API全バックエンド
class SSLClient {
void *tls_context() const; // バックエンド非依存
void set_ca_cert_path(const std::string& path);
void load_ca_cert_store(const char* pem, size_t len);
};
class SSLServer {
void *tls_context() const; // バックエンド非依存
// コールバックで ctx を設定void* を適切な型にキャスト)
SSLServer(const std::function<bool(void *ctx)> &setup_callback);
};
// バックエンド固有 API
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
class SSLClient {
SSL_CTX* ssl_context() const;
void set_ca_cert_store(X509_STORE* store);
};
class SSLServer {
SSL_CTX* ssl_context() const;
// OpenSSL ネイティブ API へのフルアクセス
SSLServer(const std::function<bool(SSL_CTX &ssl_ctx)> &setup_callback);
};
#endif
#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT
class SSLClient {
mbedtls_ssl_config* ssl_config() const;
};
class SSLServer {
mbedtls_ssl_config* ssl_config() const;
// mbedTLS ネイティブ API へのフルアクセス
SSLServer(const std::function<bool(mbedtls_ssl_config &conf)> &setup_callback);
};
#endif
```
### 実装時の教訓
1. **機能を無効化する際は代替実装を用意** - `if (false)` で無効化すると機能が失われる
2. **移行対象を完全にリストアップ** - 間接的に依存する関数も含める
3. **テスト失敗時はデバッグ出力で挙動確認**
4. **`git stash` で元コードとの比較を早めに行う**
### Mbed TLS 固有の注意点
1. **SAN フォーマット**: Mbed TLS 3.x は ASN.1 タグなしの生データDNS は ASCII、IP は 4/16 バイト)
2. **エラーキュー**: スレッドローカル変数 `mbedtls_last_error()` で管理
3. **接続状態確認**: `select()` + `recv(MSG_PEEK)` で TCP レベル確認SSL ステートを変更しない)
4. **ピア証明書保持**: `MBEDTLS_SSL_KEEP_PEER_CERTIFICATE` が有効な場合、`mbedtls_ssl_get_peer_cert()` でクライアント/サーバー両方の証明書を取得可能
---
## テスト
```bash
cd test && make # OpenSSL テスト
cd test && make test_mbedtls # Mbed TLS テスト
cd test && make test_split # 分割ビルドテスト
cd test && make proxy # プロキシテスト (Docker 必要)
```
**証明書ファイル** (`test/gen-certs.sh` で生成):
| ファイル | 用途 |
| --- | --- |
| `cert.pem`, `key.pem` | サーバー証明書 |
| `rootCA.cert.pem` | ルート CA |
| `client.cert.pem`, `client.key.pem` | クライアント証明書 |

View File

@@ -1,84 +0,0 @@
# TLS 抽象化移行チェックリスト
各フェーズで以下を確認すること。
## 基本チェック項目
- [ ] 既存の公開 API に破壊的変更がない
- [ ] 既存テスト全通過 (`cd test && make`)
- [ ] 分割ビルドテスト通過 (`cd test && make test_split`)
- [ ] 新規コードに対応するテスト追加
- [ ] メモリリークなし (valgrind/AddressSanitizer)
- [ ] コンパイル警告なし
- [ ] `make style_check` 通過
## 実装方針
詳細は [httplib_tls_migration.ja.md](httplib_tls_migration.ja.md#実装方針) を参照。
## フェーズ別チェック
### フェーズ 1: TLS 抽象化 API の追加
- [x] `namespace detail::tls` を宣言エリアに追加
- [x] `ErrorCode` enum を定義
- [x] `TlsError` 構造体を定義
- [x] `tls_ctx_t`, `tls_session_t` 型を定義
- [x] 関数宣言を追加
- [x] 実装エリアに OpenSSL バックエンド実装を追加
- [x] 既存コードは変更していない(新 API 追加のみ)
- [x] `make test_split` 通過
### フェーズ 2: SSLClient と SSLSocketStream の移行
- [x] `SSLSocketStream``tls_session_t` を受け取るように変更
- [x] `SSLSocketStream::read()``tls_read()` に置き換え
- [x] `SSLSocketStream::write()``tls_write()` に置き換え
- [x] `SSLClient` が抽象化 API を使用
- [x] `SSLClient::initialize_ssl()``tls_create_session()`, `tls_connect_nonblocking()` 等で移行
- [x] `tls_set_sni()` を追加SNI のみ設定、検証モードは変更しない)
- [x] `tls_connect_nonblocking()`, `tls_accept_nonblocking()` を追加
- [x] CA 証明書の読み込みが動作
- [x] クライアント証明書認証が動作
- [x] SNI とホスト名検証が動作
- [x] 大容量データ転送テスト通過
- [x] タイムアウト処理が正常動作
- [x] `tls_is_peer_closed()``socket_t sock` パラメータを追加して修正
- [x] 不要になった `detail::ssl_new()` 等を削除 → Phase 3 で実施
- [x] `make test_split` 通過
### フェーズ 3: SSLServer の移行
- [x] `SSLServer` が抽象化 API を使用
- [x] `SSLServer::process_and_close_socket()``tls_create_session()`, `tls_accept_nonblocking()` 等で移行
- [x] `process_server_socket_ssl()``tls_session_t` を受け取るように変更
- [x] `SSLClient::shutdown_ssl_impl()``tls_shutdown()`, `tls_free_session()` で移行
- [x] サーバー証明書の設定が動作
- [x] クライアント証明書の検証が動作 (オプション)
- [x] 不要になった `detail::*` 関数を削除
- [x] `detail::ssl_new()` 削除
- [x] `detail::ssl_delete()` 削除
- [x] `detail::ssl_connect_or_accept_nonblocking()` 削除
- [x] `detail::is_ssl_peer_could_be_closed()` 削除
- [x] `detail::load_system_certs_on_windows()` 削除(`tls_load_system_certs()` に統合済み)
- [x] `detail::load_system_certs_on_macos()` および関連ヘルパー削除(`tls_load_system_certs()` に統合済み)
- [x] `make test_split` 通過
### フェーズ 4: 残りの detail ヘルパーの削除とクリーンアップ
- [x] `detail::is_ssl_peer_could_be_closed()` が削除または置き換え済み → `tls_is_peer_closed()` で置き換え後削除
- [x] `detail::load_system_certs_on_windows()``tls_load_system_certs()` 内に統合済み → 削除完了
- [x] `detail::load_system_certs_on_macos()``tls_load_system_certs()` 内に統合済み → 削除完了
- [x] 未使用の SSL 関連 `detail::*` 関数がない
- [x] コンパイル警告なし
- [x] `make test_split` 通過
### フェーズ 5: ドキュメントの更新
- [ ] README.md が更新されている (必要であれば)
### フェーズ 6: 代替バックエンド (将来)
- [ ] mbedTLS または wolfSSL バックエンドの実装
- [ ] バックエンド固有の API が提供されている
- [ ] CI で全バックエンドをテスト

View File

@@ -1,722 +0,0 @@
# httplib TLS 抽象化 — 移行計画
## 目的
OpenSSL に直接依存している `httplib.h` の TLS 実装を、バックエンド非依存の `detail::tls` 抽象レイヤーに段階的に移行する。将来的に mbedTLS 等の代替バックエンドをサポート可能にする。
## 現状分析
### httplib.h の TLS 関連コード
現在 `httplib.h` で OpenSSL を直接呼び出している主要箇所:
| コンポーネント | 行番号 | 機能 |
| ------------------------------------------------ | ----------- | --------------------------------------- |
| `SSLClient` コンストラクタ | 12718-12775 | `SSL_CTX_new()`, クライアント証明書設定 |
| `SSLClient::initialize_ssl()` | 12949-13046 | `detail::ssl_new()`, ハンドシェイク |
| `SSLClient::verify_host()` | 13079-13105 | ホスト名検証の呼び出し元 |
| `SSLClient::verify_host_with_subject_alt_name()` | 13106-13161 | SAN (DNS/IP) による検証 |
| `SSLClient::verify_host_with_common_name()` | 13163-13200 | CN による検証 (フォールバック) |
| `SSLClient::check_host_name()` | 13179-13200 | ワイルドカードマッチング |
| `SSLClient::load_certs()` | 12917-12947 | CA 証明書読み込み (下記参照) |
| `SSLClient::set_ca_cert_store()` | 12787-12800 | CA ストアの設定 |
| `ClientImpl::create_ca_cert_store()` | 12227-12249 | PEM から X509_STORE を作成 |
| リダイレクト処理内 | 10734 | `X509_STORE_up_ref()` で CA ストア共有 |
| `detail::load_system_certs_on_windows()` | 7155-7178 | Windows 証明書ストアから読み込み |
| `detail::load_system_certs_on_macos()` | 7254-7266 | macOS Keychain から読み込み |
| `SSLServer` コンストラクタ | 12517-12608 | サーバー用 `SSL_CTX` 作成 |
| `SSLServer::process_and_close_socket()` | 12631-12670 | `SSL_accept()` |
| `SSLServer::update_certs()` | 12618-12629 | 証明書の動的更新 |
| `SSLServer::extract_ca_names_from_x509_store()` | 12673-12707 | CA 名リストの抽出 |
| `SSLSocketStream` コンストラクタ | 12393-12399 | `SSL_clear_mode(SSL_MODE_AUTO_RETRY)` |
| `SSLSocketStream::is_readable()` | 12403-12405 | `SSL_pending()` で確認 |
| `SSLSocketStream::read()` | 12425-12462 | `SSL_read()`, エラーリトライ |
| `SSLSocketStream::write()` | 12464-12494 | `SSL_write()`, エラーリトライ |
| `detail::ssl_new()` | 12287-12315 | SSL セッション作成, BIO 設定 |
| `detail::ssl_delete()` | 12317-12335 | SSL シャットダウン・解放 |
| `detail::ssl_connect_or_accept_nonblocking()` | 12338-12358 | 非ブロッキングハンドシェイク |
| `detail::is_ssl_peer_could_be_closed()` | 7143-7150 | `SSL_peek()` で接続状態確認 |
### 必要な抽象化 API
`httplib.h` の実装から逆算した、必要最小限の API:
```cpp
namespace httplib::detail::tls {
// エラー型
enum class ErrorCode : int {
Success = 0,
WantRead, // 非ブロッキング: 読み取り待ち
WantWrite, // 非ブロッキング: 書き込み待ち
PeerClosed, // 相手が接続を閉じた
Fatal, // 回復不能エラー
SyscallError, // システムコールエラー (errno)
CertVerifyFailed,
HostnameMismatch,
};
struct TlsError {
ErrorCode code = ErrorCode::Fatal;
uint64_t backend_code = 0; // OpenSSL: ERR_get_error(), mbedTLS: 戻り値
int sys_errno = 0; // SyscallError 時の errno
};
// 不透明ハンドル
using tls_ctx_t = void*;
using tls_session_t = void*;
using tls_cert_t = void*;
// グローバル初期化
bool tls_global_init();
void tls_global_cleanup();
// コンテキスト (クライアント)
tls_ctx_t tls_create_client_context();
void tls_free_context(tls_ctx_t ctx);
bool tls_set_min_version(tls_ctx_t ctx, int version); // TLS1_2_VERSION 等
bool tls_load_ca_pem(tls_ctx_t ctx, const char* pem, size_t len);
bool tls_load_ca_file(tls_ctx_t ctx, const char* file_path);
bool tls_load_ca_dir(tls_ctx_t ctx, const char* dir_path);
bool tls_load_system_certs(tls_ctx_t ctx); // OS 証明書ストア (下記参照)
bool tls_set_client_cert_pem(tls_ctx_t ctx, const char* cert,
const char* key, const char* pw);
bool tls_set_client_cert_file(tls_ctx_t ctx, const char* cert_path,
const char* key_path, const char* pw);
// コンテキスト (サーバー)
tls_ctx_t tls_create_server_context();
bool tls_set_server_cert_pem(tls_ctx_t ctx, const char* cert,
const char* key, const char* pw);
bool tls_set_server_cert_file(tls_ctx_t ctx, const char* cert_path,
const char* key_path, const char* pw);
bool tls_set_client_ca_file(tls_ctx_t ctx, const char* ca_file,
const char* ca_dir);
void tls_set_verify_client(tls_ctx_t ctx, bool require);
// セッション
tls_session_t tls_create_session(tls_ctx_t ctx, socket_t sock);
void tls_free_session(tls_session_t session);
bool tls_set_hostname(tls_session_t session, const char* hostname); // SNI
// ハンドシェイク (非ブロッキング対応)
TlsError tls_connect(tls_session_t session); // クライアント用
TlsError tls_accept(tls_session_t session); // サーバー用
// I/O (非ブロッキング対応)
ssize_t tls_read(tls_session_t session, void* buf, size_t len, TlsError& err);
ssize_t tls_write(tls_session_t session, const void* buf, size_t len,
TlsError& err);
int tls_pending(tls_session_t session); // SSL_pending() 相当
void tls_shutdown(tls_session_t session, bool graceful);
// 接続状態確認
bool tls_is_peer_closed(tls_session_t session); // SSL_peek() で確認
// 証明書検証
tls_cert_t tls_get_peer_cert(tls_session_t session);
void tls_free_cert(tls_cert_t cert);
bool tls_verify_hostname(tls_cert_t cert, const char* hostname);
long tls_get_verify_result(tls_session_t session);
// エラー情報
uint64_t tls_peek_error(); // エラーキューを消費しない
uint64_t tls_get_error(); // エラーキューを消費
std::string tls_error_string(uint64_t code);
} // namespace httplib::detail::tls
```
### SSLClient::load_certs() の詳細
この関数は `std::call_once` で一度だけ実行され、以下の優先順位で CA 証明書を読み込む:
1. `ca_cert_file_path_` が設定済み → `SSL_CTX_load_verify_locations()` でファイルから
2. `ca_cert_dir_path_` が設定済み → `SSL_CTX_load_verify_locations()` でディレクトリから
3. どちらも未設定 → OS 証明書ストアから:
- **Windows**: `CertOpenSystemStoreW(L"ROOT")` で ROOT ストアを開き、`d2i_X509()` で変換
- **macOS**: `SecItemCopyMatching()` + `SecTrustCopyAnchorCertificates()` で Keychain から取得
- **その他**: `SSL_CTX_set_default_verify_paths()` にフォールバック
`tls_load_system_certs()` はこの OS 別ロジックをカプセル化する必要がある。
**注意**: 現在の `load_certs()``std::call_once` で保護されているが、この一度限り実行のロジックは `SSLClient` 側で維持する。TLS 抽象化 API は冪等に設計し、呼び出し側が必要に応じて `std::call_once` を使用する。
## 設計上の注意点
### 1. Windows での ssize_t
Windows には `ssize_t` がないため、ヘッダで定義が必要:
```cpp
#ifdef _WIN32
using ssize_t = ptrdiff_t;
#else
#include <sys/types.h>
#endif
```
### 2. 非ブロッキング I/O のセマンティクス
`tls_read`/`tls_write``WantRead`/`WantWrite` を返した場合:
- 戻り値は `-1`
- `err.code` で待機方向を判定
- 呼び出し側は `select()`/`poll()` で待機後、同じ呼び出しを再試行
### 3. エラーキューの扱い
OpenSSL のエラーキューは副作用がある:
- `ERR_get_error()`: キューから取り出す (消費)
- `ERR_peek_last_error()`: 覗くだけ (非消費)
TLS 抽象化では `tls_peek_error()` を優先し、必要な場合のみ `tls_get_error()` を使用。
### 4. スレッド安全性
- `tls_ctx_t` の作成・更新は `std::mutex` で保護 (現状の `ctx_mutex_` を維持)
- セッションは単一スレッドで使用する前提
### 5. OpenSSL 3.0 以降の要件
最小サポートバージョンは OpenSSL 3.0:
- `SSL_get1_peer_certificate()` を使用 (`SSL_get_peer_certificate()` は非推奨)
- グローバル初期化は `OPENSSL_init_ssl()` を使用
### 6. サーバー側の SSL オプション
`SSLServer` では以下のオプションがハードコードされている:
- `SSL_OP_NO_COMPRESSION` — CRIME 攻撃対策
- `SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION` — 再ネゴシエーション時のセッション再開無効化
- 最小 TLS バージョン: `TLS1_2_VERSION`
抽象化 API では `tls_set_options()` 等で設定可能にするか、デフォルトで上記を適用するかを決める必要がある。
### 7. バックエンド固有の public API
`SSLServer` / `SSLClient` には OpenSSL 固有の型を使用する public API が存在する。
#### 他ライブラリとの比較
| ライブラリ | バックエンド抽象化方式 | バックエンド固有 API |
| --- | --- | --- |
| **libcurl** | 内部に vtls 抽象化レイヤー、ランタイム切り替え可能 | `CURLOPT_SSL_CTX_FUNCTION` コールバックでバックエンド固有の型を公開 |
| **Boost.Asio** | OpenSSL のみ (抽象化なし) | `ssl::context` は OpenSSL の薄いラッパー |
| **Poco** | プラットフォーム別ライブラリ (`NetSSL_OpenSSL`, `NetSSL_Win`) | 各ライブラリ内で固有 API |
| **cpp-httplib** | ビルド時選択 (`#ifdef`) | 共通 API + バックエンド固有 API (本方式) |
libcurl は共有ライブラリとして配布されるためランタイム切り替えをサポートするが、cpp-httplib はヘッダオンリーでアプリケーションに組み込まれるため、**ビルド時選択で十分**。
#### 現在の OpenSSL 固有 API
**SSLServer:**
- `SSLServer(X509*, EVP_PKEY*, X509_STORE*)`
- `SSLServer(std::function<bool(SSL_CTX&)>)`
- `SSL_CTX* ssl_context() const`
- `update_certs(X509*, EVP_PKEY*, X509_STORE*)`
**SSLClient:**
- `SSLClient(..., X509*, EVP_PKEY*, ...)`
- `set_ca_cert_store(X509_STORE*)`
- `long get_openssl_verify_result() const`
- `SSL_CTX* ssl_context() const`
#### 方針: 共通 API + バックエンド固有 API
**共通 API** (全バックエンドで利用可能):
```cpp
class SSLClient {
// バックエンド非依存 — 通常のユースケースをカバー
void set_ca_cert_path(const std::string& path);
void set_ca_cert_dir(const std::string& path);
void load_ca_cert_store(const char* pem, size_t len);
// ... 既存の共通メソッド
};
```
**バックエンド固有 API** (高度なカスタマイズ用):
```cpp
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
class SSLClient {
// OpenSSL 固有
SSL_CTX* ssl_context() const;
void set_ca_cert_store(X509_STORE* store);
long get_openssl_verify_result() const;
};
#endif
#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT
class SSLClient {
// mbedTLS 固有
mbedtls_ssl_config* ssl_config() const;
mbedtls_x509_crt* ca_chain() const;
uint32_t get_mbedtls_verify_result() const;
};
#endif
#ifdef CPPHTTPLIB_WOLFSSL_SUPPORT
class SSLClient {
// wolfSSL 固有
WOLFSSL_CTX* ssl_context() const;
long get_wolfssl_verify_result() const;
};
#endif
```
#### この方式の利点
- **通常のユースケース**: 共通 API で十分 (ファイルパス、PEM 文字列等)
- **高度なカスタマイズ**: バックエンド固有 API で対応
- **既存コードの破壊的変更なし**: OpenSSL 固有 API はそのまま維持
- **型安全**: キャスト不要、IDE 補完が効く
- **バックエンド切り替え時はコンパイルエラーで検出**: 安全な移行
### 8. SSL_MODE_AUTO_RETRY の無効化
`SSLSocketStream` コンストラクタで `SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY)` を呼び出している。これにより `SSL_read`/`SSL_write``SSL_ERROR_WANT_READ`/`SSL_ERROR_WANT_WRITE` を返すようになり、非ブロッキング I/O が正しく動作する。`tls_create_session()` でこれをデフォルトで設定すべき。
### 9. CMake ビルドシステムの考慮
`CMakeLists.txt` には既に以下の OpenSSL 関連オプションが存在する:
```cmake
set(_HTTPLIB_OPENSSL_MIN_VER "3.0.0")
option(HTTPLIB_USE_OPENSSL_IF_AVAILABLE ...)
option(HTTPLIB_REQUIRE_OPENSSL ...)
option(HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN ...)
```
代替バックエンド追加時 (フェーズ 6) に以下の変更が必要:
1. **新しいオプションの追加**:
- `HTTPLIB_USE_MBEDTLS_IF_AVAILABLE`
- `HTTPLIB_REQUIRE_MBEDTLS`
- `HTTPLIB_USE_WOLFSSL_IF_AVAILABLE`
- `HTTPLIB_REQUIRE_WOLFSSL`
2. **排他制御**: 複数の TLS バックエンドを同時に有効化できないようにする
3. **CI 拡張** (`.github/workflows/test.yaml`): バックエンド別のテスト matrix を追加
### 10. 既存の公開エラー API
README.md で文書化されている SSL エラー API:
```cpp
res.ssl_error() // SSL 固有のエラーコード
res.ssl_openssl_error() // OpenSSL エラーコード (ERR_get_error())
```
これらは `httplib::Result` クラスのメンバとして実装されている。バックエンド切り替え時:
- `ssl_error()` → 共通 (抽象化された `ErrorCode`)
- `ssl_openssl_error()` → OpenSSL 固有 (名前を維持して互換性確保)
- 将来: `ssl_mbedtls_error()`, `ssl_wolfssl_error()` を追加
## 段階的移行ステップ
### 実装方針
`httplib.h` に直接変更を加える。別ファイル (`httplib_tls.h`) を作成せず、始めから `httplib.h` 内に `detail::tls` namespace を追加していく。
**理由:**
- 最後の統合フェーズで発生し得る問題名前衝突、include 順序、宣言/実装の配置ミス)を回避
- `split.py` による分割ビルド (`make test_split`) との互換性を常に確認可能
- シングルファイル・ヘッダーオンリーの形態を維持
### 実装時の教訓Phase 2 より)
Phase 2 の実装で学んだ重要な教訓:
#### 1. 機能を無効化する際は代替実装を用意する
一時的な回避策として `if (false)` やコメントアウトで機能を無効化すると、テストが通っているように見えても実際には機能が失われている場合がある。
**悪い例:**
```cpp
// TODO: need tls_peek() for is_ssl_peer_could_be_closed()
// if (detail::is_ssl_peer_could_be_closed(socket_.session, socket_.sock)) {
if (false) {
is_alive = false;
}
```
**良い例:**
```cpp
// 一時的に SSL* へキャストして既存関数を使用
// TODO: tls_is_peer_closed() を TLS API に追加後、置き換える
if (detail::is_ssl_peer_could_be_closed(
static_cast<SSL *>(socket_.session), socket_.sock)) {
is_alive = false;
}
```
#### 2. 移行対象の機能を完全にリストアップする
`SSL*``void*` への型変更時、全ての使用箇所を事前にリストアップする。特に:
- 直接使用している関数
- 間接的に依存している関数(例: `is_ssl_peer_could_be_closed()``SSL_peek()` を使用)
- テストで検証されている機能
#### 3. テスト失敗時はデバッグ出力で挙動を確認する
テストが失敗した場合、「動作していない」と決めつけず、実際の挙動を確認する:
```cpp
fprintf(stderr, "[DEBUG] wait_readable: max=%ld dur=%ld actual=%ld.%ld\n",
(long)max_timeout_msec_, (long)dur, (long)read_timeout_sec,
(long)read_timeout_usec);
```
Phase 2 では、タイムアウト自体は正常に動作していたが、サーバー側の接続終了検出が無効化されていたことが原因だった。
#### 4. 元のコードとの比較を早めに行う
問題が発生したら、`git stash` で元のコードに戻してテストを実行し、変更が原因かどうかを素早く確認する:
```bash
git stash # 変更を退避
make test && ./test # 元のコードでテスト
git stash pop # 変更を復元
```
#### 5. TODO コメントは具体的なタスクとして追跡する
`// TODO:` コメントを書いただけで終わらせず、実際に実装するまで追跡する。本ドキュメントの「移行チェックリスト」または外部のタスク管理ツールで管理すること。
#### 6. テストの失敗メッセージを正確に読む
```
test.cc:11248: Failure
Value of: success
Actual: true
Expected: false
```
この情報から「サーバー側のコールバックが成功と認識している」ことが読み取れる。クライアント側ではなくサーバー側の問題であることを示唆していた。
**httplib.h の構造:**
```text
宣言エリア(クラス定義、関数宣言)
// ----------------------------------------------------------------------------
/*
* Implementation that will be part of the .cc file if split into .h + .cc.
*/
実装エリア関数実装、inline 関数)
```
上記のコメントは `split.py``.h` ファイルと `.cc` ファイルを分割する際の境界となる。
- **宣言エリア** (境界より上): クラス定義、型定義、関数宣言を配置
- **実装エリア** (境界より下): 関数の実装を配置(`inline` 必須)
各フェーズで宣言は宣言エリアに、実装は実装エリアに配置する。`make test_split` で分割ビルドが動作することを常に確認すること。
**検証コマンド:**
```bash
cd test && make # 通常ビルド + テスト
cd test && make test_split # 分割ビルドのテスト
```
### フェーズ 1: TLS 抽象化 API の追加
`httplib.h``detail::tls` namespace を追加し、抽象化 API を実装する。
**タスク:**
1. 宣言エリアに `namespace detail::tls` を追加
- `ErrorCode` enum
- `TlsError` 構造体
- `tls_ctx_t`, `tls_session_t` 型定義
- 関数宣言
2. 実装エリアに OpenSSL バックエンド実装 (`#ifdef CPPHTTPLIB_OPENSSL_SUPPORT` 内)
3. この時点では既存コードは変更しない(新 API を追加するのみ)
**検証:**
```bash
cd test && make clean && make
cd test && make test_split
```
### フェーズ 2: SSLClient と SSLSocketStream の移行
`SSLClient``SSLSocketStream``detail::tls::*` API を使うように変更。
**注意:** `SSLClient``SSLSocketStream` を内部で使用するため、両者を同時に移行する必要がある。`tls_create_session()` が返す `tls_session_t``SSLSocketStream` が受け取る形に変更する。
**変更順序:**
1. `SSLSocketStream` の変更:
- コンストラクタ: `SSL*``tls_session_t` を受け取るように変更
- `read()``tls_read()`
- `write()``tls_write()`
- `is_readable()``tls_pending()` を使用
2. `SSLClient` の変更:
- コンストラクタ → `tls_create_client_context()`, `tls_set_client_cert_*()`
- `load_certs()``tls_load_system_certs()`, `tls_load_ca_*()`
- `initialize_ssl()``tls_create_session()`, `tls_set_hostname()`, `tls_connect()`
- `verify_host()``tls_get_peer_cert()`, `tls_verify_hostname()`
- `shutdown_ssl_impl()``tls_shutdown()`, `tls_free_session()`
3. 不要になった `detail::*` 関数の削除:
- `detail::ssl_new()`
- `detail::ssl_delete()`
- `detail::ssl_connect_or_accept_nonblocking()` (クライアント部分)
**検証:**
- 既存テスト全通過
- HTTPS クライアント接続テスト (google.com 等)
- 証明書検証エラーのテスト
- 大容量データ転送テスト
- タイムアウト処理の確認
### フェーズ 3: SSLServer の移行
`SSLServer``detail::tls::*` API を使うように変更。
**変更順序:**
1. `SSLServer` コンストラクタ → `tls_create_server_context()`, `tls_set_server_cert_*()`
2. `SSLServer::process_and_close_socket()``tls_create_session()`, `tls_accept()`
3. クライアント証明書検証 → `tls_get_peer_cert()`, `tls_get_verify_result()`
4. 不要になった `detail::*` 関数の削除:
- `detail::ssl_connect_or_accept_nonblocking()` (サーバー部分、残っていれば)
**検証:**
- サーバー起動・接続テスト
- クライアント証明書認証テスト
- 全テスト通過
### フェーズ 4: 残りの detail ヘルパーの削除とクリーンアップ
残っている不要な関数を削除:
- `detail::is_ssl_peer_could_be_closed()``tls_is_peer_closed()` に置き換え済みか確認
- `detail::load_system_certs_on_windows()``tls_load_system_certs()` 内に移動済み
- `detail::load_system_certs_on_macos()``tls_load_system_certs()` 内に移動済み
- その他、使用されていない SSL 関連の `detail::*` 関数
**検証:**
- 全テスト通過
- コンパイル警告なし
- 未使用コードがないことを確認
### フェーズ 5: ドキュメントの更新
**タスク:**
1. **CLAUDE.md** の更新
- TLS 抽象化に関するセクションを実装後の状態に更新
2. **README.md** の更新 (必要であれば)
- TLS 関連の説明を追加
**検証:**
- ドキュメントが実装と一致していることを確認
### フェーズ 6: 代替バックエンド (将来)
mbedTLS / wolfSSL バックエンドの追加:
1. `#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT` または `#ifdef CPPHTTPLIB_WOLFSSL_SUPPORT` で実装を分岐
2. CI で全バックエンドをテスト
## 代替バックエンド互換性分析
本節では、設計した抽象化 API が mbedTLS および wolfSSL に対応可能かを検証する。
### API マッピング表
| 抽象化 API | OpenSSL | mbedTLS | wolfSSL |
| --- | --- | --- | --- |
| `tls_global_init()` | `OPENSSL_init_ssl()` | 不要 (ライブラリ初期化なし) | `wolfSSL_Init()` |
| `tls_global_cleanup()` | (3.x は不要) | `mbedtls_*_free()` で個別解放 | `wolfSSL_Cleanup()` |
| `tls_create_client_context()` | `SSL_CTX_new(TLS_client_method())` | `mbedtls_ssl_config` + `mbedtls_ssl_config_defaults(..., MBEDTLS_SSL_IS_CLIENT, ...)` | `wolfSSL_CTX_new(wolfTLSv1_2_client_method())` |
| `tls_create_server_context()` | `SSL_CTX_new(TLS_server_method())` | `mbedtls_ssl_config_defaults(..., MBEDTLS_SSL_IS_SERVER, ...)` | `wolfSSL_CTX_new(wolfTLSv1_2_server_method())` |
| `tls_free_context()` | `SSL_CTX_free()` | `mbedtls_ssl_config_free()` | `wolfSSL_CTX_free()` |
| `tls_load_ca_pem()` | `X509_STORE` + `PEM_read_bio_X509()` | `mbedtls_x509_crt_parse()` | `wolfSSL_CTX_load_verify_buffer()` |
| `tls_load_ca_file()` | `SSL_CTX_load_verify_locations()` | `mbedtls_x509_crt_parse_file()` | `wolfSSL_CTX_load_verify_locations()` |
| `tls_load_ca_dir()` | `SSL_CTX_load_verify_locations()` | (ループで各ファイルをパース) | `wolfSSL_CTX_load_verify_locations(ctx, NULL, path)` |
| `tls_load_system_certs()` | OS 別実装 | (組み込みなし、カスタム実装必要) | `wolfSSL_CTX_load_system_CA_certs()` |
| `tls_set_client_cert_pem()` | `SSL_CTX_use_certificate()` + `SSL_CTX_use_PrivateKey()` | `mbedtls_x509_crt_parse()` + `mbedtls_pk_parse_key()` | `wolfSSL_CTX_use_certificate_buffer()` + `wolfSSL_CTX_use_PrivateKey_buffer()` |
| `tls_set_min_version()` | `SSL_CTX_set_min_proto_version()` | `mbedtls_ssl_conf_min_version()` | `wolfSSL_CTX_SetMinVersion()` |
| `tls_create_session()` | `SSL_new()` + `SSL_set_fd()` | `mbedtls_ssl_setup()` + `mbedtls_ssl_set_bio()` | `wolfSSL_new()` + `wolfSSL_set_fd()` |
| `tls_free_session()` | `SSL_free()` | `mbedtls_ssl_free()` | `wolfSSL_free()` |
| `tls_set_hostname()` | `SSL_set_tlsext_host_name()` | `mbedtls_ssl_set_hostname()` (SNI + 検証) | `wolfSSL_UseSNI()` + `wolfSSL_check_domain_name()` |
| `tls_connect()` | `SSL_connect()` | `mbedtls_ssl_handshake()` | `wolfSSL_connect()` |
| `tls_accept()` | `SSL_accept()` | `mbedtls_ssl_handshake()` | `wolfSSL_accept()` |
| `tls_read()` | `SSL_read()` | `mbedtls_ssl_read()` | `wolfSSL_read()` |
| `tls_write()` | `SSL_write()` | `mbedtls_ssl_write()` | `wolfSSL_write()` |
| `tls_pending()` | `SSL_pending()` | `mbedtls_ssl_get_bytes_avail()` | `wolfSSL_pending()` |
| `tls_shutdown()` | `SSL_shutdown()` | `mbedtls_ssl_close_notify()` | `wolfSSL_shutdown()` |
| `tls_is_peer_closed()` | `SSL_peek()` で確認 | `mbedtls_ssl_read()` + エラーチェック | `wolfSSL_peek()` |
| `tls_get_peer_cert()` | `SSL_get1_peer_certificate()` | `mbedtls_ssl_get_peer_cert()` | `wolfSSL_get_peer_certificate()` |
| `tls_verify_hostname()` | `X509_check_host()` | `mbedtls_ssl_set_hostname()` (ハンドシェイク時に自動検証) | `wolfSSL_check_domain_name()` |
| `tls_get_verify_result()` | `SSL_get_verify_result()` | `mbedtls_ssl_get_verify_result()` | `wolfSSL_get_verify_result()` |
### 非ブロッキング I/O のエラーコード
| 抽象化 ErrorCode | OpenSSL | mbedTLS | wolfSSL |
| --- | --- | --- | --- |
| `WantRead` | `SSL_ERROR_WANT_READ` | `MBEDTLS_ERR_SSL_WANT_READ` | `SSL_ERROR_WANT_READ` |
| `WantWrite` | `SSL_ERROR_WANT_WRITE` | `MBEDTLS_ERR_SSL_WANT_WRITE` | `SSL_ERROR_WANT_WRITE` |
| `PeerClosed` | `SSL_ERROR_ZERO_RETURN` | `MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY` | `SSL_ERROR_ZERO_RETURN` |
| `SyscallError` | `SSL_ERROR_SYSCALL` | (errno をチェック) | `SSL_ERROR_SYSCALL` |
| `Fatal` | `SSL_ERROR_SSL` | 負のエラーコード | `SSL_ERROR_SSL` |
### バックエンド固有の注意点
#### mbedTLS
1. **グローバル初期化不要**: mbedTLS はグローバル状態を持たない。`tls_global_init()` は単に `true` を返す。
2. **コンテキスト構造の違い**: mbedTLS では `mbedtls_ssl_config` (設定) と `mbedtls_ssl_context` (セッション) が分離している。`tls_ctx_t``ssl_config` を保持し、追加で `mbedtls_x509_crt` (CA チェーン)、`mbedtls_pk_context` (秘密鍵) も管理する必要がある。
3. **システム証明書なし**: mbedTLS には OS 証明書ストアへのアクセス機能がない。`tls_load_system_certs()` は Windows/macOS で独自実装が必要 (OpenSSL 版と同様のロジックを共有可能)。
4. **ホスト名検証の統合**: `mbedtls_ssl_set_hostname()` は SNI 設定とホスト名検証を同時に行う。ハンドシェイク時に自動的に検証されるため、`tls_verify_hostname()` は追加チェックとして呼び出すか、mbedTLS では no-op にできる。
5. **pending の制限**: `mbedtls_ssl_get_bytes_avail()` は既に復号されたデータのみを返す。より正確な値を得るには事前に `mbedtls_ssl_read(ssl, NULL, 0)` を呼ぶ必要があるが、NULL ポインタの扱いに注意。
6. **ハンドシェイクステップ**: `mbedtls_ssl_handshake_step()` で細かい制御が可能だが、通常は `mbedtls_ssl_handshake()` で十分。
#### wolfSSL
1. **OpenSSL 互換 API**: wolfSSL は OpenSSL 互換レイヤーを提供しており、多くの関数名が同じ。移植が容易。
2. **システム証明書**: `wolfSSL_CTX_load_system_CA_certs()` が利用可能。ただし、プラットフォームによっては `NO_FILESYSTEM` が定義されている場合がある。
3. **SNI とホスト名検証の分離**: wolfSSL では `wolfSSL_UseSNI()` (SNI 設定) と `wolfSSL_check_domain_name()` (検証) が別関数。`tls_set_hostname()` で両方を呼び出す。
4. **バッファベース API**: 組み込み環境向けに `wolfSSL_CTX_load_verify_buffer()` 等のメモリバッファ API が充実。`NO_FILESYSTEM` 環境でも動作可能。
5. **DTLS サポート**: wolfSSL は DTLS もサポート。将来的な拡張の余地あり。
### API 設計の妥当性評価
| 評価項目 | 結果 | 備考 |
| --- | --- | --- |
| クライアント接続 | ✅ 対応可能 | 3 バックエンドとも問題なし |
| サーバー接続 | ✅ 対応可能 | mbedTLS はクライアント/サーバーを設定時に指定 |
| 非ブロッキング I/O | ✅ 対応可能 | 全バックエンドが WANT_READ/WANT_WRITE をサポート |
| CA 証明書 (ファイル) | ✅ 対応可能 | 全バックエンドでサポート |
| CA 証明書 (PEM 文字列) | ✅ 対応可能 | バッファ API で対応 |
| CA 証明書 (ディレクトリ) | ⚠️ 要実装 | mbedTLS はループ処理が必要 |
| システム証明書 | ⚠️ 要実装 | mbedTLS は OS 別カスタム実装が必要 |
| クライアント証明書 | ✅ 対応可能 | 全バックエンドでサポート |
| SNI | ✅ 対応可能 | 全バックエンドでサポート |
| ホスト名検証 | ✅ 対応可能 | mbedTLS はハンドシェイク統合、他は事後検証 |
| pending データ | ⚠️ 動作差異 | mbedTLS の `get_bytes_avail` は制限あり |
| TLS バージョン設定 | ✅ 対応可能 | 全バックエンドでサポート |
### 結論
設計した抽象化 API は **mbedTLS および wolfSSL に十分対応可能** である。ただし、以下の点に注意:
1. **mbedTLS のコンテキスト管理**: `tls_ctx_t` の内部構造体は OpenSSL の `SSL_CTX*` より複雑になる (設定 + 証明書 + 鍵を保持)。
2. **システム証明書**: mbedTLS バックエンドでは `tls_load_system_certs()` に OS 別実装が必要。この実装は OpenSSL 版の `load_system_certs_on_windows()` / `load_system_certs_on_macos()` と共通化可能。
3. **tls_pending() の動作差異**: mbedTLS では正確な値を得るために追加の呼び出しが必要な場合がある。httplib の `is_readable()` での使用では、「データがあるか」の判定が主目的なので許容範囲。
4. **ホスト名検証のタイミング**: mbedTLS ではハンドシェイク時に自動検証されるが、OpenSSL/wolfSSL では事後検証。抽象化層でこの差異を吸収する。
**推奨**: フェーズ 6 では wolfSSL を先に実装することを推奨。OpenSSL 互換 API により移植コストが低い
## テスト戦略
### 既存のテスト構成
テストは `test/` ディレクトリで GTest を使用:
```bash
cd test && make test # 通常テスト
cd test && make proxy # プロキシテスト (Docker 必要)
```
**証明書ファイル** (`test/gen-certs.sh` で生成):
| ファイル | 用途 |
| -------- | ---- |
| `cert.pem`, `key.pem` | サーバー証明書 |
| `cert2.pem` | SAN 付きサーバー証明書 |
| `rootCA.cert.pem`, `rootCA.key.pem` | ルート CA |
| `client.cert.pem`, `client.key.pem` | クライアント証明書 |
| `*_encrypted.*` | パスワード付き証明書 |
**主な SSL テストケース** (`test/test.cc`):
- `SSLClientTest::ServerCertificateVerification*` — 証明書検証
- `SSLClientTest::ServerHostnameVerificationError_Online` — ホスト名検証
- `SSLClientTest::WildcardHostNameMatch_Online` — ワイルドカード
- `SSLClientServerTest::ClientCert*` — クライアント証明書認証
- `SSLClientServerTest::SSLConnectTimeout` — 接続タイムアウト
- `SSLClientServerTest::CustomizeServerSSLCtx` — カスタム SSL コンテキスト
- `SSLClientRedirectTest` — リダイレクト時の CA ストア共有
### 新規テスト (TLS 抽象化用)
```cpp
// エラーマッピング
TEST(TlsError, MapSslError) {
// SSL_ERROR_WANT_READ → ErrorCode::WantRead
// SSL_ERROR_WANT_WRITE → ErrorCode::WantWrite
// SSL_ERROR_ZERO_RETURN → ErrorCode::PeerClosed
// SSL_ERROR_SYSCALL → ErrorCode::SyscallError (errno を保存)
// その他 → ErrorCode::Fatal
}
// ホスト名検証
TEST(TlsVerify, Hostname) {
// 完全一致
// ワイルドカード (*.example.com)
// SAN 優先、CN フォールバック
// IP アドレス (GEN_IPADD)
}
```
### 結合テスト
- 自己署名証明書での接続
- 不正な証明書の拒否
- ホスト名不一致の検出
- 非ブロッキング I/O のリトライ
- クライアント証明書認証
- パスワード付き秘密鍵
### CI 設定
```yaml
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
openssl: ["3.0", "3.2"]
```
**注意**:
- OpenSSL 3.2+ では `gen-certs.sh``-x509v1` フラグを使用 (証明書生成の互換性対応)
- macOS では `CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN``-framework Security` が必要
## 移行チェックリスト
詳細は [checklist.md](checklist.md) を参照。

3459
httplib.h

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,14 @@ PREFIX ?= $(shell brew --prefix)
OPENSSL_DIR = $(PREFIX)/opt/openssl@3
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPENSSL_DIR)/lib -lssl -lcrypto
MBEDTLS_DIR = $(PREFIX)/opt/mbedtls
MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -I$(MBEDTLS_DIR)/include -L$(MBEDTLS_DIR)/lib -lmbedtls -lmbedx509 -lmbedcrypto
ifneq ($(OS), Windows_NT)
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S), Darwin)
OPENSSL_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework Security
MBEDTLS_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework Security
endif
endif
@@ -33,6 +37,7 @@ ifneq ($(OS), Windows_NT)
endif
TEST_ARGS = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(ZSTD_SUPPORT) $(LIBS)
TEST_ARGS_MBEDTLS = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(MBEDTLS_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(ZSTD_SUPPORT) $(LIBS)
# By default, use standalone_fuzz_target_runner.
# This runner does no fuzzing, but simply executes the inputs
@@ -69,6 +74,25 @@ proxy : test_proxy
cd proxy && docker compose down; \
exit $$exit_code
proxy_mbedtls : test_proxy_mbedtls
@echo "Starting proxy server..."
cd proxy && \
docker compose up -d
@echo "Waiting for proxy to be ready..."
@until nc -z localhost 3128 && nc -z localhost 3129; do sleep 1; done
@echo "Proxy servers are ready, waiting additional 5 seconds for full startup..."
@sleep 5
@echo "Checking proxy server status..."
@cd proxy && docker compose ps
@echo "Checking proxy server logs..."
@cd proxy && docker compose logs --tail=20
@echo "Running proxy tests (Mbed TLS)..."
./test_proxy_mbedtls; \
exit_code=$$?; \
echo "Stopping proxy server..."; \
cd proxy && docker compose down; \
exit $$exit_code
test : test.cc include_httplib.cc ../httplib.h Makefile cert.pem
$(CXX) -o $@ -I.. $(CXXFLAGS) test.cc include_httplib.cc $(TEST_ARGS)
@file $@
@@ -78,6 +102,14 @@ test : test.cc include_httplib.cc ../httplib.h Makefile cert.pem
test_split : test.cc ../httplib.h httplib.cc Makefile cert.pem
$(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS)
# Mbed TLS backend targets
test_mbedtls : test.cc include_httplib.cc ../httplib.h Makefile cert.pem
$(CXX) -o $@ -I.. $(CXXFLAGS) test.cc include_httplib.cc $(TEST_ARGS_MBEDTLS)
@file $@
test_split_mbedtls : test.cc ../httplib.h httplib.cc Makefile cert.pem
$(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS_MBEDTLS)
check_abi:
@./check-shared-library-abi-compatibility.sh
@@ -106,6 +138,9 @@ style_check: $(STYLE_CHECK_FILES)
test_proxy : test_proxy.cc ../httplib.h Makefile cert.pem
$(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS)
test_proxy_mbedtls : test_proxy.cc ../httplib.h Makefile cert.pem
$(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS_MBEDTLS)
# Runs server_fuzzer.cc based on value of $(LIB_FUZZING_ENGINE).
# Usage: make fuzz_test LIB_FUZZING_ENGINE=/path/to/libFuzzer
fuzz_test: server_fuzzer
@@ -128,5 +163,5 @@ cert.pem:
./gen-certs.sh
clean:
rm -rf test test_split test_proxy server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM
rm -rf test test_split test_mbedtls test_split_mbedtls test_proxy test_proxy_mbedtls server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,10 @@
using namespace std;
using namespace httplib;
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) || defined(CPPHTTPLIB_MBEDTLS_SUPPORT)
#define CPPHTTPLIB_SSL_SUPPORT
#endif
std::string normalizeJson(const std::string &json) {
std::string result;
for (char c : json) {
@@ -26,7 +30,7 @@ TEST(ProxyTest, NoSSLBasic) {
ProxyTest(cli, true);
}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#ifdef CPPHTTPLIB_SSL_SUPPORT
TEST(ProxyTest, SSLBasic) {
SSLClient cli("nghttp2.org");
ProxyTest(cli, true);
@@ -51,7 +55,7 @@ void RedirectProxyText(T &cli, const char *path, bool basic) {
if (basic) {
cli.set_proxy_basic_auth("hello", "world");
} else {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#ifdef CPPHTTPLIB_SSL_SUPPORT
cli.set_proxy_digest_auth("hello", "world");
#endif
}
@@ -67,7 +71,7 @@ TEST(RedirectTest, HTTPBinNoSSLBasic) {
RedirectProxyText(cli, "/httpbin/redirect/2", true);
}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#ifdef CPPHTTPLIB_SSL_SUPPORT
TEST(RedirectTest, HTTPBinNoSSLDigest) {
Client cli("nghttp2.org");
RedirectProxyText(cli, "/httpbin/redirect/2", false);
@@ -84,7 +88,7 @@ TEST(RedirectTest, HTTPBinSSLDigest) {
}
#endif
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#ifdef CPPHTTPLIB_SSL_SUPPORT
TEST(RedirectTest, YouTubeNoSSLBasic) {
Client cli("youtube.com");
RedirectProxyText(cli, "/", true);
@@ -157,7 +161,7 @@ TEST(BaseAuthTest, NoSSL) {
BaseAuthTestFromHTTPWatch(cli);
}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#ifdef CPPHTTPLIB_SSL_SUPPORT
TEST(BaseAuthTest, SSL) {
SSLClient cli("httpcan.org");
BaseAuthTestFromHTTPWatch(cli);
@@ -166,7 +170,7 @@ TEST(BaseAuthTest, SSL) {
// ----------------------------------------------------------------------------
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#ifdef CPPHTTPLIB_SSL_SUPPORT
template <typename T> void DigestAuthTestFromHTTPWatch(T &cli) {
cli.set_proxy("localhost", 3129);
cli.set_proxy_digest_auth("hello", "world");
@@ -230,13 +234,13 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
if (basic) {
cli.set_proxy_basic_auth("hello", "world");
} else {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#ifdef CPPHTTPLIB_SSL_SUPPORT
cli.set_proxy_digest_auth("hello", "world");
#endif
}
cli.set_follow_location(true);
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#ifdef CPPHTTPLIB_SSL_SUPPORT
cli.set_digest_auth("hello", "world");
#endif
@@ -274,7 +278,7 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
}
}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#ifdef CPPHTTPLIB_SSL_SUPPORT
TEST(KeepAliveTest, NoSSLWithBasic) {
Client cli("nghttp2.org");
KeepAliveTest(cli, true);