Avoid crashing when overriding key selection on the command-line
Marco Ricci

Marco Ricci commited on 2024-07-28 17:12:05
Zeige 4 geänderte Dateien mit 71 Einfügungen und 9 Löschungen.


A control-flow error resulted in there being a code path where both
`phrase` and `key` arguments were passed to the `derivepassphrase.Vault`
constructor, which does not take a `key` argument.

So, unconditionally discard the `key` argument from the dict of
arguments to pass to the `derivepassphrase.Vault` constructor.  This
also allows streamlining the preceding phrase and key handling logic
somewhat.

For the tests, add further aliases for the second and third dummy keys,
because the comprehensive regression test for this bug needs three
different sources of key selection.
... ...
@@ -1068,18 +1068,12 @@ def derivepassphrase(
1068 1068
             # derivepassphrase.Vault.phrase_from_key if a key is
1069 1069
             # given. Finally, if nothing is set, error out.
1070 1070
             if use_key or use_phrase:
1071
-                if use_key:
1072
-                    kwargs['phrase'] = key_to_phrase(key)
1073
-                else:
1074
-                    kwargs['phrase'] = phrase
1075
-                    kwargs.pop('key', '')
1071
+                kwargs['phrase'] = key_to_phrase(key) if use_key else phrase
1076 1072
             elif kwargs.get('phrase') and kwargs.get('key'):
1077 1073
                 if any('key' in m for m in settings.maps[:2]):
1078
-                    kwargs['phrase'] = key_to_phrase(kwargs.pop('key'))
1079
-                else:
1080
-                    kwargs.pop('key')
1074
+                    kwargs['phrase'] = key_to_phrase(kwargs['key'])
1081 1075
             elif kwargs.get('key'):
1082
-                kwargs['phrase'] = key_to_phrase(kwargs.pop('key'))
1076
+                kwargs['phrase'] = key_to_phrase(kwargs['key'])
1083 1077
             elif kwargs.get('phrase'):
1084 1078
                 pass
1085 1079
             else:
... ...
@@ -1088,6 +1082,7 @@ def derivepassphrase(
1088 1082
                     'or in configuration'
1089 1083
                 )
1090 1084
                 raise click.UsageError(msg)
1085
+            kwargs.pop('key', '')
1091 1086
             vault = dpp.Vault(**kwargs)
1092 1087
             result = vault.generate(service)
1093 1088
             click.echo(result.decode('ASCII'))
... ...
@@ -327,6 +327,8 @@ DUMMY_KEY1 = SUPPORTED_KEYS['ed25519']['public_key_data']
327 327
 DUMMY_KEY1_B64 = base64.standard_b64encode(DUMMY_KEY1).decode('ASCII')
328 328
 DUMMY_KEY2 = SUPPORTED_KEYS['rsa']['public_key_data']
329 329
 DUMMY_KEY2_B64 = base64.standard_b64encode(DUMMY_KEY2).decode('ASCII')
330
+DUMMY_KEY3 = SUPPORTED_KEYS['ed448']['public_key_data']
331
+DUMMY_KEY3_B64 = base64.standard_b64encode(DUMMY_KEY3).decode('ASCII')
330 332
 DUMMY_CONFIG_SETTINGS = {
331 333
     'length': 10,
332 334
     'upper': 1,
... ...
@@ -36,6 +36,9 @@ DUMMY_PHRASE_FROM_KEY1 = tests.DUMMY_PHRASE_FROM_KEY1
36 36
 DUMMY_KEY1 = tests.DUMMY_KEY1
37 37
 DUMMY_KEY1_B64 = tests.DUMMY_KEY1_B64
38 38
 DUMMY_KEY2 = tests.DUMMY_KEY2
39
+DUMMY_KEY2_B64 = tests.DUMMY_KEY2_B64
40
+DUMMY_KEY3 = tests.DUMMY_KEY3
41
+DUMMY_KEY3_B64 = tests.DUMMY_KEY3_B64
39 42
 
40 43
 
41 44
 class IncompatibleConfiguration(NamedTuple):
... ...
@@ -358,6 +361,67 @@ class TestCLI:
358 361
                 last_line.rstrip(b'\n') == DUMMY_RESULT_KEY1
359 362
             ), 'program generated unexpected result (wrong settings?)'
360 363
 
364
+    @tests.skip_if_no_agent
365
+    @pytest.mark.parametrize(
366
+        'config',
367
+        [
368
+            pytest.param(
369
+                {
370
+                    'global': {'key': DUMMY_KEY1_B64},
371
+                    'services': {DUMMY_SERVICE: {}},
372
+                },
373
+                id='global_config',
374
+            ),
375
+            pytest.param(
376
+                {'services': {DUMMY_SERVICE: {'key': DUMMY_KEY2_B64}}},
377
+                id='service_config',
378
+            ),
379
+            pytest.param(
380
+                {
381
+                    'global': {'key': DUMMY_KEY1_B64},
382
+                    'services': {DUMMY_SERVICE: {'key': DUMMY_KEY2_B64}},
383
+                },
384
+                id='full_config',
385
+            ),
386
+        ],
387
+    )
388
+    @pytest.mark.parametrize('key_index', [1, 2, 3], ids=lambda i: f'index{i}')
389
+    def test_204c_key_override_on_command_line(
390
+        self,
391
+        monkeypatch: Any,
392
+        config: dict[str, Any],
393
+        key_index: int,
394
+    ) -> None:
395
+        def sign(
396
+            _, key: bytes | bytearray, message: bytes | bytearray
397
+        ) -> bytes:
398
+            del message  # Unused.
399
+            for value in tests.SUPPORTED_KEYS.values():
400
+                if value['public_key_data'] == key:
401
+                    assert value['expected_signature'] is not None
402
+                    return value['expected_signature']
403
+            raise AssertionError
404
+
405
+        monkeypatch.setattr(
406
+            ssh_agent_client.SSHAgentClient, 'list_keys', tests.list_keys
407
+        )
408
+        monkeypatch.setattr(ssh_agent_client.SSHAgentClient, 'sign', sign)
409
+        runner = click.testing.CliRunner(mix_stderr=False)
410
+        with tests.isolated_config(
411
+            monkeypatch=monkeypatch, runner=runner, config=config
412
+        ):
413
+            result = runner.invoke(
414
+                cli.derivepassphrase,
415
+                ['-k', DUMMY_SERVICE],
416
+                input=f'{key_index}\n'.encode('ASCII'),
417
+            )
418
+            assert result.stderr_bytes is not None
419
+            assert (
420
+                b'Error:' not in result.stderr_bytes
421
+            ), 'program exited with failure'
422
+            assert result.stdout_bytes, 'program output expected'
423
+            assert result.exit_code == 0, 'program exited with failure'
424
+
361 425
     def test_205_service_phrase_if_key_in_global_config(
362 426
         self,
363 427
         monkeypatch: Any,
... ...
@@ -0,0 +1 @@
1
+Avoid crashing when selecting a key on the command-line and there already is a key stored in the configuration.
0 2