agentremote-common 1.0.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.
- agentremote_common/__init__.py +21 -0
- agentremote_common/config.py +341 -0
- agentremote_common/crypto/__init__.py +69 -0
- agentremote_common/crypto/certificates.py +171 -0
- agentremote_common/crypto/integrity.py +129 -0
- agentremote_common/crypto/jwt_auth.py +108 -0
- agentremote_common/crypto/keys.py +128 -0
- agentremote_common/crypto/license.py +157 -0
- agentremote_common/models/__init__.py +34 -0
- agentremote_common/models/capability.py +32 -0
- agentremote_common/models/license.py +50 -0
- agentremote_common/models/node.py +47 -0
- agentremote_common/models/task.py +53 -0
- agentremote_common/mqtt_client.py +435 -0
- agentremote_common/protocol/__init__.py +21 -0
- agentremote_common/protocol/message.py +53 -0
- agentremote_common/protocol/topics.py +73 -0
- agentremote_common-1.0.0.dist-info/METADATA +9 -0
- agentremote_common-1.0.0.dist-info/RECORD +21 -0
- agentremote_common-1.0.0.dist-info/WHEEL +5 -0
- agentremote_common-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shared AgentRemote demo protocol, MQTT client and security helpers."""
|
|
2
|
+
|
|
3
|
+
from .mqtt_client import AgentRemoteMqttClient
|
|
4
|
+
from .protocol.message import MqttConfig, WSMessage, WSMessageType
|
|
5
|
+
from .protocol.topics import (
|
|
6
|
+
PLUGIN_OUTBOUND_TOPICS,
|
|
7
|
+
gateway_topic,
|
|
8
|
+
parse_plugin_topic,
|
|
9
|
+
plugin_topic,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"MqttConfig",
|
|
14
|
+
"WSMessage",
|
|
15
|
+
"WSMessageType",
|
|
16
|
+
"AgentRemoteMqttClient",
|
|
17
|
+
"PLUGIN_OUTBOUND_TOPICS",
|
|
18
|
+
"parse_plugin_topic",
|
|
19
|
+
"plugin_topic",
|
|
20
|
+
"gateway_topic",
|
|
21
|
+
]
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Role-based AgentRemote configuration helpers.
|
|
2
|
+
|
|
3
|
+
Resolution order is always: explicit CLI args, environment variables,
|
|
4
|
+
role-specific TOML config, local development defaults.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import tomllib
|
|
13
|
+
from typing import Any, Mapping
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
TRUE_VALUES = {"1", "true", "yes", "on"}
|
|
17
|
+
FALSE_VALUES = {"0", "false", "no", "off"}
|
|
18
|
+
|
|
19
|
+
ROLE_ENV_VARS = {
|
|
20
|
+
"cli": "AGENTREMOTE_CLI_CONFIG",
|
|
21
|
+
"server": "AGENTREMOTE_MIDDLEWARE_CONFIG",
|
|
22
|
+
"client": "AGENTREMOTE_RUNTIME_CONFIG",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
ROLE_CONFIG_FILES = {
|
|
26
|
+
"cli": "agentremote.cli.toml",
|
|
27
|
+
"server": "agentremote.server.toml",
|
|
28
|
+
"client": "agentremote.client.toml",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class CliDefaults:
|
|
34
|
+
middleware_url: str = "http://127.0.0.1:9100"
|
|
35
|
+
tenant: str = "acme_shanghai"
|
|
36
|
+
node: str = "sh_win_01"
|
|
37
|
+
agent: str = "agent_ai_01"
|
|
38
|
+
handle: str = ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class MiddlewareDefaults:
|
|
43
|
+
http_host: str = "127.0.0.1"
|
|
44
|
+
http_port: int = 9100
|
|
45
|
+
mqtt_broker: str = "127.0.0.1"
|
|
46
|
+
mqtt_port: int = 1883
|
|
47
|
+
auth_broker_url: str = ""
|
|
48
|
+
require_auth: bool = False
|
|
49
|
+
dev_skip_license: bool = False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class RuntimeDefaults:
|
|
54
|
+
node: str = "sh_win_01"
|
|
55
|
+
tenant: str = "acme_shanghai"
|
|
56
|
+
mqtt_broker: str = "127.0.0.1"
|
|
57
|
+
mqtt_port: int = 1883
|
|
58
|
+
plugins_dir: str = "client/plugins"
|
|
59
|
+
watch_plugins: bool = False
|
|
60
|
+
plugin_watch_interval: float = 2.0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_cli_defaults(config_path: str = "") -> CliDefaults:
|
|
64
|
+
config = load_role_config("cli", config_path)
|
|
65
|
+
return CliDefaults(
|
|
66
|
+
middleware_url=_pick_str(
|
|
67
|
+
config,
|
|
68
|
+
"cli",
|
|
69
|
+
"middleware_url",
|
|
70
|
+
"http://127.0.0.1:9100",
|
|
71
|
+
"AGENTREMOTE_MIDDLEWARE_URL",
|
|
72
|
+
"MIDDLEWARE_HTTP",
|
|
73
|
+
),
|
|
74
|
+
tenant=_pick_str(config, "cli", "tenant", "acme_shanghai", "AGENTREMOTE_TENANT", "TENANT_ID"),
|
|
75
|
+
node=_pick_str(config, "cli", "node", "sh_win_01", "AGENTREMOTE_NODE", "NODE_ID"),
|
|
76
|
+
agent=_pick_str(config, "cli", "agent", "agent_ai_01", "AGENTREMOTE_AGENT", "AGENT_ID"),
|
|
77
|
+
handle=_pick_str(config, "cli", "handle", "", "AGENTREMOTE_HANDLE"),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_middleware_defaults(config_path: str = "") -> MiddlewareDefaults:
|
|
82
|
+
config = load_role_config("server", config_path)
|
|
83
|
+
return MiddlewareDefaults(
|
|
84
|
+
http_host=_pick_str(
|
|
85
|
+
config,
|
|
86
|
+
"middleware",
|
|
87
|
+
"http_host",
|
|
88
|
+
"127.0.0.1",
|
|
89
|
+
"AGENTREMOTE_HTTP_HOST",
|
|
90
|
+
"AGENTREMOTE_MIDDLEWARE_HTTP_HOST",
|
|
91
|
+
),
|
|
92
|
+
http_port=_pick_int(
|
|
93
|
+
config,
|
|
94
|
+
"middleware",
|
|
95
|
+
"http_port",
|
|
96
|
+
9100,
|
|
97
|
+
"AGENTREMOTE_HTTP_PORT",
|
|
98
|
+
"AGENTREMOTE_MIDDLEWARE_HTTP_PORT",
|
|
99
|
+
),
|
|
100
|
+
mqtt_broker=_pick_str(
|
|
101
|
+
config,
|
|
102
|
+
"mqtt",
|
|
103
|
+
"broker_host",
|
|
104
|
+
"127.0.0.1",
|
|
105
|
+
"AGENTREMOTE_MQTT_BROKER",
|
|
106
|
+
"AGENTREMOTE_MQTT_HOST",
|
|
107
|
+
"AGENTREMOTE_MIDDLEWARE_MQTT_BROKER",
|
|
108
|
+
),
|
|
109
|
+
mqtt_port=_pick_int(
|
|
110
|
+
config,
|
|
111
|
+
"mqtt",
|
|
112
|
+
"broker_port",
|
|
113
|
+
1883,
|
|
114
|
+
"AGENTREMOTE_MQTT_PORT",
|
|
115
|
+
"AGENTREMOTE_MIDDLEWARE_MQTT_PORT",
|
|
116
|
+
),
|
|
117
|
+
auth_broker_url=_pick_str(
|
|
118
|
+
config,
|
|
119
|
+
"middleware",
|
|
120
|
+
"auth_broker_url",
|
|
121
|
+
"",
|
|
122
|
+
"AGENTREMOTE_AUTH_BROKER_URL",
|
|
123
|
+
"AUTH_BROKER_URL",
|
|
124
|
+
),
|
|
125
|
+
require_auth=_pick_bool(config, "middleware", "require_auth", False, "AGENTREMOTE_REQUIRE_AUTH"),
|
|
126
|
+
dev_skip_license=_pick_bool(
|
|
127
|
+
config,
|
|
128
|
+
"middleware",
|
|
129
|
+
"dev_skip_license",
|
|
130
|
+
False,
|
|
131
|
+
"AGENTREMOTE_DEV_SKIP_LICENSE",
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def load_runtime_defaults(config_path: str = "") -> RuntimeDefaults:
|
|
137
|
+
config = load_role_config("client", config_path)
|
|
138
|
+
return RuntimeDefaults(
|
|
139
|
+
node=_pick_str(config, "runtime", "node", "sh_win_01", "AGENTREMOTE_NODE", "NODE_ID"),
|
|
140
|
+
tenant=_pick_str(config, "runtime", "tenant", "acme_shanghai", "AGENTREMOTE_TENANT", "TENANT_ID"),
|
|
141
|
+
mqtt_broker=_pick_str(
|
|
142
|
+
config,
|
|
143
|
+
"mqtt",
|
|
144
|
+
"broker_host",
|
|
145
|
+
"127.0.0.1",
|
|
146
|
+
"AGENTREMOTE_MQTT_BROKER",
|
|
147
|
+
"AGENTREMOTE_MQTT_HOST",
|
|
148
|
+
"AGENTREMOTE_RUNTIME_MQTT_BROKER",
|
|
149
|
+
),
|
|
150
|
+
mqtt_port=_pick_int(
|
|
151
|
+
config,
|
|
152
|
+
"mqtt",
|
|
153
|
+
"broker_port",
|
|
154
|
+
1883,
|
|
155
|
+
"AGENTREMOTE_MQTT_PORT",
|
|
156
|
+
"AGENTREMOTE_RUNTIME_MQTT_PORT",
|
|
157
|
+
),
|
|
158
|
+
plugins_dir=_pick_str(
|
|
159
|
+
config,
|
|
160
|
+
"runtime",
|
|
161
|
+
"plugins_dir",
|
|
162
|
+
"client/plugins",
|
|
163
|
+
"AGENTREMOTE_PLUGINS_DIR",
|
|
164
|
+
"AGENTREMOTE_RUNTIME_PLUGINS_DIR",
|
|
165
|
+
),
|
|
166
|
+
watch_plugins=_pick_bool(
|
|
167
|
+
config,
|
|
168
|
+
"runtime",
|
|
169
|
+
"watch_plugins",
|
|
170
|
+
False,
|
|
171
|
+
"AGENTREMOTE_WATCH_PLUGINS",
|
|
172
|
+
"AGENTREMOTE_RUNTIME_WATCH_PLUGINS",
|
|
173
|
+
),
|
|
174
|
+
plugin_watch_interval=_pick_float(
|
|
175
|
+
config,
|
|
176
|
+
"runtime",
|
|
177
|
+
"plugin_watch_interval",
|
|
178
|
+
2.0,
|
|
179
|
+
"AGENTREMOTE_PLUGIN_WATCH_INTERVAL",
|
|
180
|
+
"AGENTREMOTE_RUNTIME_PLUGIN_WATCH_INTERVAL",
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def load_role_config(role: str, explicit_path: str = "") -> dict[str, Any]:
|
|
186
|
+
path = resolve_config_path(role, explicit_path)
|
|
187
|
+
if path is None:
|
|
188
|
+
return {}
|
|
189
|
+
with path.open("rb") as f:
|
|
190
|
+
data = tomllib.load(f)
|
|
191
|
+
if not isinstance(data, dict):
|
|
192
|
+
return {}
|
|
193
|
+
return data
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def resolve_config_path(role: str, explicit_path: str = "") -> Path | None:
|
|
197
|
+
if role not in ROLE_CONFIG_FILES:
|
|
198
|
+
raise ValueError(f"Unknown AgentRemote config role: {role}")
|
|
199
|
+
|
|
200
|
+
explicit = explicit_path.strip()
|
|
201
|
+
if explicit:
|
|
202
|
+
return _existing_path(explicit, "explicit")
|
|
203
|
+
|
|
204
|
+
role_env = os.environ.get(ROLE_ENV_VARS[role], "").strip()
|
|
205
|
+
if role_env:
|
|
206
|
+
return _existing_path(role_env, ROLE_ENV_VARS[role])
|
|
207
|
+
|
|
208
|
+
shared_env = os.environ.get("AGENTREMOTE_CONFIG", "").strip()
|
|
209
|
+
if shared_env:
|
|
210
|
+
return _existing_path(shared_env, "AGENTREMOTE_CONFIG")
|
|
211
|
+
|
|
212
|
+
for candidate in _default_config_candidates(role):
|
|
213
|
+
if candidate.exists():
|
|
214
|
+
return candidate
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _default_config_candidates(role: str) -> list[Path]:
|
|
219
|
+
filename = ROLE_CONFIG_FILES[role]
|
|
220
|
+
root = Path(__file__).resolve().parents[2]
|
|
221
|
+
cwd = Path.cwd()
|
|
222
|
+
|
|
223
|
+
role_dirs = {
|
|
224
|
+
"cli": ("remotecli", "config"),
|
|
225
|
+
"server": ("platform", "middleware", "config"),
|
|
226
|
+
"client": ("client", "config"),
|
|
227
|
+
}
|
|
228
|
+
role_dir = role_dirs[role]
|
|
229
|
+
candidates = [
|
|
230
|
+
cwd.joinpath(*role_dir, filename),
|
|
231
|
+
root.joinpath(*role_dir, filename),
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
result: list[Path] = []
|
|
235
|
+
for candidate in candidates:
|
|
236
|
+
if candidate not in result:
|
|
237
|
+
result.append(candidate)
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _existing_path(value: str, source: str) -> Path:
|
|
242
|
+
path = Path(value).expanduser()
|
|
243
|
+
if not path.exists():
|
|
244
|
+
raise FileNotFoundError(f"{source} config path not found: {path}")
|
|
245
|
+
return path
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _env_value(*names: str) -> str | None:
|
|
249
|
+
for name in names:
|
|
250
|
+
value = os.environ.get(name)
|
|
251
|
+
if value is not None and value.strip() != "":
|
|
252
|
+
return value.strip()
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _section(config: Mapping[str, Any], name: str) -> Mapping[str, Any]:
|
|
257
|
+
value = config.get(name, {})
|
|
258
|
+
if isinstance(value, Mapping):
|
|
259
|
+
return value
|
|
260
|
+
return {}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _config_value(config: Mapping[str, Any], section: str, key: str) -> Any:
|
|
264
|
+
return _section(config, section).get(key)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _pick_str(
|
|
268
|
+
config: Mapping[str, Any],
|
|
269
|
+
section: str,
|
|
270
|
+
key: str,
|
|
271
|
+
default: str,
|
|
272
|
+
*env_names: str,
|
|
273
|
+
) -> str:
|
|
274
|
+
env = _env_value(*env_names)
|
|
275
|
+
if env is not None:
|
|
276
|
+
return env
|
|
277
|
+
value = _config_value(config, section, key)
|
|
278
|
+
if value is None:
|
|
279
|
+
return default
|
|
280
|
+
return str(value)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _pick_int(
|
|
284
|
+
config: Mapping[str, Any],
|
|
285
|
+
section: str,
|
|
286
|
+
key: str,
|
|
287
|
+
default: int,
|
|
288
|
+
*env_names: str,
|
|
289
|
+
) -> int:
|
|
290
|
+
env = _env_value(*env_names)
|
|
291
|
+
if env is not None:
|
|
292
|
+
return int(env)
|
|
293
|
+
value = _config_value(config, section, key)
|
|
294
|
+
if value is None:
|
|
295
|
+
return default
|
|
296
|
+
return int(value)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _pick_float(
|
|
300
|
+
config: Mapping[str, Any],
|
|
301
|
+
section: str,
|
|
302
|
+
key: str,
|
|
303
|
+
default: float,
|
|
304
|
+
*env_names: str,
|
|
305
|
+
) -> float:
|
|
306
|
+
env = _env_value(*env_names)
|
|
307
|
+
if env is not None:
|
|
308
|
+
return float(env)
|
|
309
|
+
value = _config_value(config, section, key)
|
|
310
|
+
if value is None:
|
|
311
|
+
return default
|
|
312
|
+
return float(value)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _pick_bool(
|
|
316
|
+
config: Mapping[str, Any],
|
|
317
|
+
section: str,
|
|
318
|
+
key: str,
|
|
319
|
+
default: bool,
|
|
320
|
+
*env_names: str,
|
|
321
|
+
) -> bool:
|
|
322
|
+
env = _env_value(*env_names)
|
|
323
|
+
if env is not None:
|
|
324
|
+
return _to_bool(env)
|
|
325
|
+
value = _config_value(config, section, key)
|
|
326
|
+
if value is None:
|
|
327
|
+
return default
|
|
328
|
+
return _to_bool(value)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _to_bool(value: Any) -> bool:
|
|
332
|
+
if isinstance(value, bool):
|
|
333
|
+
return value
|
|
334
|
+
if isinstance(value, int):
|
|
335
|
+
return bool(value)
|
|
336
|
+
lowered = str(value).strip().lower()
|
|
337
|
+
if lowered in TRUE_VALUES:
|
|
338
|
+
return True
|
|
339
|
+
if lowered in FALSE_VALUES:
|
|
340
|
+
return False
|
|
341
|
+
raise ValueError(f"Invalid boolean config value: {value!r}")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentRemote Crypto Layer — ECDSA + JWT + AES + SHA256
|
|
3
|
+
|
|
4
|
+
Provides all cryptographic primitives for the demo security chain:
|
|
5
|
+
- keys: ECDSA P-256 key generation, signing, verification
|
|
6
|
+
- certs: X.509 self-signed device certificates
|
|
7
|
+
- jwt: JWT (ES256) authentication tokens
|
|
8
|
+
- license: AES-256-GCM license encryption/decryption
|
|
9
|
+
- integrity: SHA256 file/content hashing
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .keys import (
|
|
13
|
+
generate_keypair,
|
|
14
|
+
sign_data,
|
|
15
|
+
verify_signature,
|
|
16
|
+
load_private_key,
|
|
17
|
+
load_public_key,
|
|
18
|
+
private_to_pem,
|
|
19
|
+
public_to_pem,
|
|
20
|
+
)
|
|
21
|
+
from .certificates import (
|
|
22
|
+
generate_device_cert,
|
|
23
|
+
load_device_cert,
|
|
24
|
+
verify_device_cert,
|
|
25
|
+
)
|
|
26
|
+
from .jwt_auth import (
|
|
27
|
+
create_auth_token,
|
|
28
|
+
verify_auth_token,
|
|
29
|
+
)
|
|
30
|
+
from .license import (
|
|
31
|
+
encrypt_license,
|
|
32
|
+
decrypt_license,
|
|
33
|
+
generate_license_key,
|
|
34
|
+
load_license_key,
|
|
35
|
+
)
|
|
36
|
+
from .integrity import (
|
|
37
|
+
hash_file,
|
|
38
|
+
hash_content,
|
|
39
|
+
hash_skill_directory,
|
|
40
|
+
verify_skill_integrity,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
# keys
|
|
45
|
+
"generate_keypair",
|
|
46
|
+
"sign_data",
|
|
47
|
+
"verify_signature",
|
|
48
|
+
"load_private_key",
|
|
49
|
+
"load_public_key",
|
|
50
|
+
"private_to_pem",
|
|
51
|
+
"public_to_pem",
|
|
52
|
+
# certs
|
|
53
|
+
"generate_device_cert",
|
|
54
|
+
"load_device_cert",
|
|
55
|
+
"verify_device_cert",
|
|
56
|
+
# jwt
|
|
57
|
+
"create_auth_token",
|
|
58
|
+
"verify_auth_token",
|
|
59
|
+
# license
|
|
60
|
+
"encrypt_license",
|
|
61
|
+
"decrypt_license",
|
|
62
|
+
"generate_license_key",
|
|
63
|
+
"load_license_key",
|
|
64
|
+
# integrity
|
|
65
|
+
"hash_file",
|
|
66
|
+
"hash_content",
|
|
67
|
+
"hash_skill_directory",
|
|
68
|
+
"verify_skill_integrity",
|
|
69
|
+
]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
X.509 设备证书管理
|
|
3
|
+
|
|
4
|
+
Demo 用自签名 CA 签发的设备证书。
|
|
5
|
+
- CA 根证书 (ca_public.pem) → Middleware 内置,验证设备证书
|
|
6
|
+
- 设备证书 (device_cert.pem + device_private.pem) → Plugin 持有
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Tuple
|
|
14
|
+
|
|
15
|
+
from cryptography import x509
|
|
16
|
+
from cryptography.x509.oid import NameOID
|
|
17
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
18
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
19
|
+
|
|
20
|
+
from .keys import generate_keypair, private_to_pem, public_to_pem
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def generate_ca() -> Tuple[ec.EllipticCurvePrivateKey, x509.Certificate]:
|
|
24
|
+
"""
|
|
25
|
+
生成自签名 CA 根证书。
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
(ca_private_key, ca_certificate)
|
|
29
|
+
"""
|
|
30
|
+
ca_private, ca_public = generate_keypair()
|
|
31
|
+
|
|
32
|
+
subject = issuer = x509.Name([
|
|
33
|
+
x509.NameAttribute(NameOID.COMMON_NAME, "AgentRemote Demo CA"),
|
|
34
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "AgentRemote Demo"),
|
|
35
|
+
x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"),
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
ca_cert = (
|
|
39
|
+
x509.CertificateBuilder()
|
|
40
|
+
.subject_name(subject)
|
|
41
|
+
.issuer_name(issuer)
|
|
42
|
+
.public_key(ca_public)
|
|
43
|
+
.serial_number(x509.random_serial_number())
|
|
44
|
+
.not_valid_before(datetime.datetime.utcnow() - datetime.timedelta(days=1))
|
|
45
|
+
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=3650)) # 10 年 Demo CA
|
|
46
|
+
.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
|
|
47
|
+
.add_extension(x509.KeyUsage(
|
|
48
|
+
key_cert_sign=True,
|
|
49
|
+
crl_sign=True,
|
|
50
|
+
digital_signature=False,
|
|
51
|
+
content_commitment=False,
|
|
52
|
+
key_encipherment=False,
|
|
53
|
+
data_encipherment=False,
|
|
54
|
+
key_agreement=False,
|
|
55
|
+
encipher_only=False,
|
|
56
|
+
decipher_only=False,
|
|
57
|
+
), critical=True)
|
|
58
|
+
.add_extension(x509.SubjectKeyIdentifier.from_public_key(ca_public), critical=False)
|
|
59
|
+
.sign(ca_private, hashes.SHA256())
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return ca_private, ca_cert
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def generate_device_cert(
|
|
66
|
+
ca_private: ec.EllipticCurvePrivateKey,
|
|
67
|
+
ca_cert: x509.Certificate,
|
|
68
|
+
device_id: str = "sh_win_01",
|
|
69
|
+
tenant_id: str = "acme_shanghai",
|
|
70
|
+
) -> Tuple[ec.EllipticCurvePrivateKey, x509.Certificate]:
|
|
71
|
+
"""
|
|
72
|
+
由 CA 签发设备证书。
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
ca_private: CA 私钥
|
|
76
|
+
ca_cert: CA 证书
|
|
77
|
+
device_id: 设备标识
|
|
78
|
+
tenant_id: 租户标识
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
(device_private_key, device_certificate)
|
|
82
|
+
"""
|
|
83
|
+
device_private, device_public = generate_keypair()
|
|
84
|
+
ca_public = ca_cert.public_key()
|
|
85
|
+
|
|
86
|
+
subject = x509.Name([
|
|
87
|
+
x509.NameAttribute(NameOID.COMMON_NAME, f"plugin-{device_id}"),
|
|
88
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, tenant_id),
|
|
89
|
+
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, f"node:{device_id}"),
|
|
90
|
+
x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"),
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
device_cert = (
|
|
94
|
+
x509.CertificateBuilder()
|
|
95
|
+
.subject_name(subject)
|
|
96
|
+
.issuer_name(ca_cert.subject)
|
|
97
|
+
.public_key(device_public)
|
|
98
|
+
.serial_number(x509.random_serial_number())
|
|
99
|
+
.not_valid_before(datetime.datetime.utcnow() - datetime.timedelta(days=1))
|
|
100
|
+
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365)) # 1 年设备证书
|
|
101
|
+
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
|
|
102
|
+
.add_extension(x509.KeyUsage(
|
|
103
|
+
digital_signature=True,
|
|
104
|
+
content_commitment=False,
|
|
105
|
+
key_encipherment=False,
|
|
106
|
+
data_encipherment=False,
|
|
107
|
+
key_agreement=False,
|
|
108
|
+
key_cert_sign=False,
|
|
109
|
+
crl_sign=False,
|
|
110
|
+
encipher_only=False,
|
|
111
|
+
decipher_only=False,
|
|
112
|
+
), critical=True)
|
|
113
|
+
.add_extension(x509.SubjectKeyIdentifier.from_public_key(device_public), critical=False)
|
|
114
|
+
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_public), critical=False)
|
|
115
|
+
.sign(ca_private, hashes.SHA256())
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return device_private, device_cert
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def load_device_cert(cert_path: str | Path) -> x509.Certificate:
|
|
122
|
+
"""从 PEM 文件加载设备证书"""
|
|
123
|
+
data = Path(cert_path).read_bytes()
|
|
124
|
+
return x509.load_pem_x509_certificate(data)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def load_ca_public_key(ca_cert_path: str | Path) -> ec.EllipticCurvePublicKey:
|
|
128
|
+
"""从 CA 证书中提取公钥(用于验证设备证书签名)"""
|
|
129
|
+
ca_cert = load_device_cert(ca_cert_path)
|
|
130
|
+
pk = ca_cert.public_key()
|
|
131
|
+
if not isinstance(pk, ec.EllipticCurvePublicKey):
|
|
132
|
+
raise TypeError("CA public key is not EC")
|
|
133
|
+
return pk
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def extract_device_public_key(device_cert: x509.Certificate) -> ec.EllipticCurvePublicKey:
|
|
137
|
+
"""从设备证书中提取公钥"""
|
|
138
|
+
pk = device_cert.public_key()
|
|
139
|
+
if not isinstance(pk, ec.EllipticCurvePublicKey):
|
|
140
|
+
raise TypeError("Device public key is not EC")
|
|
141
|
+
return pk
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def verify_device_cert(
|
|
145
|
+
device_cert: x509.Certificate,
|
|
146
|
+
ca_public_key: ec.EllipticCurvePublicKey,
|
|
147
|
+
) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
验证设备证书是否由受信 CA 签发。
|
|
150
|
+
|
|
151
|
+
同时检查:
|
|
152
|
+
- 证书签名(CA 公钥验证)
|
|
153
|
+
- 有效期
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
# 验证签名 — CA 用 SHA256 签名设备证书
|
|
157
|
+
ca_public_key.verify(
|
|
158
|
+
device_cert.signature,
|
|
159
|
+
device_cert.tbs_certificate_bytes,
|
|
160
|
+
ec.ECDSA(hashes.SHA256()),
|
|
161
|
+
)
|
|
162
|
+
return True
|
|
163
|
+
except Exception as e:
|
|
164
|
+
import sys
|
|
165
|
+
print(f" [DEBUG] Cert verification error: {e}", file=sys.stderr)
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def cert_to_pem(cert: x509.Certificate) -> bytes:
|
|
170
|
+
"""证书 → PEM 字节"""
|
|
171
|
+
return cert.public_bytes(serialization.Encoding.PEM)
|