Files
SpineParticlesWeb/connector/native/local_certificate.mm
T
2026-11-09 10:08:29 +08:00

179 lines
5.7 KiB
Plaintext

#ifdef __APPLE__
#include "local_certificate.hpp"
#include "config.hpp"
#include "encoding.hpp"
#include "platform.hpp"
#include <cstdlib>
#include <fstream>
#include <sys/stat.h>
namespace connector {
namespace {
struct CertificatePaths {
std::filesystem::path directory;
std::filesystem::path ca_key;
std::filesystem::path ca;
std::filesystem::path server_key;
std::filesystem::path server_certificate;
std::filesystem::path identity;
std::filesystem::path request;
std::filesystem::path extensions;
};
CertificatePaths paths() {
const auto directory = config_path().parent_path() / "tls";
return {
directory,
directory / "ca-key.pem",
directory / "ca.pem",
directory / "server-key.pem",
directory / "server.pem",
directory / "identity.p12",
directory / "server.csr",
directory / "server.ext",
};
}
std::string process_error(const ProcessResult& result) {
std::string message = trim(result.output);
return message.empty() ? "OpenSSL 执行失败" : message;
}
bool run_openssl(const std::vector<std::string>& arguments, std::string& error) {
const ProcessResult result = run_process("/usr/bin/openssl", arguments, {}, 30);
if (result.timed_out) {
error = "生成本机 HTTPS 证书超时";
return false;
}
if (result.exit_code != 0) {
error = process_error(result);
return false;
}
return true;
}
bool certificate_is_usable(const std::filesystem::path& certificate) {
std::error_code file_error;
if (!std::filesystem::is_regular_file(certificate, file_error)) return false;
const ProcessResult result = run_process(
"/usr/bin/openssl", {"x509", "-checkend", "2592000", "-noout", "-in", certificate.string()}, {}, 10);
return !result.timed_out && result.exit_code == 0;
}
bool identity_is_usable(const std::filesystem::path& identity) {
std::error_code error;
return std::filesystem::is_regular_file(identity, error);
}
void set_private_permissions(const std::filesystem::path& path) {
chmod(path.c_str(), 0600);
}
} // namespace
std::filesystem::path local_identity_path() {
return paths().identity;
}
bool ensure_local_certificate(std::string& error) {
const CertificatePaths files = paths();
if (certificate_is_usable(files.server_certificate) && identity_is_usable(files.identity)) {
error.clear();
return true;
}
try {
std::filesystem::create_directories(files.directory);
chmod(files.directory.c_str(), 0700);
for (const auto& path : {files.ca_key, files.ca, files.server_key, files.server_certificate,
files.identity, files.request, files.extensions}) {
std::error_code ignored;
std::filesystem::remove(path, ignored);
}
} catch (const std::exception& exception) {
error = exception.what();
return false;
}
const std::string dir = files.directory.string();
if (!run_openssl({"genrsa", "-out", files.ca_key.string(), "2048"}, error)
|| !run_openssl({"req", "-x509", "-new", "-key", files.ca_key.string(), "-sha256", "-days", "3650",
"-subj", "/CN=Spine Particle Connector Local CA", "-out", files.ca.string()}, error)
|| !run_openssl({"genrsa", "-out", files.server_key.string(), "2048"}, error)
|| !run_openssl({"req", "-new", "-key", files.server_key.string(), "-subj", "/CN=127.0.0.1",
"-out", files.request.string()}, error)) {
return false;
}
{
std::ofstream extensions(files.extensions, std::ios::binary | std::ios::trunc);
if (!extensions) {
error = "无法写入证书配置";
return false;
}
extensions << "subjectAltName=IP:127.0.0.1,DNS:localhost\n"
<< "extendedKeyUsage=serverAuth\n"
<< "basicConstraints=CA:FALSE\n";
}
if (!run_openssl({"x509", "-req", "-in", files.request.string(), "-CA", files.ca.string(),
"-CAkey", files.ca_key.string(), "-CAcreateserial", "-out", files.server_certificate.string(),
"-days", "825", "-sha256", "-extfile", files.extensions.string()}, error)
|| !run_openssl({"pkcs12", "-export", "-out", files.identity.string(), "-inkey", files.server_key.string(),
"-in", files.server_certificate.string(), "-passout", "pass:spine-connector-local"}, error)) {
return false;
}
set_private_permissions(files.server_key);
set_private_permissions(files.identity);
{
std::error_code ignored;
std::filesystem::remove(files.ca_key, ignored);
}
(void)dir;
error.clear();
return true;
}
bool local_certificate_trusted() {
const CertificatePaths files = paths();
const ProcessResult result = run_process(
"/usr/bin/security",
{"verify-cert", "-c", files.server_certificate.string(), "-n", "127.0.0.1", "-p", "ssl", "-L", "-q"},
{}, 10);
return !result.timed_out && result.exit_code == 0;
}
bool trust_local_certificate(std::string& error) {
const CertificatePaths files = paths();
if (!ensure_local_certificate(error)) return false;
const char* home = std::getenv("HOME");
if (!home || !*home) {
error = "无法确定当前用户主目录";
return false;
}
const auto keychain = std::filesystem::path(home) / "Library" / "Keychains" / "login.keychain-db";
const ProcessResult result = run_process(
"/usr/bin/security",
{"add-trusted-cert", "-r", "trustRoot", "-p", "ssl", "-k", keychain.string(), files.ca.string()},
{}, 120);
if (result.timed_out || result.exit_code != 0) {
error = result.timed_out ? "等待系统证书授权超时" : process_error(result);
return false;
}
if (!local_certificate_trusted()) {
error = "证书已安装,但系统仍未信任它";
return false;
}
error.clear();
return true;
}
} // namespace connector
#endif