appguard-runtime 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. appguard_runtime-0.0.1/CHANGELOG.md +16 -0
  2. appguard_runtime-0.0.1/LICENSE +21 -0
  3. appguard_runtime-0.0.1/MANIFEST.in +16 -0
  4. appguard_runtime-0.0.1/PKG-INFO +166 -0
  5. appguard_runtime-0.0.1/README.md +137 -0
  6. appguard_runtime-0.0.1/appguard/__init__.py +1 -0
  7. appguard_runtime-0.0.1/appguard/__main__.py +111 -0
  8. appguard_runtime-0.0.1/appguard/_runtime/LICENSE +21 -0
  9. appguard_runtime-0.0.1/appguard/_runtime/appguard_flask.py +17 -0
  10. appguard_runtime-0.0.1/appguard/_runtime/appguard_host.py +166 -0
  11. appguard_runtime-0.0.1/appguard/_runtime/guard_runtime.pyx +256 -0
  12. appguard_runtime-0.0.1/appguard/_runtime/pyproject.toml +17 -0
  13. appguard_runtime-0.0.1/appguard/_runtime/setup.py +28 -0
  14. appguard_runtime-0.0.1/appguard/build.py +126 -0
  15. appguard_runtime-0.0.1/appguard/crypto.py +45 -0
  16. appguard_runtime-0.0.1/appguard/runtime.py +59 -0
  17. appguard_runtime-0.0.1/appguard_runtime.egg-info/PKG-INFO +166 -0
  18. appguard_runtime-0.0.1/appguard_runtime.egg-info/SOURCES.txt +46 -0
  19. appguard_runtime-0.0.1/appguard_runtime.egg-info/dependency_links.txt +1 -0
  20. appguard_runtime-0.0.1/appguard_runtime.egg-info/entry_points.txt +2 -0
  21. appguard_runtime-0.0.1/appguard_runtime.egg-info/requires.txt +10 -0
  22. appguard_runtime-0.0.1/appguard_runtime.egg-info/top_level.txt +1 -0
  23. appguard_runtime-0.0.1/docs/first-release.md +141 -0
  24. appguard_runtime-0.0.1/docs/keys.md +108 -0
  25. appguard_runtime-0.0.1/docs/releases.md +84 -0
  26. appguard_runtime-0.0.1/examples/flask/DEPLOY.md +46 -0
  27. appguard_runtime-0.0.1/examples/flask/Dockerfile +40 -0
  28. appguard_runtime-0.0.1/examples/flask/cli.py +14 -0
  29. appguard_runtime-0.0.1/examples/flask/guard.toml +2 -0
  30. appguard_runtime-0.0.1/examples/flask/requirements.txt +2 -0
  31. appguard_runtime-0.0.1/examples/flask/service.py +2 -0
  32. appguard_runtime-0.0.1/examples/flask/web_app.py +20 -0
  33. appguard_runtime-0.0.1/pyproject.toml +50 -0
  34. appguard_runtime-0.0.1/setup.cfg +4 -0
  35. appguard_runtime-0.0.1/setup.py +5 -0
  36. appguard_runtime-0.0.1/tests/test_audit_image.py +162 -0
  37. appguard_runtime-0.0.1/tests/test_build.py +320 -0
  38. appguard_runtime-0.0.1/tests/test_cli.py +153 -0
  39. appguard_runtime-0.0.1/tests/test_release.py +200 -0
  40. appguard_runtime-0.0.1/tests/test_runtime_build.py +48 -0
  41. appguard_runtime-0.0.1/tests/test_runtime_image.py +602 -0
  42. appguard_runtime-0.0.1/tools/audit_image.py +127 -0
  43. appguard_runtime-0.0.1/tools/check_release.py +36 -0
  44. appguard_runtime-0.0.1/tools/sanitize_metadata.py +25 -0
  45. appguard_runtime-0.0.1/tools/verify_delivery.py +180 -0
  46. appguard_runtime-0.0.1/tools/verify_http.py +111 -0
  47. appguard_runtime-0.0.1/tools/verify_package.py +158 -0
  48. appguard_runtime-0.0.1/tools/verify_pypi.py +97 -0
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## [0.0.1] - 2026-09-09
4
+
5
+ First public PyPI release of AppGuard, supporting CPython 3.11.
6
+
7
+ - Install the publisher toolkit with `pip install appguard-runtime==0.0.1`.
8
+ - Encrypt complete Python modules and sign reusable offline product licenses.
9
+ - Build a private native runtime for each product with `appguard build-runtime`.
10
+ - Integrate licensing with Flask or WSGI, including offline activation and renewal.
11
+ - Validate tests, public distributions, and Docker delivery before tagged releases.
12
+ - Publish to official PyPI through GitHub Actions Trusted Publishing.
13
+
14
+ The earlier repository-only version `0.3.0` was not a public PyPI release. The first
15
+ public distribution starts at `0.0.1`; encrypted bundle and license formats remain
16
+ at format 2.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 baiyang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,16 @@
1
+ include README.md
2
+ include CHANGELOG.md
3
+ include LICENSE
4
+ include appguard/_runtime/LICENSE
5
+ include appguard/_runtime/pyproject.toml
6
+ include appguard/_runtime/setup.py
7
+ include appguard/_runtime/guard_runtime.pyx
8
+ include appguard/_runtime/appguard_host.py
9
+ include appguard/_runtime/appguard_flask.py
10
+ recursive-include tests *.py
11
+ recursive-include tools *.py
12
+ recursive-include docs *.md
13
+ recursive-include examples *.py *.toml *.txt *.md Dockerfile
14
+ global-exclude *.pyc *.pyo *.so *.pyd *.c *.key *.pub *.license
15
+ prune .data
16
+ prune .github
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: appguard-runtime
3
+ Version: 0.0.1
4
+ Summary: Publisher tools for encrypted Python modules and offline product licensing
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/baiyang/AppGuard
7
+ Project-URL: Repository, https://github.com/baiyang/AppGuard
8
+ Project-URL: Issues, https://github.com/baiyang/AppGuard/issues
9
+ Project-URL: Documentation, https://github.com/baiyang/AppGuard/blob/main/docs/first-release.md
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: Implementation :: CPython
14
+ Classifier: Topic :: Software Development :: Build Tools
15
+ Classifier: Topic :: Security :: Cryptography
16
+ Requires-Python: <3.12,>=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: cryptography==45.0.4
20
+ Requires-Dist: build<2,>=1.2.2
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest<10,>=8; extra == "test"
23
+ Requires-Dist: Cython==3.1.2; extra == "test"
24
+ Requires-Dist: setuptools==80.9.0; extra == "test"
25
+ Requires-Dist: wheel==0.45.1; extra == "test"
26
+ Requires-Dist: Flask<4,>=3.1; extra == "test"
27
+ Requires-Dist: requests<3,>=2.32; extra == "test"
28
+ Dynamic: license-file
29
+
30
+ # AppGuard
31
+
32
+ 为 Python Web 项目提供整模块代码加密交付和离线产品授权。发行方交付镜像与许可证,客户导入许可证后即可使用后端接口。代码保护与授权判断独立,不需要额外部署授权服务器。
33
+
34
+ 当前支持 **CPython 3.11**,提供 **Flask 插件和 WSGI 中间件**。ASGI 需要另行适配;运行环境的系统、CPU 架构和 Python 版本必须与原生运行时匹配。
35
+
36
+ ## 安装
37
+
38
+ ```sh
39
+ python3.11 -m pip install appguard-runtime==0.0.1
40
+ appguard --help
41
+ ```
42
+
43
+ PyPI 上的 `appguard-runtime` 是发行方工具包,包含密钥生成、模块加密、许可证签发和原生运行时编译模板。它不包含任何产品密钥,也不直接安装客户侧的 `guard_runtime`、`appguard_host` 或 `appguard_flask`。所有命令也可通过 `python -m appguard` 调用。
44
+
45
+ 为产品生成一次密钥,然后在与目标部署相同的系统、CPU 架构和 CPython 3.11 环境中编译客户运行时(需要 C 编译器和 Python 开发头文件):
46
+
47
+ ```sh
48
+ appguard keygen --out .data/issuer.key
49
+ appguard code-keygen --out .data/code.key
50
+ appguard build-runtime --public-key .data/issuer.pub \
51
+ --code-key .data/code.key --out .data/runtime
52
+ ```
53
+
54
+ 生成的 `appguard_product_runtime-0.0.1-*.whl` 包含客户侧插件和编入产品密钥的原生模块,仅随对应产品私下交付,不能上传到公共包仓库。客户使用 `python -m pip install /path/to/appguard_product_runtime-0.0.1-*.whl` 安装;不同产品应使用各自独立的容器或虚拟环境。应用加密和完整镜像交付步骤见[首次发行指南](https://github.com/baiyang/AppGuard/blob/main/docs/first-release.md)。
55
+
56
+ ## 从这里开始
57
+
58
+ - **第一次制作交付包**:按[首次发行指南](https://github.com/baiyang/AppGuard/blob/main/docs/first-release.md)完成打包、测试和交付。
59
+ - **已收到交付包**:按[示例部署说明](https://github.com/baiyang/AppGuard/blob/main/examples/flask/DEPLOY.md)启动应用并导入许可证。
60
+ - **接入自己的后端**:参考下方接入配置;密钥保存与轮换见[密钥说明](https://github.com/baiyang/AppGuard/blob/main/docs/keys.md)。
61
+ - **维护 AppGuard 版本**:见[版本与发布流程](https://github.com/baiyang/AppGuard/blob/main/docs/releases.md)。
62
+
63
+ ## 架构与边界
64
+
65
+ ```mermaid
66
+ flowchart TB
67
+ subgraph publisher["发行方"]
68
+ source["Python 源码 + guard.toml"]
69
+ issuer["签名私钥 issuer.key"]
70
+ code_key["产品代码密钥 code.key"]
71
+ build["编译并加密整模块<br/>签署 manifest.json"]
72
+ issue["签发产品许可证"]
73
+ source --> build
74
+ issuer --> build
75
+ code_key --> build
76
+ issuer --> issue
77
+ end
78
+ subgraph customer["客户应用容器"]
79
+ web["Flask / WSGI 后端<br/>最外层授权中间件"]
80
+ portal["/_license/<br/>状态与许可证导入"]
81
+ runtime["Cython 运行时<br/>模块验签解密、许可证验签"]
82
+ bundle["加载入口 + 加密模块 + 签名清单"]
83
+ license[("授权卷<br/>license.json + 时钟记录")]
84
+ web --> runtime
85
+ web --> portal
86
+ bundle --> runtime
87
+ license --> runtime
88
+ end
89
+ build --> bundle
90
+ issue -.->|"离线交付许可证"| license
91
+ ```
92
+
93
+ 系统只维护三种长期密钥值:发行方签名私钥 `issuer.key`、对应验签公钥 `issuer.pub`、每产品固定的代码密钥 `code.key`。公钥和代码密钥编入运行时;签名私钥不交付。普通应用更新复用产品代码密钥和运行时,无需重新签发未到期的产品许可证。
94
+
95
+ - **代码保护**:构建端用 `compile` / `marshal` 生成整模块字节码,再用 AES-GCM 加密为 `.agc`。镜像内的 `.py` 仅为加载入口;运行时验签、解密后在内存执行,不将明文字节码写回磁盘。
96
+ - **后端授权**:中间件在每个业务请求进入应用之前验签并检查有效期。未授权、过期或许可证无效时,所有业务路径统一返回 **HTTP 403 JSON**,不依据 `Accept`、路径前缀或浏览器类型重定向。
97
+ - **授权页面**:`/_license/`、状态与导入接口独立开放。应用未授权也能启动,客户可随时导入续期许可证,无需重启。
98
+ - **前后端分离**:前端 HTML、JavaScript、CSS 不是加密目标。前端统一处理后端授权错误并跳转授权页;本项目不接管前端路由。不要把所有业务 403 都当作授权到期,应检查响应的授权错误码。
99
+
100
+ 加密让客户拿不到可直接阅读的业务 Python 源码,不保证抵抗主机管理员提取二进制密钥、内存代码或修改运行程序。代码密钥随运行时交付,许可证不承载解密密钥;移除授权中间件后可以调用业务逻辑。这是精简方案的明确边界。
101
+
102
+ 授权只控制新进入的 HTTP 业务请求,不控制 CLI、后台任务、直接函数调用,也不中断已开始的请求或流式响应。离线时钟回退检测只用于辅助发现异常,无法阻止管理员恢复整机或授权卷快照。产品许可证不绑定机器或构建版本,同一许可证可复制到同产品的其他部署。
103
+
104
+ ## 接入自己的后端
105
+
106
+ Flask 项目在应用与其他中间件配置完成后,最后注册 AppGuard,使授权检查位于业务入口最外层:
107
+
108
+ ```python
109
+ from appguard_flask import AppGuard
110
+
111
+ AppGuard().init_app(app)
112
+ ```
113
+
114
+ 其他 WSGI 后端在完成应用组装后包装入口:
115
+
116
+ ```python
117
+ from appguard_host import LicenseMiddleware
118
+
119
+ application = LicenseMiddleware(application)
120
+ ```
121
+
122
+ 默认没有业务路径豁免。需要存活探针时,可显式配置 `AppGuard(exempt_paths=("/healthz",))` 或 `LicenseMiddleware(application, exempt_paths=("/healthz",))`;仅完全匹配路径的 GET/HEAD 请求免授权,不按目录前缀放行。探针应只报告进程存活,不暴露业务数据。
123
+
124
+ 在 `guard.toml` 中选择交付文件,不需要配置函数、检查点或改写业务函数:
125
+
126
+ ```toml
127
+ product_id = "my-web-app"
128
+ include = ["web_app.py", "service.py", "templates/", "static/"]
129
+ exclude = ["**/__pycache__/", "**/.env*", "**/*.pyc"]
130
+ ```
131
+
132
+ 路径相对于 `build --source`,支持文件、目录和通配符。选中的 Python 模块整体加密;非 Python 文件原样复制,需排除私密配置、开发文件和生成的 C 源文件。依赖安装、数据库初始化及后台任务仍由业务项目自己的部署流程负责。
133
+
134
+ ## 签发、激活与续期
135
+
136
+ 发行方知道产品标识、客户标识和到期时间后即可签发,不再收集部署申请:
137
+
138
+ ```sh
139
+ python -m appguard issue \
140
+ --product example-web --issuer-key .data/issuer.key \
141
+ --customer customer-001 --expires 2027-12-31T23:59:59Z \
142
+ --out .data/customer-001.license
143
+ ```
144
+
145
+ 客户在 `http://服务器地址:8000/_license/` 上传许可证。授权有效后,示例的 `/api/answer?value=8` 返回 `{"answer":50}`。缺失或过期授权时,该接口及其他业务路径均返回 403 JSON。
146
+
147
+ 续期时指定新的到期时间和输出文件,重新签发后导入即可。相同发行方和产品的新构建继续使用原有效许可证;不需要保存每个构建的秘密 `release.json`。授权卷只保存许可证和辅助时钟记录,重建容器时继续挂载。
148
+
149
+ 部署端也可使用命令行查看或导入授权:
150
+
151
+ ```sh
152
+ python -m appguard_host status
153
+ python -m appguard_host install /path/to/customer-001.license
154
+ ```
155
+
156
+ ## 配置与迁移
157
+
158
+ | 环境变量 | 默认值 | 用途 |
159
+ | --- | --- | --- |
160
+ | `APPGUARD_BUNDLE` | `/opt/appguard/bundle` | 加密模块及签名清单目录 |
161
+ | `APPGUARD_LICENSE_DIR` | `/var/lib/appguard` | 可写、持久化的授权目录 |
162
+ | `APPGUARD_SECURE_COOKIE` | `0` | HTTPS 部署时设为 `1` |
163
+
164
+ 无法激活时查看 `/_license/` 的状态提示,确认许可证产品、发行方、有效期与系统时间。运行时需与产品代码密钥匹配,但不要求每次业务构建重新编译。
165
+
166
+ 旧版 `function-bodies-v1` 包和绑定 `build_id` 的许可证不能直接用于新版。首次迁移需要移除旧函数配置、生成产品代码密钥、重新构建镜像,并重新签发产品许可证。此后普通更新可复用许可证;迁移细节见[首次发行指南](https://github.com/baiyang/AppGuard/blob/main/docs/first-release.md#从旧版迁移)。
@@ -0,0 +1,137 @@
1
+ # AppGuard
2
+
3
+ 为 Python Web 项目提供整模块代码加密交付和离线产品授权。发行方交付镜像与许可证,客户导入许可证后即可使用后端接口。代码保护与授权判断独立,不需要额外部署授权服务器。
4
+
5
+ 当前支持 **CPython 3.11**,提供 **Flask 插件和 WSGI 中间件**。ASGI 需要另行适配;运行环境的系统、CPU 架构和 Python 版本必须与原生运行时匹配。
6
+
7
+ ## 安装
8
+
9
+ ```sh
10
+ python3.11 -m pip install appguard-runtime==0.0.1
11
+ appguard --help
12
+ ```
13
+
14
+ PyPI 上的 `appguard-runtime` 是发行方工具包,包含密钥生成、模块加密、许可证签发和原生运行时编译模板。它不包含任何产品密钥,也不直接安装客户侧的 `guard_runtime`、`appguard_host` 或 `appguard_flask`。所有命令也可通过 `python -m appguard` 调用。
15
+
16
+ 为产品生成一次密钥,然后在与目标部署相同的系统、CPU 架构和 CPython 3.11 环境中编译客户运行时(需要 C 编译器和 Python 开发头文件):
17
+
18
+ ```sh
19
+ appguard keygen --out .data/issuer.key
20
+ appguard code-keygen --out .data/code.key
21
+ appguard build-runtime --public-key .data/issuer.pub \
22
+ --code-key .data/code.key --out .data/runtime
23
+ ```
24
+
25
+ 生成的 `appguard_product_runtime-0.0.1-*.whl` 包含客户侧插件和编入产品密钥的原生模块,仅随对应产品私下交付,不能上传到公共包仓库。客户使用 `python -m pip install /path/to/appguard_product_runtime-0.0.1-*.whl` 安装;不同产品应使用各自独立的容器或虚拟环境。应用加密和完整镜像交付步骤见[首次发行指南](https://github.com/baiyang/AppGuard/blob/main/docs/first-release.md)。
26
+
27
+ ## 从这里开始
28
+
29
+ - **第一次制作交付包**:按[首次发行指南](https://github.com/baiyang/AppGuard/blob/main/docs/first-release.md)完成打包、测试和交付。
30
+ - **已收到交付包**:按[示例部署说明](https://github.com/baiyang/AppGuard/blob/main/examples/flask/DEPLOY.md)启动应用并导入许可证。
31
+ - **接入自己的后端**:参考下方接入配置;密钥保存与轮换见[密钥说明](https://github.com/baiyang/AppGuard/blob/main/docs/keys.md)。
32
+ - **维护 AppGuard 版本**:见[版本与发布流程](https://github.com/baiyang/AppGuard/blob/main/docs/releases.md)。
33
+
34
+ ## 架构与边界
35
+
36
+ ```mermaid
37
+ flowchart TB
38
+ subgraph publisher["发行方"]
39
+ source["Python 源码 + guard.toml"]
40
+ issuer["签名私钥 issuer.key"]
41
+ code_key["产品代码密钥 code.key"]
42
+ build["编译并加密整模块<br/>签署 manifest.json"]
43
+ issue["签发产品许可证"]
44
+ source --> build
45
+ issuer --> build
46
+ code_key --> build
47
+ issuer --> issue
48
+ end
49
+ subgraph customer["客户应用容器"]
50
+ web["Flask / WSGI 后端<br/>最外层授权中间件"]
51
+ portal["/_license/<br/>状态与许可证导入"]
52
+ runtime["Cython 运行时<br/>模块验签解密、许可证验签"]
53
+ bundle["加载入口 + 加密模块 + 签名清单"]
54
+ license[("授权卷<br/>license.json + 时钟记录")]
55
+ web --> runtime
56
+ web --> portal
57
+ bundle --> runtime
58
+ license --> runtime
59
+ end
60
+ build --> bundle
61
+ issue -.->|"离线交付许可证"| license
62
+ ```
63
+
64
+ 系统只维护三种长期密钥值:发行方签名私钥 `issuer.key`、对应验签公钥 `issuer.pub`、每产品固定的代码密钥 `code.key`。公钥和代码密钥编入运行时;签名私钥不交付。普通应用更新复用产品代码密钥和运行时,无需重新签发未到期的产品许可证。
65
+
66
+ - **代码保护**:构建端用 `compile` / `marshal` 生成整模块字节码,再用 AES-GCM 加密为 `.agc`。镜像内的 `.py` 仅为加载入口;运行时验签、解密后在内存执行,不将明文字节码写回磁盘。
67
+ - **后端授权**:中间件在每个业务请求进入应用之前验签并检查有效期。未授权、过期或许可证无效时,所有业务路径统一返回 **HTTP 403 JSON**,不依据 `Accept`、路径前缀或浏览器类型重定向。
68
+ - **授权页面**:`/_license/`、状态与导入接口独立开放。应用未授权也能启动,客户可随时导入续期许可证,无需重启。
69
+ - **前后端分离**:前端 HTML、JavaScript、CSS 不是加密目标。前端统一处理后端授权错误并跳转授权页;本项目不接管前端路由。不要把所有业务 403 都当作授权到期,应检查响应的授权错误码。
70
+
71
+ 加密让客户拿不到可直接阅读的业务 Python 源码,不保证抵抗主机管理员提取二进制密钥、内存代码或修改运行程序。代码密钥随运行时交付,许可证不承载解密密钥;移除授权中间件后可以调用业务逻辑。这是精简方案的明确边界。
72
+
73
+ 授权只控制新进入的 HTTP 业务请求,不控制 CLI、后台任务、直接函数调用,也不中断已开始的请求或流式响应。离线时钟回退检测只用于辅助发现异常,无法阻止管理员恢复整机或授权卷快照。产品许可证不绑定机器或构建版本,同一许可证可复制到同产品的其他部署。
74
+
75
+ ## 接入自己的后端
76
+
77
+ Flask 项目在应用与其他中间件配置完成后,最后注册 AppGuard,使授权检查位于业务入口最外层:
78
+
79
+ ```python
80
+ from appguard_flask import AppGuard
81
+
82
+ AppGuard().init_app(app)
83
+ ```
84
+
85
+ 其他 WSGI 后端在完成应用组装后包装入口:
86
+
87
+ ```python
88
+ from appguard_host import LicenseMiddleware
89
+
90
+ application = LicenseMiddleware(application)
91
+ ```
92
+
93
+ 默认没有业务路径豁免。需要存活探针时,可显式配置 `AppGuard(exempt_paths=("/healthz",))` 或 `LicenseMiddleware(application, exempt_paths=("/healthz",))`;仅完全匹配路径的 GET/HEAD 请求免授权,不按目录前缀放行。探针应只报告进程存活,不暴露业务数据。
94
+
95
+ 在 `guard.toml` 中选择交付文件,不需要配置函数、检查点或改写业务函数:
96
+
97
+ ```toml
98
+ product_id = "my-web-app"
99
+ include = ["web_app.py", "service.py", "templates/", "static/"]
100
+ exclude = ["**/__pycache__/", "**/.env*", "**/*.pyc"]
101
+ ```
102
+
103
+ 路径相对于 `build --source`,支持文件、目录和通配符。选中的 Python 模块整体加密;非 Python 文件原样复制,需排除私密配置、开发文件和生成的 C 源文件。依赖安装、数据库初始化及后台任务仍由业务项目自己的部署流程负责。
104
+
105
+ ## 签发、激活与续期
106
+
107
+ 发行方知道产品标识、客户标识和到期时间后即可签发,不再收集部署申请:
108
+
109
+ ```sh
110
+ python -m appguard issue \
111
+ --product example-web --issuer-key .data/issuer.key \
112
+ --customer customer-001 --expires 2027-12-31T23:59:59Z \
113
+ --out .data/customer-001.license
114
+ ```
115
+
116
+ 客户在 `http://服务器地址:8000/_license/` 上传许可证。授权有效后,示例的 `/api/answer?value=8` 返回 `{"answer":50}`。缺失或过期授权时,该接口及其他业务路径均返回 403 JSON。
117
+
118
+ 续期时指定新的到期时间和输出文件,重新签发后导入即可。相同发行方和产品的新构建继续使用原有效许可证;不需要保存每个构建的秘密 `release.json`。授权卷只保存许可证和辅助时钟记录,重建容器时继续挂载。
119
+
120
+ 部署端也可使用命令行查看或导入授权:
121
+
122
+ ```sh
123
+ python -m appguard_host status
124
+ python -m appguard_host install /path/to/customer-001.license
125
+ ```
126
+
127
+ ## 配置与迁移
128
+
129
+ | 环境变量 | 默认值 | 用途 |
130
+ | --- | --- | --- |
131
+ | `APPGUARD_BUNDLE` | `/opt/appguard/bundle` | 加密模块及签名清单目录 |
132
+ | `APPGUARD_LICENSE_DIR` | `/var/lib/appguard` | 可写、持久化的授权目录 |
133
+ | `APPGUARD_SECURE_COOKIE` | `0` | HTTPS 部署时设为 `1` |
134
+
135
+ 无法激活时查看 `/_license/` 的状态提示,确认许可证产品、发行方、有效期与系统时间。运行时需与产品代码密钥匹配,但不要求每次业务构建重新编译。
136
+
137
+ 旧版 `function-bodies-v1` 包和绑定 `build_id` 的许可证不能直接用于新版。首次迁移需要移除旧函数配置、生成产品代码密钥、重新构建镜像,并重新签发产品许可证。此后普通更新可复用许可证;迁移细节见[首次发行指南](https://github.com/baiyang/AppGuard/blob/main/docs/first-release.md#从旧版迁移)。
@@ -0,0 +1 @@
1
+ """Publisher-side AppGuard build and license tools."""
@@ -0,0 +1,111 @@
1
+ """Publisher CLI for encrypted Python applications and offline licensing."""
2
+
3
+ import argparse
4
+ import base64
5
+ import json
6
+ import os
7
+ import time
8
+ import uuid
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
13
+
14
+ from .build import build
15
+ from .crypto import private_write, product_id, signed, signer
16
+ from .runtime import build_runtime
17
+
18
+
19
+ def timestamp(value: str) -> int:
20
+ try:
21
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
22
+ if parsed.tzinfo is None:
23
+ raise ValueError("missing timezone")
24
+ result = int(parsed.timestamp())
25
+ if not 0 <= result <= 253402300799:
26
+ raise ValueError("outside supported timestamp range")
27
+ return result
28
+ except (ValueError, OverflowError) as exc:
29
+ raise argparse.ArgumentTypeError("Use a date after 1970 with an explicit timezone, e.g. 2027-03-08T00:00:00Z") from exc
30
+
31
+
32
+ def main():
33
+ parser = argparse.ArgumentParser(description=__doc__)
34
+ sub = parser.add_subparsers(dest="command", required=True)
35
+ keys = sub.add_parser("keygen", help="Generate an Ed25519 issuer key pair")
36
+ keys.add_argument("--out", type=Path, required=True)
37
+ code_keys = sub.add_parser("code-keygen", help="Generate a product AES-256 code key")
38
+ code_keys.add_argument("--out", type=Path, required=True)
39
+ runtime = sub.add_parser("build-runtime", help="Compile a private product runtime wheel (requires a C compiler)")
40
+ for name in ("public-key", "code-key", "out"):
41
+ runtime.add_argument("--" + name, type=Path, required=True)
42
+ package = sub.add_parser("build", help="Encrypt Python modules using a product code key")
43
+ for name in ("source", "config", "issuer-key", "code-key", "out"):
44
+ package.add_argument("--" + name, type=Path, required=True)
45
+ issue = sub.add_parser("issue", help="Issue a license valid across builds of one product")
46
+ for name in ("issuer-key", "out"):
47
+ issue.add_argument("--" + name, type=Path, required=True)
48
+ issue.add_argument("--product", required=True)
49
+ issue.add_argument("--customer", required=True)
50
+ issue.add_argument("--expires", required=True, type=timestamp)
51
+ issue.add_argument("--not-before", type=timestamp)
52
+ inspect = sub.add_parser("inspect", help="Display metadata without verifying its signature")
53
+ inspect.add_argument("path", type=Path)
54
+ args = parser.parse_args()
55
+ try:
56
+ if args.command == "keygen":
57
+ public_path = args.out.with_suffix(".pub")
58
+ if public_path == args.out:
59
+ raise ValueError("Private key output must not use the .pub extension")
60
+ if any(path.exists() or path.is_symlink() for path in (args.out, public_path)):
61
+ raise ValueError("Key output already exists")
62
+ key = Ed25519PrivateKey.generate()
63
+ private_write(args.out, key.private_bytes_raw().hex().encode() + b"\n")
64
+ try:
65
+ private_write(public_path, key.public_key().public_bytes_raw().hex().encode() + b"\n")
66
+ except OSError:
67
+ args.out.unlink()
68
+ raise
69
+ print(json.dumps({"private_key": str(args.out), "public_key": str(public_path)}))
70
+ elif args.command == "code-keygen":
71
+ private_write(args.out, os.urandom(32).hex().encode() + b"\n")
72
+ print(json.dumps({"code_key": str(args.out)}))
73
+ elif args.command == "build":
74
+ print(json.dumps(build(args.source, args.config, args.issuer_key, args.out, args.code_key)))
75
+ elif args.command == "build-runtime":
76
+ print(json.dumps(build_runtime(args.public_key, args.code_key, args.out)))
77
+ elif args.command == "issue":
78
+ product = product_id(args.product)
79
+ if (not args.customer or args.customer != args.customer.strip() or len(args.customer) > 512
80
+ or any(ord(character) < 32 or ord(character) == 127 for character in args.customer)):
81
+ raise ValueError("customer must contain 1 to 512 characters without surrounding whitespace or control characters")
82
+ key = signer(args.issuer_key)
83
+ now = int(time.time())
84
+ start = args.not_before if args.not_before is not None else now
85
+ if start >= args.expires:
86
+ raise ValueError("not-before must precede expires")
87
+ if args.expires <= now:
88
+ raise ValueError("expires must be in the future")
89
+ payload = {
90
+ "format": 2, "kind": "license", "license_id": uuid.uuid4().hex,
91
+ "product_id": product, "customer": args.customer,
92
+ "issued_at": now, "not_before": start, "expires_at": args.expires,
93
+ }
94
+ private_write(args.out, signed(payload, key))
95
+ print(json.dumps({"license": str(args.out), "license_id": payload["license_id"],
96
+ "expires_at": datetime.fromtimestamp(args.expires, timezone.utc).isoformat()}))
97
+ else:
98
+ envelope = json.loads(args.path.read_text(encoding="utf-8"))
99
+ if not isinstance(envelope, dict) or not isinstance(envelope.get("payload"), str):
100
+ raise ValueError("Expected a signed envelope with a base64 payload")
101
+ payload = json.loads(base64.b64decode(envelope["payload"], validate=True))
102
+ if not isinstance(payload, dict):
103
+ raise ValueError("Expected an object payload")
104
+ payload.pop("modules", None)
105
+ print(json.dumps({"verified": False, "metadata": payload}, indent=2))
106
+ except (ValueError, OSError, KeyError, SyntaxError, RuntimeError) as exc:
107
+ parser.exit(1, f"AppGuard: {exc}\n")
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 baiyang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,17 @@
1
+ """Explicit Flask extension. Does not create, import or run the business app."""
2
+
3
+ from appguard_host import LicenseMiddleware
4
+
5
+
6
+ class AppGuard:
7
+ def __init__(self, app=None, *, exempt_paths=()):
8
+ self.exempt_paths = exempt_paths
9
+ if app is not None:
10
+ self.init_app(app)
11
+
12
+ def init_app(self, app):
13
+ if "appguard" in app.extensions:
14
+ return
15
+ middleware = LicenseMiddleware(app.wsgi_app, exempt_paths=self.exempt_paths)
16
+ app.wsgi_app = middleware
17
+ app.extensions["appguard"] = self