Skip to content

Commit 5155962

Browse files
author
PencilMario
committed
fix(certifi): 改用最新 CertifiFixer 发布包修复证书
1 parent 18708e0 commit 5155962

8 files changed

Lines changed: 94 additions & 132 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,34 @@ jobs:
102102
mkdir -p game/Submods
103103
cp -R "$dependency_source" game/Submods/
104104
105+
- name: Download CertifiFixer dependency
106+
if: steps.get_version.outputs.is_development == 'false' && steps.check_release.outputs.create_release == 'true'
107+
env:
108+
GH_TOKEN: ${{ github.token }}
109+
run: |
110+
dependency_tmp_dir="$(mktemp -d)"
111+
trap 'rm -rf "$dependency_tmp_dir"' EXIT
112+
113+
gh release download --repo MAS-Submod-MoyuTeam/mas_certifi_fixer \
114+
--pattern "CertifiFixer-*.zip" --dir "$dependency_tmp_dir"
115+
dependency_archive="$(find "$dependency_tmp_dir" -maxdepth 1 -type f -name 'CertifiFixer-*.zip' -print -quit)"
116+
if [ -z "$dependency_archive" ]; then
117+
echo "CertifiFixer release archive was not found" >&2
118+
exit 1
119+
fi
120+
121+
mkdir -p "$dependency_tmp_dir/extracted"
122+
unzip -q "$dependency_archive" -d "$dependency_tmp_dir/extracted"
123+
dependency_source="$dependency_tmp_dir/extracted/game/Submods/CertifiFixer"
124+
for filename in certifi_fixer.rpy certifi/core.py certifi/__init__.py certifi/cacert.pem; do
125+
if [ ! -f "$dependency_source/$filename" ]; then
126+
echo "CertifiFixer release is missing $filename" >&2
127+
exit 1
128+
fi
129+
done
130+
131+
cp -R "$dependency_source" game/Submods/
132+
105133
- name: Copy documentation into submod package
106134
if: steps.get_version.outputs.is_development == 'false' && steps.check_release.outputs.create_release == 'true'
107135
run: |
Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
1-
# Certifi 修复流程风险记录
2-
3-
## 位置
4-
5-
- `game/Submods/MAICA_ChatSubmod/api.rpy`:`maica_download_certifi_files`(约 259-305 行)及其后台启动函数。
6-
- `game/Submods/MAICA_ChatSubmod/api.rpy`:`start_maica` 对 `mas_can_import.certifi()` 的检测和触发逻辑。
7-
8-
## 原因
9-
10-
部分设备,尤其是某些 Ren'Py/Android 运行环境,Ren'Py 内置 Python 无法正确读取设备根证书或 MAS 提供的 certifi 包。没有可信根证书时,客户端无法同时做到“从网络获取修复材料”和“验证修复材料来源”这两个条件;因此不存在在当前运行环境内完全安全的自修复方案。
11-
12-
当前流程在证书异常时使用 `verify=False` 下载 Python 源码和 CA bundle,先尝试 GitHub raw,再 fallback 到明文 HTTP 镜像,并直接覆盖本地 `certifi` 文件。该流程属于兼容性兜底,不是可信供应链更新机制。
13-
14-
## 影响范围
15-
16-
- **供应链完整性**:TLS 验证被禁用,镜像或链路被篡改可能导致任意 Python 代码或证书内容写入游戏目录。
17-
- **运行时稳定性**:下载到错误版本或截断文件会使后续 HTTPS、导入或启动失败。
18-
- **凭据与隐私**:修复失败后可能继续切换到普通连接节点,风险见 [`plaintext-provider-fallback.md`](plaintext-provider-fallback.md)。
19-
- **更新一致性**:文件来自 moving `master`/镜像,不与当前子模组版本绑定。
20-
21-
## 本次处理边界
22-
23-
本次仍不替换下载源、不改变 `verify=False` 行为,仅补齐 fallback provider 的运行时同步,并在后台修复完成后重新检查可用性。任何进一步整改都应先确定设备信任根、签名/哈希校验、原子替换和回滚方案,再单独评审。
1+
# Certifi 修复流程
2+
3+
MAICA 声明 `CertifiFixer` 为模组依赖,并在发布构建时获取它的最新 Release,
4+
加入发布包的 `game/Submods/CertifiFixer/`;本仓库不保存其源码。其 `init -100` 脚本在 MAS
5+
无法导入 `certifi` 时,将随包的 `core.py`、`__init__.py` 和 `cacert.pem` 复制到
6+
`game/python-packages/certifi/`。文件在游戏启动阶段被直接覆盖,修复后可能需要
7+
重启游戏,让 MAS 重新加载模块。
8+
9+
MAICA 在 `game/Submods/MAICA_ChatSubmod/api.rpy` 的 `start_maica()` 中重新调用
10+
`store.mas_can_import.certifi()`;只有仍无法导入时才切换到 provider 2。MAICA
11+
不再通过后台线程从网络下载修复文件。
12+
13+
随包文件需要与受支持的 MAS / Ren'Py Python 版本保持兼容。复制过程没有备份或
14+
回滚;若依赖复制后导入仍失败,MAICA 会切换 provider 2。目标目录不可写等
15+
未被 CertifiFixer 捕获的复制异常可能中断游戏初始化。
16+
该节点的传输方式取决于远端节点列表,详见
17+
[`plaintext-provider-fallback.md`](plaintext-provider-fallback.md)。

‎game/Submods/MAICA_ChatSubmod/api.rpy‎

Lines changed: 2 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -612,90 +612,13 @@ init 5 python in maica:
612612

613613
return maica_setting_pane_cache
614614

615-
maica_certifi_download_thread_running = False
616-
617615
def maica_set_plain_provider():
618616
store.persistent.maica_setting_dict['provider_id'] = 2
619617
try:
620618
store.maica.maica_instance.provider_id = 2
621619
except Exception as e:
622620
store.mas_submod_utils.submod_log.error("MAICA: failed to apply fallback provider: {}".format(e))
623621

624-
def maica_download_certifi_files(fix_certifi, basedir, android, android_masbase):
625-
global maica_certifi_download_thread_running
626-
try:
627-
import requests
628-
failed = False
629-
if fix_certifi:
630-
try:
631-
store.mas_submod_utils.submod_log.warning("Certifi broken, try to fix it")
632-
try:
633-
res = requests.get("https://raw.githubusercontent.com/Monika-After-Story/MonikaModDev/master/Monika%20After%20Story/game/python-packages/certifi/core.py", verify=False, timeout=5)
634-
res2 = requests.get("https://raw.githubusercontent.com/Monika-After-Story/MonikaModDev/master/Monika%20After%20Story/game/python-packages/certifi/__init__.py", verify=False, timeout=5)
635-
except:
636-
store.mas_submod_utils.submod_log.warning("Download from github mirror failed, try to download from 0721play")
637-
res = requests.get("http://sp2.0721play.icu/d/MAS/%E6%89%A9%E5%B1%95%E5%86%85%E5%AE%B9/%E5%AD%90%E6%A8%A1%E7%BB%84/0.12/Github%E5%AD%90%E6%A8%A1%E7%BB%84/MAICA%20%E5%85%89%E8%80%80%E4%B9%8B%E5%9C%B0/core.py", verify=False, timeout=5)
638-
res2 = requests.get("http://sp2.0721play.icu/d/MAS/%E6%89%A9%E5%B1%95%E5%86%85%E5%AE%B9/%E5%AD%90%E6%A8%A1%E7%BB%84/0.12/Github%E5%AD%90%E6%A8%A1%E7%BB%84/MAICA%20%E5%85%89%E8%80%80%E4%B9%8B%E5%9C%B0/__init__.py", verify=False, timeout=5)
639-
640-
if res.status_code == 200 and res2.status_code == 200:
641-
with open(os.path.normpath(os.path.join(basedir, "game", "python-packages", "certifi","core.py")), "wb") as file:
642-
file.write(res.content)
643-
store.mas_submod_utils.submod_log.info("MAICA: certifi core.py fixed")
644-
645-
with open(os.path.normpath(os.path.join(basedir, "game", "python-packages", "certifi", "__init__.py")), "wb") as file:
646-
file.write(res2.content)
647-
store.mas_submod_utils.submod_log.info("MAICA: certifi __init__.py fixed")
648-
store.maica.maica_instance.disable(
649-
store.maica.maica_instance.MaicaAiStatus.CERTIFI_RESTART_REQUIRED,
650-
sticky=True,
651-
)
652-
653-
else:
654-
store.mas_submod_utils.submod_log.error("MAICA: certifi core.py download failed, HTTP code:core{} init{}".format(res.status_code, res2.status_code))
655-
failed = True
656-
except Exception as e:
657-
store.mas_submod_utils.submod_log.error("MAICA: certifi core.py download failed: {}".format(e))
658-
failed = True
659-
660-
url = "https://gitee.com/mirrors/python-certifi/raw/master/certifi/cacert.pem"
661-
try:
662-
response = requests.get(url, verify=False, timeout=5)
663-
if response.status_code == 200:
664-
path = os.path.join(basedir, "game", "python-packages", "certifi", "cacert.pem") if not android else os.path.join(android_masbase, "game", "python-packages", "certifi", "cacert.pem")
665-
with open(path, "wb") as file:
666-
file.write(response.content)
667-
store.mas_submod_utils.submod_log.info("MAICA: cacert.pem downloaded use gitee mirror")
668-
else:
669-
store.mas_submod_utils.submod_log.error("MAICA: cacert download failed with gitee mirror, HTTP code:{}".format(response.status_code))
670-
failed = True
671-
except Exception as e:
672-
store.mas_submod_utils.submod_log.error("MAICA: cacert download failed with gitee mirror: {}".format(e))
673-
failed = True
674-
675-
if failed:
676-
maica_set_plain_provider()
677-
else:
678-
check_accessibility()
679-
finally:
680-
maica_certifi_download_thread_running = False
681-
682-
def maica_start_certifi_download_in_background(fix_certifi):
683-
global maica_certifi_download_thread_running
684-
if maica_certifi_download_thread_running:
685-
store.mas_submod_utils.submod_log.info("MAICA: certifi download already running")
686-
return
687-
688-
maica_certifi_download_thread_running = True
689-
basedir = renpy.config.basedir
690-
android = renpy.android
691-
android_masbase = store.ANDROID_MASBASE if android else None
692-
store.mas_submod_utils.submod_log.info("MAICA: certifi download started in background")
693-
try:
694-
renpy.invoke_in_thread(lambda: maica_download_certifi_files(fix_certifi, basedir, android, android_masbase))
695-
except Exception as e:
696-
maica_certifi_download_thread_running = False
697-
store.mas_submod_utils.submod_log.error("MAICA: certifi background download failed to start: {}".format(e))
698-
699622
@store.mas_submod_utils.functionplugin("ch30_preloop", priority=-100)
700623
def start_maica():
701624
# 如果从PC迁移到android,切换为plain节点
@@ -716,12 +639,9 @@ init 5 python in maica:
716639
if store.mas_getAPIKey("Maica_Token") != "":
717640
store.maica.maica_instance.ciphertext = store.mas_getAPIKey("Maica_Token")
718641

719-
# certifi修复,仅在MAS原生导入失败时启动
720-
certifi_broken = not store.mas_can_import.certifi()
721-
if certifi_broken:
642+
# CertifiFixer runs during init; retry the MAS import check after it.
643+
if not store.mas_can_import.certifi():
722644
maica_set_plain_provider()
723-
if certifi_broken or store.maica_can_update_cacert:
724-
maica_start_certifi_download_in_background(certifi_broken)
725645

726646
refresh_setting_pane_cache(force_version=True)
727647

@@ -754,18 +674,6 @@ init 5 python in maica:
754674

755675

756676
init -700 python:
757-
maica_can_update_cacert = False
758-
try:
759-
import os
760-
if not os.path.exists(os.path.normpath(os.path.join(renpy.config.basedir, "game", "python-packages", "certifi", "cacert.pem"))):
761-
res = mas_can_import.certifi._update_cert(force=True)
762-
if res is None or res < 0:
763-
raise RuntimeError("MAS native certifi update failed")
764-
except Exception:
765-
maica_can_update_cacert = True
766-
store.mas_submod_utils.submod_log.warning("MAS native function update cacert failed")
767-
768-
769677
import hashlib
770678

771679
def calculate_sha256(file_path):

‎game/Submods/MAICA_ChatSubmod/header.rpy‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ init -990 python:
55
name="MAICA Blessland",
66
description=_("MAICA Official Submod Frontend"),
77
version=maica_ver,
8-
dependencies={"Ignore Translation Conflicts": (None, None)},
8+
dependencies={"Ignore Translation Conflicts": (None, None), "CertifiFixer": (None, None)},
99
settings_pane="maica_setting_pane"
1010
)
1111
init -989 python:

‎tests/test_backend_v13_compat.py‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -861,20 +861,14 @@ def test_a_connection_entrypoints_wait_for_shutdown_and_block_mutation():
861861
)
862862

863863

864-
def test_a_certificate_repair_and_version_disable_are_sticky():
864+
def test_version_disable_is_sticky_and_certifi_fallback_is_conditional():
865865
api = source("game/Submods/MAICA_ChatSubmod/api.rpy")
866-
repair = function_body(api, r"maica_download_certifi_files")
867866
startup = function_body(api, r"start_maica")
868867
version_guard = function_body(api, r"check_accessibility")
869868
provider_refresh = function_body(api, r"refresh_provider_list")
870869

871870
assert "13408" not in api
872-
assert repair.count("CERTIFI_RESTART_REQUIRED") == 1
873-
assert re.search(
874-
r"disable\(\s*.*?CERTIFI_RESTART_REQUIRED\s*,\s*sticky\s*=\s*True",
875-
repair,
876-
re.S,
877-
)
871+
assert re.search(r"if not store\.mas_can_import\.certifi\(\):\s*maica_set_plain_provider\(\)", startup)
878872
assert "check_accessibility()" in startup
879873
assert re.search(
880874
r"disable\(\s*.*?VERSION_OLD\s*,\s*sticky\s*=\s*True",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from pathlib import Path
2+
3+
4+
ROOT = Path(__file__).resolve().parents[1]
5+
HEADER = ROOT / "game" / "Submods" / "MAICA_ChatSubmod" / "header.rpy"
6+
WORKFLOW = ROOT / ".github" / "workflows" / "release.yml"
7+
DEPENDENCY_REPO = "MAS-Submod-MoyuTeam/mas_certifi_fixer"
8+
9+
10+
def test_maica_declares_certifi_fixer_dependency_without_vendoring_it():
11+
header = HEADER.read_text(encoding="utf-8")
12+
13+
assert '"CertifiFixer": (None, None)' in header
14+
assert not any(
15+
path.is_file()
16+
for path in (ROOT / "game" / "Submods" / "CertifiFixer").rglob("*")
17+
)
18+
19+
20+
def test_release_stages_certifi_fixer_before_creating_package():
21+
workflow = WORKFLOW.read_text(encoding="utf-8")
22+
download_step = workflow.index("- name: Download CertifiFixer dependency")
23+
package_step = workflow.index("- name: Create zip package")
24+
dependency_block = workflow[download_step:package_step]
25+
26+
assert DEPENDENCY_REPO in dependency_block
27+
assert 'gh release download' in dependency_block
28+
assert '--pattern "CertifiFixer-*.zip"' in dependency_block
29+
assert "if: steps.get_version.outputs.is_development == 'false' && steps.check_release.outputs.create_release == 'true'" in dependency_block
30+
assert "unzip -q" in dependency_block
31+
assert "game/Submods/CertifiFixer" in dependency_block
32+
for filename in ("certifi_fixer.rpy", "core.py", "__init__.py", "cacert.pem"):
33+
assert filename in dependency_block
34+
assert 'cp -R "$dependency_source" game/Submods/' in dependency_block
35+
assert "exit 1" in dependency_block

‎tests/test_ignore_translation_conflicts_dependency.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
def test_maica_declares_unrestricted_translation_conflicts_dependency():
1414
header = HEADER.read_text(encoding="utf-8")
1515

16-
assert 'dependencies={"Ignore Translation Conflicts": (None, None)},' in header
16+
assert '"Ignore Translation Conflicts": (None, None)' in header
1717

1818

1919
def test_release_stages_dependency_before_creating_package():

‎tests/test_start_maica_background_download.py‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ def _extract_function(source, function_name):
1313
return source[start:next_def]
1414

1515

16-
def test_start_maica_schedules_certifi_download_without_direct_network_io():
17-
start_maica = _extract_function(API_RPY.read_text(encoding="utf-8"), "start_maica")
18-
19-
assert "requests.get(" not in start_maica
20-
assert "maica_start_certifi_download_in_background(" in start_maica
16+
def test_start_maica_rechecks_certifi_after_fixer_then_falls_back_on_failure():
17+
source = API_RPY.read_text(encoding="utf-8")
18+
start_maica = _extract_function(source, "start_maica")
19+
20+
assert "if not store.mas_can_import.certifi():\n maica_set_plain_provider()" in start_maica
21+
assert "maica_start_certifi_download_in_background" not in source
22+
assert "maica_download_certifi_files" not in source
23+
assert "maica_can_update_cacert" not in source
2124

2225

2326
def test_start_maica_leaves_topic_reconciliation_to_the_post_migration_hook():

0 commit comments

Comments
 (0)