Coverage for tests\__init__.py: 98.876%

1# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>

2#

3# SPDX-License-Identifier: Zlib

4

5 from __future__ import annotations (empty)

6

7 import base64 (empty)

8 import contextlib (empty)

9 import copy (empty)

10 import enum (empty)

11 import errno (empty)

12 import importlib.util (empty)

13 import json (empty)

14 import logging (empty)

15 import os (empty)

16 import pathlib (empty)

17 import re (empty)

18 import shlex (empty)

19 import stat (empty)

20 import sys (empty)

21 import tempfile (empty)

22 import types (empty)

23 import zipfile (empty)

24 from typing import TYPE_CHECKING, TypedDict (empty)

25

26 import click.testing (empty)

27 import hypothesis (empty)

28 import pytest (empty)

29 from hypothesis import strategies (empty)

30 from typing_extensions import NamedTuple, assert_never, overload (empty)

31

32 from derivepassphrase import _types, cli, ssh_agent, vault (empty)

33 from derivepassphrase._internals import cli_helpers, cli_machinery (empty)

34 from derivepassphrase.ssh_agent import socketprovider (empty)

35

36 __all__ = () (empty)

37

38 if TYPE_CHECKING: (empty)

39 import socket

40 from collections.abc import Callable, Iterator, Mapping, Sequence

41 from contextlib import AbstractContextManager

42 from typing import IO, NotRequired

43

44 from typing_extensions import Any, Buffer, Self

45

46

47 class SSHTestKey(NamedTuple): (empty)

48 """An SSH test key.

49

50 Attributes:

51 public_key:

52 The SSH public key string, as used e.g. by OpenSSH's

53 `authorized_keys` file. Includes a comment.

54 public_key_data:

55 The SSH protocol wire format of the public key.

56 private_key:

57 A base64 encoded representation of the private key, in

58 OpenSSH's v1 private key format.

59 private_key_blob:

60 The SSH protocol wire format of the private key.

61 expected_signature:

62 For deterministic signature types, this is the expected

63 signature of the vault UUID. For other types this is

64 `None`.

65 derived_passphrase:

66 For deterministic signature types, this is the "equivalent

67 master passphrase" derived from this key (a transformation

68 of [`expected_signature`][]). For other types this is

69 `None`.

70

71 """

72

73 public_key: bytes | str (empty)

74 """""" (empty)

75 public_key_data: bytes (empty)

76 """""" (empty)

77 private_key: bytes (empty)

78 """""" (empty)

79 private_key_blob: bytes | None = None (empty)

80 """""" (empty)

81 expected_signature: bytes | None = None (empty)

82 """""" (empty)

83 derived_passphrase: bytes | str | None = None (empty)

84 """""" (empty)

85

86 def is_suitable( (empty)

87 self,

88 *,

89 client: ssh_agent.SSHAgentClient | None = None,

90 ) -> bool:

91 """Return if this key is suitable for use with vault.

92

93 Args:

94 client:

95 An optional SSH agent client to check for additional

96 deterministic key types. If not given, assume no such

97 types.

98

99 """

100 return vault.Vault.is_suitable_ssh_key( (empty)

101 self.public_key_data, client=client

102 )

103

104

105 class ValidationSettings(NamedTuple): (empty)

106 """Validation settings for [`VaultTestConfig`][]s.

107

108 Attributes:

109 allow_unknown_settings:

110 See [`_types.validate_vault_config`][].

111

112 """

113

114 allow_unknown_settings: bool (empty)

115 """""" (empty)

116

117

118 class VaultTestConfig(NamedTuple): (empty)

119 """A (not necessarily valid) sample vault config, plus metadata.

120

121 Attributes:

122 config:

123 The actual configuration object. Usually a [`dict`][].

124 comment:

125 An explanatory comment for what is wrong with this config,

126 or empty if the config is valid. This is intended as

127 a debugging message to be shown to the user (e.g. when an

128 assertion fails), not as an error message to

129 programmatically match against.

130 validation_settings:

131 See [`_types.validate_vault_config`][].

132

133 """

134

135 config: Any (empty)

136 """""" (empty)

137 comment: str (empty)

138 """""" (empty)

139 validation_settings: ValidationSettings | None (empty)

140 """""" (empty)

141

142

143 TEST_CONFIGS: list[VaultTestConfig] = [ (empty)

144 VaultTestConfig(None, 'not a dict', None),

145 VaultTestConfig({}, 'missing required keys', None),

146 VaultTestConfig(

147 {'global': None, 'services': {}}, 'bad config value: global', None

148 ),

149 VaultTestConfig(

150 {'global': {'key': 123}, 'services': {}},

151 'bad config value: global.key',

152 None,

153 ),

154 VaultTestConfig(

155 {'global': {'phrase': 'abc', 'key': '...'}, 'services': {}},

156 '',

157 None,

158 ),

159 VaultTestConfig({'services': None}, 'bad config value: services', None),

160 VaultTestConfig(

161 {'services': {'1': {}, 2: {}}}, 'bad config value: services."2"', None

162 ),

163 VaultTestConfig(

164 {'services': {'1': {}, '2': 2}}, 'bad config value: services."2"', None

165 ),

166 VaultTestConfig(

167 {'services': {'sv': {'notes': ['sentinel', 'list']}}},

168 'bad config value: services.sv.notes',

169 None,

170 ),

171 VaultTestConfig(

172 {'services': {'sv': {'notes': 'blah blah blah'}}}, '', None

173 ),

174 VaultTestConfig(

175 {'services': {'sv': {'length': '200'}}},

176 'bad config value: services.sv.length',

177 None,

178 ),

179 VaultTestConfig(

180 {'services': {'sv': {'length': 0.5}}},

181 'bad config value: services.sv.length',

182 None,

183 ),

184 VaultTestConfig(

185 {'services': {'sv': {'length': ['sentinel', 'list']}}},

186 'bad config value: services.sv.length',

187 None,

188 ),

189 VaultTestConfig(

190 {'services': {'sv': {'length': -10}}},

191 'bad config value: services.sv.length',

192 None,

193 ),

194 VaultTestConfig(

195 {'services': {'sv': {'lower': '10'}}},

196 'bad config value: services.sv.lower',

197 None,

198 ),

199 VaultTestConfig(

200 {'services': {'sv': {'upper': -10}}},

201 'bad config value: services.sv.upper',

202 None,

203 ),

204 VaultTestConfig(

205 {'services': {'sv': {'number': ['sentinel', 'list']}}},

206 'bad config value: services.sv.number',

207 None,

208 ),

209 VaultTestConfig(

210 {

211 'global': {'phrase': 'my secret phrase'},

212 'services': {'sv': {'length': 10}},

213 },

214 '',

215 None,

216 ),

217 VaultTestConfig(

218 {'services': {'sv': {'length': 10, 'phrase': '...'}}}, '', None

219 ),

220 VaultTestConfig(

221 {'services': {'sv': {'length': 10, 'key': '...'}}}, '', None

222 ),

223 VaultTestConfig(

224 {'services': {'sv': {'upper': 10, 'key': '...'}}}, '', None

225 ),

226 VaultTestConfig(

227 {'services': {'sv': {'phrase': 'abc', 'key': '...'}}}, '', None

228 ),

229 VaultTestConfig(

230 {

231 'global': {'phrase': 'abc'},

232 'services': {'sv': {'phrase': 'abc', 'length': 10}},

233 },

234 '',

235 None,

236 ),

237 VaultTestConfig(

238 {

239 'global': {'key': '...'},

240 'services': {'sv': {'phrase': 'abc', 'length': 10}},

241 },

242 '',

243 None,

244 ),

245 VaultTestConfig(

246 {

247 'global': {'key': '...'},

248 'services': {'sv': {'phrase': 'abc', 'key': '...', 'length': 10}},

249 },

250 '',

251 None,

252 ),

253 VaultTestConfig(

254 {

255 'global': {'key': '...'},

256 'services': {

257 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},

258 'sv2': {'length': 10, 'repeat': 1, 'lower': 1},

259 },

260 },

261 '',

262 None,

263 ),

264 VaultTestConfig(

265 {

266 'global': {'key': '...', 'unicode_normalization_form': 'NFC'},

267 'services': {

268 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},

269 'sv2': {'length': 10, 'repeat': 1, 'lower': 1},

270 },

271 },

272 '',

273 None,

274 ),

275 VaultTestConfig(

276 {

277 'global': {'key': '...', 'unicode_normalization_form': True},

278 'services': {},

279 },

280 'bad config value: global.unicode_normalization_form',

281 None,

282 ),

283 VaultTestConfig(

284 {

285 'global': {'key': '...', 'unicode_normalization_form': 'NFC'},

286 'services': {

287 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},

288 'sv2': {'length': 10, 'repeat': 1, 'lower': 1},

289 },

290 },

291 '',

292 ValidationSettings(True),

293 ),

294 VaultTestConfig(

295 {

296 'global': {'key': '...', 'unicode_normalization_form': 'NFC'},

297 'services': {

298 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},

299 'sv2': {'length': 10, 'repeat': 1, 'lower': 1},

300 },

301 },

302 'extension/unknown key: .global.unicode_normalization_form',

303 ValidationSettings(False),

304 ),

305 VaultTestConfig(

306 {

307 'global': {'key': '...', 'unknown_key': True},

308 'services': {

309 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},

310 'sv2': {'length': 10, 'repeat': 1, 'lower': 1},

311 },

312 },

313 '',

314 ValidationSettings(True),

315 ),

316 VaultTestConfig(

317 {

318 'global': {'key': '...', 'unknown_key': True},

319 'services': {

320 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},

321 'sv2': {'length': 10, 'repeat': 1, 'lower': 1},

322 },

323 },

324 'unknown key: .global.unknown_key',

325 ValidationSettings(False),

326 ),

327 VaultTestConfig(

328 {

329 'global': {'key': '...'},

330 'services': {

331 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},

332 'sv2': {

333 'length': 10,

334 'repeat': 1,

335 'lower': 1,

336 'unknown_key': True,

337 },

338 },

339 },

340 'unknown key: .services.sv2.unknown_key',

341 ValidationSettings(False),

342 ),

343 VaultTestConfig(

344 {

345 'global': {'key': '...', 'unicode_normalization_form': 'NFC'},

346 'services': {

347 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},

348 'sv2': {

349 'length': 10,

350 'repeat': 1,

351 'lower': 1,

352 'unknown_key': True,

353 },

354 },

355 },

356 '',

357 ValidationSettings(True),

358 ),

359 VaultTestConfig(

360 {

361 'global': {'key': '...', 'unicode_normalization_form': 'NFC'},

362 'services': {

363 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},

364 'sv2': {

365 'length': 10,

366 'repeat': 1,

367 'lower': 1,

368 'unknown_key': True,

369 },

370 },

371 },

372 '',

373 ValidationSettings(True),

374 ),

375 VaultTestConfig(

376 {

377 'global': {'key': '...', 'unicode_normalization_form': 'NFC'},

378 'services': {

379 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},

380 'sv2': {

381 'length': 10,

382 'repeat': 1,

383 'lower': 1,

384 'unknown_key': True,

385 },

386 },

387 },

388 '',

389 ValidationSettings(True),

390 ),

391]

392"""The master list of test configurations for vault.""" (empty)

393

394

395 def is_valid_test_config(conf: VaultTestConfig, /) -> bool: (empty)

396 """Return true if the test config is valid.

397

398 Args:

399 conf: The test config to check.

400

401 """

402 return not conf.comment and conf.validation_settings in { (empty)

403 None,

404 (True,),

405 }

406

407

408 def _test_config_ids(val: VaultTestConfig) -> Any: # pragma: no cover (empty)

409 """pytest id function for VaultTestConfig objects."""

410 assert isinstance(val, VaultTestConfig) (empty)

411 return val[1] or (val[0], val[1], val[2]) (empty)

412

413

414@strategies.composite (empty)

415 def vault_full_service_config(draw: strategies.DrawFn) -> dict[str, int]: (empty)

416 """Hypothesis strategy for full vault service configurations.

417

418 Returns a sample configuration with restrictions on length, repeat

419 count, and all character classes, while ensuring the settings are

420 not obviously unsatisfiable.

421

422 Args:

423 draw:

424 The `draw` function, as provided for by hypothesis.

425

426 """

427 repeat = draw(strategies.integers(min_value=0, max_value=10)) 5 ctx 2e r s NbOb

428 lower = draw(strategies.integers(min_value=0, max_value=10)) 5 ctx 2e r s NbOb

429 upper = draw(strategies.integers(min_value=0, max_value=10)) 5 ctx 2e r s NbOb

430 number = draw(strategies.integers(min_value=0, max_value=10)) 5 ctx 2e r s NbOb

431 space = draw(strategies.integers(min_value=0, max_value=repeat)) 5 ctx 2e r s NbOb

432 dash = draw(strategies.integers(min_value=0, max_value=10)) 5 ctx 2e r s NbOb

433 symbol = draw(strategies.integers(min_value=0, max_value=10)) 5 ctx 2e r s NbOb

434 length = draw( 5 ctx 2e r s NbOb

435 strategies.integers(

436 min_value=max(1, lower + upper + number + space + dash + symbol),

437 max_value=70,

438 )

439 )

440 hypothesis.assume(lower + upper + number + dash + symbol > 0) 5 ctx 2e r s NbOb

441 hypothesis.assume(lower + upper + number + space + symbol > 0) 5 ctx 2e r s NbOb

442 hypothesis.assume(repeat >= space) 5 ctx 2e r s NbOb

443 return { 5 ctx 2e r s NbOb

444 'lower': lower,

445 'upper': upper,

446 'number': number,

447 'space': space,

448 'dash': dash,

449 'symbol': symbol,

450 'repeat': repeat,

451 'length': length,

452 }

453

454

455 def is_smudgable_vault_test_config(conf: VaultTestConfig) -> bool: (empty)

456 """Check whether this vault test config can be effectively smudged.

457

458 A "smudged" test config is one where falsy values (in the JavaScript

459 sense) can be replaced by other falsy values without changing the

460 meaning of the config.

461

462 Args:

463 conf: A test config to check.

464

465 Returns:

466 True if the test config can be smudged, False otherwise.

467

468 """

469 config = conf.config 3 ctx 2a Abc

470 return bool( 3 ctx 2a Abc

471 isinstance(config, dict)

472 and ('global' not in config or isinstance(config['global'], dict))

473 and ('services' in config and isinstance(config['services'], dict))

474 and all(isinstance(x, dict) for x in config['services'].values())

475 and (config['services'] or config.get('global'))

476 )

477

478

479@strategies.composite (empty)

480 def smudged_vault_test_config( (empty)

481 draw: strategies.DrawFn,

482 config: Any = strategies.sampled_from(TEST_CONFIGS).filter( # noqa: B008

483 is_smudgable_vault_test_config

484 ),

485) -> Any:

486 """Hypothesis strategy to replace falsy values with other falsy values.

487

488 Uses [`_types.js_truthiness`][] internally, which is tested

489 separately by

490 [`tests.test_derivepassphrase_types.test_100_js_truthiness`][].

491

492 Args:

493 draw:

494 The `draw` function, as provided for by hypothesis.

495 config:

496 A strategy which generates [`VaultTestConfig`][] objects.

497

498 Returns:

499 A new [`VaultTestConfig`][] where some falsy values have been

500 replaced or added.

501

502 """

503

504 falsy = (None, False, 0, 0.0, '', float('nan')) 2 ctx 2Abc

505 falsy_no_str = (None, False, 0, 0.0, float('nan')) 2 ctx 2Abc

506 falsy_no_zero = (None, False, '', float('nan')) 2 ctx 2Abc

507 conf = draw(config) 2 ctx 2Abc

508 hypothesis.assume(is_smudgable_vault_test_config(conf)) 2 ctx 2Abc

509 obj = copy.deepcopy(conf.config) 2 ctx 2Abc

510 services: list[dict[str, Any]] = list(obj['services'].values()) 2 ctx 2Abc

511 if 'global' in obj: 2 ctx 2Abc

512 services.append(obj['global']) 2 ctx 2Abc

513 assert all(isinstance(x, dict) for x in services), ( 2 ctx 2Abc

514 'is_smudgable_vault_test_config guard failed to '

515 'ensure each settings dict is a dict'

516 )

517 for service in services: 2 ctx 2Abc

518 for key in ('phrase',): 2 ctx 2Abc

519 value = service.get(key) 2 ctx 2Abc

520 if not _types.js_truthiness(value) and value != '': 2 ctx 2Abc

521 service[key] = draw(strategies.sampled_from(falsy_no_str)) 2 ctx 2Abc

522 for key in ( 2 ctx 2Abc

523 'notes',

524 'key',

525 'length',

526 'repeat',

527 ):

528 value = service.get(key) 2 ctx 2Abc

529 if not _types.js_truthiness(value): 2 ctx 2Abc

530 service[key] = draw(strategies.sampled_from(falsy)) 2 ctx 2Abc

531 for key in ( 2 ctx 2Abc

532 'lower',

533 'upper',

534 'number',

535 'space',

536 'dash',

537 'symbol',

538 ):

539 value = service.get(key) 2 ctx 2Abc

540 if not _types.js_truthiness(value) and value != 0: 2 ctx 2Abc

541 service[key] = draw(strategies.sampled_from(falsy_no_zero)) 2 ctx 2Abc

542 hypothesis.assume(obj != conf.config) 2 ctx 2Abc

543 return VaultTestConfig(obj, conf.comment, conf.validation_settings) 2 ctx 2Abc

544

545

546 class KnownSSHAgent(str, enum.Enum): (empty)

547 """Known SSH agents.

548

549 Attributes:

550 UNKNOWN (str):

551 Not a known agent, or not known statically.

552 Pageant (str):

553 The agent from Simon Tatham's PuTTY suite.

554 OpenSSHAgent (str):

555 The agent from OpenBSD's OpenSSH suite.

556 FakeSSHAgentSocket (str):

557 The fake agent pseudo-socket defined in this test suite.

558

559 """

560

561 UNKNOWN = '(unknown)' (empty)

562 """""" (empty)

563 Pageant = 'Pageant' (empty)

564 """""" (empty)

565 OpenSSHAgent = 'OpenSSHAgent' (empty)

566 """""" (empty)

567 FakeSSHAgentSocket = 'FakeSSHAgentSocket' (empty)

568 """""" (empty)

569

570

571 class SpawnedSSHAgentInfo(NamedTuple): (empty)

572 """Info about a spawned SSH agent, as provided by some fixtures.

573

574 Differs from [`RunningSSHAgentInfo`][] in that this info object

575 already provides a functional client connected to the agent, but not

576 the address.

577

578 Attributes:

579 agent_type:

580 The agent's type.

581 client:

582 An SSH agent client connected to this agent.

583 isolated:

584 Whether this agent was spawned specifically for this test

585 suite, with attempts to isolate it from the user. If false,

586 then the user may be interacting with the agent externally,

587 meaning e.g. keys other than the test keys may be visible in

588 this agent.

589

590 """

591

592 agent_type: KnownSSHAgent (empty)

593 """""" (empty)

594 client: ssh_agent.SSHAgentClient (empty)

595 """""" (empty)

596 isolated: bool (empty)

597 """""" (empty)

598

599

600 class RunningSSHAgentInfo(NamedTuple): (empty)

601 """Info about a running SSH agent, as provided by some fixtures.

602

603 Differs from [`SpawnedSSHAgentInfo`][] in that this info object

604 provides only an address of the agent, not a functional client

605 already connected to it. The running SSH agent may or may not be

606 isolated.

607

608 Attributes:

609 socket:

610 A socket address to connect to the agent.

611 agent_type:

612 The agent's type.

613

614 """

615

616 socket: str | type[_types.SSHAgentSocket] (empty)

617 """""" (empty)

618 agent_type: KnownSSHAgent (empty)

619 """""" (empty)

620

621 def require_external_address(self) -> str: # pragma: no cover (empty)

622 if not isinstance(self.socket, str): 1 ctx 2Ub

623 pytest.skip( 1 ctx 2Ub

624 reason='This test requires a real, externally resolvable ' 1 ctx 2Ub

625 'address for the SSH agent socket.'

626 )

627 return self.socket

628

629

630 ALL_KEYS: Mapping[str, SSHTestKey] = { (empty)

631 'ed25519': SSHTestKey(

632 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----

633 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW

634 QyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdgAAAKDweO7H8Hju

635 xwAAAAtzc2gtZWQyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdg

636 AAAEAbM/A869nkWZbe2tp3Dm/L6gitvmpH/aRZt8sBII3ExYF4gWgm1gJIXw//Mkhv5MEw

637 idwcakUGCekJD/vCEml2AAAAG3Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQEC

638-----END OPENSSH PRIVATE KEY-----

639""",

640 private_key_blob=bytes.fromhex("""

641 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39

642 00 00 00 20

643 81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1

644 30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76

645 00 00 00 40

646 1b 33 f0 3c eb d9 e4 59 96 de da da 77 0e 6f cb

647 ea 08 ad be 6a 47 fd a4 59 b7 cb 01 20 8d c4 c5

648 81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1

649 30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76

650 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69 74

651 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65

652"""),

653 public_key=rb"""ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2 test key without passphrase

654""",

655 public_key_data=bytes.fromhex("""

656 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39

657 00 00 00 20

658 81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1

659 30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76

660"""),

661 expected_signature=bytes.fromhex("""

662 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39

663 00 00 00 40

664 f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86

665 66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd

666 0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c

667 1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02

668 """),

669 derived_passphrase=rb'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==',

670 ),

671 # Currently only supported by PuTTY (which is deficient in other

672 # niceties of the SSH agent and the agent's client).

673 'ed448': SSHTestKey(

674 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----

675 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz

676 c2gtZWQ0NDgAAAA54vZy009Wu8wExjvEb3hqtLz1GO/+d5vmGUbErWQ4AUO9mYLT

677 zHJHc2m4s+yWzP29Cc3EcxizLG8AAAAA8BdhfCcXYXwnAAAACXNzaC1lZDQ0OAAA

678 ADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM

679/b0JzcRzGLMsbwAAAAByM7GIMRvWJB3YD6SIpAF2uudX4ozZe0X917wPwiBrs373

680 9TM1n94Nib6hrxGNmCk2iBQDe2KALPgA4vZy009Wu8wExjvEb3hqtLz1GO/+d5vm

681 GUbErWQ4AUO9mYLTzHJHc2m4s+yWzP29Cc3EcxizLG8AAAAAG3Rlc3Qga2V5IHdp

682 dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ==

683-----END OPENSSH PRIVATE KEY-----

684""",

685 private_key_blob=bytes.fromhex("""

686 00 00 00 09 73 73 68 2d 65 64 34 34 38

687 00 00 00 39 e2 f6 72 d3 4f 56 bb cc 04

688 c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19

689 46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69

690 b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00

691 00 00 00 72 33 b1

692 88 31 1b d6 24 1d d8 0f a4 88 a4 01 76 ba e7 57

693 e2 8c d9 7b 45 fd d7 bc 0f c2 20 6b b3 7e f7 f5

694 33 35 9f de 0d 89 be a1 af 11 8d 98 29 36 88 14

695 03 7b 62 80 2c f8 00 e2 f6 72 d3 4f 56 bb cc 04

696 c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19

697 46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69

698 b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00

699 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69

700 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65

701"""),

702 public_key=rb"""ssh-ed448 AAAACXNzaC1lZDQ0OAAAADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM/b0JzcRzGLMsbwA= test key without passphrase

703""",

704 public_key_data=bytes.fromhex("""

705 00 00 00 09 73 73 68 2d 65 64 34 34 38

706 00 00 00 39 e2 f6 72 d3 4f 56 bb cc 04

707 c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19

708 46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69

709 b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00

710 """),

711 expected_signature=bytes.fromhex("""

712 00 00 00 09 73 73 68 2d 65 64 34 34 38

713 00 00 00 72 06 86

714 f4 64 a4 a6 ba d9 c3 22 c4 93 49 99 fc 11 de 67

715 97 08 f2 d8 b7 3c 2c 13 e7 c5 1c 1e 92 a6 0e d8

716 2f 6d 81 03 82 00 e3 72 e4 32 6d 72 d2 6d 32 84

717 3f cc a9 1e 57 2c 00 9a b3 99 de 45 da ce 2e d1

718 db e5 89 f3 35 be 24 58 90 c6 ca 04 f0 db 88 80

719 db bd 77 7c 80 20 7f 3a 48 61 f6 1f ae a9 5e 53

720 7b e0 9d 93 1e ea dc eb b5 cd 56 4c ea 8f 08 00

721 """),

722 derived_passphrase=rb'Bob0ZKSmutnDIsSTSZn8Ed5nlwjy2Lc8LBPnxRwekqYO2C9tgQOCAONy5DJtctJtMoQ/zKkeVywAmrOZ3kXazi7R2+WJ8zW+JFiQxsoE8NuIgNu9d3yAIH86SGH2H66pXlN74J2THurc67XNVkzqjwgA',

723 ),

724 'rsa': SSHTestKey(

725 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----

726 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn

727 NhAAAAAwEAAQAAAYEAsaHu6Xs4cVsuDSNJlMCqoPVgmDgEviI8TfXmHKqX3JkIqI3LsvV7

728 Ijf8WCdTveEq7CkuZhImtsR52AOEVAoU8mDXDNr+nJ5wUPzf1UIaRjDe0lcXW4SlF01hQs

729 G4wYDuqxshwelraB/L3e0zhD7fjYHF8IbFsqGlFHWEwOtlfhhfbxJsTGguLm4A8/gdEJD5

730 2rkqDcZpIXCHtJbCzW9aQpWcs/PDw5ylwl/3dB7jfxyfrGz4O3QrzsqhWEsip97mOmwl6q

731 CHbq8V8x9zu89D/H+bG5ijqxhijbjcVUW3lZfw/97gy9J6rG31HNar5H8GycLTFwuCFepD

732 mTEpNgQLKoe8ePIEPq4WHhFUovBdwlrOByUKKqxreyvWt5gkpTARz+9Lt8OjBO3rpqK8sZ

733 VKH3sE3de2RJM3V9PJdmZSs2b8EFK3PsUGdlMPM9pn1uk4uIItKWBmooOynuD8Ll6aPwuW

734 AFn3l8nLLyWdrmmEYzHWXiRjQJxy1Bi5AbHMOWiPAAAFkDPkuBkz5LgZAAAAB3NzaC1yc2

735 EAAAGBALGh7ul7OHFbLg0jSZTAqqD1YJg4BL4iPE315hyql9yZCKiNy7L1eyI3/FgnU73h

736 KuwpLmYSJrbEedgDhFQKFPJg1wza/pyecFD839VCGkYw3tJXF1uEpRdNYULBuMGA7qsbIc

737 Hpa2gfy93tM4Q+342BxfCGxbKhpRR1hMDrZX4YX28SbExoLi5uAPP4HRCQ+dq5Kg3GaSFw

738 h7SWws1vWkKVnLPzw8OcpcJf93Qe438cn6xs+Dt0K87KoVhLIqfe5jpsJeqgh26vFfMfc7

739 vPQ/x/mxuYo6sYYo243FVFt5WX8P/e4MvSeqxt9RzWq+R/BsnC0xcLghXqQ5kxKTYECyqH

740 vHjyBD6uFh4RVKLwXcJazgclCiqsa3sr1reYJKUwEc/vS7fDowTt66aivLGVSh97BN3Xtk

741 STN1fTyXZmUrNm/BBStz7FBnZTDzPaZ9bpOLiCLSlgZqKDsp7g/C5emj8LlgBZ95fJyy8l

742 na5phGMx1l4kY0CcctQYuQGxzDlojwAAAAMBAAEAAAF/cNVYT+Om4x9+SItcz5bOByGIOj

743 yWUH8f9rRjnr5ILuwabIDgvFaVG+xM1O1hWADqzMnSEcknHRkTYEsqYPykAtxFvjOFEh70

744 6qRUJ+fVZkqRGEaI3oWyWKTOhcCIYImtONvb0LOv/HQ2H2AXCoeqjST1qr/xSuljBtcB8u

745 wxs3EqaO1yU7QoZpDcMX9plH7Rmc9nNfZcgrnktPk2deX2+Y/A5tzdVgG1IeqYp6CBMLNM

746 uhL0OPdDehgBoDujx+rhkZ1gpo1wcULIM94NL7VSHBPX0Lgh9T+3j1HVP+YnMAvhfOvfct

747 LlbJ06+TYGRAMuF2LPCAZM/m0FEyAurRgWxAjLXm+4kp2GAJXlw82deDkQ+P8cHNT6s9ZH

748 R5YSy3lpZ35594ZMOLR8KqVvhgJGF6i9019BiF91SDxjE+sp6dNGfN8W+64tHdDv2a0Mso

749+8Qjyx7sTpi++EjLU8Iy73/e4B8qbXMyheyA/UUfgMtNKShh6sLlrD9h2Sm9RFTuEAAADA

750 Jh3u7WfnjhhKZYbAW4TsPNXDMrB0/t7xyAQgFmko7JfESyrJSLg1cO+QMOiDgD7zuQ9RSp

751 NIKdPsnIna5peh979mVjb2HgnikjyJECmBpLdwZKhX7MnIvgKw5lnQXHboEtWCa1N58l7f

752 srzwbi9pFUuUp9dShXNffmlUCjDRsVLbK5C6+iaIQyCWFYK8mc6dpNkIoPKf+Xg+EJCIFQ

753 oITqeu30Gc1+M+fdZc2ghq0b6XLthh/uHEry8b68M5KglMAAAAwQDw1i+IdcvPV/3u/q9O

754/kzLpKO3tbT89sc1zhjZsDNjDAGluNr6n38iq/XYRZu7UTL9BG+EgFVfIUV7XsYT5e+BPf

755 13VS94rzZ7maCsOlULX+VdMO2zBucHIoec9RUlRZrfB21B2W7YGMhbpoa5lN3lKJQ7afHo

756 dXZUMp0cTFbOmbzJgSzO2/NE7BhVwmvcUzTDJGMMKuxBO6w99YKDKRKm0PNLFDz26rWm9L

757 dNS2MVfVuPMTpzT26HQG4pFageq9cAAADBALzRBXdZF8kbSBa5MTUBVTTzgKQm1C772gJ8

758 T01DJEXZsVtOv7mUC1/m/by6Hk4tPyvDBuGj9hHq4N7dPqGutHb1q5n0ADuoQjRW7BXw5Q

759 vC2EAD91xexdorIA5BgXU+qltBqzzBVzVtF7+jOZOjfzOlaTX9I5I5veyeTaTxZj1XXUzi

760 btBNdMEJJp7ifucYmoYAAwE7K+VlWagDEK2y8Mte9y9E+N0uO2j+h85sQt/UIb2iE/vhcg

761 Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB

762-----END OPENSSH PRIVATE KEY-----

763""",

764 private_key_blob=bytes.fromhex("""

765 00 00 00 07 73 73 68 2d 72 73 61

766 00 00 01 81 00

767 b1 a1 ee e9 7b 38 71 5b 2e 0d 23 49 94 c0 aa a0

768 f5 60 98 38 04 be 22 3c 4d f5 e6 1c aa 97 dc 99

769 08 a8 8d cb b2 f5 7b 22 37 fc 58 27 53 bd e1 2a

770 ec 29 2e 66 12 26 b6 c4 79 d8 03 84 54 0a 14 f2

771 60 d7 0c da fe 9c 9e 70 50 fc df d5 42 1a 46 30

772 de d2 57 17 5b 84 a5 17 4d 61 42 c1 b8 c1 80 ee

773 ab 1b 21 c1 e9 6b 68 1f cb dd ed 33 84 3e df 8d

774 81 c5 f0 86 c5 b2 a1 a5 14 75 84 c0 eb 65 7e 18

775 5f 6f 12 6c 4c 68 2e 2e 6e 00 f3 f8 1d 10 90 f9

776 da b9 2a 0d c6 69 21 70 87 b4 96 c2 cd 6f 5a 42

777 95 9c b3 f3 c3 c3 9c a5 c2 5f f7 74 1e e3 7f 1c

778 9f ac 6c f8 3b 74 2b ce ca a1 58 4b 22 a7 de e6

779 3a 6c 25 ea a0 87 6e af 15 f3 1f 73 bb cf 43 fc

780 7f 9b 1b 98 a3 ab 18 62 8d b8 dc 55 45 b7 95 97

781 f0 ff de e0 cb d2 7a ac 6d f5 1c d6 ab e4 7f 06

782 c9 c2 d3 17 0b 82 15 ea 43 99 31 29 36 04 0b 2a

783 87 bc 78 f2 04 3e ae 16 1e 11 54 a2 f0 5d c2 5a

784 ce 07 25 0a 2a ac 6b 7b 2b d6 b7 98 24 a5 30 11

785 cf ef 4b b7 c3 a3 04 ed eb a6 a2 bc b1 95 4a 1f

786 7b 04 dd d7 b6 44 93 37 57 d3 c9 76 66 52 b3 66

787 fc 10 52 b7 3e c5 06 76 53 0f 33 da 67 d6 e9 38

788 b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f

789 0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31

790 d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f

791 00 00 00 03 01 00 01

792 00 00 01 7f

793 70 d5 58 4f e3 a6 e3 1f 7e 48 8b 5c cf 96 ce

794 07 21 88 3a 3c 96 50 7f 1f f6 b4 63 9e be 48 2e

795 ec 1a 6c 80 e0 bc 56 95 1b ec 4c d4 ed 61 58 00

796 ea cc c9 d2 11 c9 27 1d 19 13 60 4b 2a 60 fc a4

797 02 dc 45 be 33 85 12 1e f4 ea a4 54 27 e7 d5 66

798 4a 91 18 46 88 de 85 b2 58 a4 ce 85 c0 88 60 89

799 ad 38 db db d0 b3 af fc 74 36 1f 60 17 0a 87 aa

800 8d 24 f5 aa bf f1 4a e9 63 06 d7 01 f2 ec 31 b3

801 71 2a 68 ed 72 53 b4 28 66 90 dc 31 7f 69 94 7e

802 d1 99 cf 67 35 f6 5c 82 b9 e4 b4 f9 36 75 e5 f6

803 f9 8f c0 e6 dc dd 56 01 b5 21 ea 98 a7 a0 81 30

804 b3 4c ba 12 f4 38 f7 43 7a 18 01 a0 3b a3 c7 ea

805 e1 91 9d 60 a6 8d 70 71 42 c8 33 de 0d 2f b5 52

806 1c 13 d7 d0 b8 21 f5 3f b7 8f 51 d5 3f e6 27 30

807 0b e1 7c eb df 72 d2 e5 6c 9d 3a f9 36 06 44 03

808 2e 17 62 cf 08 06 4c fe 6d 05 13 20 2e ad 18 16

809 c4 08 cb 5e 6f b8 92 9d 86 00 95 e5 c3 cd 9d 78

810 39 10 f8 ff 1c 1c d4 fa b3 d6 47 47 96 12 cb 79

811 69 67 7e 79 f7 86 4c 38 b4 7c 2a a5 6f 86 02 46

812 17 a8 bd d3 5f 41 88 5f 75 48 3c 63 13 eb 29 e9

813 d3 46 7c df 16 fb ae 2d 1d d0 ef d9 ad 0c b2 8f

814 bc 42 3c b1 ee c4 e9 8b ef 84 8c b5 3c 23 2e f7

815 fd ee 01 f2 a6 d7 33 28 5e c8 0f d4 51 f8 0c b4

816 d2 92 86 1e ac 2e 5a c3 f6 1d 92 9b d4 45 4e e1

817 00 00 00 c0

818 26 1d ee ed 67 e7 8e 18 4a 65 86 c0 5b 84 ec 3c

819 d5 c3 32 b0 74 fe de f1 c8 04 20 16 69 28 ec 97

820 c4 4b 2a c9 48 b8 35 70 ef 90 30 e8 83 80 3e f3

821 b9 0f 51 4a 93 48 29 d3 ec 9c 89 da e6 97 a1 f7

822 bf 66 56 36 f6 1e 09 e2 92 3c 89 10 29 81 a4 b7

823 70 64 a8 57 ec c9 c8 be 02 b0 e6 59 d0 5c 76 e8

824 12 d5 82 6b 53 79 f2 5e df b2 bc f0 6e 2f 69 15

825 4b 94 a7 d7 52 85 73 5f 7e 69 54 0a 30 d1 b1 52

826 db 2b 90 ba fa 26 88 43 20 96 15 82 bc 99 ce 9d

827 a4 d9 08 a0 f2 9f f9 78 3e 10 90 88 15 0a 08 4e

828 a7 ae df 41 9c d7 e3 3e 7d d6 5c da 08 6a d1 be

829 97 2e d8 61 fe e1 c4 af 2f 1b eb c3 39 2a 09 4c

830 00 00 00 c1 00

831 f0 d6 2f 88 75 cb cf 57 fd ee fe af 4e fe 4c cb

832 a4 a3 b7 b5 b4 fc f6 c7 35 ce 18 d9 b0 33 63 0c

833 01 a5 b8 da fa 9f 7f 22 ab f5 d8 45 9b bb 51 32

834 fd 04 6f 84 80 55 5f 21 45 7b 5e c6 13 e5 ef 81

835 3d fd 77 55 2f 78 af 36 7b 99 a0 ac 3a 55 0b 5f

836 e5 5d 30 ed b3 06 e7 07 22 87 9c f5 15 25 45 9a

837 df 07 6d 41 d9 6e d8 18 c8 5b a6 86 b9 94 dd e5

838 28 94 3b 69 f1 e8 75 76 54 32 9d 1c 4c 56 ce 99

839 bc c9 81 2c ce db f3 44 ec 18 55 c2 6b dc 53 34

840 c3 24 63 0c 2a ec 41 3b ac 3d f5 82 83 29 12 a6

841 d0 f3 4b 14 3c f6 ea b5 a6 f4 b7 4d 4b 63 15 7d

842 5b 8f 31 3a 73 4f 6e 87 40 6e 29 15 a8 1e ab d7

843 00 00 00 c1 00

844 bc d1 05 77 59 17 c9 1b 48 16 b9 31 35 01 55 34

845 f3 80 a4 26 d4 2e fb da 02 7c 4f 4d 43 24 45 d9

846 b1 5b 4e bf b9 94 0b 5f e6 fd bc ba 1e 4e 2d 3f

847 2b c3 06 e1 a3 f6 11 ea e0 de dd 3e a1 ae b4 76

848 f5 ab 99 f4 00 3b a8 42 34 56 ec 15 f0 e5 0b c2

849 d8 40 03 f7 5c 5e c5 da 2b 20 0e 41 81 75 3e aa

850 5b 41 ab 3c c1 57 35 6d 17 bf a3 39 93 a3 7f 33

851 a5 69 35 fd 23 92 39 bd ec 9e 4d a4 f1 66 3d 57

852 5d 4c e2 6e d0 4d 74 c1 09 26 9e e2 7e e7 18 9a

853 86 00 03 01 3b 2b e5 65 59 a8 03 10 ad b2 f0 cb

854 5e f7 2f 44 f8 dd 2e 3b 68 fe 87 ce 6c 42 df d4

855 21 bd a2 13 fb e1 72 00 60 a7 ad 78 d9 69 d2 09

856 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69

857 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65

858"""),

859 public_key=rb"""ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCxoe7pezhxWy4NI0mUwKqg9WCYOAS+IjxN9eYcqpfcmQiojcuy9XsiN/xYJ1O94SrsKS5mEia2xHnYA4RUChTyYNcM2v6cnnBQ/N/VQhpGMN7SVxdbhKUXTWFCwbjBgO6rGyHB6WtoH8vd7TOEPt+NgcXwhsWyoaUUdYTA62V+GF9vEmxMaC4ubgDz+B0QkPnauSoNxmkhcIe0lsLNb1pClZyz88PDnKXCX/d0HuN/HJ+sbPg7dCvOyqFYSyKn3uY6bCXqoIdurxXzH3O7z0P8f5sbmKOrGGKNuNxVRbeVl/D/3uDL0nqsbfUc1qvkfwbJwtMXC4IV6kOZMSk2BAsqh7x48gQ+rhYeEVSi8F3CWs4HJQoqrGt7K9a3mCSlMBHP70u3w6ME7eumoryxlUofewTd17ZEkzdX08l2ZlKzZvwQUrc+xQZ2Uw8z2mfW6Ti4gi0pYGaig7Ke4PwuXpo/C5YAWfeXycsvJZ2uaYRjMdZeJGNAnHLUGLkBscw5aI8= test key without passphrase

860""",

861 public_key_data=bytes.fromhex("""

862 00 00 00 07 73 73 68 2d 72 73 61

863 00 00 00 03 01 00 01

864 00 00 01 81 00

865 b1 a1 ee e9 7b 38 71 5b 2e 0d 23 49 94 c0 aa a0

866 f5 60 98 38 04 be 22 3c 4d f5 e6 1c aa 97 dc 99

867 08 a8 8d cb b2 f5 7b 22 37 fc 58 27 53 bd e1 2a

868 ec 29 2e 66 12 26 b6 c4 79 d8 03 84 54 0a 14 f2

869 60 d7 0c da fe 9c 9e 70 50 fc df d5 42 1a 46 30

870 de d2 57 17 5b 84 a5 17 4d 61 42 c1 b8 c1 80 ee

871 ab 1b 21 c1 e9 6b 68 1f cb dd ed 33 84 3e df 8d

872 81 c5 f0 86 c5 b2 a1 a5 14 75 84 c0 eb 65 7e 18

873 5f 6f 12 6c 4c 68 2e 2e 6e 00 f3 f8 1d 10 90 f9

874 da b9 2a 0d c6 69 21 70 87 b4 96 c2 cd 6f 5a 42

875 95 9c b3 f3 c3 c3 9c a5 c2 5f f7 74 1e e3 7f 1c

876 9f ac 6c f8 3b 74 2b ce ca a1 58 4b 22 a7 de e6

877 3a 6c 25 ea a0 87 6e af 15 f3 1f 73 bb cf 43 fc

878 7f 9b 1b 98 a3 ab 18 62 8d b8 dc 55 45 b7 95 97

879 f0 ff de e0 cb d2 7a ac 6d f5 1c d6 ab e4 7f 06

880 c9 c2 d3 17 0b 82 15 ea 43 99 31 29 36 04 0b 2a

881 87 bc 78 f2 04 3e ae 16 1e 11 54 a2 f0 5d c2 5a

882 ce 07 25 0a 2a ac 6b 7b 2b d6 b7 98 24 a5 30 11

883 cf ef 4b b7 c3 a3 04 ed eb a6 a2 bc b1 95 4a 1f

884 7b 04 dd d7 b6 44 93 37 57 d3 c9 76 66 52 b3 66

885 fc 10 52 b7 3e c5 06 76 53 0f 33 da 67 d6 e9 38

886 b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f

887 0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31

888 d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f

889"""),

890 expected_signature=bytes.fromhex("""

891 00 00 00 07 73 73 68 2d 72 73 61

892 00 00 01 80

893 a2 10 7c 2e f6 bb 53 a8 74 2a a1 19 99 ad 81 be

894 79 9c ed d6 9d 09 4e 6e c5 18 48 33 90 77 99 68

895 f7 9e 03 5a cd 4e 18 eb 89 7d 85 a2 ee ae 4a 92

896 f6 6f ce b9 fe 86 7f 2a 6b 31 da 6e 1a fe a2 a5

897 88 b8 44 7f a1 76 73 b3 ec 75 b5 d0 a6 b9 15 97

898 65 09 13 7d 94 21 d1 fb 5d 0f 8b 23 04 77 c2 c3

899 55 22 b1 a0 09 8a f5 38 2a d6 7f 1b 87 29 a0 25

900 d3 25 6f cb 64 61 07 98 dc 14 c5 84 f8 92 24 5e

901 50 11 6b 49 e5 f0 cc 29 cb 29 a9 19 d8 a7 71 1f

902 91 0b 05 b1 01 4b c2 5f 00 a5 b6 21 bf f8 2c 9d

903 67 9b 47 3b 0a 49 6b 79 2d fc 1d ec 0c b0 e5 27

904 22 d5 a9 f8 d3 c3 f9 df 48 68 e9 fb ef 3c dc 26

905 bf cf ea 29 43 01 a6 e3 c5 51 95 f4 66 6d 8a 55

906 e2 47 ec e8 30 45 4c ae 47 e7 c9 a4 21 8b 64 ba

907 b6 88 f6 21 f8 73 b9 cb 11 a1 78 75 92 c6 5a e5

908 64 fe ed 42 d9 95 99 e6 2b 6f 3c 16 3c 28 74 a4

909 72 2f 0d 3f 2c 33 67 aa 35 19 8e e7 b5 11 2f b3

910 f7 6a c5 02 e2 6f a3 42 e3 62 19 99 03 ea a5 20

911 e7 a1 e3 bc c8 06 a3 b5 7c d6 76 5d df 6f 60 46

912 83 2a 08 00 d6 d3 d9 a4 c1 41 8c f8 60 56 45 81

913 da 3b a2 16 1f 9e 4e 75 83 17 da c3 53 c3 3e 19

914 a4 1b bc d2 29 b8 78 61 2b 78 e6 b1 52 b0 d5 ec

915 de 69 2c 48 62 d9 fd d1 9b 6b b0 49 db d3 ff 38

916 e7 10 d9 2d ce 9f 0d 5e 09 7b 37 d2 7b c3 bf ce

917"""),

918 derived_passphrase=rb'ohB8Lva7U6h0KqEZma2Bvnmc7dadCU5uxRhIM5B3mWj3ngNazU4Y64l9haLurkqS9m/Ouf6GfyprMdpuGv6ipYi4RH+hdnOz7HW10Ka5FZdlCRN9lCHR+10PiyMEd8LDVSKxoAmK9Tgq1n8bhymgJdMlb8tkYQeY3BTFhPiSJF5QEWtJ5fDMKcspqRnYp3EfkQsFsQFLwl8ApbYhv/gsnWebRzsKSWt5Lfwd7Ayw5Sci1an408P530ho6fvvPNwmv8/qKUMBpuPFUZX0Zm2KVeJH7OgwRUyuR+fJpCGLZLq2iPYh+HO5yxGheHWSxlrlZP7tQtmVmeYrbzwWPCh0pHIvDT8sM2eqNRmO57URL7P3asUC4m+jQuNiGZkD6qUg56HjvMgGo7V81nZd329gRoMqCADW09mkwUGM+GBWRYHaO6IWH55OdYMX2sNTwz4ZpBu80im4eGEreOaxUrDV7N5pLEhi2f3Rm2uwSdvT/zjnENktzp8NXgl7N9J7w7/O',

919 ),

920 'dsa1024': SSHTestKey(

921 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----

922 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH

923 NzAAAAgQC7KAZXqBGNVLBQPrcMYAoNW54BhD8aIhe7BDWYzJcsaMt72VKSkguZ8+XR7nRa

924 0C/ZsBi+uJp0dpxy9ZMTOWX4u5YPMeQcXEdGExZIfimGqSOAsy6fCld2IfJZJZExcCmhe9

925 Ssjsd3YSAPJRluOXFQc95MZoR5hMwlIDD8QzrE7QAAABUA99nOZOgd7aHMVGoXpUEBcn7H

926 ossAAACALr2Ag3hxM3rKdxzVUw8fX0VVPXO+3+Kr8hGe0Kc/7NwVaBVL1GQ8fenBuWynpA

927 UbH0wo3h1wkB/8hX6p+S8cnu5rIBlUuVNwLw/bIYohK98LfqTYK/V+g6KD+8m34wvEiXZm

928 qywY54n2bksch1Nqvj/tNpLzExSx/XS0kSM1aigAAACAbQNRPcVEuGDrEcf+xg5tgAejPX

929 BPXr/Jss+Chk64km3mirMYjAWyWYtVcgT+7hOYxtYRin8LyMLqKRmqa0Q5UrvDfChgLhvs

930 G9YSb/Mpw5qm8PiHSafwhkaz/te3+8hKogqoe7sd+tCF06IpJr5k70ACiNtRGqssNF8Elr

931 l1efYAAAH4swlfVrMJX1YAAAAHc3NoLWRzcwAAAIEAuygGV6gRjVSwUD63DGAKDVueAYQ/

932 GiIXuwQ1mMyXLGjLe9lSkpILmfPl0e50WtAv2bAYvriadHaccvWTEzll+LuWDzHkHFxHRh

933 MWSH4phqkjgLMunwpXdiHyWSWRMXApoXvUrI7Hd2EgDyUZbjlxUHPeTGaEeYTMJSAw/EM6

934 xO0AAAAVAPfZzmToHe2hzFRqF6VBAXJ+x6LLAAAAgC69gIN4cTN6yncc1VMPH19FVT1zvt

935/iq/IRntCnP+zcFWgVS9RkPH3pwblsp6QFGx9MKN4dcJAf/IV+qfkvHJ7uayAZVLlTcC8P

936 2yGKISvfC36k2Cv1foOig/vJt+MLxIl2ZqssGOeJ9m5LHIdTar4/7TaS8xMUsf10tJEjNW

937 ooAAAAgG0DUT3FRLhg6xHH/sYObYAHoz1wT16/ybLPgoZOuJJt5oqzGIwFslmLVXIE/u4T

938 mMbWEYp/C8jC6ikZqmtEOVK7w3woYC4b7BvWEm/zKcOapvD4h0mn8IZGs/7Xt/vISqIKqH

939 u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW

940 0gAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UBAgMEBQYH

941-----END OPENSSH PRIVATE KEY-----

942""",

943 private_key_blob=bytes.fromhex("""

944 00 00 00 07 73 73 68 2d 64 73 73

945 00 00 00 81 00

946 bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d

947 5b 9e 01 84 3f 1a 22 17 bb 04 35 98 cc 97 2c 68

948 cb 7b d9 52 92 92 0b 99 f3 e5 d1 ee 74 5a d0 2f

949 d9 b0 18 be b8 9a 74 76 9c 72 f5 93 13 39 65 f8

950 bb 96 0f 31 e4 1c 5c 47 46 13 16 48 7e 29 86 a9

951 23 80 b3 2e 9f 0a 57 76 21 f2 59 25 91 31 70 29

952 a1 7b d4 ac 8e c7 77 61 20 0f 25 19 6e 39 71 50

953 73 de 4c 66 84 79 84 cc 25 20 30 fc 43 3a c4 ed

954 00 00 00 15 00 f7 d9 ce 64

955 e8 1d ed a1 cc 54 6a 17 a5 41 01 72 7e c7 a2 cb

956 00 00 00 80

957 2e bd 80 83 78 71 33 7a ca 77 1c d5 53 0f 1f 5f

958 45 55 3d 73 be df e2 ab f2 11 9e d0 a7 3f ec dc

959 15 68 15 4b d4 64 3c 7d e9 c1 b9 6c a7 a4 05 1b

960 1f 4c 28 de 1d 70 90 1f fc 85 7e a9 f9 2f 1c 9e

961 ee 6b 20 19 54 b9 53 70 2f 0f db 21 8a 21 2b df

962 0b 7e a4 d8 2b f5 7e 83 a2 83 fb c9 b7 e3 0b c4

963 89 76 66 ab 2c 18 e7 89 f6 6e 4b 1c 87 53 6a be

964 3f ed 36 92 f3 13 14 b1 fd 74 b4 91 23 35 6a 28

965 00 00 00 80

966 6d 03 51 3d c5 44 b8 60 eb 11 c7 fe c6 0e 6d 80

967 07 a3 3d 70 4f 5e bf c9 b2 cf 82 86 4e b8 92 6d

968 e6 8a b3 18 8c 05 b2 59 8b 55 72 04 fe ee 13 98

969 c6 d6 11 8a 7f 0b c8 c2 ea 29 19 aa 6b 44 39 52

970 bb c3 7c 28 60 2e 1b ec 1b d6 12 6f f3 29 c3 9a

971 a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a

972 a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef

973 40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6

974 00 00 00 15 00 c9 1d f7 a7

975 8f 81 09 f5 69 1e 86 97 49 6a d3 c1 96 a0 96 d2

976 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69

977 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65

978"""),

979 public_key=rb"""ssh-dss AAAAB3NzaC1kc3MAAACBALsoBleoEY1UsFA+twxgCg1bngGEPxoiF7sENZjMlyxoy3vZUpKSC5nz5dHudFrQL9mwGL64mnR2nHL1kxM5Zfi7lg8x5BxcR0YTFkh+KYapI4CzLp8KV3Yh8lklkTFwKaF71KyOx3dhIA8lGW45cVBz3kxmhHmEzCUgMPxDOsTtAAAAFQD32c5k6B3tocxUahelQQFyfseiywAAAIAuvYCDeHEzesp3HNVTDx9fRVU9c77f4qvyEZ7Qpz/s3BVoFUvUZDx96cG5bKekBRsfTCjeHXCQH/yFfqn5Lxye7msgGVS5U3AvD9shiiEr3wt+pNgr9X6DooP7ybfjC8SJdmarLBjnifZuSxyHU2q+P+02kvMTFLH9dLSRIzVqKAAAAIBtA1E9xUS4YOsRx/7GDm2AB6M9cE9ev8myz4KGTriSbeaKsxiMBbJZi1VyBP7uE5jG1hGKfwvIwuopGaprRDlSu8N8KGAuG+wb1hJv8ynDmqbw+IdJp/CGRrP+17f7yEqiCqh7ux360IXToikmvmTvQAKI21Eaqyw0XwSWuXV59g== test key without passphrase

980""",

981 public_key_data=bytes.fromhex("""

982 00 00 00 07 73 73 68 2d 64 73 73

983 00 00 00 81 00

984 bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d

985 5b 9e 01 84 3f 1a 22 17 bb 04 35 98 cc 97 2c 68

986 cb 7b d9 52 92 92 0b 99 f3 e5 d1 ee 74 5a d0 2f

987 d9 b0 18 be b8 9a 74 76 9c 72 f5 93 13 39 65 f8

988 bb 96 0f 31 e4 1c 5c 47 46 13 16 48 7e 29 86 a9

989 23 80 b3 2e 9f 0a 57 76 21 f2 59 25 91 31 70 29

990 a1 7b d4 ac 8e c7 77 61 20 0f 25 19 6e 39 71 50

991 73 de 4c 66 84 79 84 cc 25 20 30 fc 43 3a c4 ed

992 00 00 00 15 00 f7 d9 ce 64

993 e8 1d ed a1 cc 54 6a 17 a5 41 01 72 7e c7 a2 cb

994 00 00 00 80

995 2e bd 80 83 78 71 33 7a ca 77 1c d5 53 0f 1f 5f

996 45 55 3d 73 be df e2 ab f2 11 9e d0 a7 3f ec dc

997 15 68 15 4b d4 64 3c 7d e9 c1 b9 6c a7 a4 05 1b

998 1f 4c 28 de 1d 70 90 1f fc 85 7e a9 f9 2f 1c 9e

999 ee 6b 20 19 54 b9 53 70 2f 0f db 21 8a 21 2b df

1000 0b 7e a4 d8 2b f5 7e 83 a2 83 fb c9 b7 e3 0b c4

1001 89 76 66 ab 2c 18 e7 89 f6 6e 4b 1c 87 53 6a be

1002 3f ed 36 92 f3 13 14 b1 fd 74 b4 91 23 35 6a 28

1003 00 00 00 80

1004 6d 03 51 3d c5 44 b8 60 eb 11 c7 fe c6 0e 6d 80

1005 07 a3 3d 70 4f 5e bf c9 b2 cf 82 86 4e b8 92 6d

1006 e6 8a b3 18 8c 05 b2 59 8b 55 72 04 fe ee 13 98

1007 c6 d6 11 8a 7f 0b c8 c2 ea 29 19 aa 6b 44 39 52

1008 bb c3 7c 28 60 2e 1b ec 1b d6 12 6f f3 29 c3 9a

1009 a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a

1010 a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef

1011 40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6

1012"""),

1013 expected_signature=None,

1014 derived_passphrase=None,

1015 ),

1016 'ecdsa256': SSHTestKey(

1017 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----

1018 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS

1019 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTLbU0zDwsk2Dvp+VYIrsNVf5gWwz2S

1020 3SZ8TbxiQRkpnGSVqyIoHJOJc+NQItAa7xlJ/8Z6gfz57Z3apUkaMJm6AAAAuKeY+YinmP

1021 mIAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5

1022 Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmb

1023 oAAAAhAKIl/3n0pKVIxpZkXTGtii782Qr4yIcvHdpxjO/QsIqKAAAAG3Rlc3Qga2V5IHdp

1024 dGhvdXQgcGFzc3BocmFzZQECAwQ=

1025-----END OPENSSH PRIVATE KEY-----

1026""",

1027 private_key_blob=bytes.fromhex("""

1028 00 00 00 13 65 63 64

1029 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36

1030 00 00 00 08 6e 69 73 74 70 32 35 36

1031 00 00 00 41 04

1032 cb 6d 4d 33 0f 0b 24 d8 3b e9 f9 56 08 ae c3 55

1033 7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c

1034 64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19

1035 49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba

1036 00 00 00 21 00

1037 a2 25 ff 79 f4 a4 a5 48 c6 96 64 5d 31 ad 8a 2e

1038 fc d9 0a f8 c8 87 2f 1d da 71 8c ef d0 b0 8a 8a

1039 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69

1040 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65

1041"""),

1042 public_key=rb"""ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmbo= test key without passphrase

1043""",

1044 public_key_data=bytes.fromhex("""

1045 00 00 00 13 65 63 64

1046 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36

1047 00 00 00 08 6e 69 73 74 70 32 35 36

1048 00 00 00 41 04

1049 cb 6d 4d 33 0f 0b 24 d8 3b e9 f9 56 08 ae c3 55

1050 7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c

1051 64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19

1052 49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba

1053"""),

1054 expected_signature=None,

1055 derived_passphrase=None,

1056 ),

1057 'ecdsa384': SSHTestKey(

1058 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----

1059 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS

1060 1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSgkOjkAvq7v5vHuj3KBL4/EAWcn5hZ

1061 DyKcbyV0eBMGFq7hKXQlZqIahLVqeMR0QqmkxNJ2rly2VHcXneq3vZ+9fIsWCOdYk5WP3N

1062 ZPzv911Xn7wbEkC7QndD5zKlm4pBUAAADomhj+IZoY/iEAAAATZWNkc2Etc2hhMi1uaXN0

1063 cDM4NAAAAAhuaXN0cDM4NAAAAGEEoJDo5AL6u7+bx7o9ygS+PxAFnJ+YWQ8inG8ldHgTBh

1064 au4Sl0JWaiGoS1anjEdEKppMTSdq5ctlR3F53qt72fvXyLFgjnWJOVj9zWT87/ddV5+8Gx

1065 JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B

1066 2OdqVOVRkwZWRROz0AAAAbdGVzdCBrZXkgd2l0aG91dCBwYXNzcGhyYXNlAQIDBA==

1067-----END OPENSSH PRIVATE KEY-----

1068""",

1069 private_key_blob=bytes.fromhex("""

1070 00 00 00 13 65 63 64

1071 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34

1072 00 00 00 08 6e 69 73 74 70 33 38 34

1073 00 00 00 61 04

1074 a0 90 e8 e4 02 fa bb bf 9b c7 ba 3d ca 04 be 3f

1075 10 05 9c 9f 98 59 0f 22 9c 6f 25 74 78 13 06 16

1076 ae e1 29 74 25 66 a2 1a 84 b5 6a 78 c4 74 42 a9

1077 a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd

1078 7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5

1079 79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15

1080 00 00 00 31 00

1081 f9 b1 3c bc a7 e0 75 72 7f c3 84 e9 97 aa eb a2

1082 d4 17 31 bc 04 aa cf 37 af 90 19 5b 42 80 5a ef

1083 7c b4 e0 1d 8e 76 a5 4e 55 19 30 65 64 51 3b 3d

1084 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69

1085 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65

1086"""),

1087 public_key=rb"""ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKCQ6OQC+ru/m8e6PcoEvj8QBZyfmFkPIpxvJXR4EwYWruEpdCVmohqEtWp4xHRCqaTE0nauXLZUdxed6re9n718ixYI51iTlY/c1k/O/3XVefvBsSQLtCd0PnMqWbikFQ== test key without passphrase

1088""",

1089 public_key_data=bytes.fromhex("""

1090 00 00 00 13 65 63 64

1091 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34

1092 00 00 00 08 6e 69 73 74 70 33 38 34

1093 00 00 00 61 04

1094 a0 90 e8 e4 02 fa bb bf 9b c7 ba 3d ca 04 be 3f

1095 10 05 9c 9f 98 59 0f 22 9c 6f 25 74 78 13 06 16

1096 ae e1 29 74 25 66 a2 1a 84 b5 6a 78 c4 74 42 a9

1097 a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd

1098 7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5

1099 79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15

1100"""),

1101 expected_signature=None,

1102 derived_passphrase=None,

1103 ),

1104 'ecdsa521': SSHTestKey(

1105 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----

1106 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS

1107 1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQASVOdwDznmlcGqiLvFtYeVtrAEiVz

1108 iIfsL7jEM8Utu/m8WSkPFQtjwqdFw+WfZ0mi6qMbEFgi/ELzZSKVteCSbcMAhqAkOMFKiD

1109 u4bxvsM6bT02Ru7q2yT41ySyGhUD0QySBnI6Ckt/wnQ1TEpj8zDKiRErxs9e6QLGElNRkz

1110 LPMs+mMAAAEY2FXeh9hV3ocAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ

1111 AAAIUEAElTncA855pXBqoi7xbWHlbawBIlc4iH7C+4xDPFLbv5vFkpDxULY8KnRcPln2dJ

1112 ouqjGxBYIvxC82UilbXgkm3DAIagJDjBSog7uG8b7DOm09Nkbu6tsk+NckshoVA9EMkgZy

1113 OgpLf8J0NUxKY/MwyokRK8bPXukCxhJTUZMyzzLPpjAAAAQSFqUmKK7lGQzxT6GKZSLDju

1114 U3otwLYnuj+/5AdzuB/zotu95UdFv9I2DNXzd9E4WAyz6IqBBNcsMkxrzHAdqsYDAAAAG3

1115 Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQ==

1116-----END OPENSSH PRIVATE KEY-----

1117""",

1118 private_key_blob=bytes.fromhex("""

1119 00 00 00 13 65 63 64

1120 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31

1121 00 00 00 08 6e 69 73 74 70 35 32 31

1122 00 00 00 85 04 00 49 53 9d

1123 c0 3c e7 9a 57 06 aa 22 ef 16 d6 1e 56 da c0 12

1124 25 73 88 87 ec 2f b8 c4 33 c5 2d bb f9 bc 59 29

1125 0f 15 0b 63 c2 a7 45 c3 e5 9f 67 49 a2 ea a3 1b

1126 10 58 22 fc 42 f3 65 22 95 b5 e0 92 6d c3 00 86

1127 a0 24 38 c1 4a 88 3b b8 6f 1b ec 33 a6 d3 d3 64

1128 6e ee ad b2 4f 8d 72 4b 21 a1 50 3d 10 c9 20 67

1129 23 a0 a4 b7 fc 27 43 54 c4 a6 3f 33 0c a8 91 12

1130 bc 6c f5 ee 90 2c 61 25 35 19 33 2c f3 2c fa 63

1131 00 00 00 41 21

1132 6a 52 62 8a ee 51 90 cf 14 fa 18 a6 52 2c 38 ee

1133 53 7a 2d c0 b6 27 ba 3f bf e4 07 73 b8 1f f3 a2

1134 db bd e5 47 45 bf d2 36 0c d5 f3 77 d1 38 58 0c

1135 b3 e8 8a 81 04 d7 2c 32 4c 6b cc 70 1d aa c6 03

1136 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69

1137 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65

1138"""),

1139 public_key=rb"""ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBABJU53APOeaVwaqIu8W1h5W2sASJXOIh+wvuMQzxS27+bxZKQ8VC2PCp0XD5Z9nSaLqoxsQWCL8QvNlIpW14JJtwwCGoCQ4wUqIO7hvG+wzptPTZG7urbJPjXJLIaFQPRDJIGcjoKS3/CdDVMSmPzMMqJESvGz17pAsYSU1GTMs8yz6Yw== test key without passphrase

1140""",

1141 public_key_data=bytes.fromhex("""

1142 00 00 00 13 65 63 64

1143 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36

1144 00 00 00 08 6e 69 73 74 70 35 32 31

1145 00 00 00 85 04 00 49 53 9d

1146 c0 3c e7 9a 57 06 aa 22 ef 16 d6 1e 56 da c0 12

1147 25 73 88 87 ec 2f b8 c4 33 c5 2d bb f9 bc 59 29

1148 0f 15 0b 63 c2 a7 45 c3 e5 9f 67 49 a2 ea a3 1b

1149 10 58 22 fc 42 f3 65 22 95 b5 e0 92 6d c3 00 86

1150 a0 24 38 c1 4a 88 3b b8 6f 1b ec 33 a6 d3 d3 64

1151 6e ee ad b2 4f 8d 72 4b 21 a1 50 3d 10 c9 20 67

1152 23 a0 a4 b7 fc 27 43 54 c4 a6 3f 33 0c a8 91 12

1153 bc 6c f5 ee 90 2c 61 25 35 19 33 2c f3 2c fa 63

1154"""),

1155 expected_signature=None,

1156 derived_passphrase=None,

1157 ),

1158}

1159"""The master list of SSH test keys.""" (empty)

1160 SUPPORTED_KEYS: Mapping[str, SSHTestKey] = { (empty)

1161 k: v for k, v in ALL_KEYS.items() if v.is_suitable()

1162}

1163"""The subset of SSH test keys suitable for use with vault.""" (empty)

1164 UNSUITABLE_KEYS: Mapping[str, SSHTestKey] = { (empty)

1165 k: v for k, v in ALL_KEYS.items() if not v.is_suitable()

1166}

1167"""The subset of SSH test keys not suitable for use with vault.""" (empty)

1168

1169 DUMMY_SERVICE = 'service1' (empty)

1170"""A standard/sample service name.""" (empty)

1171 DUMMY_PASSPHRASE = 'my secret passphrase' (empty)

1172"""A standard/sample passphrase.""" (empty)

1173 DUMMY_KEY1 = SUPPORTED_KEYS['ed25519'].public_key_data (empty)

1174"""A sample universally supported SSH test key (in wire format).""" (empty)

1175 DUMMY_KEY1_B64 = base64.standard_b64encode(DUMMY_KEY1).decode('ASCII') (empty)

1176""" (empty)

1177 A sample universally supported SSH test key (in `authorized_keys` format).

1178"""

1179 DUMMY_KEY2 = SUPPORTED_KEYS['rsa'].public_key_data (empty)

1180"""A second supported SSH test key (in wire format).""" (empty)

1181 DUMMY_KEY2_B64 = base64.standard_b64encode(DUMMY_KEY2).decode('ASCII') (empty)

1182"""A second supported SSH test key (in `authorized_keys` format).""" (empty)

1183 DUMMY_KEY3 = SUPPORTED_KEYS['ed448'].public_key_data (empty)

1184"""A third supported SSH test key (in wire format).""" (empty)

1185 DUMMY_KEY3_B64 = base64.standard_b64encode(DUMMY_KEY3).decode('ASCII') (empty)

1186"""A third supported SSH test key (in `authorized_keys` format).""" (empty)

1187 DUMMY_CONFIG_SETTINGS = { (empty)

1188 'length': 10,

1189 'upper': 1,

1190 'lower': 1,

1191 'repeat': 5,

1192 'number': 1,

1193 'space': 1,

1194 'dash': 1,

1195 'symbol': 1,

1196}

1197"""Sample vault settings.""" (empty)

1198 DUMMY_RESULT_PASSPHRASE = b'.2V_QJkd o' (empty)

1199""" (empty)

1200 The passphrase derived from [`DUMMY_SERVICE`][] using [`DUMMY_PASSPHRASE`][].

1201"""

1202 DUMMY_RESULT_KEY1 = b'E<b<{ -7iG' (empty)

1203""" (empty)

1204 The passphrase derived from [`DUMMY_SERVICE`][] using [`DUMMY_KEY1`][].

1205"""

1206 DUMMY_PHRASE_FROM_KEY1_RAW = ( (empty)

1207 b'\x00\x00\x00\x0bssh-ed25519'

1208 b'\x00\x00\x00@\xf0\x98\x19\x80l\x1a\x97\xd5&\x03n'

1209 b'\xcc\xe3e\x8f\x86f\x07\x13\x19\x13\t!33\xf9\xe46S'

1210 b'\x1d\xaf\xfd\r\x08\x1f\xec\xf8s\x9b\x8c_U9\x16|ST,'

1211 b'\x1eR\xbb0\xed\x7f\x89\xe2/iQU\xd8\x9e\xa6\x02'

1212)

1213""" (empty)

1214 The "equivalent master passphrase" derived from [`DUMMY_KEY1`][] (raw format).

1215"""

1216 DUMMY_PHRASE_FROM_KEY1 = b'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==' (empty)

1217""" (empty)

1218 The "equivalent master passphrase" derived from [`DUMMY_KEY1`][] (in base64).

1219"""

1220

1221 VAULT_MASTER_KEY = 'vault key' (empty)

1222""" (empty)

1223 The storage passphrase used to encrypt all sample vault native configurations.

1224"""

1225 VAULT_V02_CONFIG = 'P7xeh5y4jmjpJ2pFq4KUcTVoaE9ZOEkwWmpVTURSSWQxbGt6emN4aFE4eFM3anVPbDRNTGpOLzY3eDF5aE1YTm5LNWh5Q1BwWTMwM3M5S083MWRWRFlmOXNqSFJNcStGMWFOS3c2emhiOUNNenZYTmNNMnZxaUErdlRoOGF2ZHdGT1ZLNTNLOVJQcU9jWmJrR3g5N09VcVBRZ0ZnSFNUQy9HdFVWWnFteVhRVkY3MHNBdnF2ZWFEbFBseWRGelE1c3BFTnVUckRQdWJSL29wNjFxd2Y2ZVpob3VyVzRod3FKTElTenJ1WTZacTJFOFBtK3BnVzh0QWVxcWtyWFdXOXYyenNQeFNZbWt1MDU2Vm1kVGtISWIxWTBpcWRFbyswUVJudVVhZkVlNVpGWDA4WUQ2Q2JTWW81SnlhQ2Zxa3cxNmZoQjJES0Uyd29rNXpSck5iWVBrVmEwOXFya1NpMi9saU5LL3F0M3N3MjZKekNCem9ER2svWkZ0SUJLdmlHRno0VlQzQ3pqZTBWcTM3YmRiNmJjTkhqUHZoQ0NxMW1ldW1XOFVVK3pQMEtUMkRMVGNvNHFlOG40ck5KcGhsYXg1b1VzZ1NYU1B2T3RXdEkwYzg4NWE3YWUzOWI1MDI0MThhMWZjODQ3MDA2OTJmNDQ0MDkxNGFiNmRlMGQ2YjZiNjI5NGMwN2IwMmI4MGZi' (empty)

1226""" (empty)

1227 A sample vault native configuration, in v0.2 format, encoded in base64

1228 and encrypted with [`VAULT_MASTER_KEY`][].

1229"""

1230 VAULT_V02_CONFIG_DATA = { (empty)

1231 'global': {

1232 'phrase': DUMMY_PASSPHRASE.rstrip('\n'),

1233 },

1234 'services': {

1235 '(meta)': {

1236 'notes': 'This config was originally in v0.2 format.',

1237 },

1238 DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),

1239 },

1240}

1241""" (empty)

1242 The plaintext contents (a vault native configuration) stored in

1243[`VAULT_V02_CONFIG`][].

1244"""

1245 VAULT_V03_CONFIG = 'sBPBrr8BFHPxSJkV/A53zk9zwDQHFxLe6UIusCVvzFQre103pcj5xxmE11lMTA0U2QTYjkhRXKkH5WegSmYpAnzReuRsYZlWWp6N4kkubf+twZ9C3EeggPm7as2Af4TICHVbX4uXpIHeQJf9y1OtqrO+SRBrgPBzgItoxsIxebxVKgyvh1CZQOSkn7BIzt9xKhDng3ubS4hQ91fB0QCumlldTbUl8tj4Xs5JbvsSlUMxRlVzZ0OgAOrSsoWELXmsp6zXFa9K6wIuZa4wQuMLQFHiA64JO1CR3I+rviWCeMlbTOuJNx6vMB5zotKJqA2hIUpN467TQ9vI4g/QTo40m5LT2EQKbIdTvBQAzcV4lOcpr5Lqt4LHED5mKvm/4YfpuuT3I3XCdWfdG5SB7ciiB4Go+xQdddy3zZMiwm1fEwIB8XjFf2cxoJdccLQ2yxf+9diedBP04EsMHrvxKDhQ7/vHl7xF2MMFTDKl3WFd23vvcjpR1JgNAKYprG/e1p/7' (empty)

1246""" (empty)

1247 A sample vault native configuration, in v0.3 format, encoded in base64

1248 and encrypted with [`VAULT_MASTER_KEY`][].

1249"""

1250 VAULT_V03_CONFIG_DATA = { (empty)

1251 'global': {

1252 'phrase': DUMMY_PASSPHRASE.rstrip('\n'),

1253 },

1254 'services': {

1255 '(meta)': {

1256 'notes': 'This config was originally in v0.3 format.',

1257 },

1258 DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),

1259 },

1260}

1261""" (empty)

1262 The plaintext contents (a vault native configuration) stored in

1263[`VAULT_V03_CONFIG`][].

1264"""

1265 VAULT_STOREROOM_CONFIG_ZIPPED = b""" (empty)

1266 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

1316"""

1317""" (empty)

1318 A sample vault native configuration, in storeroom format, encrypted with

1319[`VAULT_MASTER_KEY`][]. The configuration is compressed (zip archive)

1320 and then encoded in base64.

1321"""

1322 VAULT_STOREROOM_CONFIG_DATA = { (empty)

1323 'global': {

1324 'phrase': DUMMY_PASSPHRASE.rstrip('\n'),

1325 },

1326 'services': {

1327 '(meta)': {

1328 'notes': 'This config was originally in storeroom format.',

1329 },

1330 DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),

1331 },

1332}

1333""" (empty)

1334 The parsed vault configuration stored in

1335[`VAULT_STOREROOM_CONFIG_ZIPPED`][].

1336"""

1337

1338 VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED_JAVASCRIPT_SOURCE = """ (empty)

1339// Executed in the top-level directory of the vault project code, in Node.js.

1340 const storeroom = require('storeroom')

1341 const Store = require('./lib/store.js')

1342 let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')

1343 await store._storeroom.put('/services/array/', ['entry1','entry2'])

1344// The resulting "broken-dir" was then zipped manually.

1345"""

1346""" (empty)

1347 The JavaScript source for the script that generated the storeroom

1348 archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED`][].

1349"""

1350 VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED = b""" (empty)

1351 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

1385"""

1386""" (empty)

1387 A sample corrupted storeroom archive, encrypted with

1388[`VAULT_MASTER_KEY`][]. The configuration is compressed (zip archive)

1389 and then encoded in base64.

1390

1391 The archive contains a directory `/services/array/` that claims to have

1392 two child items 'entry1' and 'entry2', but no such child items are

1393 present in the archive. See

1394[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED_JAVASCRIPT_SOURCE`][] for

1395 the exact script that created this archive.

1396"""

1397

1398 VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2_JAVASCRIPT_SOURCE = """ (empty)

1399// Executed in the top-level directory of the vault project code, in Node.js.

1400 const storeroom = require('storeroom')

1401 const Store = require('./lib/store.js')

1402 let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')

1403 await store._storeroom.put('/services/array/', 'not a directory index')

1404// The resulting "broken-dir" was then zipped manually.

1405"""

1406""" (empty)

1407 The JavaScript source for the script that generated the storeroom

1408 archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2`][].

1409"""

1410 VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2 = b""" (empty)

1411 UEsDBAoAAAAAAM6NSVmrcHdV5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV3ZS9LZkJp

1412 L0V0OUcrZmxYM3gxaFU4ZjE4YlE3S253bHoxN0IxSDE3cUhVOGdWK2RpWWY5MTdFZ0YrSStidEpZ

1413 VXBzWVZVck45OC9uLzdsZnl2NUdGVEg2NWZxVy93YjlOc2MxeEZ4ck43Q3p4eTZ5MVAxZzFPb2VK

1414 b0RZU3J6YXlwT0E2M3pidmk0ZTRiREMyNXhPTXl5NHBoMDFGeGdnQmpSNnpUcmR2UDk2UlZQd0I5

1415 WitOZkZWZUlXT1NQN254ZFNYMGdFbkZ4SDBmWDkzNTFaTTZnPVBLAwQKAAAAAADOjUlZJg3/BhcC

1416 AAAXAgAAAgAAADBieyJ2ZXJzaW9uIjoxfQpBVXJJMjNDQ2VpcW14cUZRMlV4SUpBaUoxNEtyUzh2

1417 SXpIa2xROURBaFRlVHNFMmxPVUg4WUhTcUk1cXRGSHBqY3c1WkRkZmRtUlEwQXVGRjllY3lkam14

1418 dDdUemRYLzNmNFUvTGlVV2dLRmQ1K1FEN3BlVlE1bWpqeHNlUEpHTDlhTWlKaGxSUVB4SmtUbjBx

1419 U2poM1RUT0ZZbVAzV0JkdlUyWnF2RzhaSDk2cU1WcnZsQ0dMRmZTc2svVXlvcHZKdENONUVXcTRZ

1420 SDUwNFNiejFIUVhWd2RjejlrS1BuR3J6SVA4ZmZtZnhXQ0U0TmtLb0ZPQXZuNkZvS3FZdGlGbFE9

1421 PQpBVXBMUVMrMG9VeEZTeCtxbTB3SUtyM1MvTVJxYWJJTFlEUnY0aHlBMVE2TGR2Nlk0UmJ0enVz

1422 NzRBc0cxbVhhenlRU2hlZVowdk0xM2ZyTFA4YlV0VHBaRyszNXF1eUhLM2NaWVJRZUxKM0JzejZz

1423 b0xaQjNZTkpNenFxTTQrdzM1U0FZZ2lMU1NkN05NeWVrTHNhRUIzRDFOajlTRk85K3NGNEpFMWVL

1424 UXpNMkltNk9qOUNVQjZUSTV3UitibksxN1BnY2RaeTZUMVRMWElVREVxcDg4dWdsWmRFTVcrNU9k

1425 aE5ZbXEzZERWVWV4UnJpM1AwUmVBSi9KMGdJNkNoUUE9PVBLAwQKAAAAAADOjUlZTNfdphcCAAAX

1426 AgAAAgAAADBmeyJ2ZXJzaW9uIjoxfQpBWVJqOVpIUktGUEVKOHM2YVY2TkRoTk5jQlZ5cGVYUmdz

1427 cnBldFQ0cGhJRGROWFdGYzRia0daYkJxMngwRDFkcVNjYWk5UzEveDZ2K28zRE0rVEF2OVE3ZFVR

1428 QWVKR3RmRkhJZDZxWW0ybEdNSnF5WTRNWm14aE9YdXliend0V3Q4Mnhvb041QTZNcWpINmxKQllD

1429 UUN3ZEJjb3RER0EwRnlnVTEzeHV2WnIzT1puZnFFRGRqbzMxNkw5aExDN1RxMTYwUHpBOXJOSDMz

1430 ZkNBcUhIVXZiYlFQQWErekw1d3dEN3FlWkY2MHdJaEwvRmk5L3JhNGJDcHZRNC9ORWpRd3c9PQpB

1431 WWNGUDB1Y2xMMHh3ZDM2UXZXbm4wWXFsOU5WV0s3c05CMTdjdmM3N3VDZ0J2OE9XYkR5UHk5d05h

1432 R2NQQzdzcVdZdHpZRlBHR0taVjhVUzA1YTVsV1BabDNGVFNuQXNtekxPelBlcFZxaitleDU3aEsx

1433 QnV1bHkrUCtYQkE0YUtsaDM3c0RJL3I0UE1BVlJuMDNoSDJ5dEhDMW9PbjF0V1M5Q1NLV1pSMThh

1434 djdTT0RBMVBNRnFYTmZKZVNTaVJiQ2htbDdOcFVLbjlXSGJZandybDlqN0JSdy9kWjhNQldCb3Ns

1435 Nlc1dGZtdnJMVHhGRFBXYUgzSUp0T0czMEI1M3c9PVBLAwQKAAAAAADOjUlZn9rNID8CAAA/AgAA

1436 AgAAADFkeyJ2ZXJzaW9uIjoxfQpBYWFBb3lqaGljVDZ4eXh1c0U0RVlDZCtxbE81Z0dEYTBNSFVS

1437 MmgrSW9QMHV4UkY3b1BRS2czOHlQUEN3Ny9MYVJLQ0dQZ0RyZ2RpTWJTeUwzZ3ZNMFhseVpVMVBW

1438 QVJvNEFETU9lbXgrOWhtS0hjQWNKMG5EeW5oSkhGYTYyb2xyQUNxekZzblhKNVBSeEVTVzVEbUh0

1439 Ui9nRm5Wa1FvalhyVW4ybmpYMjVVanZQaXhlMU96Y0daMmQ0MjdVTGdnY1hqMkhSdjJiZldDNDUw

1440 SGFXS3FDckZlYWlrQ2xkUUM2WGV3SkxZUjdvQUY3UjVha2ttK3M2MXNCRTVCaTg0QmJLWHluc1NG

1441 ejE0TXFrd2JMK1VMYVk9CkFUT3dqTUFpa3Q4My9NTW5KRXQ2b3EyNFN4KzJKNDc2K2gyTmEzbHUr

1442 MDg0cjlBT25aaUk0TmlYV0N1Q0lzakEzcTBwUHFJS1VXZHlPQW9uM2VHY0huZUppWUtVYllBaUJI

1443 MVNmbnhQQkMzZkFMRklybkQ4Y0VqeGpPcUFUaTQ5dE1mRmtib0dNQ3dEdFY0V3NJL0tLUlRCOFd1

1444 MnNXK2J0V3QzVWlvZG9ZeUVLTDk3ekNNemZqdGptejF4SDhHTXY5WDVnaG9NSW5RQVNvYlRreVZ4

1445 dWo5YnlDazdNbU0vK21ZL3AwZE9oYVY0Nncwcm04UGlvWEtzdzR4bXB3ditDWC9PRXV3Uy9meDJT

1446 Y0lOQnNuYVRiWT1QSwECHgMKAAAAAADOjUlZq3B3VeYAAADmAAAABQAAAAAAAAAAAAAApIEAAAAA

1447 LmtleXNQSwECHgMKAAAAAADOjUlZJg3/BhcCAAAXAgAAAgAAAAAAAAAAAAAApIEJAQAAMGJQSwEC

1448 HgMKAAAAAADOjUlZTNfdphcCAAAXAgAAAgAAAAAAAAAAAAAApIFAAwAAMGZQSwECHgMKAAAAAADO

1449 jUlZn9rNID8CAAA/AgAAAgAAAAAAAAAAAAAApIF3BQAAMWRQSwUGAAAAAAQABADDAAAA1gcAAAAA

1450"""

1451""" (empty)

1452 A sample corrupted storeroom archive, encrypted with

1453[`VAULT_MASTER_KEY`][]. The configuration is compressed (zip archive)

1454 and then encoded in base64.

1455

1456 The archive contains a directory `/services/array/` whose list of child

1457 items does not adhere to the serialization format. See

1458[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2_JAVASCRIPT_SOURCE`][] for

1459 the exact script that created this archive.

1460"""

1461

1462 VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3_JAVASCRIPT_SOURCE = """ (empty)

1463// Executed in the top-level directory of the vault project code, in Node.js.

1464 const storeroom = require('storeroom')

1465 const Store = require('./lib/store.js')

1466 let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')

1467 await store._storeroom.put('/services/array/', [null, 1, true, [], {}])

1468// The resulting "broken-dir" was then zipped manually.

1469"""

1470""" (empty)

1471 The JavaScript source for the script that generated the storeroom

1472 archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3`][].

1473"""

1474 VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3 = b""" (empty)

1475 UEsDBAoAAAAAAEOPSVnVlcff5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV4dVBHUDBi

1476 YkxrUVdvWnV5ZUJQRy8xdmM2MCt6MThOa3BsS09ydFAvUTVnQmxkYVpIOG10dTE5VWZFNGdGRGRj

1477 eHJtWUd4eXZDZFNqcVlOaDh4cTlzM3VydkdRTWFwcnhtdlZGZUxoSW4zZnVlTDAweEk0ZmlLenZN

1478 MmthUlRsNWNORGh3eUNlWVk4dzhBcXNhYjNyVWVsOEE0eVQ0cHU2d2tmQ3dTWUdqeG5HR29EcWJK

1479 VnVJVWNpZVBEcU9PTzU2b0MyMG9lT01adFVkTUtxV28zYnFZPVBLAwQKAAAAAABDj0lZ77OVHxcC

1480 AAAXAgAAAgAAADBjeyJ2ZXJzaW9uIjoxfQpBZllFQVVobEkyU2lZeGlrdWh0RzRNbUN3L1V2THBN

1481 VVhwVlB0NlRwdzRyNGdocVJhbGZWZ0hxUHFtbTczSnltdFFrNnZnR2JRdUpiQmVlYjYwOHNrMGk4

1482 ZFJVZjNwdlc2SnUyejljQkdwOG5mTFpTdlNad1lLN09UK2gzSDNDcmoxbXNicEZUcHVldW81NXc1

1483 dGdYMnBuWXNWTVcrczdjaHEyMUIya2lIVEZrdGt1MXlaRzhPYkVUQjNCOFNGODVVbi9CUjFEMHJ1

1484 ME9zOWl4ZWM2VmNTMitTZndtNnNtSlk2ZW9ZNTJzOGJNRGdYMndjQ0srREdkOEo2VWp0NG5OQVE9

1485 PQpBUWlPRnRZcmJybWUycEwxRFpGT1BjU0RHOUN2cVkvbHhTWGIwaVJUdmtIWFc2bEtHL0p4RUtU

1486 d3RTc0RTeDhsMTUvaHRmbWpOQ2tuTzhLVEFoKzhRQm5FbjZ0a2x5Y3BmeEIrTUxLRjFCM1Q1bjcv

1487 T0VUMExMdmgxU2k1bnRRNXhTUHZZNWtXeUMyZjhXUXFZb3FSNU5JVENMeDV6dWNsQ3dGb2kvVXc4

1488 OWNNWjM1MHBSbThzUktJbjJFeDUrQ1JwS3ZHdnBHbFJaTmk5VHZmVkNic1FCalR3MC9aeklTdzVQ

1489 NW9BVWE2U1ExUVFnNHg4VUNkY0s2QUNLaFluY0d4TVE9PVBLAwQKAAAAAABDj0lZGk9LVj8CAAA/

1490 AgAAAgAAADE0eyJ2ZXJzaW9uIjoxfQpBY1g2NVpMUWk4ck9pUlIyWGEwQlFHQVhQVWF2aHNJVGVY

1491 c2dzRk9OUmFTRzJCQlg0SGxJRHpwRUd5aDUrZ2czZVRwWDFNOERua3pMeTVzcWRkMFpmK3padTgz

1492 Qm52Y1JPREVIVDllUW91YUtPTWltdlRYanNuSXAxUHo5VGY1TlRkRjNJVTd2V1lhUDg4WTI5NG1i

1493 c1VVL2RKVTZqZ3ZDbUw2cE1VZ28xUU12bGJnaVp3cDV1RDFQZXlrSXdKVWdJSEgxTEpnYi9xU2tW

1494 c25leW1XY1RXR0NobzRvZGx3S2hJWmFCelhvNFhlN2U1V2I2VHA3Rkk5VUpVcmZIRTAvcVdrZUZE

1495 VmxlazY3cUx3ZFZXcU9DdFk9CkFhSGR0QjhydmQ0U3N4ZmJ5eU1OOHIzZEoxeHA5NmFIRTQvalNi

1496 Z05hZWttaDkyb2ROM1F4MUlqYXZsYVkxeEt1eFF3KzlwTHFIcTF5a1JSRjQzL2RVWGFIRk5UU0NX

1497 OVFsdmd3KzMwa1ZhSEdXRllvbFRnRWE4djQ3b3VrbGlmc01PZGM0YVNKb2R4ZUFJcVc3Q1cwdDVR

1498 b2RUbWREUXpqc3phZkQ4R2VOd2NFQjdGMHI2RzNoZEJlQndxd3Z6eENVYnpSUmU5bEQ3NjQ3RFp1

1499 bEo1U3c4amlvV0paTW40NlZhV3BYUXk4UnNva3hHaW00WUpybUZIQ2JkVU9qSWJsUmQ1Z3VhUDNU

1500 M0NxeHRPdC94b1BhOD1QSwMECgAAAAAAQ49JWVJM8QYXAgAAFwIAAAIAAAAxNnsidmVyc2lvbiI6

1501 MX0KQVlCWDF6M21qUlQrand4M2FyNkFpemxnalJZbUM0ZHg5NkxVQVBTVHNMWXJKVHFtWnd5N0Jy

1502 OFlCcElVamorMHdlT3lNaUtLVnFwaER3RXExNWFqUmlSZUVEQURTVHZwWmlLZUlnZjR5elUzZXNP

1503 eDJ2U2J1bXhTK0swUGZVa2tsSy9TRmRiU3EvUHFMRjBDRTVCMXNyKzJLYTB2WlJmak94R3VFeFRD

1504 RXozN0ZlWDNNR3NCNkhZVHEzaUJWcUR6NVB6eHpCWWM5Kyt6RitLS1RnMVp2NGRtRmVQTC9JSEY5

1505 WnV6TWlqRXdCRkE3WnJ0dkRqd3ZYcWtsMVpsR0c4eUV3PT0KQVhUWkRLVnNleldpR1RMUVZqa2hX

1506 bXBnK05MYlM0M2MxZEpvK2xGcC9yWUJYZkw3Wll5cGdjWE5IWXNzd01nc2VSSTAzNmt6bGZkdGNa

1507 bTdiUUN6M2JuQmZ6ZlorZFFuT2Y5STVSU2l0QzB2UmsydkQrOFdwbmRPSzNucGY5S0VpWklOSzVq

1508 TEZGTTJDTkNmQzBabXNRUlF3T0k2N3l5ZHhjVnFDMXBnWHV6QXRXamlsSUpnN0p6eUtsY3BJUGJu

1509 SUc0UzRSUlhIdW1wZnpoeWFZWkd6T0FDamRSYTZIMWJxYkJkZXFaSHMvQXJvM25mVjdlbjhxSUE5

1510 aVUrbnNweXFnPT1QSwECHgMKAAAAAABDj0lZ1ZXH3+YAAADmAAAABQAAAAAAAAAAAAAApIEAAAAA

1511 LmtleXNQSwECHgMKAAAAAABDj0lZ77OVHxcCAAAXAgAAAgAAAAAAAAAAAAAApIEJAQAAMGNQSwEC

1512 HgMKAAAAAABDj0lZGk9LVj8CAAA/AgAAAgAAAAAAAAAAAAAApIFAAwAAMTRQSwECHgMKAAAAAABD

1513 j0lZUkzxBhcCAAAXAgAAAgAAAAAAAAAAAAAApIGfBQAAMTZQSwUGAAAAAAQABADDAAAA1gcAAAAA

1514"""

1515""" (empty)

1516 A sample corrupted storeroom archive, encrypted with

1517[`VAULT_MASTER_KEY`][]. The configuration is compressed (zip archive)

1518 and then encoded in base64.

1519

1520 The archive contains a directory `/services/array/` whose list of child

1521 items are not all valid item names. See

1522[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3_JAVASCRIPT_SOURCE`][] for

1523 the exact script that created this archive.

1524"""

1525

1526 VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4_JAVASCRIPT_SOURCE = """ (empty)

1527// Executed in the top-level directory of the vault project code, in Node.js.

1528 const storeroom = require('storeroom')

1529 const Store = require('./lib/store.js')

1530 let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')

1531 await store._storeroom.put('/dir/subdir/', [])

1532 await store._storeroom.put('/dir/', [])

1533// The resulting "broken-dir" was then zipped manually.

1534"""

1535""" (empty)

1536 The JavaScript source for the script that generated the storeroom

1537 archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4`][].

1538"""

1539 VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4 = b""" (empty)

1540 UEsDBAoAAAAAAE+5SVloORS+5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV6dWRoNkRQ

1541 YTlNSWFabHZ5TytVYTFuamhjV2hIaTFBU0lKYW5zcXBxVlA0blN2V0twUzdZOUc2bjFSbi8vUnVM

1542 VitwcHp5SC9RQk83R0hFenNVMzdCUzFwUmVVeGhxUVlVTE56OXZvQ0crM1ZaL3VncU44dDJiU05m

1543 Nyt5K3hiNng2aVlFUmNZYTJ0UkhzZVdIc0laTE9ha2lDb0lRVGV3cndwYjVMM2pnd0E3SXBzaDkz

1544 QkxHSzM5dXNYNmo0R0I2WkRUeW5JcGk4V3JkbDhnWVZCN0tVPVBLAwQKAAAAAABPuUlZ663uUhcC

1545 AAAXAgAAAgAAADAzeyJ2ZXJzaW9uIjoxfQpBV2wzS2gzd21ZSFVZZU1RR3BLSVowdVd1VXFna09h

1546 YmRjNzNYYXVsZTNtVS9sN2Zvd1AyS21jbFp3ZDM5V3lYVzRTcEw4R0l4YStDZW51S3V0Wm5nb0FR

1547 bWlnaUJUbkFaais5TENCcGNIWlZNY2RBVkgxKzBFNGpsanZ1UkVwZ0tPS05LZjRsTUl1QnZ4VmFB

1548 ZkdwNHJYNEZ4MmpPSlk1Y3NQZzBBRFBoZVAwN29GWVQ3alorSUNEK1AxNGZPdWpwMGRUeDRrTDIy

1549 LzlqalRDNXBCNVF5NW5iOUx3Zk5DUWViSUVpaTZpbU0vRmFrK1dtV05tMndqMERSTEc4RHY3ZkE9

1550 PQpBU0c3NTNGTVVwWmxjK3E1YXRzcC93OUNqN2JPOFlpY24wZHg2UGloTmwzUS9WSjVVeGJmU3l0

1551 ZDFDNDBRU2xXeTJqOTJDWUd3VER6eEdBMXVnb0FCYi9kTllTelVwbHJFb3BuUVphYXdsdTVwV2x0

1552 Y1E5WTcveWN4S2E4b0JaaGY3RkFYcGo2c01wUW9zNzI5VFVabFd4UmI4VFRtN2FrVnR1OXcvYXlK

1553 RS9reDh4ZUYxSGJlc3Q4N1IxTGg2ODd3dS9XVUN2ZjNXYXo1VjNnZWY0RnpUTXg0bkpqSlZOd0U0

1554 SzAxUTlaVzQ0bmVvbExPUVI1MkZDeDZvbml3RW9tenc9PVBLAwQKAAAAAABPuUlZRXky4CsCAAAr

1555 AgAAAgAAADEweyJ2ZXJzaW9uIjoxfQpBWmlYWVlvNUdCY2d5dkFRaGtyK2ZjUkdVSkdabDd2dE5w

1556 T2Mrd1VzbXJhQWhRN3dKdlYraGhKcTlrcWNKQnBWU0gyUTBTTVVhb29iNjBJM1NYNUNtTkJRU2FH

1557 M3prd0Y0T2F4TnpCZUh0NFlpaDd4Y3p2ak4xR0hISDJQYW0xam05K09ja3JLVmNMVURtNXRKb2ZC

1558 Z1E4Q2NwMGZMVkdEaURjNWF0MjVMc2piQVcvNkZFSnJ5VVBHWis4UVdYRmlWMGdtVVZybVc3VUFy

1559 dGhJQitWNTdZS1BORi95Nng2OU43UTFQbmp1cUczdlpybzljMEJ3d012NWoyc3BMMTJHcTdzTDZE

1560 alB1d0dHbnB2MkVZQTFLbmc9CkFTdjQwUkgzRmxzbGVlU1NjRlZNRmh3dEx6eEYxK2xpcmxEL29X

1561 alJLQ05qVWZhUVpJTWpqMWRoVkhOakNUTWhWZ1ZONkl3b04xTnFOMEV6cmdhaTFBWnNiMm9UczYw

1562 QkI1UGh0U0hhQ2U2WllUeE1JemFPS2FIK0w2eHhtaXIrTlQxNTRXS0x5amJMams3MU1na3Nwa0Yy

1563 WDBJMnlaWW5IUUM0bmdEL24yZzRtSVI2Q1hWL0JOUXNzeTBEeXdGLzN6eGRRYWw5cFBtVk1qYnFu

1564 cHY5SFNqRTg4S25naVpBWFhJWU1OVGF2L3Q3Y3dEWGdNekhKTlU0Y2xnVUtIQVZ3QT09UEsDBAoA

1565 AAAAAE+5SVkPfKx9FwIAABcCAAACAAAAMWR7InZlcnNpb24iOjF9CkFYbHNLRzQwZG5ibTJvcXdY

1566 U2ZrSWp3Mmxpa0lDS3hVOXU3TU52VkZ1NEJ2R1FVVitSVVdsS3MxL25TSlBtM2U2OTRvVHdoeDFo

1567 RFF3U0M5U0QvbXd5bnpjSTloUnRCUWVXMkVMOVU5L1ZGcHFsVWY3Z1ZOMHZ0ZWpXYnV4QnhsZlRD

1568 Tys4SFBwU2Zaa2VOUld5R2JNdzBFSU9LTmxRYjk3OUF0c1g3THR0NytaTkJnakZHYkZxaHdwa3kx

1569 WUNDVng1UmNZZ2tma2ZjWnVncGpzc1RzNVFvK1p3QXBEcDZ4V3JjSHMxUDhvNktBRzAwcjZZbkNM

1570 N2ErU1dwZmVNTUJhZz09CkFadVF0cFZMWmVvb292NkdyQlpnb3B6VmRGUXBlK1h6QXZuZ2dPVnZM

1571 VWtCYVF2akl5K1VLdXVUVlFoQ1JiMVp6dGZQL2dsNnoxOEsyZW5sQlo2bGJTZnoxTlBWeUVzYXB3

1572 dDVpUVh4azd5UkJlZks1cFlsNTduUXlmcFZQbzlreFpnOVdHTkV3NVJ5MkExemhnNGl6TWxLRmJh

1573 UjZFZ0FjQ3NFOXAveGRLa29ZNjhOUlZmNXJDM3lMQjc3ZWgyS1hCUld2WDNZcE9XdW00OGtsbmtI

1574 akJjMFpiQmUrT3NZb3d5cXpoRFA2ZGQxRlFnMlFjK09vc3B4V0sycld4M01HZz09UEsBAh4DCgAA

1575 AAAAT7lJWWg5FL7mAAAA5gAAAAUAAAAAAAAAAAAAAKSBAAAAAC5rZXlzUEsBAh4DCgAAAAAAT7lJ

1576 Weut7lIXAgAAFwIAAAIAAAAAAAAAAAAAAKSBCQEAADAzUEsBAh4DCgAAAAAAT7lJWUV5MuArAgAA

1577 KwIAAAIAAAAAAAAAAAAAAKSBQAMAADEwUEsBAh4DCgAAAAAAT7lJWQ98rH0XAgAAFwIAAAIAAAAA

1578 AAAAAAAAAKSBiwUAADFkUEsFBgAAAAAEAAQAwwAAAMIHAAAAAA==

1579"""

1580""" (empty)

1581 A sample corrupted storeroom archive, encrypted with

1582[`VAULT_MASTER_KEY`][]. The configuration is compressed (zip archive)

1583 and then encoded in base64.

1584

1585 The archive contains two directories `/dir/` and `/dir/subdir/`, where

1586`/dir/subdir/` is a correctly serialized directory, but `/dir/` does not

1587 contain `/dir/subdir/` in its list of child items. See

1588[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4_JAVASCRIPT_SOURCE`][] for

1589 the exact script that created this archive.

1590"""

1591

1592 CANNOT_LOAD_CRYPTOGRAPHY = ( (empty)

1593 "Cannot load the required Python module 'cryptography'."

1594)

1595""" (empty)

1596 The expected `derivepassphrase` error message when the `cryptography`

1597 module cannot be loaded, which is needed e.g. by the `export vault`

1598 subcommands.

1599"""

1600

1601 skip_if_cryptography_support = pytest.mark.skipif( (empty)

1602 importlib.util.find_spec('cryptography') is not None,

1603 reason='cryptography support available; cannot test "no support" scenario',

1604)

1605""" (empty)

1606 A cached pytest mark to skip this test if cryptography support is

1607 available. Usually this means that the test targets

1608`derivepassphrase`'s fallback functionality, which is not available

1609 whenever the primary functionality is.

1610"""

1611 skip_if_no_cryptography_support = pytest.mark.skipif( (empty)

1612 importlib.util.find_spec('cryptography') is None,

1613 reason='no "cryptography" support',

1614)

1615""" (empty)

1616 A cached pytest mark to skip this test if cryptography support is not

1617 available. Usually this means that the test targets the

1618`derivepassphrase export vault` subcommand, whose functionality depends

1619 on cryptography support being available.

1620"""

1621 skip_if_on_the_annoying_os = pytest.mark.skipif( (empty)

1622 sys.platform == 'win32',

1623 reason='The Annoying OS behaves differently.',

1624)

1625""" (empty)

1626 A cached pytest mark to skip this test if running on The Annoying

1627 Operating System, a.k.a. Microsoft Windows. Usually this is due to

1628 unnecessary and stupid differences in the OS internals, and these

1629 differences are deemed irreconcilable in the context of the decorated

1630 test, so the test is to be skipped.

1631

1632 See also:

1633 [`xfail_on_the_annoying_os`][]

1634

1635"""

1636 skip_if_no_multiprocessing_support = pytest.mark.skipif( (empty)

1637 importlib.util.find_spec('multiprocessing') is None,

1638 reason='no "multiprocessing" support',

1639)

1640""" (empty)

1641 A cached pytest mark to skip this test if multiprocessing support is not

1642 available. Usually this means that the test targets the concurrency

1643 features of `derivepassphrase`, which is generally only possible to test

1644 in separate processes because the testing machinery operates on

1645 process-global state.

1646"""

1647

1648 MIN_CONCURRENCY = 4 (empty)

1649""" (empty)

1650 The minimum amount of concurrent threads used for testing.

1651"""

1652

1653

1654 def get_concurrency_limit() -> int: (empty)

1655 """Return the imposed limit on the number of concurrent threads.

1656

1657 We use [`os.process_cpu_count`][] as the limit on Python 3.13 and

1658 higher, and [`os.cpu_count`][] on Python 3.12 and below. On

1659 Python 3.12 and below, we explicitly support the `PYTHON_CPU_COUNT`

1660 environment variable. We guarantee at least [`MIN_CONCURRENCY`][]

1661 many threads in any case.

1662

1663 """ # noqa: RUF002

1664 result: int | None = None 2 ctx 1ae

1665 if sys.version_info >= (3, 13): 2 ctx 1ae

1666 result = os.process_cpu_count() 2 ctx 1ae

1667 else:

1668 with contextlib.suppress(KeyError, ValueError): 2 ctx 1ae

1669 result = result or int(os.environ['PYTHON_CPU_COUNT'], 10) 2 ctx 1ae

1670 with contextlib.suppress(AttributeError): 2 ctx 1ae

1671 result = result or len(os.sched_getaffinity(os.getpid())) 2 ctx 1ae

1672 return max(result if result is not None else 0, MIN_CONCURRENCY) 2 ctx 1ae

1673

1674

1675 def get_concurrency_step_count( (empty)

1676 settings: hypothesis.settings | None = None,

1677) -> int:

1678 """Return the desired step count for concurrency-related tests.

1679

1680 This is the smaller of the [general concurrency

1681 limit][tests.get_concurrency_limit] and the step count from the

1682 current hypothesis settings.

1683

1684 Args:

1685 settings:

1686 The hypothesis settings for a specific tests. If not given,

1687 then the current profile will be queried directly.

1688

1689 """

1690 if settings is None: # pragma: no cover 2 ctx 1ae

1691 settings = hypothesis.settings() (empty)

1692 return min(get_concurrency_limit(), settings.stateful_step_count) 2 ctx 1ae

1693

1694

1695 def xfail_on_the_annoying_os( (empty)

1696 f: Callable | None = None, (empty)

1697 /,

1698 *,

1699 reason: str = '', (empty)

1700) -> pytest.MarkDecorator | Any: # pragma: no cover (empty)

1701 """Annotate a test which fails on The Annoying OS.

1702

1703 Annotate a test to indicate that it fails on The Annoying Operating

1704 System, a.k.a. Microsoft Windows. Usually this is due to

1705 differences in the design of OS internals, and usually, these

1706 differences are both unnecessary and stupid.

1707

1708 Args:

1709 f:

1710 A callable to decorate. If not given, return the pytest

1711 mark directly.

1712 reason:

1713 An optional, more detailed reason stating why this test

1714 fails on The Annoying OS.

1715

1716 Returns:

1717 The callable, marked as an expected failure on the Annoying OS,

1718 or alternatively a suitable pytest mark if no callable was

1719 passed. The reason will begin with the phrase "The Annoying OS

1720 behaves differently.", and the optional detailed reason, if not

1721 empty, will follow.

1722

1723 """

1724 base_reason = 'The Annoying OS behaves differently.'

1725 full_reason = base_reason if not reason else f'{base_reason} {reason}'

1726 mark = pytest.mark.xfail(

1727 sys.platform == 'win32',

1728 reason=full_reason,

1729 raises=(AssertionError, hypothesis.errors.FailedHealthCheck),

1730 strict=True,

1731 )

1732 return mark if f is None else mark(f)

1733

1734

1735@socketprovider.SocketProvider.register('fake') (empty)

1736 class FakeSSHAgentSocket: (empty)

1737 """A faked SSH agent presenting an [`_types.SSHAgentSocket`][]."""

1738

1739 _SOCKET_IS_CLOSED = 'Socket is closed.' (empty)

1740 _NO_FLAG_SUPPORT = 'This fake SSH agent socket does not support flags.' (empty)

1741 _PROTOCOL_VIOLATION = 'SSH agent protocol violation.' (empty)

1742 _INVALID_REQUEST = 'Invalid request.' (empty)

1743 _UNSUPPORTED_REQUEST = 'Unsupported request.' (empty)

1744

1745 HEADER_SIZE = 4 (empty)

1746 CODE_SIZE = 1 (empty)

1747

1748 def __init__(self) -> None: (empty)

1749 """Initialize the agent."""

1750 self.send_to_client = bytearray() 26 ctx 2a k f b g q h i j D v JbPbQbRbY Lb_ | - TbMbSb6 L Gb

1751 self.receive_from_client = bytearray() 26 ctx 2a k f b g q h i j D v JbPbQbRbY Lb_ | - TbMbSb6 L Gb

1752 self.closed = False 26 ctx 2a k f b g q h i j D v JbPbQbRbY Lb_ | - TbMbSb6 L Gb

1753

1754 def __enter__(self) -> Self: (empty)

1755 """Return self."""

1756 return self 20 ctx 2a k f b g h i j D v Y Lb_ | - TbMbSb6 L

1757

1758 def __exit__(self, *args: object) -> None: (empty)

1759 """Mark the agent's socket as closed."""

1760 self.closed = True 20 ctx 2a k f b g h i j D v Y Lb_ | - TbMbSb6 L

1761

1762 def sendall(self, data: Buffer, flags: int = 0, /) -> None: (empty)

1763 """Send data to the SSH agent.

1764

1765 The signature, and behavior, is identical to

1766 [`socket.socket.sendall`][]. Upon successful sending, this

1767 agent will parse the request, call the appropriate handler, and

1768 buffer the result such that it can be read via [`recv`][], in

1769 accordance with the SSH agent protocol.

1770

1771 Args:

1772 data: Binary data to send to the agent.

1773 flags: Reserved. Must be 0.

1774

1775 Returns:

1776 Nothing. The result should be requested via [`recv`][], and

1777 interpreted in accordance with the SSH agent protocol.

1778

1779 Raises:

1780 AssertionError:

1781 The flags argument, if specified, must be 0.

1782 ValueError:

1783 The agent's socket is already closed. No further

1784 requests can be sent.

1785

1786 """

1787 assert not flags, self._NO_FLAG_SUPPORT 14 ctx 2a b D v 7 Bbo Y _ | - Mb6 L

1788 if self.closed: 14 ctx 2a b D v 7 Bbo Y _ | - Mb6 L

1789 raise ValueError(self._SOCKET_IS_CLOSED) 1 ctx 2Mb

1790 self.receive_from_client.extend(memoryview(data)) 13 ctx 2a b D v 7 Bbo Y _ | - 6 L

1791 try: 13 ctx 2a b D v 7 Bbo Y _ | - 6 L

1792 self.parse_client_request_and_dispatch() 13 ctx 2a b D v 7 Bbo Y _ | - 6 L

1793 except ValueError: 3 ctx 1a6L

1794 payload = int.to_bytes(_types.SSH_AGENT.FAILURE.value, 1, 'big') 3 ctx 1a6L

1795 self.send_to_client.extend(int.to_bytes(len(payload), 4, 'big')) 3 ctx 1a6L

1796 self.send_to_client.extend(payload) 3 ctx 1a6L

1797 finally:

1798 self.receive_from_client.clear() 13 ctx 2a b D v 7 Bbo Y _ | - 6 L

1799

1800 def recv(self, count: int, flags: int = 0, /) -> bytes: (empty)

1801 """Read data from the SSH agent.

1802

1803 As per the SSH agent protocol, data is only available to be read

1804 immediately after a request via [`sendall`][]. Calls to

1805 [`recv`][] at other points in time that attempt to read data

1806 violate the protocol, and will fail. Notwithstanding the last

1807 sentence, at any point in time, though pointless, it is

1808 additionally permissible to read 0 bytes from the agent, or any

1809 number of bytes from a closed socket.

1810

1811 Args:

1812 count:

1813 Number of bytes to read from the agent.

1814 flags:

1815 Reserved. Must be 0.

1816

1817 Returns:

1818 (A chunk of) the SSH agent's response to the most recent

1819 request. If reading 0 bytes, or if reading from a closed

1820 socket, the returned chunk is always an empty byte string.

1821

1822 Raises:

1823 AssertionError:

1824 The flags argument, if specified, must be 0.

1825

1826 Alternatively, `recv` was called when there was no

1827 response to be obtained, in violation of the SSH agent

1828 protocol.

1829

1830 """

1831 assert not flags, self._NO_FLAG_SUPPORT 15 ctx 2a b D v 7 Bbo Y _ | - MbSb6 L

1832 assert not count or self.closed or self.send_to_client, ( 15 ctx 2a b D v 7 Bbo Y _ | - MbSb6 L

1833 self._PROTOCOL_VIOLATION

1834 )

1835 ret = bytes(self.send_to_client[:count]) 14 ctx 2a b D v 7 Bbo Y _ | - Mb6 L

1836 del self.send_to_client[:count] 14 ctx 2a b D v 7 Bbo Y _ | - Mb6 L

1837 return ret 14 ctx 2a b D v 7 Bbo Y _ | - Mb6 L

1838

1839 def parse_client_request_and_dispatch(self) -> None: (empty)

1840 """Parse the client request and call the matching handler.

1841

1842 This agent supports the

1843 [`SSH_AGENTC_REQUEST_IDENTITIES`][_types.SSH_AGENTC.REQUEST_IDENTITIES],

1844 [`SSH_AGENTC_SIGN_REQUEST`][_types.SSH_AGENTC.SIGN_REQUEST] and

1845 the [`SSH_AGENTC_EXTENSION`][_types.SSH_AGENTC.EXTENSION]

1846 request types.

1847

1848 """

1849 def as_extension(name: str, payload: Buffer = b'') -> bytes: 13 ctx 2a b D v 7 Bbo Y _ | - 6 L

1850 string = ssh_agent.SSHAgentClient.string 5 ctx 1bDo_6

1851 return string( 5 ctx 1bDo_6

1852 b'\x1b' + string(name.encode('ascii')) + bytes(payload)

1853 )

1854

1855 if len(self.receive_from_client) < self.HEADER_SIZE + self.CODE_SIZE: 13 ctx 2a b D v 7 Bbo Y _ | - 6 L

1856 raise ValueError(self._INVALID_REQUEST) 1 ctx 16

1857 target_header = ssh_agent.SSHAgentClient.uint32( 13 ctx 2a b D v 7 Bbo Y _ | - 6 L

1858 len(self.receive_from_client) - self.HEADER_SIZE

1859 )

1860 if target_header != self.receive_from_client[: self.HEADER_SIZE]: 13 ctx 2a b D v 7 Bbo Y _ | - 6 L

1861 raise ValueError(self._INVALID_REQUEST) 1 ctx 16

1862 code = _types.SSH_AGENTC( 13 ctx 2a b D v 7 Bbo Y _ | - 6 L

1863 int.from_bytes(

1864 self.receive_from_client[

1865 self.HEADER_SIZE : self.HEADER_SIZE + self.CODE_SIZE

1866 ],

1867 'big',

1868 )

1869 )

1870

1871 result: Buffer | Iterator[int]

1872 if code == _types.SSH_AGENTC.REQUEST_IDENTITIES: 13 ctx 2a b D v 7 Bbo Y _ | - 6 L

1873 result = self.request_identities(list_extended=False) 6 ctx 2a v 7 BbY |

1874 elif code == _types.SSH_AGENTC.SIGN_REQUEST: 9 ctx 1abD7o_-6L

1875 result = self.sign() 4 ctx 17-6L

1876 elif code == _types.SSH_AGENTC.EXTENSION and bytes( 6 ctx 1abDo_6

1877 self.receive_from_client

1878 ) == as_extension('query'):

1879 result = self.query_extensions() 4 ctx 1bDo_

1880 elif code == _types.SSH_AGENTC.EXTENSION and bytes( 1880 ↛ 1883line 1880 didn't jump to line 1883 because the condition on line 1880 was never true 2 ctx 1a6

1881 self.receive_from_client

1882 ) == as_extension('list-extended@putty.projects.tartarus.org'):

1883 result = self.request_identities(list_extended=True)

1884 else:

1885 raise ValueError(self._UNSUPPORTED_REQUEST) 2 ctx 1a6

1886 self.send_to_client.extend( 11 ctx 2a b D v 7 Bbo Y _ | -

1887 ssh_agent.SSHAgentClient.string(bytes(result))

1888 )

1889

1890 def query_extensions(self) -> Iterator[int]: (empty)

1891 """Answer an `SSH_AGENTC_EXTENSION` request.

1892

1893 Yields:

1894 The bytes payload of the response, without the protocol

1895 framing. The payload is yielded byte by byte, as an

1896 iterable of 8-bit integers.

1897

1898 """

1899 yield from b'\x1d' 4 ctx 1bDo_

1900 yield from ssh_agent.SSHAgentClient.string(b'query') 4 ctx 1bDo_

1901 extension_answers = [ 4 ctx 1bDo_

1902 b'query',

1903 b'list-extended@putty.projects.tartarus.org',

1904 ]

1905 for a in extension_answers: 4 ctx 1bDo_

1906 yield from ssh_agent.SSHAgentClient.string(a) 4 ctx 1bDo_

1907

1908 def request_identities(self, *, list_extended: bool = False) -> Iterator[int]: (empty)

1909 """Answer an `SSH_AGENTC_REQUEST_IDENTITIES` request.

1910

1911 Args:

1912 list_extended:

1913 If true, answer an `SSH_AGENTC_EXTENSION` request for

1914 the `list-extended@putty.projects.tartarus.org`

1915 extension. Otherwise, answer an

1916 `SSH_AGENTC_REQUEST_IDENTITIES` request.

1917

1918 Yields:

1919 The bytes payload of the response, without the protocol

1920 framing. The payload is yielded byte by byte, as an

1921 iterable of 8-bit integers.

1922

1923 """

1924 if list_extended: 1924 ↛ 1925line 1924 didn't jump to line 1925 because the condition on line 1924 was never true 6 ctx 2a v 7 BbY |

1925 yield from b'\x1d'

1926 yield from ssh_agent.SSHAgentClient.string(

1927 b'list-extended@putty.projects.tartarus.org'

1928 )

1929 else:

1930 yield from b'\x0c' 6 ctx 2a v 7 BbY |

1931 keys = [v for v in ALL_KEYS.values() if v.expected_signature] 6 ctx 2a v 7 BbY |

1932 yield from ssh_agent.SSHAgentClient.uint32(len(keys)) 6 ctx 2a v 7 BbY |

1933 for key in keys: 6 ctx 2a v 7 BbY |

1934 yield from ssh_agent.SSHAgentClient.string(key.public_key_data) 6 ctx 2a v 7 BbY |

1935 yield from ssh_agent.SSHAgentClient.string( 6 ctx 2a v 7 BbY |

1936 b'test key without passphrase'

1937 )

1938 if list_extended: 1938 ↛ 1939line 1938 didn't jump to line 1939 because the condition on line 1938 was never true 6 ctx 2a v 7 BbY |

1939 yield from ssh_agent.SSHAgentClient.uint32(0)

1940

1941 def sign(self) -> bytes: (empty)

1942 """Answer an `SSH_AGENTC_SIGN_REQUEST` request.

1943

1944 Returns:

1945 The bytes payload of the response, without the protocol

1946 framing.

1947

1948 """

1949 key_blob, rest = ssh_agent.SSHAgentClient.unstring_prefix( 4 ctx 17-6L

1950 self.receive_from_client[self.HEADER_SIZE + self.CODE_SIZE :]

1951 )

1952 sign_data, rest = ssh_agent.SSHAgentClient.unstring_prefix(rest) 4 ctx 17-6L

1953 if len(rest) != 4: 4 ctx 17-6L

1954 raise ValueError(self._INVALID_REQUEST) 1 ctx 16

1955 flags = int.from_bytes(rest, 'big') 3 ctx 17-L

1956 if flags: 3 ctx 17-L

1957 raise ValueError(self._UNSUPPORTED_REQUEST) 1 ctx 1L

1958 if sign_data != vault.Vault.UUID: 3 ctx 17-L

1959 raise ValueError(self._UNSUPPORTED_REQUEST) 1 ctx 1L

1960 for key in ALL_KEYS.values(): 3 ctx 17-L

1961 if key.public_key_data == key_blob: 3 ctx 17-L

1962 if not key.expected_signature: 3 ctx 17-L

1963 raise ValueError(self._UNSUPPORTED_REQUEST) 1 ctx 1L

1964 return b'\x0e' + ssh_agent.SSHAgentClient.string( 2 ctx 17-

1965 key.expected_signature

1966 )

1967 raise ValueError(self._UNSUPPORTED_REQUEST) 1 ctx 1L

1968

1969

1970@socketprovider.SocketProvider.register('fake_with_address') (empty)

1971 class FakeSSHAgentSocketWithAddress(FakeSSHAgentSocket): (empty)

1972 """A [`FakeSSHAgentSocket`][] requiring a specific address."""

1973

1974 ADDRESS = 'fake-ssh-agent:' (empty)

1975 """The correct address for connecting to this fake agent.""" (empty)

1976

1977 def __init__(self) -> None: (empty)

1978 """Initialize the agent, based on `SSH_AUTH_SOCK`.

1979

1980 Socket addresses of the form `fake-ssh-agent:<errno_value>` will

1981 raise an [`OSError`][] (or the respective subclass) with the

1982 specified [`errno`][] value. For example,

1983 `fake-ssh-agent:EPERM` will raise a [`PermissionError`][].

1984

1985 Raises:

1986 KeyError:

1987 The `SSH_AUTH_SOCK` environment variable is not set.

1988 OSError:

1989 The address in `SSH_AUTH_SOCK` is unsuited.

1990

1991 """

1992 super().__init__() 18 ctx 2a k f b g q h i j D v JbPbQbRbY LbGb

1993 try: 18 ctx 2a k f b g q h i j D v JbPbQbRbY LbGb

1994 orig_address = os.environ['SSH_AUTH_SOCK'] 18 ctx 2a k f b g q h i j D v JbPbQbRbY LbGb

1995 except KeyError as exc: 3 ctx 2q v Gb

1996 msg = 'SSH_AUTH_SOCK environment variable' 3 ctx 2q v Gb

1997 raise KeyError(msg) from exc 3 ctx 2q v Gb

1998 address = orig_address 17 ctx 2a k f b g h i j D v JbPbQbRbY LbGb

1999 if not address.startswith(self.ADDRESS): 17 ctx 2a k f b g h i j D v JbPbQbRbY LbGb

2000 address = self.ADDRESS + 'ENOENT' 2 ctx 2JbGb

2001 errcode = address.removeprefix(self.ADDRESS) 17 ctx 2a k f b g h i j D v JbPbQbRbY LbGb

2002 if errcode and not ( 17 ctx 2a k f b g h i j D v JbPbQbRbY LbGb

2003 errcode.startswith('E') and hasattr(errno, errcode)

2004 ):

2005 errcode = 'EINVAL' 2 ctx 2v Gb

2006 if errcode: 17 ctx 2a k f b g h i j D v JbPbQbRbY LbGb

2007 errno_val = getattr(errno, errcode) 3 ctx 2v JbGb

2008 raise OSError(errno_val, os.strerror(errno_val), orig_address) 3 ctx 2v JbGb

2009

2010

2011 def list_keys(self: Any = None) -> list[_types.SSHKeyCommentPair]: (empty)

2012 """Return a list of all SSH test keys, as key/comment pairs.

2013

2014 Intended as a monkeypatching replacement for

2015 [`ssh_agent.SSHAgentClient.list_keys`][].

2016

2017 """

2018 del self # Unused. 3 ctx 1bDo

2019 Pair = _types.SSHKeyCommentPair # noqa: N806 3 ctx 1bDo

2020 return [ 3 ctx 1bDo

2021 Pair(value.public_key_data, f'{key} test key'.encode('ASCII'))

2022 for key, value in ALL_KEYS.items()

2023 ]

2024

2025

2026 def sign( (empty)

2027 self: Any, key: bytes | bytearray, message: bytes | bytearray

2028) -> bytes:

2029 """Return the signature of `message` under `key`.

2030

2031 Can only handle keys in [`SUPPORTED_KEYS`][], and only the vault

2032 UUID as the message.

2033

2034 Intended as a monkeypatching replacement for

2035 [`ssh_agent.SSHAgentClient.sign`][].

2036

2037 """

2038 del self # Unused. 2 ctx 1bg

2039 assert message == vault.Vault.UUID 2 ctx 1bg

2040 for value in SUPPORTED_KEYS.values(): 2 ctx 1bg

2041 if value.public_key_data == key: # pragma: no branch 2 ctx 1bg

2042 assert value.expected_signature is not None 2 ctx 1bg

2043 return value.expected_signature 2 ctx 1bg

2044 raise AssertionError

2045

2046

2047 def list_keys_singleton(self: Any = None) -> list[_types.SSHKeyCommentPair]: (empty)

2048 """Return a singleton list of the first supported SSH test key.

2049

2050 The key is returned as a key/comment pair.

2051

2052 Intended as a monkeypatching replacement for

2053 [`ssh_agent.SSHAgentClient.list_keys`][].

2054

2055 """

2056 del self # Unused. 2 ctx 1ao

2057 Pair = _types.SSHKeyCommentPair # noqa: N806 2 ctx 1ao

2058 list1 = [ 2 ctx 1ao

2059 Pair(value.public_key_data, f'{key} test key'.encode('ASCII'))

2060 for key, value in SUPPORTED_KEYS.items()

2061 ]

2062 return list1[:1] 2 ctx 1ao

2063

2064

2065 def suitable_ssh_keys(conn: Any) -> Iterator[_types.SSHKeyCommentPair]: (empty)

2066 """Return a two-item list of SSH test keys (key/comment pairs).

2067

2068 Intended as a monkeypatching replacement for

2069 `cli_machinery.get_suitable_ssh_keys` to better script and test the

2070 interactive key selection. When used this way, `derivepassphrase`

2071 believes that only those two keys are loaded and suitable.

2072

2073 """

2074 del conn # Unused. 3 ctx 1f8F

2075 Pair = _types.SSHKeyCommentPair # noqa: N806 3 ctx 1f8F

2076 yield from [ 3 ctx 1f8F

2077 Pair(DUMMY_KEY1, b'no comment'),

2078 Pair(DUMMY_KEY2, b'a comment'),

2079 ]

2080

2081

2082 def phrase_from_key( (empty)

2083 key: bytes,

2084 /,

2085 *,

2086 conn: ssh_agent.SSHAgentClient | socket.socket | None = None,

2087) -> bytes:

2088 """Return the "equivalent master passphrase" for key.

2089

2090 Only works for key [`DUMMY_KEY1`][].

2091

2092 Intended as a monkeypatching replacement for

2093 [`vault.Vault.phrase_from_key`][], bypassing communication with an

2094 actual SSH agent.

2095

2096 """

2097 del conn 2 ctx 1kf

2098 if key == DUMMY_KEY1: # pragma: no branch 2 ctx 1kf

2099 return DUMMY_PHRASE_FROM_KEY1 2 ctx 1kf

2100 raise KeyError(key) # pragma: no cover

2101

2102

2103 def provider_entry_provider() -> _types.SSHAgentSocket: # pragma: no cover (empty)

2104 """A pseudo provider for a [`_types.SSHAgentSocketProviderEntry`][]."""

2105 msg = 'We are not supposed to be called!'

2106 raise AssertionError(msg)

2107

2108

2109 provider_entry1 = _types.SSHAgentSocketProviderEntry( (empty)

2110 provider_entry_provider, 'entry1', ('entry1a', 'entry1b', 'entry1c')

2111)

2112"""A sample [`_types.SSHAgentSocketProviderEntry`][].""" (empty)

2113

2114 provider_entry2 = _types.SSHAgentSocketProviderEntry( (empty)

2115 provider_entry_provider, 'entry2', ('entry2d', 'entry2e')

2116)

2117"""A sample [`_types.SSHAgentSocketProviderEntry`][].""" (empty)

2118

2119 posix_entry = _types.SSHAgentSocketProviderEntry( (empty)

2120 socketprovider.SocketProvider.resolve('posix'), 'posix', ()

2121)

2122""" (empty)

2123 The standard [`_types.SSHAgentSocketProviderEntry`][] for the UNIX

2124 domain socket handler on POSIX systems.

2125"""

2126

2127 the_annoying_os_entry = _types.SSHAgentSocketProviderEntry( (empty)

2128 socketprovider.SocketProvider.resolve('the_annoying_os'),

2129 'the_annoying_os',

2130 (),

2131)

2132""" (empty)

2133 The standard [`_types.SSHAgentSocketProviderEntry`][] for the named pipe

2134 handler on The Annoying Operating System.

2135"""

2136

2137 faulty_entry_callable = _types.SSHAgentSocketProviderEntry( (empty)

2138 (), # type: ignore[arg-type]

2139 'tuple',

2140 (),

2141)

2142""" (empty)

2143 A faulty [`_types.SSHAgentSocketProviderEntry`][]: the indicated handler

2144 is not a callable.

2145"""

2146

2147 faulty_entry_name_exists = _types.SSHAgentSocketProviderEntry( (empty)

2148 socketprovider.SocketProvider.resolve('the_annoying_os'), 'posix', ()

2149)

2150""" (empty)

2151 A faulty [`_types.SSHAgentSocketProviderEntry`][]: the indicated handler

2152 is already registered with a different callable.

2153"""

2154

2155 faulty_entry_alias_exists = _types.SSHAgentSocketProviderEntry( (empty)

2156 socketprovider.SocketProvider.resolve('posix'),

2157 'posix',

2158 ('unix_domain', 'the_annoying_os'),

2159)

2160""" (empty)

2161 A faulty [`_types.SSHAgentSocketProviderEntry`][]: the alias is already

2162 registered with a different callable.

2163"""

2164

2165

2166@contextlib.contextmanager (empty)

2167 def faked_entry_point_list( # noqa: C901 (empty)

2168 additional_entry_points: Sequence[importlib.metadata.EntryPoint],

2169 remove_conflicting_entries: bool = False,

2170) -> Iterator[Sequence[str]]:

2171 """Yield a context where additional entry points are visible.

2172

2173 Args:

2174 additional_entry_points:

2175 A sequence of entry point objects that should additionally

2176 be visible.

2177 remove_conflicting_entries:

2178 If true, remove all names provided by the additional entry

2179 points, otherwise leave them untouched.

2180

2181 Yields:

2182 A sequence of registry names that are newly available within the

2183 context.

2184

2185 """

2186 true_entry_points = importlib.metadata.entry_points() 2 ctx 1lm

2187 additional_entry_points = list(additional_entry_points) 2 ctx 1lm

2188

2189 if sys.version_info >= (3, 12): 2 ctx 1lm

2190 new_entry_points = importlib.metadata.EntryPoints( 2 ctx 1lm

2191 list(true_entry_points) + additional_entry_points

2192 )

2193

2194 @overload 2 ctx 1lm

2195 def mangled_entry_points( 2 ctx 1lm

2196 *, group: None = None 2 ctx 1lm

2197 ) -> importlib.metadata.EntryPoints: ... 2 ctx 1lm

2198

2199 @overload 2 ctx 1lm

2200 def mangled_entry_points( 2 ctx 1lm

2201 *, group: str 2 ctx 1lm

2202 ) -> importlib.metadata.EntryPoints: ... 2 ctx 1lm

2203

2204 def mangled_entry_points( 2 ctx 1lm

2205 **params: Any,

2206 ) -> importlib.metadata.EntryPoints:

2207 return new_entry_points.select(**params) 2 ctx 1lm

2208

2209 elif sys.version_info >= (3, 10): 2 ctx 1lm

2210 # Compatibility concerns within importlib.metadata: depending on

2211 # whether the .select() API is used, the result is either the dict

2212 # of groups of points (as in < 3.10), or the EntryPoints iterable

2213 # (as in >= 3.12). So our wrapper needs to duplicate that

2214 # interface. FUN.

2215 new_entry_points_dict = { 2 ctx 1lm

2216 k: list(v) for k, v in true_entry_points.items()

2217 }

2218 for ep in additional_entry_points: 2 ctx 1lm

2219 new_entry_points_dict.setdefault(ep.group, []).append(ep) 2 ctx 1lm

2220 new_entry_points = importlib.metadata.EntryPoints([ 2 ctx 1lm

2221 ep for group in new_entry_points_dict.values() for ep in group

2222 ])

2223

2224 @overload 2 ctx 1lm

2225 def mangled_entry_points( 2 ctx 1lm

2226 *, group: None = None 2 ctx 1lm

2227 ) -> dict[ 2 ctx 1lm

2228 str,

2229 list[importlib.metadata.EntryPoint]

2230 | tuple[importlib.metadata.EntryPoint, ...],

2231 ]: ...

2232

2233 @overload 2 ctx 1lm

2234 def mangled_entry_points( 2 ctx 1lm

2235 *, group: str 2 ctx 1lm

2236 ) -> importlib.metadata.EntryPoints: ... 2 ctx 1lm

2237

2238 def mangled_entry_points( 2 ctx 1lm

2239 **params: Any,

2240 ) -> (

2241 importlib.metadata.EntryPoints

2242 | dict[

2243 str,

2244 list[importlib.metadata.EntryPoint]

2245 | tuple[importlib.metadata.EntryPoint, ...],

2246 ]

2247 ):

2248 return ( 2 ctx 1lm

2249 new_entry_points.select(**params)

2250 if params

2251 else new_entry_points_dict

2252 )

2253

2254 else:

2255 new_entry_points: dict[ 2 ctx 1lm

2256 str,

2257 list[importlib.metadata.EntryPoint]

2258 | tuple[importlib.metadata.EntryPoint, ...],

2259 ] = {

2260 group_name: list(group)

2261 for group_name, group in true_entry_points.items()

2262 }

2263 for ep in additional_entry_points: 2 ctx 1lm

2264 new_entry_points.setdefault(ep.group, []) 2 ctx 1lm

2265 new_entry_points[ep.group].append(ep) 2 ctx 1lm

2266 new_entry_points = { 2 ctx 1lm

2267 group_name: tuple(group)

2268 for group_name, group in new_entry_points.items()

2269 }

2270

2271 @overload 2 ctx 1lm

2272 def mangled_entry_points( 2 ctx 1lm

2273 *, group: None = None 2 ctx 1lm

2274 ) -> dict[str, tuple[importlib.metadata.EntryPoint, ...]]: ... 2 ctx 1lm

2275

2276 @overload 2 ctx 1lm

2277 def mangled_entry_points( 2 ctx 1lm

2278 *, group: str 2 ctx 1lm

2279 ) -> tuple[importlib.metadata.EntryPoint, ...]: ... 2 ctx 1lm

2280

2281 def mangled_entry_points( 2 ctx 1lm

2282 *, group: str | None = None

2283 ) -> (

2284 dict[str, tuple[importlib.metadata.EntryPoint, ...]]

2285 | tuple[importlib.metadata.EntryPoint, ...]

2286 ):

2287 return (

2288 new_entry_points.get(group, ())

2289 if group is not None

2290 else new_entry_points

2291 )

2292

2293 registry = socketprovider.SocketProvider.registry 2 ctx 1lm

2294 new_registry = registry.copy() 2 ctx 1lm

2295 keys = [ep.load().key for ep in additional_entry_points] 2 ctx 1lm

2296 aliases = [a for ep in additional_entry_points for a in ep.load().aliases] 2 ctx 1lm

2297 # This functionality is currently unused, so excluded from coverage.

2298 if remove_conflicting_entries: # pragma: no cover 2 ctx 1lm

2299 for name in [*keys, *aliases]:

2300 new_registry.pop(name, None)

2301

2302 with pytest.MonkeyPatch.context() as monkeypatch: 2 ctx 1lm

2303 monkeypatch.setattr( 2 ctx 1lm

2304 socketprovider.SocketProvider, 'registry', new_registry

2305 )

2306 monkeypatch.setattr( 2 ctx 1lm

2307 importlib.metadata, 'entry_points', mangled_entry_points

2308 )

2309 yield (*keys, *aliases) 2 ctx 1lm

2310

2311

2312@contextlib.contextmanager (empty)

2313 def isolated_config( (empty)

2314 monkeypatch: pytest.MonkeyPatch,

2315 runner: CliRunner,

2316 main_config_str: str | None = None,

2317) -> Iterator[None]:

2318 """Provide an isolated configuration setup, as a context.

2319

2320 This context manager sets up (and changes into) a temporary

2321 directory, which holds the user configuration specified in

2322 `main_config_str`, if any. The manager also ensures that the

2323 environment variables `HOME` and `USERPROFILE` are set, and that

2324 `DERIVEPASSPHRASE_PATH` is unset. Upon exiting the context, the

2325 changes are undone and the temporary directory is removed.

2326

2327 Args:

2328 monkeypatch:

2329 A monkeypatch fixture object.

2330 runner:

2331 A `click` CLI runner harness.

2332 main_config_str:

2333 Optional TOML file contents, to be used as the user

2334 configuration.

2335

2336 Returns:

2337 A context manager, without a return value.

2338

2339 """

2340 prog_name = cli_helpers.PROG_NAME 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2341 env_name = prog_name.replace(' ', '_').upper() + '_PATH' 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2342 # TODO(the-13th-letter): Rewrite using parenthesized with-statements.

2343 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9

2344 with contextlib.ExitStack() as stack: 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2345 stack.enter_context(runner.isolated_filesystem()) 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2346 stack.enter_context( 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2347 cli_machinery.StandardCLILogging.ensure_standard_logging()

2348 )

2349 stack.enter_context( 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2350 cli_machinery.StandardCLILogging.ensure_standard_warnings_logging()

2351 )

2352 cwd = str(pathlib.Path.cwd().resolve()) 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2353 monkeypatch.setenv('HOME', cwd) 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2354 monkeypatch.setenv('APPDATA', cwd) 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2355 monkeypatch.setenv('LOCALAPPDATA', cwd) 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2356 monkeypatch.delenv(env_name, raising=False) 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2357 config_dir = cli_helpers.config_filename(subsystem=None) 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2358 config_dir.mkdir(parents=True, exist_ok=True) 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2359 if isinstance(main_config_str, str): 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2360 cli_helpers.config_filename('user configuration').write_text( 5 ctx 1JPQRS

2361 main_config_str, encoding='UTF-8'

2362 )

2363 try: 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2364 yield 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2365 finally:

2366 cli_helpers.config_filename('write lock').unlink(missing_ok=True) 86 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2367

2368

2369@contextlib.contextmanager (empty)

2370 def isolated_vault_config( (empty)

2371 monkeypatch: pytest.MonkeyPatch,

2372 runner: CliRunner,

2373 vault_config: Any,

2374 main_config_str: str | None = None,

2375) -> Iterator[None]:

2376 """Provide an isolated vault configuration setup, as a context.

2377

2378 Uses [`isolated_config`][] internally. Beyond those actions, this

2379 manager also loads the specified vault configuration into the

2380 context.

2381

2382 Args:

2383 monkeypatch:

2384 A monkeypatch fixture object.

2385 runner:

2386 A `click` CLI runner harness.

2387 vault_config:

2388 A valid vault configuration, to be integrated into the

2389 context.

2390 main_config_str:

2391 Optional TOML file contents, to be used as the user

2392 configuration.

2393

2394 Returns:

2395 A context manager, without a return value.

2396

2397 """

2398 with isolated_config( 51 ctx 2e k f b g M G N O I ( Z 0 8 F q p 1 2 h i j J P Q R S z rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2399 monkeypatch=monkeypatch, runner=runner, main_config_str=main_config_str

2400 ):

2401 config_filename = cli_helpers.config_filename(subsystem='vault') 51 ctx 2e k f b g M G N O I ( Z 0 8 F q p 1 2 h i j J P Q R S z rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2402 with config_filename.open('w', encoding='UTF-8') as outfile: 51 ctx 2e k f b g M G N O I ( Z 0 8 F q p 1 2 h i j J P Q R S z rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2403 json.dump(vault_config, outfile) 51 ctx 2e k f b g M G N O I ( Z 0 8 F q p 1 2 h i j J P Q R S z rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2404 yield 51 ctx 2e k f b g M G N O I ( Z 0 8 F q p 1 2 h i j J P Q R S z rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X

2405

2406

2407@contextlib.contextmanager (empty)

2408 def isolated_vault_exporter_config( (empty)

2409 monkeypatch: pytest.MonkeyPatch,

2410 runner: CliRunner,

2411 vault_config: str | bytes | None = None,

2412 vault_key: str | None = None,

2413) -> Iterator[None]:

2414 """Provide an isolated vault configuration setup, as a context.

2415

2416 Works similarly to [`isolated_config`][], except that no user

2417 configuration is accepted or integrated into the context. This

2418 manager also accepts a serialized vault-native configuration and

2419 a vault encryption key to integrate into the context.

2420

2421 Args:

2422 monkeypatch:

2423 A monkeypatch fixture object.

2424 runner:

2425 A `click` CLI runner harness.

2426 vault_config:

2427 An optional serialized vault-native configuration, to be

2428 integrated into the context. If a text string, then the

2429 contents are written to the file `.vault`. If a byte

2430 string, then it is treated as base64-encoded zip file

2431 contents, which---once inside the `.vault` directory---will

2432 be extracted into the current directory.

2433 vault_key:

2434 An optional encryption key presumably for the stored

2435 vault-native configuration. If given, then the environment

2436 variable `VAULT_KEY` will be populated with this key while

2437 the context is active.

2438

2439 Returns:

2440 A context manager, without a return value.

2441

2442 """

2443 # TODO(the-13th-letter): Remove the fallback implementation.

2444 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.10

2445 if TYPE_CHECKING: 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2446 chdir: Callable[..., AbstractContextManager]

2447 else:

2448 try: 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2449 chdir = contextlib.chdir # type: ignore[attr] 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2450 except AttributeError: 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2451

2452 @contextlib.contextmanager 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2453 def chdir( 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2454 newpath: str | bytes | os.PathLike,

2455 ) -> Iterator[None]: # pragma: no branch

2456 oldpath = pathlib.Path.cwd().resolve() 6 ctx 1nAEBCd

2457 os.chdir(newpath) 6 ctx 1nAEBCd

2458 yield 6 ctx 1nAEBCd

2459 os.chdir(oldpath) 6 ctx 1nAEBCd

2460

2461 with runner.isolated_filesystem(): 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2462 cwd = str(pathlib.Path.cwd().resolve()) 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2463 monkeypatch.setenv('HOME', cwd) 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2464 monkeypatch.setenv('USERPROFILE', cwd) 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2465 monkeypatch.delenv( 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2466 cli_helpers.PROG_NAME.replace(' ', '_').upper() + '_PATH',

2467 raising=False,

2468 )

2469 monkeypatch.delenv('VAULT_PATH', raising=False) 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2470 monkeypatch.delenv('VAULT_KEY', raising=False) 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2471 monkeypatch.delenv('LOGNAME', raising=False) 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2472 monkeypatch.delenv('USER', raising=False) 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2473 monkeypatch.delenv('USERNAME', raising=False) 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2474 if vault_key is not None: 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2475 monkeypatch.setenv('VAULT_KEY', vault_key) 14 ctx 2H # w t x u y A B C nbobK d

2476 vault_config_path = pathlib.Path('.vault').resolve() 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2477 # TODO(the-13th-letter): Rewrite using structural pattern matching.

2478 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9

2479 if isinstance(vault_config, str): 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2480 vault_config_path.write_text(f'{vault_config}\n', encoding='UTF-8') 14 ctx 2H # ' n w t x u y nbobpbK d

2481 elif isinstance(vault_config, bytes): 8 ctx 1nAEBC}d~

2482 vault_config_path.mkdir(parents=True, mode=0o700, exist_ok=True) 6 ctx 1nAEBCd

2483 # TODO(the-13th-letter): Rewrite using parenthesized

2484 # with-statements.

2485 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9

2486 with contextlib.ExitStack() as stack: 6 ctx 1nAEBCd

2487 stack.enter_context(chdir(vault_config_path)) 6 ctx 1nAEBCd

2488 tmpzipfile = stack.enter_context( 6 ctx 1nAEBCd

2489 tempfile.NamedTemporaryFile(suffix='.zip')

2490 )

2491 for line in vault_config.splitlines(): 6 ctx 1nAEBCd

2492 tmpzipfile.write(base64.standard_b64decode(line)) 6 ctx 1nAEBCd

2493 tmpzipfile.flush() 6 ctx 1nAEBCd

2494 tmpzipfile.seek(0, 0) 6 ctx 1nAEBCd

2495 with zipfile.ZipFile(tmpzipfile.file) as zipfileobj: 6 ctx 1nAEBCd

2496 zipfileobj.extractall() 6 ctx 1nAEBCd

2497 elif vault_config is None: 2 ctx 1}~

2498 pass 2 ctx 1}~

2499 else: # pragma: no cover

2500 assert_never(vault_config)

2501 try: 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2502 yield 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2503 finally:

2504 cli_helpers.config_filename('write lock').unlink(missing_ok=True) 20 ctx 2H # ' n w t x u y A E B C nbobpb} K d ~

2505

2506

2507 def auto_prompt(*args: Any, **kwargs: Any) -> str: (empty)

2508 """Return [`DUMMY_PASSPHRASE`][].

2509

2510 Intended as a monkeypatching replacement for

2511 `cli.prompt_for_passphrase` to better script and test the

2512 interactive passphrase queries.

2513

2514 """

2515 del args, kwargs # Unused. 4 ctx 1`{G9

2516 return DUMMY_PASSPHRASE 4 ctx 1`{G9

2517

2518

2519 def make_file_readonly( (empty)

2520 pathname: str | bytes | os.PathLike[str],

2521 /,

2522 *,

2523 try_race_free_implementation: bool = True,

2524) -> None:

2525 """Mark a file as read-only.

2526

2527 On POSIX, this entails removing the write permission bits for user,

2528 group and other, and ensuring the read permission bit for user is

2529 set.

2530

2531 Unfortunately, The Annoying OS (a.k.a. Microsoft Windows) has its

2532 own rules: Set exactly(?) the read permission bit for user to make

2533 the file read-only, and set exactly(?) the write permission bit for

2534 user to make the file read/write; all other permission bit settings

2535 are ignored.

2536

2537 The cross-platform procedure therefore is:

2538

2539 1. Call `os.stat` on the file, noting the permission bits.

2540 2. Calculate the new permission bits POSIX-style.

2541 3. Call `os.chmod` with permission bit `stat.S_IREAD`.

2542 4. Call `os.chmod` with the correct POSIX-style permissions.

2543

2544 If the platform supports it, we use a file descriptor instead of

2545 a path name. Otherwise, we use the same path name multiple times,

2546 and are susceptible to race conditions.

2547

2548 """

2549 fname: int | str | bytes | os.PathLike

2550 if try_race_free_implementation and {os.stat, os.chmod} <= os.supports_fd: 1 ctx 1p

2551 # The Annoying OS (v11 at least) supports fstat and fchmod, but

2552 # does not support changing the file mode on file descriptors

2553 # for read-only files.

2554 fname = os.open( 1 ctx 1p

2555 pathname,

2556 os.O_RDWR

2557 | getattr(os, 'O_CLOEXEC', 0)

2558 | getattr(os, 'O_NOCTTY', 0),

2559 )

2560 else:

2561 fname = pathname 1 ctx 1p

2562 try: 1 ctx 1p

2563 orig_mode = os.stat(fname).st_mode # noqa: PTH116 1 ctx 1p

2564 new_mode = ( 1 ctx 1p

2565 orig_mode & ~stat.S_IWUSR & ~stat.S_IWGRP & ~stat.S_IWOTH

2566 | stat.S_IREAD

2567 )

2568 os.chmod(fname, stat.S_IREAD) # noqa: PTH101 1 ctx 1p

2569 os.chmod(fname, new_mode) # noqa: PTH101 1 ctx 1p

2570 finally:

2571 if isinstance(fname, int): 1 ctx 1p

2572 os.close(fname) 1 ctx 1p

2573

2574

2575 class ReadableResult(NamedTuple): (empty)

2576 """Helper class for formatting and testing click.testing.Result objects."""

2577

2578 exception: BaseException | None (empty)

2579 exit_code: int (empty)

2580 stdout: str (empty)

2581 stderr: str (empty)

2582

2583 def clean_exit( (empty)

2584 self, *, output: str = '', empty_stderr: bool = False

2585 ) -> bool:

2586 """Return whether the invocation exited cleanly.

2587

2588 Args:

2589 output:

2590 An expected output string.

2591

2592 """

2593 return ( 51 ctx 2a e abbbcbdbebfbgbhbibjbkb` { k f b g M G N O ( lb8 mbJ H 9 $ % HbIb) T r * s + , c ! U V W X # ' n o

2594 (

2595 not self.exception

2596 or (

2597 isinstance(self.exception, SystemExit)

2598 and self.exit_code == 0

2599 )

2600 )

2601 and (not output or output in self.stdout)

2602 and (not empty_stderr or not self.stderr)

2603 )

2604

2605 def error_exit( (empty)

2606 self,

2607 *,

2608 error: str | re.Pattern[str] | type[BaseException] = BaseException,

2609 record_tuples: Sequence[tuple[str, int, str]] = (),

2610 ) -> bool:

2611 """Return whether the invocation exited uncleanly.

2612

2613 Args:

2614 error:

2615 An expected error message, or an expected numeric error

2616 code, or an expected exception type.

2617

2618 """

2619

2620 def error_match(error: str | re.Pattern[str], line: str) -> bool: 40 ctx 2. G / : ; I Z = ? 0 F q p 1 2 h i j @ [ ] ^ P Q R S z 3 4 HbIbV 5 w t x u y K d

2621 return ( 32 ctx 1.G/:;IZ=?0Fqp12hij@[]^PQRSz34V5K

2622 error in line

2623 if isinstance(error, str)

2624 else error.match(line) is not None

2625 )

2626

2627 # TODO(the-13th-letter): Rewrite using structural pattern matching.

2628 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9

2629 if isinstance(error, type): 40 ctx 2. G / : ; I Z = ? 0 F q p 1 2 h i j @ [ ] ^ P Q R S z 3 4 HbIbV 5 w t x u y K d

2630 return isinstance(self.exception, error) 2 ctx 2HbIb

2631 else: # noqa: RET505

2632 assert isinstance(error, (str, re.Pattern)) 38 ctx 1.G/:;IZ=?0Fqp12hij@[]^PQRSz34V5wtxuyKd

2633 return ( 38 ctx 1.G/:;IZ=?0Fqp12hij@[]^PQRSz34V5wtxuyKd

2634 isinstance(self.exception, SystemExit)

2635 and self.exit_code > 0

2636 and (

2637 not error

2638 or any(

2639 error_match(error, line)

2640 for line in self.stderr.splitlines(True)

2641 )

2642 or error_emitted(error, record_tuples)

2643 )

2644 )

2645

2646

2647 class CliRunner: (empty)

2648 """An abstracted CLI runner class.

2649

2650 Intended to provide similar functionality and scope as the

2651 [`click.testing.CliRunner`][] class, though not necessarily

2652 `click`-specific. Also allows for seamless migration away from

2653 `click`, if/when we decide this.

2654

2655 """

2656

2657 _SUPPORTS_MIX_STDERR_ATTRIBUTE = not hasattr(click.testing, 'StreamMixer') (empty)

2658 """ (empty)

2659 True if and only if [`click.testing.CliRunner`][] supports the

2660 `mix_stderr` attribute. It was removed in 8.2.0 in favor of the

2661 `click.testing.StreamMixer` class.

2662

2663 See also

2664 [`pallets/click#2523`](https://github.com/pallets/click/pull/2523).

2665 """

2666

2667 def __init__( (empty)

2668 self,

2669 *,

2670 mix_stderr: bool = False,

2671 color: bool | None = None,

2672 ) -> None:

2673 self.color = color 110 ctx 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbHbIbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~ o

2674 self.mix_stderr = mix_stderr 110 ctx 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbHbIbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~ o

2675

2676 class MixStderrAttribute(TypedDict): 110 ctx 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbHbIbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~ o

2677 mix_stderr: NotRequired[bool] 110 ctx 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbHbIbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~ o

2678

2679 mix_stderr_args: MixStderrAttribute = ( 110 ctx 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbHbIbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~ o

2680 {'mix_stderr': mix_stderr}

2681 if self._SUPPORTS_MIX_STDERR_ATTRIBUTE

2682 else {}

2683 )

2684 self.click_testing_clirunner = click.testing.CliRunner( 110 ctx 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbHbIbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~ o

2685 **mix_stderr_args

2686 )

2687

2688 def invoke( (empty)

2689 self,

2690 cli: click.BaseCommand,

2691 args: Sequence[str] | str | None = None,

2692 input: str | bytes | IO[Any] | None = None,

2693 env: Mapping[str, str | None] | None = None,

2694 catch_exceptions: bool = True,

2695 color: bool | None = None,

2696 **extra: Any,

2697 ) -> ReadableResult:

2698 if color is None: # pragma: no cover 88 ctx 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z H 3 9 4 $ % HbIb) T r * s + , c ! U V W 5 X # ' n w t x u y K d o

2699 color = self.color if self.color is not None else False 87 ctx 2a e abbbcbdbebfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z H 3 9 4 $ % HbIb) T r * s + , c ! U V W 5 X # ' n w t x u y K d o

2700 raw_result = self.click_testing_clirunner.invoke( 88 ctx 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z H 3 9 4 $ % HbIb) T r * s + , c ! U V W 5 X # ' n w t x u y K d o

2701 cli,

2702 args=args,

2703 input=input,

2704 env=env,

2705 catch_exceptions=catch_exceptions,

2706 color=color,

2707 **extra,

2708 )

2709 # In 8.2.0, r.stdout is no longer a property aliasing the

2710 # `output` attribute, but rather the raw stdout value.

2711 try: 88 ctx 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z H 3 9 4 $ % HbIb) T r * s + , c ! U V W 5 X # ' n w t x u y K d o

2712 stderr = raw_result.stderr 88 ctx 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z H 3 9 4 $ % HbIb) T r * s + , c ! U V W 5 X # ' n w t x u y K d o

2713 except ValueError: 4 ctx 2HbIb! o

2714 stderr = raw_result.stdout 4 ctx 2HbIb! o

2715 return ReadableResult( 88 ctx 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z H 3 9 4 $ % HbIb) T r * s + , c ! U V W 5 X # ' n w t x u y K d o

2716 raw_result.exception,

2717 raw_result.exit_code,

2718 (raw_result.stdout if not self.mix_stderr else raw_result.output)

2719 or '',

2720 stderr or '',

2721 )

2722 return ReadableResult.parse(raw_result)

2723

2724 def isolated_filesystem( (empty)

2725 self,

2726 temp_dir: str | os.PathLike[str] | None = None,

2727 ) -> AbstractContextManager[str]:

2728 return self.click_testing_clirunner.isolated_filesystem( 106 ctx 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~

2729 temp_dir=temp_dir

2730 )

2731

2732

2733 def parse_sh_export_line(line: str, *, env_name: str) -> str: (empty)

2734 """Parse the output of typical SSH agents' SSH_AUTH_SOCK lines.

2735

2736 Intentionally parses only a small subset of sh(1) syntax which works

2737 with current OpenSSH and PuTTY output. We require exactly one

2738 variable setting, and one export instruction, both on the same line,

2739 and perhaps combined into one statement. Terminating semicolons

2740 after each command are ignored.

2741

2742 Args:

2743 line:

2744 A line of sh(1) script to parse.

2745 env_name:

2746 The name of the environment variable to expect.

2747

2748 Returns:

2749 The parsed environment variable value.

2750

2751 Raises:

2752 ValueError:

2753 Cannot parse the sh script. Perhaps it is too complex,

2754 perhaps it is malformed.

2755

2756 """

2757 line = line.rstrip('\r\n') 1 ctx 2Kb

2758 shlex_parser = shlex.shlex( 1 ctx 2Kb

2759 instream=line, posix=True, punctuation_chars=True

2760 )

2761 shlex_parser.whitespace = ' \t' 1 ctx 2Kb

2762 tokens = list(shlex_parser) 1 ctx 2Kb

2763 orig_tokens = tokens.copy() 1 ctx 2Kb

2764 if tokens[-1] == ';': 1 ctx 2Kb

2765 tokens.pop() 1 ctx 2Kb

2766 if tokens[-3:] == [';', 'export', env_name]: 1 ctx 2Kb

2767 tokens[-3:] = [] 1 ctx 2Kb

2768 tokens[:0] = ['export'] 1 ctx 2Kb

2769 if not ( 1 ctx 2Kb

2770 len(tokens) == 2

2771 and tokens[0] == 'export'

2772 and tokens[1].startswith(f'{env_name}=')

2773 ):

2774 msg = f'Cannot parse sh line: {orig_tokens! r} -> {tokens! r}' 1 ctx 2Kb

2775 raise ValueError(msg) 1 ctx 2Kb

2776 return tokens[1].split('=', 1)[1] 1 ctx 2Kb

2777

2778

2779 def message_emitted_factory( (empty)

2780 level: int,

2781 *,

2782 logger_name: str = cli.PROG_NAME,

2783) -> Callable[[str | re.Pattern[str], Sequence[tuple[str, int, str]]], bool]:

2784 """Return a function to test if a matching message was emitted.

2785

2786 Args:

2787 level: The level to match messages at.

2788 logger_name: The name of the logger to match against.

2789

2790 """

2791

2792 def message_emitted( (empty)

2793 text: str | re.Pattern[str],

2794 record_tuples: Sequence[tuple[str, int, str]],

2795 ) -> bool:

2796 """Return true if a matching message was emitted.

2797

2798 Args:

2799 text: Substring or pattern to match against.

2800 record_tuples: Items to match.

2801

2802 """

2803

2804 def check_record(record: tuple[str, int, str]) -> bool: 23 ctx 1MNOIJzH394$%TcUWXwtxuyd

2805 if record[:2] != (logger_name, level): 22 ctx 1MNOJzH394$%TcUWXwtxuyd

2806 return False 4 ctx 1$%tu

2807 if isinstance(text, str): 22 ctx 1MNOJzH394$%TcUWXwtxuyd

2808 return text in record[2] 22 ctx 1MNOJzH394$%TcUWXwtxuyd

2809 return text.match(record[2]) is not None # pragma: no cover

2810

2811 return any(map(check_record, record_tuples)) 23 ctx 1MNOIJzH394$%TcUWXwtxuyd

2812

2813 return message_emitted (empty)

2814

2815

2816# No need to assert debug messages as of yet.

2817 info_emitted = message_emitted_factory(logging.INFO) (empty)

2818 warning_emitted = message_emitted_factory(logging.WARNING) (empty)

2819 deprecation_warning_emitted = message_emitted_factory( (empty)

2820 logging.WARNING, logger_name=f'{cli.PROG_NAME}.deprecation'

2821)

2822 deprecation_info_emitted = message_emitted_factory( (empty)

2823 logging.INFO, logger_name=f'{cli.PROG_NAME}.deprecation'

2824)

2825 error_emitted = message_emitted_factory(logging.ERROR) (empty)

2826

2827

2828 class Parametrize(types.SimpleNamespace): (empty)

2829 VAULT_CONFIG_FORMATS_DATA = pytest.mark.parametrize( (empty)

2830 ['config', 'format', 'config_data'],

2831 [

2832 pytest.param(

2833 VAULT_V02_CONFIG,

2834 'v0.2',

2835 VAULT_V02_CONFIG_DATA,

2836 id='0.2',

2837 ),

2838 pytest.param(

2839 VAULT_V03_CONFIG,

2840 'v0.3',

2841 VAULT_V03_CONFIG_DATA,

2842 id='0.3',

2843 ),

2844 pytest.param(

2845 VAULT_STOREROOM_CONFIG_ZIPPED,

2846 'storeroom',

2847 VAULT_STOREROOM_CONFIG_DATA,

2848 id='storeroom',

2849 ),

2850 ],

2851 )