From 595594bea49a6fffa780bc91defe690649b420bd Mon Sep 17 00:00:00 2001 From: yhirose Date: Sun, 28 Dec 2025 20:57:16 -0500 Subject: [PATCH] Phase 1 --- docs/tls/checklist.md | 16 +- httplib.h | 535 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 543 insertions(+), 8 deletions(-) diff --git a/docs/tls/checklist.md b/docs/tls/checklist.md index 0feb0d6..4ecb1cf 100644 --- a/docs/tls/checklist.md +++ b/docs/tls/checklist.md @@ -20,14 +20,14 @@ ### フェーズ 1: TLS 抽象化 API の追加 -- [ ] `namespace detail::tls` を宣言エリアに追加 -- [ ] `ErrorCode` enum を定義 -- [ ] `TlsError` 構造体を定義 -- [ ] `tls_ctx_t`, `tls_session_t` 型を定義 -- [ ] 関数宣言を追加 -- [ ] 実装エリアに OpenSSL バックエンド実装を追加 -- [ ] 既存コードは変更していない(新 API 追加のみ) -- [ ] `make test_split` 通過 +- [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 の移行 diff --git a/httplib.h b/httplib.h index 526623a..5bcc3c9 100644 --- a/httplib.h +++ b/httplib.h @@ -2760,6 +2760,94 @@ bool is_field_value(const std::string &s); } // namespace fields +// TLS abstraction layer +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +namespace tls { + +// Error codes for TLS operations (backend-independent) +enum class ErrorCode : int { + Success = 0, + WantRead, // Non-blocking: need to wait for read + WantWrite, // Non-blocking: need to wait for write + PeerClosed, // Peer closed the connection + Fatal, // Unrecoverable error + SyscallError, // System call error (check sys_errno) + CertVerifyFailed, // Certificate verification failed + HostnameMismatch, // Hostname verification failed +}; + +// TLS error information +struct TlsError { + ErrorCode code = ErrorCode::Fatal; + uint64_t backend_code = 0; // OpenSSL: ERR_get_error(), mbedTLS: return value + int sys_errno = 0; // errno when SyscallError +}; + +// Opaque handles (defined as void* for abstraction) +using tls_ctx_t = void *; +using tls_session_t = void *; +using tls_cert_t = void *; + +// Global initialization +bool tls_global_init(); +void tls_global_cleanup(); + +// Client context +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); +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); +bool tls_set_client_cert_pem(tls_ctx_t ctx, const char *cert, const char *key, + const char *password); +bool tls_set_client_cert_file(tls_ctx_t ctx, const char *cert_path, + const char *key_path, const char *password); + +// Server context +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 *password); +bool tls_set_server_cert_file(tls_ctx_t ctx, const char *cert_path, + const char *key_path, const char *password); +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); + +// Session management +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); + +// Handshake (non-blocking capable) +TlsError tls_connect(tls_session_t session); +TlsError tls_accept(tls_session_t session); + +// I/O (non-blocking capable) +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); +void tls_shutdown(tls_session_t session, bool graceful); + +// Connection state +bool tls_is_peer_closed(tls_session_t session); + +// Certificate verification +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); + +// Error information +uint64_t tls_peek_error(); +uint64_t tls_get_error(); +std::string tls_error_string(uint64_t code); + +} // namespace tls +#endif + } // namespace detail namespace stream { @@ -12314,6 +12402,453 @@ inline void ClientImpl::set_error_logger(ErrorLogger error_logger) { error_logger_ = std::move(error_logger); } +/* + * TLS Abstraction Layer Implementation + */ +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +namespace detail { +namespace tls { + +// Helper to map OpenSSL SSL_get_error to ErrorCode +inline ErrorCode map_ssl_error(int ssl_error, int &out_errno) { + switch (ssl_error) { + case SSL_ERROR_NONE: return ErrorCode::Success; + case SSL_ERROR_WANT_READ: return ErrorCode::WantRead; + case SSL_ERROR_WANT_WRITE: return ErrorCode::WantWrite; + case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed; + case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError; + case SSL_ERROR_SSL: + default: return ErrorCode::Fatal; + } +} + +inline bool tls_global_init() { + // OpenSSL 3.0+: OPENSSL_init_ssl() is called automatically + return OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | + OPENSSL_INIT_LOAD_CRYPTO_STRINGS, + nullptr) == 1; +} + +inline void tls_global_cleanup() { + // OpenSSL 3.0+: cleanup is automatic +} + +inline tls_ctx_t tls_create_client_context() { + SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); + if (ctx) { + // Disable auto-retry to properly handle non-blocking I/O + SSL_CTX_clear_mode(ctx, SSL_MODE_AUTO_RETRY); + // Set minimum TLS version + SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); + } + return static_cast(ctx); +} + +inline void tls_free_context(tls_ctx_t ctx) { + if (ctx) { SSL_CTX_free(static_cast(ctx)); } +} + +inline bool tls_set_min_version(tls_ctx_t ctx, int version) { + if (!ctx) return false; + return SSL_CTX_set_min_proto_version(static_cast(ctx), version) == + 1; +} + +inline bool tls_load_ca_pem(tls_ctx_t ctx, const char *pem, size_t len) { + if (!ctx || !pem || len == 0) return false; + + auto ssl_ctx = static_cast(ctx); + auto store = SSL_CTX_get_cert_store(ssl_ctx); + if (!store) return false; + + auto bio = BIO_new_mem_buf(pem, static_cast(len)); + if (!bio) return false; + + bool ok = true; + X509 *cert = nullptr; + while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != + nullptr) { + if (X509_STORE_add_cert(store, cert) != 1) { + // Ignore duplicate errors + auto err = ERR_peek_last_error(); + if (ERR_GET_REASON(err) != X509_R_CERT_ALREADY_IN_HASH_TABLE) { + ok = false; + } + } + X509_free(cert); + if (!ok) break; + } + BIO_free(bio); + + // Clear any "no more certificates" errors + ERR_clear_error(); + return ok; +} + +inline bool tls_load_ca_file(tls_ctx_t ctx, const char *file_path) { + if (!ctx || !file_path) return false; + return SSL_CTX_load_verify_locations(static_cast(ctx), file_path, + nullptr) == 1; +} + +inline bool tls_load_ca_dir(tls_ctx_t ctx, const char *dir_path) { + if (!ctx || !dir_path) return false; + return SSL_CTX_load_verify_locations(static_cast(ctx), nullptr, + dir_path) == 1; +} + +inline bool tls_load_system_certs(tls_ctx_t ctx) { + if (!ctx) return false; + auto ssl_ctx = static_cast(ctx); + +#ifdef _WIN32 + // Windows: Load from system certificate store + auto store = SSL_CTX_get_cert_store(ssl_ctx); + if (!store) return false; + + auto hStore = CertOpenSystemStoreW(NULL, L"ROOT"); + if (!hStore) return false; + + bool loaded_any = false; + PCCERT_CONTEXT pContext = nullptr; + while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != + nullptr) { + const unsigned char *data = pContext->pbCertEncoded; + auto x509 = d2i_X509(nullptr, &data, pContext->cbCertEncoded); + if (x509) { + if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; } + X509_free(x509); + } + } + CertCloseStore(hStore, 0); + return loaded_any; + +#elif defined(__APPLE__) +#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN + // macOS: Load from Keychain + auto store = SSL_CTX_get_cert_store(ssl_ctx); + if (!store) return false; + + CFArrayRef certs = nullptr; + if (SecTrustCopyAnchorCertificates(&certs) != errSecSuccess || !certs) { + return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; + } + + bool loaded_any = false; + auto count = CFArrayGetCount(certs); + for (CFIndex i = 0; i < count; i++) { + auto cert = reinterpret_cast( + const_cast(CFArrayGetValueAtIndex(certs, i))); + CFDataRef der = SecCertificateCopyData(cert); + if (der) { + const unsigned char *data = CFDataGetBytePtr(der); + auto x509 = d2i_X509(nullptr, &data, CFDataGetLength(der)); + if (x509) { + if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; } + X509_free(x509); + } + CFRelease(der); + } + } + CFRelease(certs); + return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; +#else + return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; +#endif + +#else + // Other Unix: use default verify paths + return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; +#endif +} + +inline bool tls_set_client_cert_pem(tls_ctx_t ctx, const char *cert, + const char *key, const char *password) { + if (!ctx || !cert || !key) return false; + + auto ssl_ctx = static_cast(ctx); + + // Load certificate + auto cert_bio = BIO_new_mem_buf(cert, -1); + if (!cert_bio) return false; + + auto x509 = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr); + BIO_free(cert_bio); + if (!x509) return false; + + auto cert_ok = SSL_CTX_use_certificate(ssl_ctx, x509) == 1; + X509_free(x509); + if (!cert_ok) return false; + + // Load private key + auto key_bio = BIO_new_mem_buf(key, -1); + if (!key_bio) return false; + + auto pkey = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, + password ? const_cast(password) + : nullptr); + BIO_free(key_bio); + if (!pkey) return false; + + auto key_ok = SSL_CTX_use_PrivateKey(ssl_ctx, pkey) == 1; + EVP_PKEY_free(pkey); + + return key_ok && SSL_CTX_check_private_key(ssl_ctx) == 1; +} + +inline bool tls_set_client_cert_file(tls_ctx_t ctx, const char *cert_path, + const char *key_path, + const char *password) { + if (!ctx || !cert_path || !key_path) return false; + + auto ssl_ctx = static_cast(ctx); + + if (password && password[0] != '\0') { + SSL_CTX_set_default_passwd_cb_userdata( + ssl_ctx, reinterpret_cast(const_cast(password))); + } + + return SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_path) == 1 && + SSL_CTX_use_PrivateKey_file(ssl_ctx, key_path, SSL_FILETYPE_PEM) == 1; +} + +inline tls_ctx_t tls_create_server_context() { + SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); + if (ctx) { + SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); + } + return static_cast(ctx); +} + +inline bool tls_set_server_cert_pem(tls_ctx_t ctx, const char *cert, + const char *key, const char *password) { + // Same implementation as client cert + return tls_set_client_cert_pem(ctx, cert, key, password); +} + +inline bool tls_set_server_cert_file(tls_ctx_t ctx, const char *cert_path, + const char *key_path, + const char *password) { + // Same implementation as client cert file + return tls_set_client_cert_file(ctx, cert_path, key_path, password); +} + +inline bool tls_set_client_ca_file(tls_ctx_t ctx, const char *ca_file, + const char *ca_dir) { + if (!ctx) return false; + auto ssl_ctx = static_cast(ctx); + + if (ca_file || ca_dir) { + if (SSL_CTX_load_verify_locations(ssl_ctx, ca_file, ca_dir) != 1) { + return false; + } + } + + // Set CA list for client certificate request + if (ca_file) { + auto list = SSL_load_client_CA_file(ca_file); + if (list) { SSL_CTX_set_client_CA_list(ssl_ctx, list); } + } + + return true; +} + +inline void tls_set_verify_client(tls_ctx_t ctx, bool require) { + if (!ctx) return; + SSL_CTX_set_verify(static_cast(ctx), + require + ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) + : SSL_VERIFY_NONE, + nullptr); +} + +inline tls_session_t tls_create_session(tls_ctx_t ctx, socket_t sock) { + if (!ctx || sock == INVALID_SOCKET) return nullptr; + + auto ssl_ctx = static_cast(ctx); + SSL *ssl = SSL_new(ssl_ctx); + if (!ssl) return nullptr; + + // Disable auto-retry for proper non-blocking I/O handling + SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY); + + auto bio = BIO_new_socket(static_cast(sock), BIO_NOCLOSE); + if (!bio) { + SSL_free(ssl); + return nullptr; + } + + SSL_set_bio(ssl, bio, bio); + return static_cast(ssl); +} + +inline void tls_free_session(tls_session_t session) { + if (session) { SSL_free(static_cast(session)); } +} + +inline bool tls_set_hostname(tls_session_t session, const char *hostname) { + if (!session || !hostname) return false; + + auto ssl = static_cast(session); + + // Set SNI (Server Name Indication) + if (SSL_set_tlsext_host_name(ssl, hostname) != 1) { return false; } + + // Enable hostname verification + auto param = SSL_get0_param(ssl); + if (!param) return false; + + X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); + if (X509_VERIFY_PARAM_set1_host(param, hostname, 0) != 1) { return false; } + + SSL_set_verify(ssl, SSL_VERIFY_PEER, nullptr); + return true; +} + +inline TlsError tls_connect(tls_session_t session) { + if (!session) { return TlsError(); } + + auto ssl = static_cast(session); + auto ret = SSL_connect(ssl); + + TlsError err; + if (ret == 1) { + err.code = ErrorCode::Success; + } else { + auto ssl_err = SSL_get_error(ssl, ret); + err.code = map_ssl_error(ssl_err, err.sys_errno); + if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } + } + return err; +} + +inline TlsError tls_accept(tls_session_t session) { + if (!session) { return TlsError(); } + + auto ssl = static_cast(session); + auto ret = SSL_accept(ssl); + + TlsError err; + if (ret == 1) { + err.code = ErrorCode::Success; + } else { + auto ssl_err = SSL_get_error(ssl, ret); + err.code = map_ssl_error(ssl_err, err.sys_errno); + if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } + } + return err; +} + +inline ssize_t tls_read(tls_session_t session, void *buf, size_t len, + TlsError &err) { + if (!session || !buf) { + err.code = ErrorCode::Fatal; + return -1; + } + + auto ssl = static_cast(session); + auto ret = SSL_read(ssl, buf, static_cast(len)); + + if (ret > 0) { + err.code = ErrorCode::Success; + return ret; + } + + auto ssl_err = SSL_get_error(ssl, ret); + err.code = map_ssl_error(ssl_err, err.sys_errno); + if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } + return -1; +} + +inline ssize_t tls_write(tls_session_t session, const void *buf, size_t len, + TlsError &err) { + if (!session || !buf) { + err.code = ErrorCode::Fatal; + return -1; + } + + auto ssl = static_cast(session); + auto ret = SSL_write(ssl, buf, static_cast(len)); + + if (ret > 0) { + err.code = ErrorCode::Success; + return ret; + } + + auto ssl_err = SSL_get_error(ssl, ret); + err.code = map_ssl_error(ssl_err, err.sys_errno); + if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } + return -1; +} + +inline int tls_pending(tls_session_t session) { + if (!session) return 0; + return SSL_pending(static_cast(session)); +} + +inline void tls_shutdown(tls_session_t session, bool graceful) { + if (!session) return; + + auto ssl = static_cast(session); + if (graceful) { + // First call sends close_notify + if (SSL_shutdown(ssl) == 0) { + // Second call waits for peer's close_notify + SSL_shutdown(ssl); + } + } +} + +inline bool tls_is_peer_closed(tls_session_t session) { + if (!session) return true; + + auto ssl = static_cast(session); + char buf; + auto ret = SSL_peek(ssl, &buf, 1); + if (ret > 0) return false; + + auto err = SSL_get_error(ssl, ret); + return err == SSL_ERROR_ZERO_RETURN || err == SSL_ERROR_SYSCALL; +} + +inline tls_cert_t tls_get_peer_cert(tls_session_t session) { + if (!session) return nullptr; + return static_cast( + SSL_get1_peer_certificate(static_cast(session))); +} + +inline void tls_free_cert(tls_cert_t cert) { + if (cert) { X509_free(static_cast(cert)); } +} + +inline bool tls_verify_hostname(tls_cert_t cert, const char *hostname) { + if (!cert || !hostname) return false; + + auto x509 = static_cast(cert); + return X509_check_host(x509, hostname, strlen(hostname), 0, nullptr) == 1; +} + +inline long tls_get_verify_result(tls_session_t session) { + if (!session) return X509_V_ERR_UNSPECIFIED; + return SSL_get_verify_result(static_cast(session)); +} + +inline uint64_t tls_peek_error() { return ERR_peek_last_error(); } + +inline uint64_t tls_get_error() { return ERR_get_error(); } + +inline std::string tls_error_string(uint64_t code) { + char buf[256]; + ERR_error_string_n(static_cast(code), buf, sizeof(buf)); + return std::string(buf); +} + +} // namespace tls +} // namespace detail +#endif + /* * SSL Implementation */