Skip to content

Commit dead098

Browse files
committed
style(sealed-env): satisfy Python checks across CLI and SDK
Signed-off-by: Kevin Wang <wy721@qq.com>
1 parent 7d7d8bd commit dead098

6 files changed

Lines changed: 227 additions & 70 deletions

File tree

‎dstack/vmm/src/tests/test_vmm_cli.py‎

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ def test_kms_urls_are_sent_to_upgrade_app(self) -> None:
6464

6565

6666
class SealedEnvTests(unittest.TestCase):
67+
"""Exercise signed recipient selection and SealedEnv compatibility."""
68+
6769
def setUp(self):
70+
"""Create synthetic keys, a signed response and an isolated client."""
6871
self.app_id = "11" * 20
6972
self.epoch = 3
7073
self.timestamp = 1700000000
@@ -102,9 +105,12 @@ def setUp(self):
102105
self.envs = {"SECRET": "synthetic test value", "UNICODE": "测试"}
103106

104107
def decrypt(self, payload):
108+
"""Check plaintext recovery and rejection under a different epoch context."""
105109
self.assertNotIn("encrypted_env", payload)
106110
envelope = json.loads(bytes.fromhex(payload["sealed_env"]))
107-
self.assertEqual(set(envelope), {"version", "root_epoch", "app_epoch", "enc", "ciphertext"})
111+
self.assertEqual(
112+
set(envelope), {"version", "root_epoch", "app_epoch", "enc", "ciphertext"}
113+
)
108114
self.assertEqual(envelope["version"], 1)
109115
self.assertEqual(envelope["root_epoch"], self.epoch)
110116
enc = base64.b64decode(envelope["enc"], validate=True)
@@ -125,19 +131,25 @@ def decrypt(self, payload):
125131
wrong_epoch.open(ciphertext, aad=b"")
126132

127133
def test_sealed_payload_round_trip_and_normalized_pins(self):
134+
"""Accept equivalent pin encodings without using legacy encryption."""
128135
for pin in [self.pin, "0x" + self.pin.upper()]:
129136
with (
130137
self.subTest(pin=pin),
131138
patch.object(VMM_CLI, "load_whitelist", return_value=[pin]),
132139
):
133-
self.decrypt(self.cli.encrypt_env_payload(self.envs, self.app_id, None, True))
140+
self.decrypt(
141+
self.cli.encrypt_env_payload(self.envs, self.app_id, None, True)
142+
)
134143
self.cli.get_app_env_encrypt_pub_key.assert_not_called()
135144

136145
def test_app_epoch_is_signed_and_used_for_sealing(self):
146+
"""Bind the app epoch in both the signature and HPKE context."""
137147
self.response["app_epoch"] = 2
138148
digest = keccak(
139-
b"dstack-sealed-env-pubkey-v1:" + bytes.fromhex(self.app_id)
140-
+ self.epoch.to_bytes(4, "big") + (2).to_bytes(4, "big")
149+
b"dstack-sealed-env-pubkey-v1:"
150+
+ bytes.fromhex(self.app_id)
151+
+ self.epoch.to_bytes(4, "big")
152+
+ (2).to_bytes(4, "big")
141153
+ bytes.fromhex(self.response["public_key"])
142154
+ self.timestamp.to_bytes(8, "big")
143155
)
@@ -147,16 +159,20 @@ def test_app_epoch_is_signed_and_used_for_sealing(self):
147159
self.assertEqual(envelope["version"], 1)
148160
self.assertEqual(envelope["app_epoch"], 2)
149161
receiver = self.suite.create_recipient_context(
150-
base64.b64decode(envelope["enc"]), self.recipient.private_key,
162+
base64.b64decode(envelope["enc"]),
163+
self.recipient.private_key,
151164
info=f"dstack-sealed-env-v1:{self.epoch}:2".encode(),
152165
)
153166
plaintext = json.loads(receiver.open(base64.b64decode(envelope["ciphertext"])))
154-
self.assertEqual(plaintext, {"env": [{"key": k, "value": v} for k, v in self.envs.items()]})
167+
self.assertEqual(
168+
plaintext, {"env": [{"key": k, "value": v} for k, v in self.envs.items()]}
169+
)
155170
self.response["app_epoch"] = 1
156171
with self.assertRaises(ValueError):
157172
self.cli.encrypt_env_payload(self.envs, self.app_id, None, True)
158173

159174
def test_unpinned_signer_never_becomes_trusted(self):
175+
"""Reject recipients without an independently trusted signer."""
160176
for pins in [
161177
[],
162178
[keys.PrivateKey(b"\x33" * 32).public_key.to_compressed_bytes().hex()],
@@ -172,6 +188,7 @@ def test_unpinned_signer_never_becomes_trusted(self):
172188
self.cli.get_app_env_encrypt_pub_key.assert_not_called()
173189

174190
def test_signature_binds_every_field_and_app(self):
191+
"""Reject mutations of the signed claim or application identity."""
175192
cases = [
176193
("public_key", "44" * 32),
177194
("root_epoch", 4),
@@ -201,20 +218,26 @@ def test_signature_binds_every_field_and_app(self):
201218
self.cli.get_app_env_encrypt_pub_key.assert_not_called()
202219

203220
def test_pinned_signed_key_timestamp_window(self):
221+
"""Enforce the freshness window for a trusted signer."""
204222
for age in [-60, 0, 300]:
205-
with self.subTest(age=age), patch.object(
206-
VMM_CLI.time, "time", return_value=self.timestamp + age
223+
with (
224+
self.subTest(age=age),
225+
patch.object(VMM_CLI.time, "time", return_value=self.timestamp + age),
207226
):
208-
self.decrypt(self.cli.encrypt_env_payload(self.envs, self.app_id, None, True))
227+
self.decrypt(
228+
self.cli.encrypt_env_payload(self.envs, self.app_id, None, True)
229+
)
209230
for age, reason in [(-61, "future"), (301, "stale")]:
210-
with self.subTest(age=age), patch.object(
211-
VMM_CLI.time, "time", return_value=self.timestamp + age
231+
with (
232+
self.subTest(age=age),
233+
patch.object(VMM_CLI.time, "time", return_value=self.timestamp + age),
212234
):
213235
with self.assertRaisesRegex(ValueError, reason):
214236
self.cli.encrypt_env_payload(self.envs, self.app_id, None, True)
215237
self.cli.get_app_env_encrypt_pub_key.assert_not_called()
216238

217239
def test_public_key_request_contains_only_app_id(self):
240+
"""Keep epoch selection on the KMS rather than in the lookup request."""
218241
response = self.cli.get_sealed_env_pub_key(self.app_id)
219242
self.assertEqual(
220243
self.cli.client.request.call_args.kwargs["body"],
@@ -223,8 +246,10 @@ def test_public_key_request_contains_only_app_id(self):
223246
self.assertEqual(response["root_epoch"], self.epoch)
224247

225248
def test_missing_rpc_cannot_replay_a_legacy_recipient(self):
249+
"""Reject capability downgrade attempts using a captured legacy key."""
226250
legacy_digest = keccak(
227-
b"dstack-env-encrypt-pubkey:" + bytes.fromhex(self.app_id)
251+
b"dstack-env-encrypt-pubkey:"
252+
+ bytes.fromhex(self.app_id)
228253
+ bytes.fromhex(self.response["public_key"])
229254
)
230255
legacy = {
@@ -234,27 +259,48 @@ def test_missing_rpc_cannot_replay_a_legacy_recipient(self):
234259
missing = {"error": "Service not found: GetSealedEnvPubKey"}
235260
errors = [(status, missing) for status in [400, 404]]
236261
errors += [
237-
(400, {"error": f"Request failed with status={status}, error={json.dumps(missing)}"})
262+
(
263+
400,
264+
{
265+
"error": f"Request failed with status={status}, error={json.dumps(missing)}"
266+
},
267+
)
238268
for status in ["400 Bad Request", "404 Not Found"]
239269
]
240270
for error in errors:
241-
with self.subTest(error=error), patch.object(VMM_CLI, "VmmClient") as client:
271+
with (
272+
self.subTest(error=error),
273+
patch.object(VMM_CLI, "VmmClient") as client,
274+
):
242275
client.return_value.request.side_effect = [error, (200, legacy)]
243276
with self.assertRaises(RuntimeError):
244-
self.cli.encrypt_env_payload(self.envs, self.app_id, "https://kms.example", True)
277+
self.cli.encrypt_env_payload(
278+
self.envs, self.app_id, "https://kms.example", True
279+
)
245280
client.return_value.request.assert_called_once()
246281
self.cli.get_app_env_encrypt_pub_key.assert_not_called()
247282

248283
def test_errors_and_bad_responses_do_not_downgrade(self):
284+
"""Fail closed on lookup errors and malformed key responses."""
249285
for status, body in [
250286
(401, {"error": "Service not found: GetSealedEnvPubKey"}),
251287
(403, {"error": "forbidden"}),
252288
(404, "Not Found"),
253289
(400, {"error": "invalid app"}),
254290
(500, {"error": "unavailable"}),
255-
(400, {"error": 'Request failed with status=500 Internal Server Error, error={"error":"Service not found: GetSealedEnvPubKey"}'}),
256-
(400, {"error": 'Request failed with status=404 Not Found, error={"error":"other service"}'}),
257-
(400, {"error": 'Request failed with status=404 Not Found, error=invalid'}),
291+
(
292+
400,
293+
{
294+
"error": 'Request failed with status=500 Internal Server Error, error={"error":"Service not found: GetSealedEnvPubKey"}'
295+
},
296+
),
297+
(
298+
400,
299+
{
300+
"error": 'Request failed with status=404 Not Found, error={"error":"other service"}'
301+
},
302+
),
303+
(400, {"error": "Request failed with status=404 Not Found, error=invalid"}),
258304
(200, {}),
259305
(200, "invalid JSON"),
260306
]:
@@ -268,6 +314,7 @@ def test_errors_and_bad_responses_do_not_downgrade(self):
268314
self.cli.get_app_env_encrypt_pub_key.assert_not_called()
269315

270316
def test_direct_kms_verifies_pin_and_does_not_forward_vmm_credentials(self):
317+
"""Authenticate direct KMS keys without forwarding VMM credentials."""
271318
with patch.object(VMM_CLI, "VmmClient") as client:
272319
client.return_value.request.return_value = (200, self.response)
273320
self.decrypt(
@@ -279,17 +326,34 @@ def test_direct_kms_verifies_pin_and_does_not_forward_vmm_credentials(self):
279326
self.cli.get_app_env_encrypt_pub_key.assert_not_called()
280327

281328
def test_image_downgrade_and_env_replacement_share_one_request(self):
329+
"""Replace the image and environment format in the same update."""
330+
282331
def rpc(method, params=None):
283332
if method == "GetInfo":
284-
return {"found": True, "info": {"app_id": self.app_id, "configuration": {
285-
"image": "new", "compose_file": '{"kms_enabled":true}',
286-
}}}
333+
return {
334+
"found": True,
335+
"info": {
336+
"app_id": self.app_id,
337+
"configuration": {
338+
"image": "new",
339+
"compose_file": '{"kms_enabled":true}',
340+
},
341+
},
342+
}
287343
if method == "ListImages":
288-
return {"images": [{"name": "new", "sealed_env": True}, {"name": "old"}]}
344+
return {
345+
"images": [{"name": "new", "sealed_env": True}, {"name": "old"}]
346+
}
289347
return {}
348+
290349
self.cli.rpc_call = Mock(side_effect=rpc)
291-
with patch.object(VMM_CLI, "parse_env_file", return_value=self.envs), contextlib.redirect_stdout(io.StringIO()):
292-
self.cli.update_vm("vm", image="old", env_file="env", kms_urls=["https://kms.example"])
350+
with (
351+
patch.object(VMM_CLI, "parse_env_file", return_value=self.envs),
352+
contextlib.redirect_stdout(io.StringIO()),
353+
):
354+
self.cli.update_vm(
355+
"vm", image="old", env_file="env", kms_urls=["https://kms.example"]
356+
)
293357
calls = self.cli.rpc_call.call_args_list
294358
self.assertNotIn("ResizeVm", [call.args[0] for call in calls])
295359
method, payload = calls[-1].args
@@ -300,6 +364,7 @@ def rpc(method, params=None):
300364
self.cli.client.request.assert_not_called()
301365

302366
def test_create_update_and_update_env_send_only_the_negotiated_field(self):
367+
"""Use the selected ciphertext field consistently across CLI commands."""
303368
compose = json.dumps({"kms_enabled": True})
304369
args = argparse.Namespace(
305370
compose="compose.json",

‎dstack/vmm/src/vmm-cli.py‎

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -324,9 +324,7 @@ def seal_env(envs, hex_public_key: str, root_epoch: int, app_epoch: int = 0) ->
324324
public_key = suite.kem.deserialize_public_key(
325325
bytes.fromhex(hex_public_key.removeprefix("0x"))
326326
)
327-
enc, context = suite.create_sender_context(
328-
public_key, info=info.encode("utf-8")
329-
)
327+
enc, context = suite.create_sender_context(public_key, info=info.encode("utf-8"))
330328
plaintext = json.dumps({"env": envs}).encode("utf-8")
331329
envelope = {
332330
"version": 1,
@@ -341,6 +339,7 @@ def seal_env(envs, hex_public_key: str, root_epoch: int, app_epoch: int = 0) ->
341339

342340

343341
def validate_uint(value: int, bits: int, name: str) -> None:
342+
"""Require an integer in the specified unsigned range."""
344343
if type(value) is not int or not 0 <= value < 1 << bits:
345344
raise ValueError(f"{name} must be a uint{bits}")
346345

@@ -849,10 +848,14 @@ def list_images(self, json_output: bool = False) -> None:
849848
print(format_table(rows, headers))
850849

851850
def image_supports_sealed_env(self, image: Optional[str]) -> bool:
851+
"""Read the selected image capability, treating missing metadata as legacy."""
852852
if not image:
853853
return False
854854
images = self.rpc_call("ListImages").get("images", [])
855-
return any(item.get("name") == image and item.get("sealed_env") is True for item in images)
855+
return any(
856+
item.get("name") == image and item.get("sealed_env") is True
857+
for item in images
858+
)
856859

857860
def get_sealed_env_pub_key(
858861
self,
@@ -877,15 +880,23 @@ def get_sealed_env_pub_key(
877880
return response
878881

879882
def encrypt_env_payload(
880-
self, envs: Dict[str, str], app_id: str, kms_url: Optional[str],
883+
self,
884+
envs: Dict[str, str],
885+
app_id: str,
886+
kms_url: Optional[str],
881887
supports_sealed_env: bool,
882888
) -> Dict:
883889
"""Select encryption using the target image capability and KMS RPC support."""
884890
envs_list = [{"key": k, "value": v} for k, v in envs.items()]
885891
if supports_sealed_env:
886892
key = self.get_sealed_env_pub_key(app_id, kms_url)
887893
return {
888-
"sealed_env": seal_env(envs_list, key["public_key"], key["root_epoch"], key.get("app_epoch", 0))
894+
"sealed_env": seal_env(
895+
envs_list,
896+
key["public_key"],
897+
key["root_epoch"],
898+
key.get("app_epoch", 0),
899+
)
889900
}
890901
public_key = self.get_app_env_encrypt_pub_key(app_id, kms_url)
891902
return {"encrypted_env": encrypt_env(envs_list, public_key)}
@@ -1153,7 +1164,14 @@ def create_vm(self, args) -> None:
11531164
encrypt_url = args.kms_encrypt_url or (
11541165
args.kms_url[0] if args.kms_url else None
11551166
)
1156-
params.update(self.encrypt_env_payload(envs, app_id, encrypt_url, self.image_supports_sealed_env(args.image)))
1167+
params.update(
1168+
self.encrypt_env_payload(
1169+
envs,
1170+
app_id,
1171+
encrypt_url,
1172+
self.image_supports_sealed_env(args.image),
1173+
)
1174+
)
11571175
response = self.rpc_call("CreateVm", params)
11581176
print(f"Created VM with ID: {response.get('id')}")
11591177
return response.get("id")
@@ -1179,9 +1197,14 @@ def update_vm_env(
11791197
compose_file = vm_configuration.get("compose_file")
11801198

11811199
payload = {"id": vm_id}
1182-
payload.update(self.encrypt_env_payload(
1183-
envs, app_id, kms_urls[0], self.image_supports_sealed_env(vm_configuration.get("image"))
1184-
))
1200+
payload.update(
1201+
self.encrypt_env_payload(
1202+
envs,
1203+
app_id,
1204+
kms_urls[0],
1205+
self.image_supports_sealed_env(vm_configuration.get("image")),
1206+
)
1207+
)
11851208

11861209
if compose_file:
11871210
try:
@@ -1345,10 +1368,18 @@ def update_vm(
13451368
vm_configuration = vm_info_response["info"].get("configuration") or {}
13461369
compose_file_content = vm_configuration.get("compose_file")
13471370

1348-
upgrade_params.update(self.encrypt_env_payload(
1349-
envs, app_id, kms_urls[0],
1350-
self.image_supports_sealed_env(image if image is not None else vm_configuration.get("image")),
1351-
))
1371+
upgrade_params.update(
1372+
self.encrypt_env_payload(
1373+
envs,
1374+
app_id,
1375+
kms_urls[0],
1376+
self.image_supports_sealed_env(
1377+
image
1378+
if image is not None
1379+
else vm_configuration.get("image")
1380+
),
1381+
)
1382+
)
13521383
updates.append("environment variables")
13531384

13541385
# update allowed_envs in compose file if needed

‎sdk/python/src/dstack_sdk/__init__.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,17 @@
2727
from .dstack_client_v1 import InfoResponseV1
2828
from .dstack_client_v1 import IssueCertResponseV1
2929
from .dstack_client_v1 import VersionResponseV1
30-
from .seal_env import seal_env
3130
from .encrypt_env_vars import EnvVar
3231
from .encrypt_env_vars import encrypt_env_vars
3332
from .encrypt_env_vars import encrypt_env_vars_sync
3433
from .get_compose_hash import AppCompose
3534
from .get_compose_hash import DockerConfig
3635
from .get_compose_hash import Requirements
3736
from .get_compose_hash import get_compose_hash
37+
from .seal_env import seal_env
3838
from .verify_env_encrypt_public_key import verify_env_encrypt_public_key
39-
from .verify_env_encrypt_public_key import verify_sealed_env_public_key
4039
from .verify_env_encrypt_public_key import verify_env_encrypt_public_key_legacy
40+
from .verify_env_encrypt_public_key import verify_sealed_env_public_key
4141

4242
__all__ = [
4343
"seal_env",

0 commit comments

Comments
 (0)