Windows Automatic Install Certificate

This commit is contained in:
wanghongenpin
2025-09-18 23:55:23 +08:00
parent 384fe9e48c
commit f861beaa88
14 changed files with 399 additions and 77 deletions

View File

@@ -336,5 +336,8 @@
"appUpdateIgnoreBtnTxt": "Ignore",
"requestMap": "Request Map",
"requestMapDescribe": "Do not request remote services, use local configuration or script for response"
"requestMapDescribe": "Do not request remote services, use local configuration or script for response",
"automatic": "Automatic",
"manual": "Manual"
}

View File

@@ -1979,6 +1979,18 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Do not request remote services, use local configuration or script for response'**
String get requestMapDescribe;
/// No description provided for @automatic.
///
/// In en, this message translates to:
/// **'Automatic'**
String get automatic;
/// No description provided for @manual.
///
/// In en, this message translates to:
/// **'Manual'**
String get manual;
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -978,4 +978,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get requestMapDescribe => 'Do not request remote services, use local configuration or script for response';
@override
String get automatic => 'Automatic';
@override
String get manual => 'Manual';
}

View File

@@ -966,6 +966,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get requestMapDescribe => '不请求远程服务,使用本地配置或脚本进行响应';
@override
String get automatic => '自动安装';
@override
String get manual => '手动安装';
}
/// The translations for Chinese, using the Han script (`zh_Hant`).

View File

@@ -335,5 +335,8 @@
"appUpdateIgnoreBtnTxt": "忽略",
"requestMap": "请求映射",
"requestMapDescribe": "不请求远程服务,使用本地配置或脚本进行响应"
"requestMapDescribe": "不请求远程服务,使用本地配置或脚本进行响应",
"automatic": "自动安装",
"manual": "手动安装"
}

View File

@@ -21,7 +21,6 @@ class X509CertificateData {
Map<String, String?> issuer;
/// The validity of the certificate
@Deprecated('Use tbsCertificate.validity instead')
X509CertificateValidity validity;
/// The sha1 thumbprint for the certificate

View File

@@ -19,11 +19,8 @@ import 'dart:core';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:path_provider/path_provider.dart';
import 'package:pointycastle/api.dart';
import 'package:pointycastle/asymmetric/api.dart';
import 'package:proxypin/network/util/cert/basic_constraints.dart';
import 'package:pointycastle/export.dart';
import 'package:proxypin/network/util/cert/pkcs12.dart';
import 'package:proxypin/network/util/cert/x509.dart';
import 'package:proxypin/network/util/logger.dart';
@@ -31,6 +28,7 @@ import 'package:proxypin/network/util/random.dart';
import 'package:proxypin/utils/lang.dart';
import 'cache.dart';
import 'cert/basic_constraints.dart';
import 'cert/cert_data.dart';
import 'cert/extension.dart';
import 'cert/key_usage.dart';
@@ -39,7 +37,6 @@ import 'file_read.dart';
Future<void> main() async {
await CertificateManager.getCertificateContext('www.jianshu.com');
CertificateManager.caCert.tbsCertificateSeqAsString;
}
enum StartState { uninitialized, initializing, initialized }
@@ -53,7 +50,7 @@ class CertificateManager {
static AsymmetricKeyPair _serverKeyPair = CryptoUtils.generateRSAKeyPair();
/// ca证书
static late X509CertificateData _caCert;
static X509CertificateData? _caCert;
/// ca私钥
static late RSAPrivateKey _caPriKey;
@@ -66,7 +63,7 @@ class CertificateManager {
return _certificateMap[host];
}
static X509CertificateData get caCert => _caCert;
static X509CertificateData? get caCert => _caCert;
/// 清除缓存
static void cleanCache() {
@@ -84,7 +81,7 @@ class CertificateManager {
await initCAConfig();
}
String cer = generate(_caCert, _serverKeyPair.publicKey as RSAPublicKey, _caPriKey, host);
String cer = generate(_caCert!, _serverKeyPair.publicKey as RSAPublicKey, _caPriKey, host);
var rsaPrivateKey = _serverKeyPair.privateKey as RSAPrivateKey;
@@ -122,7 +119,7 @@ class CertificateManager {
await initCAConfig();
}
var subject = caCert.subject;
var subject = caCert!.subject;
return '${X509Utils.getSubjectHashName(subject)}.0';
}
@@ -147,7 +144,7 @@ class CertificateManager {
x509Subject['CN'] = 'ProxyPin CA (${DateTime.now().dateFormat()},${RandomUtil.randomString(6).toUpperCase()})';
var csrPem = X509Utils.generateSelfSignedCertificate(
_caCert,
_caCert!,
serverPubKey,
serverPriKey,
825,
@@ -230,6 +227,12 @@ class CertificateManager {
return caFile;
}
///证书pem格式内容
static Future<String> certificatePem() async {
var caFile = await certificateFile();
return caFile.readAsString();
}
/// 私钥文件
static Future<File> privateKeyFile() async {
final String appPath = await getApplicationSupportDirectory().then((value) => value.path);
@@ -265,4 +268,12 @@ class CertificateManager {
cleanCache();
_state = StartState.uninitialized;
}
/// 获取证书详细信息
static Future<X509CertificateData> getCertificateDetails() async {
if (_state != StartState.initialized) {
await initCAConfig();
}
return caCert!;
}
}

View File

@@ -0,0 +1,62 @@
import 'dart:convert';
import 'dart:io';
import 'package:proxypin/network/util/cert/cert_data.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:x509_cert_store/x509_cert_store.dart';
class CertInstaller {
static Future<bool> installCertificate(File certFile) async {
try {
// Read the certificate file and encode it to Base64
final certBytes = await certFile.readAsBytes();
final certificateBase64 = base64.encode(certBytes);
// Initialize the X509CertStore plugin
final x509CertStorePlugin = X509CertStore();
// Add the certificate to the trusted root store
final result = await x509CertStorePlugin.addCertificate(
storeName: X509StoreName.root, // Add to the trusted root store
certificateBase64: certificateBase64, // Base64-encoded certificate
addType: X509AddType.addNewer, // Replace if it already exists
setTrusted: Platform.isMacOS, // Mark the certificate as trusted
);
logger.d('Certificate successfully installed to the trusted root store. Result: ${result.code} $result');
return result.isOk || result.code == X509ErrorCode.alreadyExist.getString();
} catch (e) {
logger.e('Failed to install certificate: $e');
return false;
}
}
/// 检查证书是否已安装
static Future<bool> isCertInstalled(X509CertificateData caCert) async {
String commonName = caCert.subject['2.5.4.3'] ?? 'ProxyPin CA';
String? sha1 = caCert.sha1Thumbprint;
try {
if (Platform.isWindows) {
List<String> args = ['-user', '-store', 'root'];
if (sha1 != null) {
args.add(sha1);
}
var res = await Process.run('certutil', args);
return res.stdout.toString().toLowerCase().contains(commonName.toLowerCase());
} else if (Platform.isMacOS) {
var res = await Process.run('security', ['find-certificate', '-c', commonName, '-a']);
return (res.stdout as String).isNotEmpty;
} else if (Platform.isLinux) {
// check common locations
var paths = [
'/usr/local/share/ca-certificates/$commonName.crt',
'/etc/ssl/certs/$commonName.crt',
];
for (var p in paths) if (await File(p).exists()) return true;
// fallback: search /etc/ssl/certs for subject text
var res = await Process.run('grep', ['-i', commonName, '-R', '/etc/ssl/certs']);
return (res.stdout as String).isNotEmpty;
}
} catch (_) {}
return false;
}
}

View File

@@ -0,0 +1,274 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:proxypin/l10n/app_localizations.dart';
import 'package:proxypin/network/util/cert/cert_data.dart';
import 'package:proxypin/network/util/crts.dart';
import 'package:proxypin/ui/component/app_dialog.dart';
import 'package:proxypin/ui/desktop/ssl/cert_installer.dart';
import 'package:url_launcher/url_launcher.dart';
class PCCert extends StatefulWidget {
const PCCert({super.key});
@override
State<PCCert> createState() => _PCCertState();
}
class _PCCertState extends State<PCCert> with TickerProviderStateMixin {
late TabController _tabController;
final RxnBool isCertInstalled = RxnBool(true);
X509CertificateData? certDetails;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
certDetails = CertificateManager.caCert;
_checkCertStatus();
if (certDetails == null) {
CertificateManager.getCertificateDetails().then((value) => setState(() {
certDetails = value;
}));
}
}
void _checkCertStatus() async {
final details = certDetails ?? await CertificateManager.getCertificateDetails();
isCertInstalled.value = await CertInstaller.isCertInstalled(details);
}
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
return SimpleDialog(
titlePadding: const EdgeInsets.symmetric(),
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 15),
title: Row(children: [
const Expanded(child: SizedBox()),
Text(isCN ? "安装证书" : "Install Certificate", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const Expanded(child: SizedBox()),
Align(alignment: Alignment.topRight, child: CloseButton())
]),
children: [
TabBar(
controller: _tabController,
tabs: [
Tab(text: localizations.automatic),
Tab(text: localizations.manual),
],
),
SizedBox(
width: 700,
height: 470,
child: TabBarView(
controller: _tabController,
children: [
_buildAutomaticTab(context),
_buildManualTab(context),
],
),
),
],
);
}
Widget _buildAutomaticTab(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 16.0),
child: Obx(() => Column(children: buildAutomaticChildren())),
);
}
List<Widget> buildAutomaticChildren() {
final localizations = AppLocalizations.of(context)!;
final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
final subtitleStyle = Theme.of(context).textTheme.bodyMedium;
final infoLabelStyle = Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[600]);
final infoValueStyle = Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500);
List<Widget> children = [
const SizedBox(height: 8),
Text(isCN ? "通过安装并信任 ProxyPin CA" : "Install and Trust ProxyPin CA Certificate",
style: subtitleStyle, textAlign: TextAlign.center),
const SizedBox(height: 3),
Text(
isCN
? "ProxyPin 可以动态解密 HTTPS 流量以展示原始请求/响应。"
: "ProxyPin can decrypt encrypted traffic on the fly and enable to see raw HTTPS requests and responses.",
style: subtitleStyle,
textAlign: TextAlign.center),
const SizedBox(height: 45),
];
if (isCertInstalled.value == false) {
children.add(const SizedBox(height: 20));
children.add(Icon(Icons.error_outline, color: Colors.red, size: 56));
children.add(const SizedBox(height: 12));
children.add(Text(isCN ? '证书未安装' : 'Certificate Not Installed',
textAlign: TextAlign.center, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)));
children.add(const SizedBox(height: 20));
children.add(
FilledButton(
onPressed: _installCert,
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 19)),
child: Text(localizations.install)),
);
} else if (isCertInstalled.value == true) {
children.add(Card(
elevation: 2,
color: Theme.brightnessOf(context) == Brightness.light ? Colors.grey[50] : Colors.grey[800],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 18.0),
child: Column(children: [
Icon(Icons.verified_rounded, color: Colors.green, size: 56),
const SizedBox(height: 12),
Text(isCN ? "证书已安装" : "Certificate Installed", style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
if (certDetails != null) ...[
const Divider(),
const SizedBox(height: 8),
// certificate details
Row(children: [
Text('Name', style: infoLabelStyle),
Expanded(
child: SelectableText(certDetails!.subject['2.5.4.3'] ?? 'ProxyPin CA',
style: infoValueStyle, textAlign: TextAlign.right)),
]),
const SizedBox(height: 6),
Row(children: [
Text('Expires', style: infoLabelStyle),
Expanded(
child: SelectableText(certDetails!.validity.notAfter.toLocal().toString().split(' ').first,
style: infoValueStyle, textAlign: TextAlign.right)),
]),
const SizedBox(height: 6),
Row(children: [
Text('Fingerprint', style: infoLabelStyle),
Expanded(
child: SelectableText(certDetails!.sha1Thumbprint ?? '-',
style: infoValueStyle, textAlign: TextAlign.right),
),
])
]
]),
),
));
}
return children;
}
Widget _buildManualTab(BuildContext context) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(top: 12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildChildren(context),
)),
);
}
List<Widget> _buildChildren(BuildContext context) {
if (Platform.isMacOS || Platform.isWindows) {
return _buildWindowsAndMacContent(context);
}
return _buildLinuxContent(context);
}
List<Widget> _buildWindowsAndMacContent(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
return [
isCN
? Text(" 安装证书到本系统,${Platform.isMacOS ? "安装完双击选择“始终信任此证书”。 如安装打开失败,请导出证书拖拽到系统证书里" : "选择“受信任的根证书颁发机构”"}")
: Text(
" Install certificate to this system${Platform.isMacOS ? "After installation, double-click to select “Always Trust”。\n If installation and opening failPlease export the certificate and drag it to the system certificate" : "choice“Trusted Root Certificate Authority”"}"),
const SizedBox(height: 10),
SizedBox(
width: double.maxFinite,
child: FilledButton(
onPressed: () => _manualInstallCert(),
style: FilledButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
child: Text(localizations.installRootCa))),
const SizedBox(height: 10),
Platform.isMacOS
? Image.network("https://foruda.gitee.com/images/1689323260158189316/c2d881a4_1073801.png",
width: 800, height: 500)
: Row(children: [
Image.network("https://foruda.gitee.com/images/1689335589122168223/c904a543_1073801.png",
width: 370, height: 380),
const SizedBox(width: 10),
Image.network("https://foruda.gitee.com/images/1689335334688878324/f6aa3a3a_1073801.png",
width: 370, height: 380)
])
];
}
List<Widget> _buildLinuxContent(BuildContext context) {
final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
return [
Text(isCN
? "安装证书到本系统以Ubuntu为例 下载证书:\n"
"先把证书复制到 /usr/local/share/ca-certificates/,然后执行 update-ca-certificates 即可。\n"
"其他系统请网上搜索安装根证书"
: "Install the certificate to this system), take Ubuntu as an example to download the certificate:\n"
"First copy the certificate to /usr/local/share/ca-certificates/, and then execute update-ca-certificates.\n"
"For other systems, please search online for installing root certificates."),
const SizedBox(height: 5),
Text(
isCN
? "提示FireFox有自己的信任证书库所以要手动在设置中导入需要导入的证书。"
: "Note: FireFox has its own trusted certificate library, so you need to manually import the required certificates in the settings.",
style: TextStyle(fontSize: 12)),
const SizedBox(height: 10),
const SelectableText.rich(
textAlign: TextAlign.justify,
TextSpan(style: TextStyle(color: Color(0xff6a8759)), children: [
TextSpan(text: " sudo cp ProxyPinCA.crt /usr/local/share/ca-certificates/ \n"),
TextSpan(text: " sudo update-ca-certificates")
])),
const SizedBox(height: 10)
];
}
void _installCert() async {
final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
var caFile = await CertificateManager.certificateFile();
bool success = await CertInstaller.installCertificate(caFile);
CertificateManager.cleanCache();
if (!mounted) {
return;
}
if (success) {
isCertInstalled.value = true;
CustomToast.success(isCN ? "证书安装成功" : "Certificate installed successfully").show(context);
} else {
isCertInstalled.value = false;
final isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
CustomToast.error(isCN ? "证书安装失败,请尝试手动安装" : "Certificate installation failed, please try manual installation")
.show(context);
}
}
void _manualInstallCert() async {
var caFile = await CertificateManager.certificateFile();
launchUrl(Uri.file(caFile.path)).then((_) {
CertificateManager.cleanCache();
isCertInstalled.value = null;
});
}
}

View File

@@ -8,6 +8,7 @@ import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/util/crts.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/ui/component/utils.dart';
import 'package:proxypin/ui/desktop/ssl/pc_cert.dart';
import 'package:proxypin/utils/ip.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -42,7 +43,8 @@ class _SslState extends State<SslWidget> {
},
menuChildren: [
_Switch(proxyServer: widget.proxyServer, onEnableChange: (val) => setState(() {})),
item(localizations.installCaLocal, onPressed: pcCer),
item(localizations.installCaLocal,
onPressed: () => showDialog(context: context, builder: (context) => PCCert())),
item("${localizations.installRootCa} iOS", onPressed: () async => iosCer(await localIp())),
item("${localizations.installRootCa} Android", onPressed: () async => androidCer(await localIp())),
const Divider(thickness: 0.3, height: 3),
@@ -202,64 +204,6 @@ class _SslState extends State<SslWidget> {
child: Text(text, style: const TextStyle(fontSize: 14))));
}
void pcCer() async {
bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
List<Widget> list = [];
if (Platform.isMacOS || Platform.isWindows) {
list = [
isCN
? Text(" 安装证书到本系统,${Platform.isMacOS ? "安装完双击选择“始终信任此证书”。 如安装打开失败,请导出证书拖拽到系统证书里" : "选择“受信任的根证书颁发机构”"}")
: Text(" Install certificate to this system${Platform.isMacOS ? "After installation, double-click to select “Always Trust”。\n"
" If installation and opening failPlease export the certificate and drag it to the system certificate" : "choice“Trusted Root Certificate Authority”"}"),
const SizedBox(height: 10),
FilledButton(onPressed: _installCert, child: Text(localizations.installRootCa)),
const SizedBox(height: 10),
Platform.isMacOS
? Image.network("https://foruda.gitee.com/images/1689323260158189316/c2d881a4_1073801.png",
width: 800, height: 500)
: Row(children: [
Image.network("https://foruda.gitee.com/images/1689335589122168223/c904a543_1073801.png",
width: 400, height: 400),
const SizedBox(width: 10),
Image.network("https://foruda.gitee.com/images/1689335334688878324/f6aa3a3a_1073801.png",
width: 400, height: 400)
])
];
} else {
list.add(const Text("安装证书到本系统以Ubuntu为例 下载证书:\n"
"先把证书复制到 /usr/local/share/ca-certificates/,然后执行 update-ca-certificates 即可。\n"
"其他系统请网上搜索安装根证书"));
list.add(const SizedBox(height: 5));
list.add(const Text("提示FireFox有自己的信任证书库所以要手动在设置中导入需要导入的证书。", style: TextStyle(fontSize: 12)));
list.add(const SizedBox(height: 10));
list.add(const SelectableText.rich(
textAlign: TextAlign.justify,
TextSpan(style: TextStyle(color: Color(0xff6a8759)), children: [
TextSpan(text: " sudo cp ProxyPinCA.crt /usr/local/share/ca-certificates/ \n"),
TextSpan(text: " sudo update-ca-certificates")
])));
list.add(const SizedBox(height: 10));
}
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return SimpleDialog(
contentPadding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
title: Row(children: [
const Expanded(child: SizedBox()),
Text(isCN ? "电脑HTTPS抓包配置" : "Computer HTTPS Packet Capture Configuration",
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const Expanded(child: SizedBox()),
Align(alignment: Alignment.topRight, child: CloseButton())
]),
alignment: Alignment.center,
children: list);
});
}
void iosCer(String host) {
showDialog(
context: context,
@@ -402,11 +346,6 @@ class _SslState extends State<SslWidget> {
))));
});
}
void _installCert() async {
var caFile = await CertificateManager.certificateFile();
launchUrl(Uri.file(caFile.path)).then((value) => CertificateManager.cleanCache());
}
}
class _Switch extends StatefulWidget {

View File

@@ -18,6 +18,7 @@ import share_plus
import shared_preferences_foundation
import url_launcher_macos
import window_manager
import x509_cert_store
import zstandard_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@@ -34,5 +35,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
X509CertStorePlugin.register(with: registry.registrar(forPlugin: "X509CertStorePlugin"))
ZstandardMacosPlugin.register(with: registry.registrar(forPlugin: "ZstandardMacosPlugin"))
}

View File

@@ -47,6 +47,7 @@ dependencies:
macos_window_utils: ^1.9.0
win32audio: ^1.3.1
vclibs: ^0.1.3
x509_cert_store: ^1.2.1
dev_dependencies:
flutter_test:

View File

@@ -17,6 +17,7 @@
#include <vclibs/vclibs_plugin_c_api.h>
#include <win32audio/win32audio_plugin_c_api.h>
#include <window_manager/window_manager_plugin.h>
#include <x509_cert_store/x509_cert_store_plugin_c_api.h>
#include <zstandard_windows/zstandard_windows_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@@ -42,6 +43,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("Win32audioPluginCApi"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
X509CertStorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("X509CertStorePluginCApi"));
ZstandardWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ZstandardWindowsPluginCApi"));
}

View File

@@ -14,6 +14,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
vclibs
win32audio
window_manager
x509_cert_store
zstandard_windows
)