tcaicc 1.4.0__py3-none-any.whl
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.
- tcaicc/__init__.py +121 -0
- tcaicc/common/__init__.py +1 -0
- tcaicc/common/cipherx/__init__.py +151 -0
- tcaicc/common/cipherx/_aead.py +43 -0
- tcaicc/common/cipherx/_binding.py +208 -0
- tcaicc/common/cipherx/_constants.py +17 -0
- tcaicc/common/cipherx/_decrypt.py +109 -0
- tcaicc/common/cipherx/_encrypt.py +127 -0
- tcaicc/common/cipherx/_envelope.py +264 -0
- tcaicc/common/cipherx/_errors.py +67 -0
- tcaicc/common/cipherx/_hash.py +14 -0
- tcaicc/common/cipherx/_kat.py +82 -0
- tcaicc/common/cipherx/_metadata.py +141 -0
- tcaicc/common/cipherx/_padding.py +68 -0
- tcaicc/common/cipherx/_prefix.py +11 -0
- tcaicc/common/cipherx/_requestid.py +26 -0
- tcaicc/common/cipherx/_stream.py +189 -0
- tcaicc/common/cryptox/__init__.py +54 -0
- tcaicc/common/cryptox/_aead.py +43 -0
- tcaicc/common/cryptox/_dek.py +36 -0
- tcaicc/common/cryptox/_errors.py +22 -0
- tcaicc/common/cryptox/_kdf.py +36 -0
- tcaicc/common/cryptox/_kex.py +73 -0
- tcaicc/common/cryptox/_lp.py +17 -0
- tcaicc/common/cryptox/_mac.py +13 -0
- tcaicc/common/cryptox/_nonce.py +7 -0
- tcaicc/common/cryptox/_subtle.py +10 -0
- tcaicc/common/security/__init__.py +185 -0
- tcaicc/common/security/_body.py +242 -0
- tcaicc/common/security/_client.py +1122 -0
- tcaicc/common/security/_config.py +26 -0
- tcaicc/common/security/_crypto_errors.py +58 -0
- tcaicc/common/security/_derive.py +59 -0
- tcaicc/common/security/_detect.py +33 -0
- tcaicc/common/security/_endpoint_pool.py +149 -0
- tcaicc/common/security/_endpoints.py +29 -0
- tcaicc/common/security/_errors.py +53 -0
- tcaicc/common/security/_field_path_aliases.py +86 -0
- tcaicc/common/security/_flags.py +12 -0
- tcaicc/common/security/_http.py +310 -0
- tcaicc/common/security/_keysched.py +95 -0
- tcaicc/common/security/_log.py +166 -0
- tcaicc/common/security/_model_pool.py +355 -0
- tcaicc/common/security/_policy.py +179 -0
- tcaicc/common/security/_pool.py +319 -0
- tcaicc/common/security/_proto.py +74 -0
- tcaicc/common/security/_request.py +221 -0
- tcaicc/common/security/_sev.py +254 -0
- tcaicc/common/security/_stream.py +93 -0
- tcaicc/common/security/_tls.py +213 -0
- tcaicc/common/security/_transcript.py +116 -0
- tcaicc/common/security/_transport.py +80 -0
- tcaicc/common/security/_trust_anchors.py +127 -0
- tcaicc/common/security/trust_anchors/bergamo_ark.pem +37 -0
- tcaicc/common/security/trust_anchors/bergamo_ask.pem +37 -0
- tcaicc/common/security/trust_anchors/genoa_ark.pem +37 -0
- tcaicc/common/security/trust_anchors/genoa_ask.pem +37 -0
- tcaicc/common/security/trust_anchors/milan_ark.pem +37 -0
- tcaicc/common/security/trust_anchors/milan_ask.pem +37 -0
- tcaicc/common/security/trust_anchors/siena_ark.pem +37 -0
- tcaicc/common/security/trust_anchors/siena_ask.pem +37 -0
- tcaicc/common/security/trust_anchors/turin_ark.pem +37 -0
- tcaicc/common/security/trust_anchors/turin_ask.pem +37 -0
- tcaicc-1.4.0.dist-info/METADATA +27 -0
- tcaicc-1.4.0.dist-info/RECORD +67 -0
- tcaicc-1.4.0.dist-info/WHEEL +4 -0
- tcaicc-1.4.0.dist-info/licenses/THIRD_PARTY_NOTICES +26 -0
tcaicc/__init__.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
__version__ = "1.4.0"
|
|
2
|
+
|
|
3
|
+
from .common.cipherx import (
|
|
4
|
+
ChunkDecryptor,
|
|
5
|
+
ChunkEncryptor,
|
|
6
|
+
CipherType,
|
|
7
|
+
CipherTypeError,
|
|
8
|
+
CipherxError,
|
|
9
|
+
DecryptError,
|
|
10
|
+
Envelope,
|
|
11
|
+
FormatError,
|
|
12
|
+
HashMismatchError,
|
|
13
|
+
KeySizeError,
|
|
14
|
+
Metadata,
|
|
15
|
+
StreamClosedError,
|
|
16
|
+
StreamVerifyError,
|
|
17
|
+
TooLargeError,
|
|
18
|
+
compute_prefix,
|
|
19
|
+
encrypt_prompt,
|
|
20
|
+
encrypt_response,
|
|
21
|
+
parse,
|
|
22
|
+
)
|
|
23
|
+
from .common.cryptox import (
|
|
24
|
+
CryptoxError,
|
|
25
|
+
DecryptError as CryptoxDecryptError,
|
|
26
|
+
HybridEphemeral,
|
|
27
|
+
HybridShare,
|
|
28
|
+
constant_time_equal,
|
|
29
|
+
derive_dek,
|
|
30
|
+
encode_lp,
|
|
31
|
+
hkdf_expand,
|
|
32
|
+
hkdf_extract,
|
|
33
|
+
hmac_sha256,
|
|
34
|
+
open_,
|
|
35
|
+
open_base64,
|
|
36
|
+
random_nonce,
|
|
37
|
+
respond_hybrid_exchange,
|
|
38
|
+
seal,
|
|
39
|
+
seal_base64,
|
|
40
|
+
zero,
|
|
41
|
+
)
|
|
42
|
+
from .common.security import (
|
|
43
|
+
ENDPOINT_NOT_FOUND,
|
|
44
|
+
FLAG_PROMPT_LONG,
|
|
45
|
+
FLAG_PROMPT_SHORT,
|
|
46
|
+
NO_ENDPOINT,
|
|
47
|
+
TLS_INSECURE,
|
|
48
|
+
TLS_NONE,
|
|
49
|
+
TLS_TLS,
|
|
50
|
+
ClosedError,
|
|
51
|
+
CreateResult,
|
|
52
|
+
CryptoError,
|
|
53
|
+
DeriveResult,
|
|
54
|
+
EncodingError,
|
|
55
|
+
EndpointNotFoundError,
|
|
56
|
+
EndpointPool,
|
|
57
|
+
HttpClient,
|
|
58
|
+
HttpTransportResponse,
|
|
59
|
+
ImportResult,
|
|
60
|
+
InsecureTLSWarning,
|
|
61
|
+
KeyGeneratorError,
|
|
62
|
+
KeySizeError as CryptoKeySizeError,
|
|
63
|
+
Logger,
|
|
64
|
+
LogLevel,
|
|
65
|
+
ModelPool,
|
|
66
|
+
NoEndpointError,
|
|
67
|
+
Policy,
|
|
68
|
+
Protocol,
|
|
69
|
+
Request,
|
|
70
|
+
SecuritySession,
|
|
71
|
+
SecuritySessionError,
|
|
72
|
+
SessionFactory,
|
|
73
|
+
SessionIDError,
|
|
74
|
+
SessionPool,
|
|
75
|
+
StreamDecryptor,
|
|
76
|
+
TimeoutException,
|
|
77
|
+
TlsMode,
|
|
78
|
+
Transport,
|
|
79
|
+
TransportError,
|
|
80
|
+
TransportRequest,
|
|
81
|
+
TransportResponse,
|
|
82
|
+
binding_key,
|
|
83
|
+
build_ssl_context,
|
|
84
|
+
default_policy,
|
|
85
|
+
describe_endpoints,
|
|
86
|
+
detect_envelope,
|
|
87
|
+
set_log_level,
|
|
88
|
+
set_logger,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
"__version__",
|
|
93
|
+
# cryptox(低层原语)
|
|
94
|
+
"CryptoxError", "CryptoxDecryptError", "HybridEphemeral", "HybridShare",
|
|
95
|
+
"constant_time_equal", "derive_dek", "encode_lp", "hkdf_expand", "hkdf_extract",
|
|
96
|
+
"hmac_sha256", "open_", "open_base64", "random_nonce",
|
|
97
|
+
"respond_hybrid_exchange", "seal", "seal_base64", "zero",
|
|
98
|
+
# cipherx(信封格式)
|
|
99
|
+
"CipherxError", "KeySizeError", "FormatError", "TooLargeError", "DecryptError",
|
|
100
|
+
"HashMismatchError", "CipherTypeError", "StreamClosedError", "StreamVerifyError",
|
|
101
|
+
"encrypt_prompt", "encrypt_response", "parse",
|
|
102
|
+
"ChunkEncryptor", "ChunkDecryptor", "Envelope", "Metadata", "CipherType",
|
|
103
|
+
"compute_prefix",
|
|
104
|
+
# 数据面加解密客户端
|
|
105
|
+
"CryptoError", "CryptoKeySizeError", "SessionIDError", "ClosedError", "EncodingError",
|
|
106
|
+
"Request", "StreamDecryptor", "detect_envelope",
|
|
107
|
+
"Policy", "Protocol", "default_policy",
|
|
108
|
+
# 传输(TLS 形态由 base_url scheme 决定,无 mTLS)
|
|
109
|
+
"HttpClient", "TransportResponse", "TransportRequest", "Transport",
|
|
110
|
+
"HttpTransportResponse", "InsecureTLSWarning", "build_ssl_context", "TlsMode",
|
|
111
|
+
"TLS_TLS", "TLS_INSECURE", "TLS_NONE",
|
|
112
|
+
# 安全会话
|
|
113
|
+
"SessionFactory", "SessionPool", "SecuritySession",
|
|
114
|
+
"EndpointPool", "ModelPool", "describe_endpoints", "binding_key",
|
|
115
|
+
"FLAG_PROMPT_SHORT", "FLAG_PROMPT_LONG",
|
|
116
|
+
"EndpointNotFoundError", "NoEndpointError", "ENDPOINT_NOT_FOUND", "NO_ENDPOINT",
|
|
117
|
+
"CreateResult", "ImportResult", "DeriveResult",
|
|
118
|
+
"SecuritySessionError", "TransportError", "TimeoutException", "KeyGeneratorError",
|
|
119
|
+
# 进程级日志(四级阈值,默认 error 即零输出)
|
|
120
|
+
"set_logger", "set_log_level", "LogLevel", "Logger",
|
|
121
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""common 共享子包。"""
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""cipherx —— LLM prompt/response/流式 chunk 的 Envelope 密文格式。
|
|
2
|
+
|
|
3
|
+
逐字节移植 ``aicc-common/cipherx``(wire 规范权威,配套设计
|
|
4
|
+
``docs/superpowers/specs/2026-08-06-cipherx-envelope-design.md``)。
|
|
5
|
+
**wire 变更须与 Go 端同步并重冻 KAT**(见 ``_kat``)。
|
|
6
|
+
|
|
7
|
+
Wire 布局(自尾部解析)::
|
|
8
|
+
|
|
9
|
+
prompt = [Prefix][Ciphertext][Hash][Metadata][Suffix]
|
|
10
|
+
response = [Ciphertext][Metadata][Suffix]
|
|
11
|
+
chunk 中间 = [Ciphertext][Metadata][Suffix] (Metadata 仅 fp/fs/pl)
|
|
12
|
+
chunk 尾 = [Ciphertext][Metadata][Suffix] (Flags.IsFinal=1)
|
|
13
|
+
|
|
14
|
+
Suffix 10B = Magic "AICCv1"(6B) ‖ CipherType(1B: 1=prompt 2=response 3=chunk) ‖
|
|
15
|
+
Flags(1B: bit0 HasPrefix, bit1 HasCiphertext, bit2 HasHash, bit3 HasMetadata,
|
|
16
|
+
bit4 HasSuffix, bit5 IsFinal) ‖ MetadataLength(2B 大端)。
|
|
17
|
+
Ciphertext = GCM nonce(12B) ‖ AES-256-GCM 密文(含 16B tag);prompt / response 的 GCM 明文是内层
|
|
18
|
+
BE32(n) ‖ 真实数据 ‖ 随机填充(长取 1.5 倍几何桶,256 起共 27 桶、顶桶 9,695,112;pl = 桶长,
|
|
19
|
+
Parse 核 pl ∈ 桶表;chunk 不填充、pl 为真实长;spec 2026-10-08);
|
|
20
|
+
Hash = HMAC-SHA256("TCAICC", SHA-256(真实数据)) 32B;Metadata = UTF-8 JSON(短 key,
|
|
21
|
+
除 ``pl`` 外全可选;明文可读、AAD 保完整性、不承诺机密性)。
|
|
22
|
+
|
|
23
|
+
AAD(绑定除 Ciphertext 外的全部组件)::
|
|
24
|
+
|
|
25
|
+
非流式: AAD = Prefix ‖ Hash ‖ Metadata ‖ Suffix (缺失组件为空串)
|
|
26
|
+
chunk : AAD = LP(request_id) ‖ Metadata ‖ Suffix (request_id 隐式,两端各自持有)
|
|
27
|
+
|
|
28
|
+
流式:一条流一把独立 key;中间片 Metadata 仅位置,尾片带完整绑定与
|
|
29
|
+
stream_hash(hex(SHA256(全流明文,含尾片)))收尾校验;乱序/丢包/重放因 AAD 或
|
|
30
|
+
位置比对失败,传输 EOF 时 ``finished`` 为 False 即截断。聚合流(``aggregated=True``)
|
|
31
|
+
专供"流式密文被中间层拼回非流式字段"的场景:尾片不比 stream_hash。
|
|
32
|
+
|
|
33
|
+
密钥为 32 字节(通常是安全会话派生的 request_dek/response_dek:prompt 用
|
|
34
|
+
request_dek,response 与 chunk 用 response_dek)。本库不做 IO 与密钥管理。
|
|
35
|
+
非流式函数并发安全;``ChunkEncryptor``/``ChunkDecryptor`` 非并发安全。
|
|
36
|
+
|
|
37
|
+
公开面镜像 Go cipherx 包:``encrypt_prompt``/``encrypt_response``/``decrypt``/``parse``、
|
|
38
|
+
``ChunkEncryptor``/``ChunkDecryptor``、``compute_prefix``;Go 的 ``With*`` Option
|
|
39
|
+
用关键字参数(``session_id``/``nonce``/``token``/``encrypt_time``/``key_id``)表达。
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
from ._aead import open_with_aad, seal_with_aad
|
|
44
|
+
from ._binding import (
|
|
45
|
+
Binding,
|
|
46
|
+
FieldPathAliases,
|
|
47
|
+
Mismatch,
|
|
48
|
+
Mode,
|
|
49
|
+
Position,
|
|
50
|
+
Protocol,
|
|
51
|
+
mode_string,
|
|
52
|
+
mode_valid,
|
|
53
|
+
protocol_string,
|
|
54
|
+
protocol_valid,
|
|
55
|
+
validate_field_path,
|
|
56
|
+
validate_field_path_aliases,
|
|
57
|
+
)
|
|
58
|
+
from ._constants import (
|
|
59
|
+
GCM_NONCE_SIZE,
|
|
60
|
+
GCM_TAG_SIZE,
|
|
61
|
+
HASH_SIZE,
|
|
62
|
+
KEY_SIZE,
|
|
63
|
+
MAGIC,
|
|
64
|
+
MAX_ENVELOPE,
|
|
65
|
+
MAX_METADATA_LEN,
|
|
66
|
+
MAX_PLAINTEXT,
|
|
67
|
+
MAX_PREFIX_LEN,
|
|
68
|
+
MIN_CIPHERTEXT,
|
|
69
|
+
SUFFIX_SIZE,
|
|
70
|
+
)
|
|
71
|
+
from ._decrypt import decrypt_prompt, decrypt_response
|
|
72
|
+
from ._encrypt import encrypt_prompt, encrypt_response
|
|
73
|
+
from ._envelope import (
|
|
74
|
+
FLAG_CIPHERTEXT,
|
|
75
|
+
FLAG_FINAL,
|
|
76
|
+
FLAG_HASH,
|
|
77
|
+
FLAG_METADATA,
|
|
78
|
+
FLAG_PREFIX,
|
|
79
|
+
FLAG_SUFFIX,
|
|
80
|
+
FLAGS_CHUNK_FINAL,
|
|
81
|
+
FLAGS_CHUNK_MID,
|
|
82
|
+
FLAGS_PROMPT,
|
|
83
|
+
FLAGS_RESPONSE,
|
|
84
|
+
CipherType,
|
|
85
|
+
Envelope,
|
|
86
|
+
assemble,
|
|
87
|
+
build_suffix,
|
|
88
|
+
chunk_aad,
|
|
89
|
+
has_hash,
|
|
90
|
+
is_final,
|
|
91
|
+
non_stream_aad,
|
|
92
|
+
parse,
|
|
93
|
+
parse_envelope,
|
|
94
|
+
split_stream,
|
|
95
|
+
type_label,
|
|
96
|
+
valid_flags,
|
|
97
|
+
)
|
|
98
|
+
from ._errors import (
|
|
99
|
+
BindingError,
|
|
100
|
+
BindingMismatchError,
|
|
101
|
+
CipherTypeError,
|
|
102
|
+
CipherxError,
|
|
103
|
+
DecryptError,
|
|
104
|
+
FormatError,
|
|
105
|
+
HashMismatchError,
|
|
106
|
+
KeySizeError,
|
|
107
|
+
RequestIdError,
|
|
108
|
+
RequestMismatchError,
|
|
109
|
+
StreamClosedError,
|
|
110
|
+
StreamVerifyError,
|
|
111
|
+
TooLargeError,
|
|
112
|
+
)
|
|
113
|
+
from ._hash import PROMPT_HASH_KEY, prompt_hash
|
|
114
|
+
from ._metadata import Metadata, decode_metadata, encode_metadata
|
|
115
|
+
from ._padding import MAX_PADDED_PLAINTEXT_LEN, padded_len
|
|
116
|
+
from ._prefix import compute_prefix
|
|
117
|
+
from ._requestid import MAX_REQUEST_ID_LEN, validate_request_id
|
|
118
|
+
from ._stream import ChunkDecryptor, ChunkEncryptor
|
|
119
|
+
|
|
120
|
+
__all__ = [
|
|
121
|
+
# 加解密入口
|
|
122
|
+
"encrypt_prompt", "encrypt_response", "decrypt_prompt", "decrypt_response", "parse",
|
|
123
|
+
"ChunkEncryptor", "ChunkDecryptor", "compute_prefix", "prompt_hash", "PROMPT_HASH_KEY",
|
|
124
|
+
# 类型
|
|
125
|
+
"Envelope", "Metadata", "CipherType",
|
|
126
|
+
# 绑定上下文
|
|
127
|
+
"Protocol", "Mode", "Binding", "Position", "Mismatch",
|
|
128
|
+
"protocol_valid", "protocol_string", "mode_valid", "mode_string",
|
|
129
|
+
"validate_field_path", "validate_field_path_aliases", "FieldPathAliases",
|
|
130
|
+
"validate_request_id", "MAX_REQUEST_ID_LEN",
|
|
131
|
+
# 常量
|
|
132
|
+
"MAGIC", "SUFFIX_SIZE", "GCM_NONCE_SIZE", "GCM_TAG_SIZE", "MIN_CIPHERTEXT",
|
|
133
|
+
"HASH_SIZE", "KEY_SIZE", "MAX_PLAINTEXT", "MAX_ENVELOPE", "MAX_METADATA_LEN",
|
|
134
|
+
"MAX_PREFIX_LEN", "MAX_PADDED_PLAINTEXT_LEN",
|
|
135
|
+
# 内层填充
|
|
136
|
+
"padded_len",
|
|
137
|
+
# flags
|
|
138
|
+
"FLAG_PREFIX", "FLAG_CIPHERTEXT", "FLAG_HASH", "FLAG_METADATA", "FLAG_SUFFIX",
|
|
139
|
+
"FLAG_FINAL", "FLAGS_PROMPT", "FLAGS_RESPONSE", "FLAGS_CHUNK_MID", "FLAGS_CHUNK_FINAL",
|
|
140
|
+
# wire 组装/解析(高级用法)
|
|
141
|
+
"build_suffix", "assemble", "non_stream_aad", "chunk_aad",
|
|
142
|
+
"parse_envelope", "split_stream", "valid_flags", "type_label", "has_hash", "is_final",
|
|
143
|
+
# metadata
|
|
144
|
+
"encode_metadata", "decode_metadata",
|
|
145
|
+
# AEAD 低层(高级用法)
|
|
146
|
+
"seal_with_aad", "open_with_aad",
|
|
147
|
+
# 错误
|
|
148
|
+
"CipherxError", "KeySizeError", "FormatError", "TooLargeError", "DecryptError",
|
|
149
|
+
"HashMismatchError", "CipherTypeError", "StreamClosedError", "StreamVerifyError",
|
|
150
|
+
"RequestIdError", "RequestMismatchError", "BindingError", "BindingMismatchError",
|
|
151
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""AES-256-GCM 封解:随机 12B nonce 前置,AAD 由调用层传入(绑定信封其余组件)。
|
|
2
|
+
|
|
3
|
+
镜像 aicc-common/cipherx/aead.go。``_rand_read`` 是 KAT 测试缝(默认 ``os.urandom``);
|
|
4
|
+
生产路径每封一随机 nonce。认证失败统一 ``DecryptError``,不细分原因。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
11
|
+
|
|
12
|
+
from ._constants import GCM_NONCE_SIZE, KEY_SIZE, MIN_CIPHERTEXT
|
|
13
|
+
from ._errors import DecryptError, KeySizeError
|
|
14
|
+
|
|
15
|
+
# KAT 测试缝:替换为确定性字节流,使信封字节可复现(Go randRead 同义)。生产恒为 os.urandom。
|
|
16
|
+
_rand_read = os.urandom
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _new_gcm(key: bytes) -> AESGCM:
|
|
20
|
+
if len(key) != KEY_SIZE:
|
|
21
|
+
raise KeySizeError(len(key), KEY_SIZE)
|
|
22
|
+
return AESGCM(key)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def seal_with_aad(key: bytes, plaintext: bytes, aad: bytes) -> bytes:
|
|
26
|
+
"""加密:返回 ``nonce(12)‖GCM 密文+tag``,AAD 绑定信封其余组件。"""
|
|
27
|
+
gcm = _new_gcm(key)
|
|
28
|
+
nonce = _rand_read(GCM_NONCE_SIZE)
|
|
29
|
+
ct = gcm.encrypt(nonce, plaintext, aad) # ct = 密文 ‖ 16B tag
|
|
30
|
+
return nonce + ct
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def open_with_aad(key: bytes, sealed: bytes, aad: bytes) -> bytes:
|
|
34
|
+
"""解密:``sealed = nonce(12)‖密文+tag``;认证失败统一 DecryptError(防 oracle)。"""
|
|
35
|
+
gcm = _new_gcm(key)
|
|
36
|
+
if len(sealed) < MIN_CIPHERTEXT:
|
|
37
|
+
raise DecryptError()
|
|
38
|
+
nonce = sealed[:GCM_NONCE_SIZE]
|
|
39
|
+
ct = sealed[GCM_NONCE_SIZE:]
|
|
40
|
+
try:
|
|
41
|
+
return gcm.decrypt(nonce, ct, aad)
|
|
42
|
+
except Exception:
|
|
43
|
+
raise DecryptError() from None
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""绑定上下文:协议 / 模式枚举、字段位置、入参校验、Simple 归一化与解密侧比对原语
|
|
2
|
+
(镜像 aicc-common/cipherx/binding.go,规范 spec 2026-09-14 §3.1 / §3.6 / §4.2)。
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import IntEnum
|
|
8
|
+
|
|
9
|
+
from ._errors import BindingError, BindingMismatchError, FormatError
|
|
10
|
+
from ._metadata import Metadata
|
|
11
|
+
from ._requestid import validate_request_id
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Protocol(IntEnum):
|
|
15
|
+
"""密文所属的 LLM 协议;int 上 wire,0 保留为"未设置"。"""
|
|
16
|
+
|
|
17
|
+
OPENAI_CHAT_COMPLETIONS = 1
|
|
18
|
+
OPENAI_RESPONSES = 2
|
|
19
|
+
ANTHROPIC_MESSAGES = 3
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Mode(IntEnum):
|
|
23
|
+
"""位置校验模式;int 上 wire,0 保留为"未设置"。"""
|
|
24
|
+
|
|
25
|
+
STRICT = 1 # 位置 / 协议对不上即拒
|
|
26
|
+
COMPATIBLE = 2 # 对不上记 mismatches 后照解
|
|
27
|
+
SIMPLE = 3 # 不做位置绑定:field_path 恒空、field_seq 恒 0;协议仍记
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def protocol_valid(p: int) -> bool:
|
|
31
|
+
return Protocol.OPENAI_CHAT_COMPLETIONS <= p <= Protocol.ANTHROPIC_MESSAGES
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def protocol_string(p: int) -> str:
|
|
35
|
+
"""可读名,只用于日志与错误信息,不上 wire。"""
|
|
36
|
+
if p == Protocol.OPENAI_CHAT_COMPLETIONS:
|
|
37
|
+
return "openai_chat_completions"
|
|
38
|
+
if p == Protocol.OPENAI_RESPONSES:
|
|
39
|
+
return "openai_responses"
|
|
40
|
+
if p == Protocol.ANTHROPIC_MESSAGES:
|
|
41
|
+
return "anthropic_messages"
|
|
42
|
+
return f"protocol({p})"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def mode_valid(m: int) -> bool:
|
|
46
|
+
return Mode.STRICT <= m <= Mode.SIMPLE
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def mode_string(m: int) -> str:
|
|
50
|
+
"""可读名,只用于日志与错误信息,不上 wire。"""
|
|
51
|
+
if m == Mode.STRICT:
|
|
52
|
+
return "strict"
|
|
53
|
+
if m == Mode.COMPATIBLE:
|
|
54
|
+
return "compatible"
|
|
55
|
+
if m == Mode.SIMPLE:
|
|
56
|
+
return "simple"
|
|
57
|
+
return f"mode({m})"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class Binding:
|
|
62
|
+
"""请求 / 流级绑定上下文,进 Metadata(随之进 AAD)。"""
|
|
63
|
+
|
|
64
|
+
request_id: str
|
|
65
|
+
protocol: int
|
|
66
|
+
mode: int
|
|
67
|
+
|
|
68
|
+
def validate(self) -> None:
|
|
69
|
+
"""加密侧与解密期望值共用:request_id 规则不变,protocol / mode 须已知。"""
|
|
70
|
+
validate_request_id(self.request_id)
|
|
71
|
+
if not protocol_valid(self.protocol):
|
|
72
|
+
raise BindingError(f"unknown protocol {int(self.protocol)}")
|
|
73
|
+
if not mode_valid(self.mode):
|
|
74
|
+
raise BindingError(f"unknown mode {int(self.mode)}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
MAX_FIELD_PATH_LEN = 1024
|
|
78
|
+
MAX_FIELD_SEQ = 2**31 - 1 # 与 proto int32 往返不截断
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_field_path(field_path: str) -> None:
|
|
82
|
+
"""格式校验:UTF-8 字节数 ≤ 1024、合法 UTF-8、无控制字符;空串合法(空与否由模式规则决定)。"""
|
|
83
|
+
try:
|
|
84
|
+
n = len(field_path.encode("utf-8"))
|
|
85
|
+
except UnicodeEncodeError:
|
|
86
|
+
raise BindingError("field_path invalid utf-8") from None
|
|
87
|
+
if n > MAX_FIELD_PATH_LEN:
|
|
88
|
+
raise BindingError(f"field_path {n} bytes, max {MAX_FIELD_PATH_LEN}")
|
|
89
|
+
for ch in field_path:
|
|
90
|
+
c = ord(ch)
|
|
91
|
+
if c < 0x20 or c == 0x7F:
|
|
92
|
+
raise BindingError(f"field_path control character {hex(c)}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class Position:
|
|
97
|
+
"""字段级位置:field_path 为命中该字段的点分路径(不含数组下标),field_seq 为该路径内第几次加密(从 0 起)。"""
|
|
98
|
+
|
|
99
|
+
field_path: str = ""
|
|
100
|
+
field_seq: int = 0
|
|
101
|
+
|
|
102
|
+
def validate(self) -> None:
|
|
103
|
+
"""格式 + 范围;解密期望值只用这一层(空 field_path 合法)。"""
|
|
104
|
+
validate_field_path(self.field_path)
|
|
105
|
+
if self.field_seq < 0 or self.field_seq > MAX_FIELD_SEQ:
|
|
106
|
+
raise BindingError(f"field_seq {self.field_seq} out of range")
|
|
107
|
+
|
|
108
|
+
def normalize(self, mode: int) -> Position:
|
|
109
|
+
"""加密侧:Simple 归一为零值(调用方不必感知模式);其余模式校验并要求 field_path 非空。"""
|
|
110
|
+
if mode == Mode.SIMPLE:
|
|
111
|
+
return Position()
|
|
112
|
+
self.validate()
|
|
113
|
+
if self.field_path == "":
|
|
114
|
+
raise BindingError(f"field_path required in {mode_string(mode)} mode")
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
#: 解密侧容忍表(单协议、key 不带前缀):expected field_path → 可接受的 carried field_path 列表;
|
|
119
|
+
#: 命中即视同相等(不产生 field_path 项,field_seq 照比);None / 空 = 无别名。
|
|
120
|
+
FieldPathAliases = dict[str, list[str]]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_field_path_aliases(a: FieldPathAliases | None) -> None:
|
|
124
|
+
"""key 与每个 value 非空且过 ``validate_field_path``;不合规抛 ``BindingError``。"""
|
|
125
|
+
if not a:
|
|
126
|
+
return
|
|
127
|
+
for k, vs in a.items():
|
|
128
|
+
if k == "":
|
|
129
|
+
raise BindingError("alias key empty")
|
|
130
|
+
try:
|
|
131
|
+
validate_field_path(k)
|
|
132
|
+
except BindingError as exc:
|
|
133
|
+
raise BindingError(f'alias key "{k}": {exc}') from None
|
|
134
|
+
for v in vs:
|
|
135
|
+
if v == "":
|
|
136
|
+
raise BindingError(f'alias of "{k}" empty')
|
|
137
|
+
try:
|
|
138
|
+
validate_field_path(v)
|
|
139
|
+
except BindingError as exc:
|
|
140
|
+
raise BindingError(f'alias of "{k}": {exc}') from None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def alias_accepts(a: FieldPathAliases | None, expected: str, carried: str) -> bool:
|
|
144
|
+
"""carried 是否在 expected 的别名列表内。"""
|
|
145
|
+
return bool(a) and carried in a.get(expected, ())
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass(frozen=True)
|
|
149
|
+
class Mismatch:
|
|
150
|
+
"""一项被容忍(Compatible / Simple)的不一致;Strict 下同样内容进 BindingMismatchError 文本。"""
|
|
151
|
+
|
|
152
|
+
field: str # "protocol" | "field_path" | "field_seq"
|
|
153
|
+
expected: str
|
|
154
|
+
carried: str
|
|
155
|
+
|
|
156
|
+
def __str__(self) -> str:
|
|
157
|
+
"""形如 ``field_path expected "a" carried "b"``(与 Go %q 一致)。"""
|
|
158
|
+
return f'{self.field} expected "{self.expected}" carried "{self.carried}"'
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def require_binding(md: Metadata | None) -> None:
|
|
162
|
+
"""带完整 Metadata 的信封(prompt / response / 尾片):request_id / protocol 存在、mode 已知。"""
|
|
163
|
+
if md is None:
|
|
164
|
+
raise FormatError("metadata required")
|
|
165
|
+
if not md.request_id:
|
|
166
|
+
raise FormatError("metadata missing request_id")
|
|
167
|
+
if not md.protocol:
|
|
168
|
+
raise FormatError("metadata missing protocol")
|
|
169
|
+
if not mode_valid(md.mode):
|
|
170
|
+
raise FormatError(f"metadata mode {md.mode} missing or unknown")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def check_mode_echo(md: Metadata, want: int) -> None:
|
|
174
|
+
"""response / 尾片的 mode 须等于本地配置(回文 mode 由 agent 复制自 prompt),任何模式恒拒。"""
|
|
175
|
+
if md.mode != want:
|
|
176
|
+
raise BindingMismatchError(
|
|
177
|
+
f'mode expected "{mode_string(want)}" carried "{mode_string(md.mode)}"'
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def mismatch_protocol(md: Metadata, want: int) -> list[Mismatch]:
|
|
182
|
+
"""协议比对(三模式都比);中间片不带 protocol,调用方不调它。"""
|
|
183
|
+
if md.protocol == want:
|
|
184
|
+
return []
|
|
185
|
+
return [Mismatch("protocol", protocol_string(want), protocol_string(md.protocol))]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def mismatch_position(
|
|
189
|
+
md: Metadata, mode: int, want: Position, aliases: FieldPathAliases | None = None
|
|
190
|
+
) -> list[Mismatch]:
|
|
191
|
+
"""位置比对;Simple 不比;carried field_path 在 aliases[want.field_path] 内视同相等(field_seq 照比)。"""
|
|
192
|
+
if mode == Mode.SIMPLE:
|
|
193
|
+
return []
|
|
194
|
+
mm: list[Mismatch] = []
|
|
195
|
+
if md.field_path != want.field_path and not alias_accepts(
|
|
196
|
+
aliases, want.field_path, md.field_path
|
|
197
|
+
):
|
|
198
|
+
mm.append(Mismatch("field_path", want.field_path, md.field_path))
|
|
199
|
+
if md.field_seq != want.field_seq:
|
|
200
|
+
mm.append(Mismatch("field_seq", str(want.field_seq), str(md.field_seq)))
|
|
201
|
+
return mm
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def settle(mm: list[Mismatch], mode: int) -> list[Mismatch]:
|
|
205
|
+
"""按 mode 落定:Strict 有不一致即抛(文本列全部项);其余模式原样回报给调用方记日志。"""
|
|
206
|
+
if mm and mode == Mode.STRICT:
|
|
207
|
+
raise BindingMismatchError("; ".join(str(m) for m in mm))
|
|
208
|
+
return mm
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""cipherx wire 常量(镜像 aicc-common/cipherx/envelope.go 的常量块)。
|
|
2
|
+
|
|
3
|
+
独立模块以打破 ``_envelope`` ↔ ``_metadata`` 的循环导入,并作单一事实源。
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
MAGIC = b"AICCv1"
|
|
8
|
+
SUFFIX_SIZE = 10
|
|
9
|
+
GCM_NONCE_SIZE = 12
|
|
10
|
+
GCM_TAG_SIZE = 16
|
|
11
|
+
MIN_CIPHERTEXT = GCM_NONCE_SIZE + GCM_TAG_SIZE # 28
|
|
12
|
+
HASH_SIZE = 32
|
|
13
|
+
KEY_SIZE = 32
|
|
14
|
+
MAX_PLAINTEXT = 64 << 20
|
|
15
|
+
MAX_ENVELOPE = 65 << 20
|
|
16
|
+
MAX_METADATA_LEN = 65535
|
|
17
|
+
MAX_PREFIX_LEN = 1024
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""非流式解密(镜像 aicc-common/cipherx/decrypt.go)。
|
|
2
|
+
|
|
3
|
+
结构 → 类型 → 必填绑定字段存在性 → AAD 重建(wire 原始切片)→ GCM → 剥内层填充 → Hash
|
|
4
|
+
→ request_id → mode → protocol / 位置比对(spec 2026-09-14 §4.2)。
|
|
5
|
+
chunk 属流式语义,须用 ``ChunkDecryptor``。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ..cryptox._subtle import constant_time_equal
|
|
10
|
+
from ._aead import open_with_aad
|
|
11
|
+
from ._binding import (
|
|
12
|
+
Binding,
|
|
13
|
+
FieldPathAliases,
|
|
14
|
+
Position,
|
|
15
|
+
check_mode_echo,
|
|
16
|
+
mismatch_position,
|
|
17
|
+
mismatch_protocol,
|
|
18
|
+
protocol_valid,
|
|
19
|
+
require_binding,
|
|
20
|
+
settle,
|
|
21
|
+
validate_field_path_aliases,
|
|
22
|
+
)
|
|
23
|
+
from ._envelope import (
|
|
24
|
+
CipherType,
|
|
25
|
+
_to_envelope,
|
|
26
|
+
has_hash,
|
|
27
|
+
non_stream_aad,
|
|
28
|
+
parse_envelope,
|
|
29
|
+
type_label,
|
|
30
|
+
)
|
|
31
|
+
from ._errors import (
|
|
32
|
+
BindingError,
|
|
33
|
+
CipherTypeError,
|
|
34
|
+
FormatError,
|
|
35
|
+
HashMismatchError,
|
|
36
|
+
RequestIdError,
|
|
37
|
+
RequestMismatchError,
|
|
38
|
+
)
|
|
39
|
+
from ._hash import prompt_hash
|
|
40
|
+
from ._padding import unpad
|
|
41
|
+
from ._requestid import validate_request_id
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _open_non_stream(key: bytes, p):
|
|
45
|
+
"""两类型共通尾段:AAD 原样重建 → GCM → 剥内层填充(spec 2026-10-08 §4)→ Hash(仅 prompt,对真实数据)。"""
|
|
46
|
+
inner = open_with_aad(key, p.ciphertext, non_stream_aad(p.prefix, p.hash, p.meta_raw, p.suffix))
|
|
47
|
+
pt = unpad(inner)
|
|
48
|
+
if has_hash(p.flags) and not constant_time_equal(prompt_hash(pt), p.hash):
|
|
49
|
+
raise HashMismatchError()
|
|
50
|
+
env = _to_envelope(p)
|
|
51
|
+
env.plaintext = pt
|
|
52
|
+
return env
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def decrypt_prompt(key: bytes, envelope: bytes, protocol: int, pos: Position):
|
|
56
|
+
"""解 prompt 信封:request_id / mode 取 wire(缺失 / 不合规 → FormatError)。
|
|
57
|
+
|
|
58
|
+
protocol 与 pos 为解密方(gateway 端点与 JSON 遍历)算出的期望值,按 wire mode
|
|
59
|
+
比对(Strict 拒 / Compatible 记 / Simple 只比 protocol);非 prompt → CipherTypeError。
|
|
60
|
+
"""
|
|
61
|
+
if not protocol_valid(protocol):
|
|
62
|
+
raise BindingError(f"unknown protocol {int(protocol)}")
|
|
63
|
+
pos.validate()
|
|
64
|
+
p = parse_envelope(envelope)
|
|
65
|
+
if p.typ != CipherType.PROMPT:
|
|
66
|
+
raise CipherTypeError(f"{type_label(p.typ)} envelope is not a prompt")
|
|
67
|
+
# flags 白名单保证 prompt 必有 metadata。
|
|
68
|
+
try:
|
|
69
|
+
validate_request_id(p.meta.request_id if p.meta is not None else "")
|
|
70
|
+
except RequestIdError as exc:
|
|
71
|
+
raise FormatError(f"metadata request_id: {exc}") from None
|
|
72
|
+
require_binding(p.meta)
|
|
73
|
+
env = _open_non_stream(key, p)
|
|
74
|
+
md = env.metadata
|
|
75
|
+
env.mismatches = settle(
|
|
76
|
+
mismatch_protocol(md, protocol) + mismatch_position(md, md.mode, pos), md.mode
|
|
77
|
+
)
|
|
78
|
+
return env
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def decrypt_response(
|
|
82
|
+
key: bytes,
|
|
83
|
+
b: Binding,
|
|
84
|
+
pos: Position,
|
|
85
|
+
envelope: bytes,
|
|
86
|
+
*,
|
|
87
|
+
aliases: FieldPathAliases | None = None,
|
|
88
|
+
):
|
|
89
|
+
"""解 response 信封:b 为本请求的 request_id / 端点协议 / 本地 mode,pos 为自算位置。
|
|
90
|
+
|
|
91
|
+
GCM 通过后:request_id 相等(RequestMismatchError)→ mode 回显相等(恒拒)
|
|
92
|
+
→ protocol / 位置按本地 mode 比对(位置经别名表 aliases)。
|
|
93
|
+
"""
|
|
94
|
+
b.validate()
|
|
95
|
+
pos.validate()
|
|
96
|
+
validate_field_path_aliases(aliases)
|
|
97
|
+
p = parse_envelope(envelope)
|
|
98
|
+
if p.typ != CipherType.RESPONSE:
|
|
99
|
+
raise CipherTypeError(f"{type_label(p.typ)} envelope is not a response")
|
|
100
|
+
require_binding(p.meta)
|
|
101
|
+
env = _open_non_stream(key, p)
|
|
102
|
+
md = env.metadata
|
|
103
|
+
if md.request_id != b.request_id:
|
|
104
|
+
raise RequestMismatchError(f'envelope carries "{md.request_id}"')
|
|
105
|
+
check_mode_echo(md, b.mode)
|
|
106
|
+
env.mismatches = settle(
|
|
107
|
+
mismatch_protocol(md, b.protocol) + mismatch_position(md, b.mode, pos, aliases), b.mode
|
|
108
|
+
)
|
|
109
|
+
return env
|