All checks were successful
Test / test (push) Successful in 1m45s
- Migrate SQLModel base classes, mixins, and database management to external sqlmodel-ext package; remove sqlmodels/base/, sqlmodels/mixin/, and sqlmodels/database.py - Add file viewer/editor system with WOPI protocol support for collaborative editing (OnlyOffice, Collabora) - Add enterprise edition license verification module (ee/) - Add Dockerfile multi-stage build with Cython compilation support - Add new dependencies: sqlmodel-ext, cryptography, whatthepatch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
87 lines
2.9 KiB
Python
87 lines
2.9 KiB
Python
"""
|
|
RSA-PSS 许可证验签核心(编译为 .so 后公钥藏入二进制)
|
|
|
|
此文件只包含纯函数和常量,不包含 SQLModel 类。
|
|
"""
|
|
import base64
|
|
from datetime import datetime, timezone
|
|
|
|
import orjson
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
|
|
|
|
_PUBLIC_KEY_PEM: bytes = b"""-----BEGIN PUBLIC KEY-----
|
|
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyNltXQ/Nuechx3kjj3T5
|
|
oR6pZvTmpsDowqqxXJy7FXUI8d7XprhV+HrBQPsrT/Ngo9FwW3XyiK10m1WrzpGW
|
|
eaf9990Z5Z2naEn5TzGrh71p/D7mZcNGVumo9uAuhtNEemm6xB3FoyGYZj7X0cwA
|
|
VDvIiKAwYyRJX2LqVh1/tZM6tTO3oaGZXRMZzCNUPFSo4ZZudU3Boa5oQg08evu4
|
|
vaOqeFrMX47R3MSUmO9hOh+NS53XNqO0f0zw5sv95CtyR5qvJ4gpkgYaRCSQFd19
|
|
TnHU5saFVrH9jdADz1tdkMYcyYE+uJActZBapxCHSYB2tSCKWjDxeUFl/oY/ZFtY
|
|
l4MNz1ovkjNhpmR3g+I5fbvN0cxDIjnZ9vJ84ozGqTGT9s1jHaLbpLri/vhuT4F2
|
|
7kifXk8ImwtMZpZvzhmucH9/5VgcWKNuMATzEMif+YjFpuOGx8gc1XL1W/3q+dH0
|
|
EFESp+/knjcVIfwpAkIKyV7XvDgFHsif1SeI0zZMW4utowVvGocP1ZzK5BGNTk2z
|
|
CEtQDO7Rqo+UDckOJSG66VW3c2QO8o6uuy6fzx7q0MFEmUMwGf2iMVtR/KnXe99C
|
|
enOT0BpU1EQvqssErUqivDss7jm98iD8M/TCE7pFboqZ+SC9G+QAqNIQNFWh8bWA
|
|
R9hyXM/x5ysHd6MC4eEQnhMCAwEAAQ==
|
|
-----END PUBLIC KEY-----"""
|
|
|
|
|
|
class LicenseError(Exception):
|
|
"""许可证验证基础异常"""
|
|
|
|
|
|
class LicenseExpiredError(LicenseError):
|
|
"""许可证已过期"""
|
|
|
|
|
|
def verify_license(raw: str) -> dict:
|
|
"""
|
|
验证许可证字符串并返回载荷字典。
|
|
|
|
:param raw: 格式为 ``base64(json_payload).base64(signature)``
|
|
:returns: 解析后的载荷字典
|
|
:raises LicenseError: 格式无效或签名验证失败
|
|
:raises LicenseExpiredError: 许可证已过期
|
|
"""
|
|
parts = raw.strip().split(".")
|
|
if len(parts) != 2:
|
|
raise LicenseError("许可证格式无效:需要 payload.signature")
|
|
|
|
payload_b64, signature_b64 = parts
|
|
|
|
try:
|
|
payload_bytes = base64.urlsafe_b64decode(payload_b64)
|
|
signature = base64.urlsafe_b64decode(signature_b64)
|
|
except Exception as exc:
|
|
raise LicenseError(f"许可证 base64 解码失败: {exc}") from exc
|
|
|
|
public_key = serialization.load_pem_public_key(_PUBLIC_KEY_PEM)
|
|
|
|
try:
|
|
public_key.verify( # type: ignore[union-attr]
|
|
signature,
|
|
payload_bytes,
|
|
padding.PSS(
|
|
mgf=padding.MGF1(hashes.SHA256()),
|
|
salt_length=padding.PSS.MAX_LENGTH,
|
|
),
|
|
hashes.SHA256(),
|
|
)
|
|
except Exception as exc:
|
|
raise LicenseError(f"许可证签名验证失败: {exc}") from exc
|
|
|
|
data: dict = orjson.loads(payload_bytes)
|
|
|
|
expires_at_str: str | None = data.get('expires_at')
|
|
if not expires_at_str:
|
|
raise LicenseError("许可证缺少 expires_at 字段")
|
|
|
|
expires_at = datetime.fromisoformat(expires_at_str)
|
|
if expires_at < datetime.now(timezone.utc):
|
|
raise LicenseExpiredError(
|
|
f"许可证已过期: {expires_at.isoformat()}"
|
|
)
|
|
|
|
return data
|