Marco Ricci commited on 2024-06-30 16:27:37
Zeige 8 geänderte Dateien mit 1293 Einfügungen und 1152 Löschungen.
Adhere to standard testing best practices: name tests after the module they test (and move all such tests into the same testing module), group related tests within a module into classes, and move common testing functionality and test data into separate modules. Grouping related tests into classes also means that tests with common prerequisites (such as an available SSH agent) can then be skipped in bulk, if necessary.
... | ... |
@@ -1,3 +1,389 @@ |
1 | 1 |
# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info> |
2 | 2 |
# |
3 | 3 |
# SPDX-License-Identifier: MIT |
4 |
+ |
|
5 |
+from __future__ import annotations |
|
6 |
+ |
|
7 |
+import base64 |
|
8 |
+from collections.abc import Iterator, Mapping |
|
9 |
+import contextlib |
|
10 |
+import json |
|
11 |
+import os |
|
12 |
+from typing import Any, TYPE_CHECKING, TypedDict |
|
13 |
+ |
|
14 |
+import derivepassphrase |
|
15 |
+import derivepassphrase.cli |
|
16 |
+import derivepassphrase.types |
|
17 |
+import ssh_agent_client |
|
18 |
+import ssh_agent_client.types |
|
19 |
+import pytest |
|
20 |
+ |
|
21 |
+__all__ = () |
|
22 |
+ |
|
23 |
+if TYPE_CHECKING: |
|
24 |
+ class SSHTestKey(TypedDict): |
|
25 |
+ private_key: bytes |
|
26 |
+ public_key: bytes | str |
|
27 |
+ public_key_data: bytes |
|
28 |
+ expected_signature: bytes | None |
|
29 |
+ derived_passphrase: bytes | str | None |
|
30 |
+ |
|
31 |
+SUPPORTED_KEYS: Mapping[str, SSHTestKey] = { |
|
32 |
+ 'ed25519': { |
|
33 |
+ 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
34 |
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW |
|
35 |
+QyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdgAAAKDweO7H8Hju |
|
36 |
+xwAAAAtzc2gtZWQyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdg |
|
37 |
+AAAEAbM/A869nkWZbe2tp3Dm/L6gitvmpH/aRZt8sBII3ExYF4gWgm1gJIXw//Mkhv5MEw |
|
38 |
+idwcakUGCekJD/vCEml2AAAAG3Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQEC |
|
39 |
+-----END OPENSSH PRIVATE KEY----- |
|
40 |
+''', |
|
41 |
+ 'public_key': rb'''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2 test key without passphrase |
|
42 |
+''', |
|
43 |
+ 'public_key_data': bytes.fromhex(''' |
|
44 |
+ 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39 |
|
45 |
+ 00 00 00 20 |
|
46 |
+ 81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1 |
|
47 |
+ 30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76 |
|
48 |
+'''), |
|
49 |
+ 'expected_signature': bytes.fromhex(''' |
|
50 |
+ 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39 |
|
51 |
+ 00 00 00 40 |
|
52 |
+ f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86 |
|
53 |
+ 66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd |
|
54 |
+ 0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c |
|
55 |
+ 1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02 |
|
56 |
+ '''), |
|
57 |
+ 'derived_passphrase': rb'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==', |
|
58 |
+ }, |
|
59 |
+ # Currently only supported by PuTTY (which is deficient in other |
|
60 |
+ # niceties of the SSH agent and the agent's client). |
|
61 |
+ 'ed448': { |
|
62 |
+ 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
63 |
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz |
|
64 |
+c2gtZWQ0NDgAAAA54vZy009Wu8wExjvEb3hqtLz1GO/+d5vmGUbErWQ4AUO9mYLT |
|
65 |
+zHJHc2m4s+yWzP29Cc3EcxizLG8AAAAA8BdhfCcXYXwnAAAACXNzaC1lZDQ0OAAA |
|
66 |
+ADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM |
|
67 |
+/b0JzcRzGLMsbwAAAAByM7GIMRvWJB3YD6SIpAF2uudX4ozZe0X917wPwiBrs373 |
|
68 |
+9TM1n94Nib6hrxGNmCk2iBQDe2KALPgA4vZy009Wu8wExjvEb3hqtLz1GO/+d5vm |
|
69 |
+GUbErWQ4AUO9mYLTzHJHc2m4s+yWzP29Cc3EcxizLG8AAAAAG3Rlc3Qga2V5IHdp |
|
70 |
+dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ== |
|
71 |
+-----END OPENSSH PRIVATE KEY----- |
|
72 |
+''', |
|
73 |
+ 'public_key': rb'''ssh-ed448 AAAACXNzaC1lZDQ0OAAAADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM/b0JzcRzGLMsbwA= test key without passphrase |
|
74 |
+''', |
|
75 |
+ 'public_key_data': bytes.fromhex(''' |
|
76 |
+ 00 00 00 09 73 73 68 2d 65 64 34 34 38 |
|
77 |
+ 00 00 00 39 |
|
78 |
+ e2 f6 72 d3 4f 56 bb cc 04 c6 3b c4 6f 78 6a b4 |
|
79 |
+ bc f5 18 ef fe 77 9b e6 19 46 c4 ad 64 38 01 43 |
|
80 |
+ bd 99 82 d3 cc 72 47 73 69 b8 b3 ec 96 cc fd bd |
|
81 |
+ 09 cd c4 73 18 b3 2c 6f 00 |
|
82 |
+ '''), |
|
83 |
+ |
|
84 |
+ 'expected_signature': bytes.fromhex(''' |
|
85 |
+ 00 00 00 09 73 73 68 2d 65 64 34 34 38 |
|
86 |
+ 00 00 00 72 06 86 |
|
87 |
+ f4 64 a4 a6 ba d9 c3 22 c4 93 49 99 fc 11 de 67 |
|
88 |
+ 97 08 f2 d8 b7 3c 2c 13 e7 c5 1c 1e 92 a6 0e d8 |
|
89 |
+ 2f 6d 81 03 82 00 e3 72 e4 32 6d 72 d2 6d 32 84 |
|
90 |
+ 3f cc a9 1e 57 2c 00 9a b3 99 de 45 da ce 2e d1 |
|
91 |
+ db e5 89 f3 35 be 24 58 90 c6 ca 04 f0 db 88 80 |
|
92 |
+ db bd 77 7c 80 20 7f 3a 48 61 f6 1f ae a9 5e 53 |
|
93 |
+ 7b e0 9d 93 1e ea dc eb b5 cd 56 4c ea 8f 08 00 |
|
94 |
+ '''), |
|
95 |
+ 'derived_passphrase': rb'Bob0ZKSmutnDIsSTSZn8Ed5nlwjy2Lc8LBPnxRwekqYO2C9tgQOCAONy5DJtctJtMoQ/zKkeVywAmrOZ3kXazi7R2+WJ8zW+JFiQxsoE8NuIgNu9d3yAIH86SGH2H66pXlN74J2THurc67XNVkzqjwgA', |
|
96 |
+ }, |
|
97 |
+ 'rsa': { |
|
98 |
+ 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
99 |
+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 |
|
135 |
+-----END OPENSSH PRIVATE KEY----- |
|
136 |
+''', |
|
137 |
+ '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 |
|
138 |
+''', |
|
139 |
+ 'public_key_data': bytes.fromhex(''' |
|
140 |
+ 00 00 00 07 73 73 68 2d 72 73 61 |
|
141 |
+ 00 00 00 03 01 00 01 |
|
142 |
+ 00 00 01 81 00 |
|
143 |
+ b1 a1 ee e9 7b 38 71 5b 2e 0d 23 49 94 c0 aa a0 |
|
144 |
+ f5 60 98 38 04 be 22 3c 4d f5 e6 1c aa 97 dc 99 |
|
145 |
+ 08 a8 8d cb b2 f5 7b 22 37 fc 58 27 53 bd e1 2a |
|
146 |
+ ec 29 2e 66 12 26 b6 c4 79 d8 03 84 54 0a 14 f2 |
|
147 |
+ 60 d7 0c da fe 9c 9e 70 50 fc df d5 42 1a 46 30 |
|
148 |
+ de d2 57 17 5b 84 a5 17 4d 61 42 c1 b8 c1 80 ee |
|
149 |
+ ab 1b 21 c1 e9 6b 68 1f cb dd ed 33 84 3e df 8d |
|
150 |
+ 81 c5 f0 86 c5 b2 a1 a5 14 75 84 c0 eb 65 7e 18 |
|
151 |
+ 5f 6f 12 6c 4c 68 2e 2e 6e 00 f3 f8 1d 10 90 f9 |
|
152 |
+ da b9 2a 0d c6 69 21 70 87 b4 96 c2 cd 6f 5a 42 |
|
153 |
+ 95 9c b3 f3 c3 c3 9c a5 c2 5f f7 74 1e e3 7f 1c |
|
154 |
+ 9f ac 6c f8 3b 74 2b ce ca a1 58 4b 22 a7 de e6 |
|
155 |
+ 3a 6c 25 ea a0 87 6e af 15 f3 1f 73 bb cf 43 fc |
|
156 |
+ 7f 9b 1b 98 a3 ab 18 62 8d b8 dc 55 45 b7 95 97 |
|
157 |
+ f0 ff de e0 cb d2 7a ac 6d f5 1c d6 ab e4 7f 06 |
|
158 |
+ c9 c2 d3 17 0b 82 15 ea 43 99 31 29 36 04 0b 2a |
|
159 |
+ 87 bc 78 f2 04 3e ae 16 1e 11 54 a2 f0 5d c2 5a |
|
160 |
+ ce 07 25 0a 2a ac 6b 7b 2b d6 b7 98 24 a5 30 11 |
|
161 |
+ cf ef 4b b7 c3 a3 04 ed eb a6 a2 bc b1 95 4a 1f |
|
162 |
+ 7b 04 dd d7 b6 44 93 37 57 d3 c9 76 66 52 b3 66 |
|
163 |
+ fc 10 52 b7 3e c5 06 76 53 0f 33 da 67 d6 e9 38 |
|
164 |
+ b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f |
|
165 |
+ 0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31 |
|
166 |
+ d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f |
|
167 |
+'''), |
|
168 |
+ 'expected_signature': bytes.fromhex(''' |
|
169 |
+ 00 00 00 07 73 73 68 2d 72 73 61 |
|
170 |
+ 00 00 01 80 |
|
171 |
+ a2 10 7c 2e f6 bb 53 a8 74 2a a1 19 99 ad 81 be |
|
172 |
+ 79 9c ed d6 9d 09 4e 6e c5 18 48 33 90 77 99 68 |
|
173 |
+ f7 9e 03 5a cd 4e 18 eb 89 7d 85 a2 ee ae 4a 92 |
|
174 |
+ f6 6f ce b9 fe 86 7f 2a 6b 31 da 6e 1a fe a2 a5 |
|
175 |
+ 88 b8 44 7f a1 76 73 b3 ec 75 b5 d0 a6 b9 15 97 |
|
176 |
+ 65 09 13 7d 94 21 d1 fb 5d 0f 8b 23 04 77 c2 c3 |
|
177 |
+ 55 22 b1 a0 09 8a f5 38 2a d6 7f 1b 87 29 a0 25 |
|
178 |
+ d3 25 6f cb 64 61 07 98 dc 14 c5 84 f8 92 24 5e |
|
179 |
+ 50 11 6b 49 e5 f0 cc 29 cb 29 a9 19 d8 a7 71 1f |
|
180 |
+ 91 0b 05 b1 01 4b c2 5f 00 a5 b6 21 bf f8 2c 9d |
|
181 |
+ 67 9b 47 3b 0a 49 6b 79 2d fc 1d ec 0c b0 e5 27 |
|
182 |
+ 22 d5 a9 f8 d3 c3 f9 df 48 68 e9 fb ef 3c dc 26 |
|
183 |
+ bf cf ea 29 43 01 a6 e3 c5 51 95 f4 66 6d 8a 55 |
|
184 |
+ e2 47 ec e8 30 45 4c ae 47 e7 c9 a4 21 8b 64 ba |
|
185 |
+ b6 88 f6 21 f8 73 b9 cb 11 a1 78 75 92 c6 5a e5 |
|
186 |
+ 64 fe ed 42 d9 95 99 e6 2b 6f 3c 16 3c 28 74 a4 |
|
187 |
+ 72 2f 0d 3f 2c 33 67 aa 35 19 8e e7 b5 11 2f b3 |
|
188 |
+ f7 6a c5 02 e2 6f a3 42 e3 62 19 99 03 ea a5 20 |
|
189 |
+ e7 a1 e3 bc c8 06 a3 b5 7c d6 76 5d df 6f 60 46 |
|
190 |
+ 83 2a 08 00 d6 d3 d9 a4 c1 41 8c f8 60 56 45 81 |
|
191 |
+ da 3b a2 16 1f 9e 4e 75 83 17 da c3 53 c3 3e 19 |
|
192 |
+ a4 1b bc d2 29 b8 78 61 2b 78 e6 b1 52 b0 d5 ec |
|
193 |
+ de 69 2c 48 62 d9 fd d1 9b 6b b0 49 db d3 ff 38 |
|
194 |
+ e7 10 d9 2d ce 9f 0d 5e 09 7b 37 d2 7b c3 bf ce |
|
195 |
+'''), |
|
196 |
+ 'derived_passphrase': rb'ohB8Lva7U6h0KqEZma2Bvnmc7dadCU5uxRhIM5B3mWj3ngNazU4Y64l9haLurkqS9m/Ouf6GfyprMdpuGv6ipYi4RH+hdnOz7HW10Ka5FZdlCRN9lCHR+10PiyMEd8LDVSKxoAmK9Tgq1n8bhymgJdMlb8tkYQeY3BTFhPiSJF5QEWtJ5fDMKcspqRnYp3EfkQsFsQFLwl8ApbYhv/gsnWebRzsKSWt5Lfwd7Ayw5Sci1an408P530ho6fvvPNwmv8/qKUMBpuPFUZX0Zm2KVeJH7OgwRUyuR+fJpCGLZLq2iPYh+HO5yxGheHWSxlrlZP7tQtmVmeYrbzwWPCh0pHIvDT8sM2eqNRmO57URL7P3asUC4m+jQuNiGZkD6qUg56HjvMgGo7V81nZd329gRoMqCADW09mkwUGM+GBWRYHaO6IWH55OdYMX2sNTwz4ZpBu80im4eGEreOaxUrDV7N5pLEhi2f3Rm2uwSdvT/zjnENktzp8NXgl7N9J7w7/O', |
|
197 |
+ }, |
|
198 |
+} |
|
199 |
+ |
|
200 |
+UNSUITABLE_KEYS: Mapping[str, SSHTestKey] = { |
|
201 |
+ 'dsa1024': { |
|
202 |
+ 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
203 |
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH |
|
204 |
+NzAAAAgQC7KAZXqBGNVLBQPrcMYAoNW54BhD8aIhe7BDWYzJcsaMt72VKSkguZ8+XR7nRa |
|
205 |
+0C/ZsBi+uJp0dpxy9ZMTOWX4u5YPMeQcXEdGExZIfimGqSOAsy6fCld2IfJZJZExcCmhe9 |
|
206 |
+Ssjsd3YSAPJRluOXFQc95MZoR5hMwlIDD8QzrE7QAAABUA99nOZOgd7aHMVGoXpUEBcn7H |
|
207 |
+ossAAACALr2Ag3hxM3rKdxzVUw8fX0VVPXO+3+Kr8hGe0Kc/7NwVaBVL1GQ8fenBuWynpA |
|
208 |
+UbH0wo3h1wkB/8hX6p+S8cnu5rIBlUuVNwLw/bIYohK98LfqTYK/V+g6KD+8m34wvEiXZm |
|
209 |
+qywY54n2bksch1Nqvj/tNpLzExSx/XS0kSM1aigAAACAbQNRPcVEuGDrEcf+xg5tgAejPX |
|
210 |
+BPXr/Jss+Chk64km3mirMYjAWyWYtVcgT+7hOYxtYRin8LyMLqKRmqa0Q5UrvDfChgLhvs |
|
211 |
+G9YSb/Mpw5qm8PiHSafwhkaz/te3+8hKogqoe7sd+tCF06IpJr5k70ACiNtRGqssNF8Elr |
|
212 |
+l1efYAAAH4swlfVrMJX1YAAAAHc3NoLWRzcwAAAIEAuygGV6gRjVSwUD63DGAKDVueAYQ/ |
|
213 |
+GiIXuwQ1mMyXLGjLe9lSkpILmfPl0e50WtAv2bAYvriadHaccvWTEzll+LuWDzHkHFxHRh |
|
214 |
+MWSH4phqkjgLMunwpXdiHyWSWRMXApoXvUrI7Hd2EgDyUZbjlxUHPeTGaEeYTMJSAw/EM6 |
|
215 |
+xO0AAAAVAPfZzmToHe2hzFRqF6VBAXJ+x6LLAAAAgC69gIN4cTN6yncc1VMPH19FVT1zvt |
|
216 |
+/iq/IRntCnP+zcFWgVS9RkPH3pwblsp6QFGx9MKN4dcJAf/IV+qfkvHJ7uayAZVLlTcC8P |
|
217 |
+2yGKISvfC36k2Cv1foOig/vJt+MLxIl2ZqssGOeJ9m5LHIdTar4/7TaS8xMUsf10tJEjNW |
|
218 |
+ooAAAAgG0DUT3FRLhg6xHH/sYObYAHoz1wT16/ybLPgoZOuJJt5oqzGIwFslmLVXIE/u4T |
|
219 |
+mMbWEYp/C8jC6ikZqmtEOVK7w3woYC4b7BvWEm/zKcOapvD4h0mn8IZGs/7Xt/vISqIKqH |
|
220 |
+u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW |
|
221 |
+0gAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UBAgMEBQYH |
|
222 |
+-----END OPENSSH PRIVATE KEY----- |
|
223 |
+''', |
|
224 |
+ 'public_key': rb'''ssh-dss AAAAB3NzaC1kc3MAAACBALsoBleoEY1UsFA+twxgCg1bngGEPxoiF7sENZjMlyxoy3vZUpKSC5nz5dHudFrQL9mwGL64mnR2nHL1kxM5Zfi7lg8x5BxcR0YTFkh+KYapI4CzLp8KV3Yh8lklkTFwKaF71KyOx3dhIA8lGW45cVBz3kxmhHmEzCUgMPxDOsTtAAAAFQD32c5k6B3tocxUahelQQFyfseiywAAAIAuvYCDeHEzesp3HNVTDx9fRVU9c77f4qvyEZ7Qpz/s3BVoFUvUZDx96cG5bKekBRsfTCjeHXCQH/yFfqn5Lxye7msgGVS5U3AvD9shiiEr3wt+pNgr9X6DooP7ybfjC8SJdmarLBjnifZuSxyHU2q+P+02kvMTFLH9dLSRIzVqKAAAAIBtA1E9xUS4YOsRx/7GDm2AB6M9cE9ev8myz4KGTriSbeaKsxiMBbJZi1VyBP7uE5jG1hGKfwvIwuopGaprRDlSu8N8KGAuG+wb1hJv8ynDmqbw+IdJp/CGRrP+17f7yEqiCqh7ux360IXToikmvmTvQAKI21Eaqyw0XwSWuXV59g== test key without passphrase |
|
225 |
+''', |
|
226 |
+ 'public_key_data': bytes.fromhex(''' |
|
227 |
+ 00 00 00 07 73 73 68 2d 64 73 73 |
|
228 |
+ 00 00 00 81 00 |
|
229 |
+ bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d |
|
230 |
+ 5b 9e 01 84 3f 1a 22 17 bb 04 35 98 cc 97 2c 68 |
|
231 |
+ cb 7b d9 52 92 92 0b 99 f3 e5 d1 ee 74 5a d0 2f |
|
232 |
+ d9 b0 18 be b8 9a 74 76 9c 72 f5 93 13 39 65 f8 |
|
233 |
+ bb 96 0f 31 e4 1c 5c 47 46 13 16 48 7e 29 86 a9 |
|
234 |
+ 23 80 b3 2e 9f 0a 57 76 21 f2 59 25 91 31 70 29 |
|
235 |
+ a1 7b d4 ac 8e c7 77 61 20 0f 25 19 6e 39 71 50 |
|
236 |
+ 73 de 4c 66 84 79 84 cc 25 20 30 fc 43 3a c4 ed |
|
237 |
+ 00 00 00 15 00 f7 d9 ce 64 |
|
238 |
+ e8 1d ed a1 cc 54 6a 17 a5 41 01 72 7e c7 a2 cb |
|
239 |
+ 00 00 00 80 |
|
240 |
+ 2e bd 80 83 78 71 33 7a ca 77 1c d5 53 0f 1f 5f |
|
241 |
+ 45 55 3d 73 be df e2 ab f2 11 9e d0 a7 3f ec dc |
|
242 |
+ 15 68 15 4b d4 64 3c 7d e9 c1 b9 6c a7 a4 05 1b |
|
243 |
+ 1f 4c 28 de 1d 70 90 1f fc 85 7e a9 f9 2f 1c 9e |
|
244 |
+ ee 6b 20 19 54 b9 53 70 2f 0f db 21 8a 21 2b df |
|
245 |
+ 0b 7e a4 d8 2b f5 7e 83 a2 83 fb c9 b7 e3 0b c4 |
|
246 |
+ 89 76 66 ab 2c 18 e7 89 f6 6e 4b 1c 87 53 6a be |
|
247 |
+ 3f ed 36 92 f3 13 14 b1 fd 74 b4 91 23 35 6a 28 |
|
248 |
+ 00 00 00 80 |
|
249 |
+ 6d 03 51 3d c5 44 b8 60 eb 11 c7 fe c6 0e 6d 80 |
|
250 |
+ 07 a3 3d 70 4f 5e bf c9 b2 cf 82 86 4e b8 92 6d |
|
251 |
+ e6 8a b3 18 8c 05 b2 59 8b 55 72 04 fe ee 13 98 |
|
252 |
+ c6 d6 11 8a 7f 0b c8 c2 ea 29 19 aa 6b 44 39 52 |
|
253 |
+ bb c3 7c 28 60 2e 1b ec 1b d6 12 6f f3 29 c3 9a |
|
254 |
+ a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a |
|
255 |
+ a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef |
|
256 |
+ 40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6 |
|
257 |
+'''), |
|
258 |
+ 'expected_signature': None, |
|
259 |
+ 'derived_passphrase': None, |
|
260 |
+ }, |
|
261 |
+ 'ecdsa256': { |
|
262 |
+ 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
263 |
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS |
|
264 |
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTLbU0zDwsk2Dvp+VYIrsNVf5gWwz2S |
|
265 |
+3SZ8TbxiQRkpnGSVqyIoHJOJc+NQItAa7xlJ/8Z6gfz57Z3apUkaMJm6AAAAuKeY+YinmP |
|
266 |
+mIAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5 |
|
267 |
+Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmb |
|
268 |
+oAAAAhAKIl/3n0pKVIxpZkXTGtii782Qr4yIcvHdpxjO/QsIqKAAAAG3Rlc3Qga2V5IHdp |
|
269 |
+dGhvdXQgcGFzc3BocmFzZQECAwQ= |
|
270 |
+-----END OPENSSH PRIVATE KEY----- |
|
271 |
+''', |
|
272 |
+ 'public_key': rb'''ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmbo= test key without passphrase |
|
273 |
+''', |
|
274 |
+ 'public_key_data': bytes.fromhex(''' |
|
275 |
+ 00 00 00 13 65 63 64 73 61 2d 73 68 61 32 2d 6e |
|
276 |
+ 69 73 74 70 32 35 36 |
|
277 |
+ 00 00 00 08 6e 69 73 74 70 32 35 36 |
|
278 |
+ 00 00 00 41 04 |
|
279 |
+ cb 6d 4d 33 0f 0b 24 d8 3b e9 f9 56 08 ae c3 55 |
|
280 |
+ 7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c |
|
281 |
+ 64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19 |
|
282 |
+ 49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba |
|
283 |
+'''), |
|
284 |
+ 'expected_signature': None, |
|
285 |
+ 'derived_passphrase': None, |
|
286 |
+ }, |
|
287 |
+ 'ecdsa384': { |
|
288 |
+ 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
289 |
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS |
|
290 |
+1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSgkOjkAvq7v5vHuj3KBL4/EAWcn5hZ |
|
291 |
+DyKcbyV0eBMGFq7hKXQlZqIahLVqeMR0QqmkxNJ2rly2VHcXneq3vZ+9fIsWCOdYk5WP3N |
|
292 |
+ZPzv911Xn7wbEkC7QndD5zKlm4pBUAAADomhj+IZoY/iEAAAATZWNkc2Etc2hhMi1uaXN0 |
|
293 |
+cDM4NAAAAAhuaXN0cDM4NAAAAGEEoJDo5AL6u7+bx7o9ygS+PxAFnJ+YWQ8inG8ldHgTBh |
|
294 |
+au4Sl0JWaiGoS1anjEdEKppMTSdq5ctlR3F53qt72fvXyLFgjnWJOVj9zWT87/ddV5+8Gx |
|
295 |
+JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B |
|
296 |
+2OdqVOVRkwZWRROz0AAAAbdGVzdCBrZXkgd2l0aG91dCBwYXNzcGhyYXNlAQIDBA== |
|
297 |
+-----END OPENSSH PRIVATE KEY----- |
|
298 |
+''', |
|
299 |
+ 'public_key': rb'''ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKCQ6OQC+ru/m8e6PcoEvj8QBZyfmFkPIpxvJXR4EwYWruEpdCVmohqEtWp4xHRCqaTE0nauXLZUdxed6re9n718ixYI51iTlY/c1k/O/3XVefvBsSQLtCd0PnMqWbikFQ== test key without passphrase |
|
300 |
+''', |
|
301 |
+ 'public_key_data': bytes.fromhex(''' |
|
302 |
+ 00 00 00 13 |
|
303 |
+ 65 63 64 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 |
|
304 |
+ 33 38 34 |
|
305 |
+ 00 00 00 08 6e 69 73 74 70 33 38 34 |
|
306 |
+ 00 00 00 61 04 |
|
307 |
+ a0 90 e8 e4 02 fa bb bf 9b c7 ba 3d ca 04 be 3f |
|
308 |
+ 10 05 9c 9f 98 59 0f 22 9c 6f 25 74 78 13 06 16 |
|
309 |
+ ae e1 29 74 25 66 a2 1a 84 b5 6a 78 c4 74 42 a9 |
|
310 |
+ a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd |
|
311 |
+ 7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5 |
|
312 |
+ 79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15 |
|
313 |
+'''), |
|
314 |
+ 'expected_signature': None, |
|
315 |
+ 'derived_passphrase': None, |
|
316 |
+ }, |
|
317 |
+} |
|
318 |
+ |
|
319 |
+DUMMY_SERVICE = 'service1' |
|
320 |
+DUMMY_PASSPHRASE = b'my secret passphrase\n' |
|
321 |
+DUMMY_KEY1 = SUPPORTED_KEYS['ed25519']['public_key_data'] |
|
322 |
+DUMMY_KEY1_B64 = base64.standard_b64encode(DUMMY_KEY1).decode('ASCII') |
|
323 |
+DUMMY_KEY2 = SUPPORTED_KEYS['rsa']['public_key_data'] |
|
324 |
+DUMMY_KEY2_B64 = base64.standard_b64encode(DUMMY_KEY2).decode('ASCII') |
|
325 |
+DUMMY_CONFIG_SETTINGS = {"length": 10, "upper": 1, "lower": 1, "repeat": 5, |
|
326 |
+ "number": 1, "space": 1, "dash": 1, "symbol": 1} |
|
327 |
+DUMMY_RESULT_PASSPHRASE = b'.2V_QJkd o' |
|
328 |
+DUMMY_RESULT_KEY1 = b'E<b<{ -7iG' |
|
329 |
+DUMMY_PHRASE_FROM_KEY1_RAW = ( |
|
330 |
+ b'\x00\x00\x00\x0bssh-ed25519' |
|
331 |
+ b'\x00\x00\x00@\xf0\x98\x19\x80l\x1a\x97\xd5&\x03n' |
|
332 |
+ b'\xcc\xe3e\x8f\x86f\x07\x13\x19\x13\t!33\xf9\xe46S' |
|
333 |
+ b'\x1d\xaf\xfd\r\x08\x1f\xec\xf8s\x9b\x8c_U9\x16|ST,' |
|
334 |
+ b'\x1eR\xbb0\xed\x7f\x89\xe2/iQU\xd8\x9e\xa6\x02' |
|
335 |
+) |
|
336 |
+DUMMY_PHRASE_FROM_KEY1 = b'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==' |
|
337 |
+ |
|
338 |
+skip_if_no_agent = pytest.mark.skipif( |
|
339 |
+ not os.environ.get('SSH_AUTH_SOCK'), reason='running SSH agent required') |
|
340 |
+ |
|
341 |
+def list_keys( |
|
342 |
+ self: Any = None, |
|
343 |
+) -> list[ssh_agent_client.types.KeyCommentPair]: |
|
344 |
+ Pair = ssh_agent_client.types.KeyCommentPair |
|
345 |
+ list1 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII')) |
|
346 |
+ for key, value in SUPPORTED_KEYS.items()] |
|
347 |
+ list2 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII')) |
|
348 |
+ for key, value in UNSUITABLE_KEYS.items()] |
|
349 |
+ return list1 + list2 |
|
350 |
+ |
|
351 |
+def list_keys_singleton( |
|
352 |
+ self: Any = None, |
|
353 |
+) -> list[ssh_agent_client.types.KeyCommentPair]: |
|
354 |
+ Pair = ssh_agent_client.types.KeyCommentPair |
|
355 |
+ list1 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII')) |
|
356 |
+ for key, value in SUPPORTED_KEYS.items()] |
|
357 |
+ return list1[:1] |
|
358 |
+ |
|
359 |
+def suitable_ssh_keys( |
|
360 |
+ conn: Any |
|
361 |
+) -> Iterator[ssh_agent_client.types.KeyCommentPair]: |
|
362 |
+ yield from [ |
|
363 |
+ ssh_agent_client.types.KeyCommentPair(DUMMY_KEY1, b'no comment'), |
|
364 |
+ ssh_agent_client.types.KeyCommentPair(DUMMY_KEY2, b'a comment'), |
|
365 |
+ ] |
|
366 |
+ |
|
367 |
+def phrase_from_key(key: bytes) -> bytes: |
|
368 |
+ if key == DUMMY_KEY1: # pragma: no branch |
|
369 |
+ return DUMMY_PHRASE_FROM_KEY1 |
|
370 |
+ raise KeyError(key) # pragma: no cover |
|
371 |
+ |
|
372 |
+@contextlib.contextmanager |
|
373 |
+def isolated_config( |
|
374 |
+ monkeypatch: Any, runner: click.testing.CliRunner, config: Any, |
|
375 |
+): |
|
376 |
+ prog_name = derivepassphrase.cli.prog_name |
|
377 |
+ env_name = prog_name.replace(' ', '_').upper() + '_PATH' |
|
378 |
+ with runner.isolated_filesystem(): |
|
379 |
+ monkeypatch.setenv('HOME', os.getcwd()) |
|
380 |
+ monkeypatch.setenv('USERPROFILE', os.getcwd()) |
|
381 |
+ monkeypatch.delenv(env_name, raising=False) |
|
382 |
+ os.makedirs(os.path.dirname(derivepassphrase.cli._config_filename()), |
|
383 |
+ exist_ok=True) |
|
384 |
+ with open(derivepassphrase.cli._config_filename(), 'wt') as outfile: |
|
385 |
+ json.dump(config, outfile) |
|
386 |
+ yield |
|
387 |
+ |
|
388 |
+def auto_prompt(*args: Any, **kwargs: Any) -> str: |
|
389 |
+ return DUMMY_PASSPHRASE.decode('UTF-8') |
... | ... |
@@ -0,0 +1,209 @@ |
1 |
+# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info> |
|
2 |
+# |
|
3 |
+# SPDX-License-Identifier: MIT |
|
4 |
+ |
|
5 |
+"""Test passphrase generation via derivepassphrase.Vault.""" |
|
6 |
+ |
|
7 |
+from __future__ import annotations |
|
8 |
+ |
|
9 |
+import math |
|
10 |
+from typing import Any |
|
11 |
+ |
|
12 |
+import derivepassphrase |
|
13 |
+import sequin |
|
14 |
+import pytest |
|
15 |
+import tests |
|
16 |
+ |
|
17 |
+Vault = derivepassphrase.Vault |
|
18 |
+ |
|
19 |
+class TestVault: |
|
20 |
+ |
|
21 |
+ phrase = b'She cells C shells bye the sea shoars' |
|
22 |
+ google_phrase = rb': 4TVH#5:aZl8LueOT\{' |
|
23 |
+ twitter_phrase = rb"[ (HN_N:lI&<ro=)3'g9" |
|
24 |
+ |
|
25 |
+ @pytest.mark.parametrize(['service', 'expected'], [ |
|
26 |
+ (b'google', google_phrase), |
|
27 |
+ ('twitter', twitter_phrase), |
|
28 |
+ ]) |
|
29 |
+ def test_200_basic_configuration(self, service, expected): |
|
30 |
+ assert Vault(phrase=self.phrase).generate(service) == expected |
|
31 |
+ |
|
32 |
+ def test_201_phrase_dependence(self): |
|
33 |
+ assert ( |
|
34 |
+ Vault(phrase=(self.phrase + b'X')).generate('google') == |
|
35 |
+ b'n+oIz6sL>K*lTEWYRO%7' |
|
36 |
+ ) |
|
37 |
+ |
|
38 |
+ def test_202_reproducibility_and_bytes_service_name(self): |
|
39 |
+ assert ( |
|
40 |
+ Vault(phrase=self.phrase).generate(b'google') == |
|
41 |
+ Vault(phrase=self.phrase).generate('google') |
|
42 |
+ ) |
|
43 |
+ |
|
44 |
+ def test_203_reproducibility_and_bytearray_service_name(self): |
|
45 |
+ assert ( |
|
46 |
+ Vault(phrase=self.phrase).generate(b'google') == |
|
47 |
+ Vault(phrase=self.phrase).generate(bytearray(b'google')) |
|
48 |
+ ) |
|
49 |
+ |
|
50 |
+ def test_210_nonstandard_length(self): |
|
51 |
+ assert ( |
|
52 |
+ Vault(phrase=self.phrase, length=4).generate('google') |
|
53 |
+ == b'xDFu' |
|
54 |
+ ) |
|
55 |
+ |
|
56 |
+ def test_211_repetition_limit(self): |
|
57 |
+ assert ( |
|
58 |
+ Vault(phrase=b'', length=24, symbol=0, number=0, |
|
59 |
+ repeat=1).generate('asd') == |
|
60 |
+ b'IVTDzACftqopUXqDHPkuCIhV' |
|
61 |
+ ) |
|
62 |
+ |
|
63 |
+ def test_212_without_symbols(self): |
|
64 |
+ assert ( |
|
65 |
+ Vault(phrase=self.phrase, symbol=0).generate('google') == |
|
66 |
+ b'XZ4wRe0bZCazbljCaMqR' |
|
67 |
+ ) |
|
68 |
+ |
|
69 |
+ def test_213_no_numbers(self): |
|
70 |
+ assert ( |
|
71 |
+ Vault(phrase=self.phrase, number=0).generate('google') == |
|
72 |
+ b'_*$TVH.%^aZl(LUeOT?>' |
|
73 |
+ ) |
|
74 |
+ |
|
75 |
+ def test_214_no_lowercase_letters(self): |
|
76 |
+ assert ( |
|
77 |
+ Vault(phrase=self.phrase, lower=0).generate('google') == |
|
78 |
+ b':{?)+7~@OA:L]!0E$)(+' |
|
79 |
+ ) |
|
80 |
+ |
|
81 |
+ def test_215_at_least_5_digits(self): |
|
82 |
+ assert ( |
|
83 |
+ Vault(phrase=self.phrase, length=8, number=5) |
|
84 |
+ .generate('songkick') == b'i0908.7[' |
|
85 |
+ ) |
|
86 |
+ |
|
87 |
+ def test_216_lots_of_spaces(self): |
|
88 |
+ assert ( |
|
89 |
+ Vault(phrase=self.phrase, space=12) |
|
90 |
+ .generate('songkick') == b' c 6 Bq % 5fR ' |
|
91 |
+ ) |
|
92 |
+ |
|
93 |
+ def test_217_all_character_classes(self): |
|
94 |
+ assert ( |
|
95 |
+ Vault(phrase=self.phrase, lower=2, upper=2, number=1, |
|
96 |
+ space=3, dash=2, symbol=1) |
|
97 |
+ .generate('google') == b': : fv_wqt>a-4w1S R' |
|
98 |
+ ) |
|
99 |
+ |
|
100 |
+ def test_218_only_numbers_and_very_high_repetition_limit(self): |
|
101 |
+ generated = Vault(phrase=b'', length=40, lower=0, upper=0, space=0, |
|
102 |
+ dash=0, symbol=0, repeat=4).generate('abcdef') |
|
103 |
+ forbidden_substrings = {b'0000', b'1111', b'2222', b'3333', b'4444', |
|
104 |
+ b'5555', b'6666', b'7777', b'8888', b'9999'} |
|
105 |
+ for substring in forbidden_substrings: |
|
106 |
+ assert substring not in generated |
|
107 |
+ |
|
108 |
+ def test_219_very_limited_character_set(self): |
|
109 |
+ generated = Vault(phrase=b'', length=24, lower=0, upper=0, |
|
110 |
+ space=0, symbol=0).generate('testing') |
|
111 |
+ assert b'763252593304946694588866' == generated |
|
112 |
+ |
|
113 |
+ def test_220_character_set_subtraction(self): |
|
114 |
+ assert Vault._subtract(b'be', b'abcdef') == bytearray(b'acdf') |
|
115 |
+ |
|
116 |
+ @pytest.mark.parametrize(['length', 'settings', 'entropy'], [ |
|
117 |
+ (20, {}, math.log2(math.factorial(20)) + 20 * math.log2(94)), |
|
118 |
+ ( |
|
119 |
+ 20, |
|
120 |
+ {'upper': 0, 'number': 0, 'space': 0, 'symbol': 0}, |
|
121 |
+ math.log2(math.factorial(20)) + 20 * math.log2(26) |
|
122 |
+ ), |
|
123 |
+ (0, {}, float('-inf')), |
|
124 |
+ (0, {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0}, float('-inf')), |
|
125 |
+ (1, {}, math.log2(94)), |
|
126 |
+ (1, {'upper': 0, 'lower': 0, 'number': 0, 'symbol': 0}, 0.0), |
|
127 |
+ ]) |
|
128 |
+ def test_221_entropy( |
|
129 |
+ self, length: int, settings: dict[str, int], entropy: int |
|
130 |
+ ) -> None: |
|
131 |
+ v = Vault(length=length, **settings) # type: ignore[arg-type] |
|
132 |
+ assert math.isclose(v._entropy(), entropy) |
|
133 |
+ assert v._estimate_sufficient_hash_length() > 0 |
|
134 |
+ if math.isfinite(entropy) and entropy: |
|
135 |
+ assert ( |
|
136 |
+ v._estimate_sufficient_hash_length(1.0) == |
|
137 |
+ math.ceil(entropy / 8) |
|
138 |
+ ) |
|
139 |
+ assert v._estimate_sufficient_hash_length(8.0) >= entropy |
|
140 |
+ |
|
141 |
+ def test_222_hash_length_estimation(self) -> None: |
|
142 |
+ v = Vault(phrase=self.phrase) |
|
143 |
+ v2 = Vault(phrase=self.phrase, lower=0, upper=0, number=0, |
|
144 |
+ symbol=0, space=1, length=1) |
|
145 |
+ assert v2._entropy() == 0.0 |
|
146 |
+ assert v2._estimate_sufficient_hash_length() > 0 |
|
147 |
+ |
|
148 |
+ @pytest.mark.parametrize(['service', 'expected'], [ |
|
149 |
+ (b'google', google_phrase), |
|
150 |
+ ('twitter', twitter_phrase), |
|
151 |
+ ]) |
|
152 |
+ def test_223_hash_length_expansion( |
|
153 |
+ self, monkeypatch: Any, service: str | bytes, expected: bytes |
|
154 |
+ ) -> None: |
|
155 |
+ v = Vault(phrase=self.phrase) |
|
156 |
+ monkeypatch.setattr(v, |
|
157 |
+ '_estimate_sufficient_hash_length', |
|
158 |
+ lambda *args, **kwargs: 1) |
|
159 |
+ assert v._estimate_sufficient_hash_length() < len(self.phrase) |
|
160 |
+ assert v.generate(service) == expected |
|
161 |
+ |
|
162 |
+ @pytest.mark.parametrize(['s', 'raises'], [ |
|
163 |
+ ('ñ', True), ('Düsseldorf', True), |
|
164 |
+ ('liberté, egalité, fraternité', True), ('ASCII', False), |
|
165 |
+ ('Düsseldorf'.encode('UTF-8'), False), |
|
166 |
+ (bytearray([2, 3, 5, 7, 11, 13]), False), |
|
167 |
+ ]) |
|
168 |
+ def test_224_binary_strings( |
|
169 |
+ self, s: str | bytes | bytearray, raises: bool |
|
170 |
+ ) -> None: |
|
171 |
+ binstr = derivepassphrase.Vault._get_binary_string |
|
172 |
+ AmbiguousByteRepresentationError = ( |
|
173 |
+ derivepassphrase.AmbiguousByteRepresentationError |
|
174 |
+ ) |
|
175 |
+ if raises: |
|
176 |
+ with pytest.raises(AmbiguousByteRepresentationError): |
|
177 |
+ binstr(s) |
|
178 |
+ elif isinstance(s, str): |
|
179 |
+ assert binstr(s) == s.encode('UTF-8') |
|
180 |
+ assert binstr(binstr(s)) == s.encode('UTF-8') |
|
181 |
+ else: |
|
182 |
+ assert binstr(s) == bytes(s) |
|
183 |
+ assert binstr(binstr(s)) == bytes(s) |
|
184 |
+ |
|
185 |
+ def test_310_too_many_symbols(self): |
|
186 |
+ with pytest.raises(ValueError, |
|
187 |
+ match='requested passphrase length too short'): |
|
188 |
+ Vault(phrase=self.phrase, symbol=100) |
|
189 |
+ |
|
190 |
+ def test_311_no_viable_characters(self): |
|
191 |
+ with pytest.raises(ValueError, |
|
192 |
+ match='no allowed characters left'): |
|
193 |
+ Vault(phrase=self.phrase, lower=0, upper=0, number=0, |
|
194 |
+ space=0, dash=0, symbol=0) |
|
195 |
+ |
|
196 |
+ def test_320_character_set_subtraction_duplicate(self): |
|
197 |
+ with pytest.raises(ValueError, match='duplicate characters'): |
|
198 |
+ Vault._subtract(b'abcdef', b'aabbccddeeff') |
|
199 |
+ with pytest.raises(ValueError, match='duplicate characters'): |
|
200 |
+ Vault._subtract(b'aabbccddeeff', b'abcdef') |
|
201 |
+ |
|
202 |
+ def test_322_hash_length_estimation(self) -> None: |
|
203 |
+ v = Vault(phrase=self.phrase) |
|
204 |
+ with pytest.raises(ValueError, |
|
205 |
+ match='invalid safety factor'): |
|
206 |
+ assert v._estimate_sufficient_hash_length(-1.0) |
|
207 |
+ with pytest.raises(TypeError, |
|
208 |
+ match='invalid safety factor: not a float'): |
|
209 |
+ assert v._estimate_sufficient_hash_length(None) # type: ignore |
... | ... |
@@ -5,10 +5,9 @@ |
5 | 5 |
from __future__ import annotations |
6 | 6 |
|
7 | 7 |
import base64 |
8 |
-import contextlib |
|
9 |
-import errno |
|
10 | 8 |
import json |
11 | 9 |
import os |
10 |
+import socket |
|
12 | 11 |
from typing import Any, cast, TYPE_CHECKING, NamedTuple |
13 | 12 |
|
14 | 13 |
import click.testing |
... | ... |
@@ -16,25 +15,19 @@ import derivepassphrase as dpp |
16 | 15 |
import derivepassphrase.cli as cli |
17 | 16 |
import ssh_agent_client.types |
18 | 17 |
import pytest |
18 |
+import tests |
|
19 | 19 |
|
20 |
-DUMMY_SERVICE = 'service1' |
|
21 |
-DUMMY_PASSPHRASE = b'my secret passphrase\n' |
|
22 |
-DUMMY_CONFIG_SETTINGS = {"length": 10, "upper": 1, "lower": 1, "repeat": 5, |
|
23 |
- "number": 1, "space": 1, "dash": 1, "symbol": 1} |
|
24 |
-DUMMY_RESULT_PASSPHRASE = b'.2V_QJkd o' |
|
25 |
-DUMMY_RESULT_KEY1 = b'E<b<{ -7iG' |
|
26 |
-DUMMY_PHRASE_FROM_KEY1_RAW = ( |
|
27 |
- b'\x00\x00\x00\x0bssh-ed25519' |
|
28 |
- b'\x00\x00\x00@\xf0\x98\x19\x80l\x1a\x97\xd5&\x03n' |
|
29 |
- b'\xcc\xe3e\x8f\x86f\x07\x13\x19\x13\t!33\xf9\xe46S' |
|
30 |
- b'\x1d\xaf\xfd\r\x08\x1f\xec\xf8s\x9b\x8c_U9\x16|ST,' |
|
31 |
- b'\x1eR\xbb0\xed\x7f\x89\xe2/iQU\xd8\x9e\xa6\x02' |
|
32 |
-) |
|
33 |
-DUMMY_PHRASE_FROM_KEY1 = b'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==' |
|
20 |
+DUMMY_SERVICE = tests.DUMMY_SERVICE |
|
21 |
+DUMMY_PASSPHRASE = tests.DUMMY_PASSPHRASE |
|
22 |
+DUMMY_CONFIG_SETTINGS = tests.DUMMY_CONFIG_SETTINGS |
|
23 |
+DUMMY_RESULT_PASSPHRASE = tests.DUMMY_RESULT_PASSPHRASE |
|
24 |
+DUMMY_RESULT_KEY1 = tests.DUMMY_RESULT_KEY1 |
|
25 |
+DUMMY_PHRASE_FROM_KEY1_RAW = tests.DUMMY_PHRASE_FROM_KEY1_RAW |
|
26 |
+DUMMY_PHRASE_FROM_KEY1 = tests.DUMMY_PHRASE_FROM_KEY1 |
|
34 | 27 |
|
35 |
-# See Ed25519 and RSA test keys in test_key_signing.py |
|
36 |
-DUMMY_KEY1 = 'AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2' |
|
37 |
-DUMMY_KEY2 = 'AAAAB3NzaC1yc2EAAAADAQABAAABgQCxoe7pezhxWy4NI0mUwKqg9WCYOAS+IjxN9eYcqpfcmQiojcuy9XsiN/xYJ1O94SrsKS5mEia2xHnYA4RUChTyYNcM2v6cnnBQ/N/VQhpGMN7SVxdbhKUXTWFCwbjBgO6rGyHB6WtoH8vd7TOEPt+NgcXwhsWyoaUUdYTA62V+GF9vEmxMaC4ubgDz+B0QkPnauSoNxmkhcIe0lsLNb1pClZyz88PDnKXCX/d0HuN/HJ+sbPg7dCvOyqFYSyKn3uY6bCXqoIdurxXzH3O7z0P8f5sbmKOrGGKNuNxVRbeVl/D/3uDL0nqsbfUc1qvkfwbJwtMXC4IV6kOZMSk2BAsqh7x48gQ+rhYeEVSi8F3CWs4HJQoqrGt7K9a3mCSlMBHP70u3w6ME7eumoryxlUofewTd17ZEkzdX08l2ZlKzZvwQUrc+xQZ2Uw8z2mfW6Ti4gi0pYGaig7Ke4PwuXpo/C5YAWfeXycsvJZ2uaYRjMdZeJGNAnHLUGLkBscw5aI8=' |
|
28 |
+DUMMY_KEY1 = tests.DUMMY_KEY1 |
|
29 |
+DUMMY_KEY1_B64 = tests.DUMMY_KEY1_B64 |
|
30 |
+DUMMY_KEY2 = tests.DUMMY_KEY2 |
|
38 | 31 |
|
39 | 32 |
|
40 | 33 |
class IncompatibleConfiguration(NamedTuple): |
... | ... |
@@ -156,151 +149,9 @@ for opt, config in SINGLES.items(): |
156 | 149 |
input=config.input, |
157 | 150 |
check_success=config.check_success)) |
158 | 151 |
|
159 |
-@contextlib.contextmanager |
|
160 |
-def isolated_config( |
|
161 |
- monkeypatch: Any, runner: click.testing.CliRunner, config: Any, |
|
162 |
-): |
|
163 |
- with runner.isolated_filesystem(): |
|
164 |
- monkeypatch.setenv('HOME', os.getcwd()) |
|
165 |
- monkeypatch.setenv('USERPROFILE', os.getcwd()) |
|
166 |
- monkeypatch.delenv(cli.prog_name.replace(' ', '_').upper() + '_PATH', |
|
167 |
- raising=False) |
|
168 |
- os.makedirs(os.path.dirname(cli._config_filename()), exist_ok=True) |
|
169 |
- with open(cli._config_filename(), 'wt') as outfile: |
|
170 |
- json.dump(config, outfile) |
|
171 |
- yield |
|
172 |
- |
|
173 |
- |
|
174 |
-def test_100_save_bad_config(monkeypatch: Any) -> None: |
|
175 |
- runner = click.testing.CliRunner() |
|
176 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, config={}): |
|
177 |
- with pytest.raises(ValueError, match='Invalid vault config'): |
|
178 |
- cli._save_config(None) # type: ignore |
|
179 |
- |
|
180 |
- |
|
181 |
-def test_101_prompt_for_selection_multiple(monkeypatch: Any) -> None: |
|
182 |
- @click.command() |
|
183 |
- @click.option('--heading', default='Our menu:') |
|
184 |
- @click.argument('items', nargs=-1) |
|
185 |
- def driver(heading, items): |
|
186 |
- # from https://montypython.fandom.com/wiki/Spam#The_menu |
|
187 |
- items = items or [ |
|
188 |
- 'Egg and bacon', |
|
189 |
- 'Egg, sausage and bacon', |
|
190 |
- 'Egg and spam', |
|
191 |
- 'Egg, bacon and spam', |
|
192 |
- 'Egg, bacon, sausage and spam', |
|
193 |
- 'Spam, bacon, sausage and spam', |
|
194 |
- 'Spam, egg, spam, spam, bacon and spam', |
|
195 |
- 'Spam, spam, spam, egg and spam', |
|
196 |
- ('Spam, spam, spam, spam, spam, spam, baked beans, ' |
|
197 |
- 'spam, spam, spam and spam'), |
|
198 |
- ('Lobster thermidor aux crevettes with a mornay sauce ' |
|
199 |
- 'garnished with truffle paté, brandy ' |
|
200 |
- 'and a fried egg on top and spam'), |
|
201 |
- ] |
|
202 |
- index = cli._prompt_for_selection(items, heading=heading) |
|
203 |
- click.echo('A fine choice: ', nl=False) |
|
204 |
- click.echo(items[index]) |
|
205 |
- click.echo('(Note: Vikings strictly optional.)') |
|
206 |
- runner = click.testing.CliRunner(mix_stderr=True) |
|
207 |
- result = runner.invoke(driver, [], input='9') |
|
208 |
- assert result.exit_code == 0, 'driver program failed' |
|
209 |
- assert result.stdout == '''\ |
|
210 |
-Our menu: |
|
211 |
-[1] Egg and bacon |
|
212 |
-[2] Egg, sausage and bacon |
|
213 |
-[3] Egg and spam |
|
214 |
-[4] Egg, bacon and spam |
|
215 |
-[5] Egg, bacon, sausage and spam |
|
216 |
-[6] Spam, bacon, sausage and spam |
|
217 |
-[7] Spam, egg, spam, spam, bacon and spam |
|
218 |
-[8] Spam, spam, spam, egg and spam |
|
219 |
-[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam |
|
220 |
-[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam |
|
221 |
-Your selection? (1-10, leave empty to abort): 9 |
|
222 |
-A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam |
|
223 |
-(Note: Vikings strictly optional.) |
|
224 |
-''', 'driver program produced unexpected output' |
|
225 |
- result = runner.invoke(driver, ['--heading='], input='', |
|
226 |
- catch_exceptions=True) |
|
227 |
- assert result.exit_code > 0, 'driver program succeeded?!' |
|
228 |
- assert result.stdout == '''\ |
|
229 |
-[1] Egg and bacon |
|
230 |
-[2] Egg, sausage and bacon |
|
231 |
-[3] Egg and spam |
|
232 |
-[4] Egg, bacon and spam |
|
233 |
-[5] Egg, bacon, sausage and spam |
|
234 |
-[6] Spam, bacon, sausage and spam |
|
235 |
-[7] Spam, egg, spam, spam, bacon and spam |
|
236 |
-[8] Spam, spam, spam, egg and spam |
|
237 |
-[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam |
|
238 |
-[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam |
|
239 |
-Your selection? (1-10, leave empty to abort): \n''', ( |
|
240 |
- 'driver program produced unexpected output' |
|
241 |
- ) |
|
242 |
- assert isinstance(result.exception, IndexError), ( |
|
243 |
- 'driver program did not raise IndexError?!' |
|
244 |
- ) |
|
245 |
- |
|
246 |
- |
|
247 |
-def test_102_prompt_for_selection_single(monkeypatch: Any) -> None: |
|
248 |
- @click.command() |
|
249 |
- @click.option('--item', default='baked beans') |
|
250 |
- @click.argument('prompt') |
|
251 |
- def driver(item, prompt): |
|
252 |
- try: |
|
253 |
- cli._prompt_for_selection([item], heading='', |
|
254 |
- single_choice_prompt=prompt) |
|
255 |
- except IndexError as e: |
|
256 |
- click.echo('Boo.') |
|
257 |
- raise e |
|
258 |
- else: |
|
259 |
- click.echo('Great!') |
|
260 |
- runner = click.testing.CliRunner(mix_stderr=True) |
|
261 |
- result = runner.invoke(driver, ['Will replace with spam. Confirm, y/n?'], |
|
262 |
- input='y') |
|
263 |
- assert result.exit_code == 0, 'driver program failed' |
|
264 |
- assert result.stdout == '''\ |
|
265 |
-[1] baked beans |
|
266 |
-Will replace with spam. Confirm, y/n? y |
|
267 |
-Great! |
|
268 |
-''', 'driver program produced unexpected output' |
|
269 |
- result = runner.invoke(driver, |
|
270 |
- ['Will replace with spam, okay? ' + |
|
271 |
- '(Please say "y" or "n".)'], |
|
272 |
- input='') |
|
273 |
- assert result.exit_code > 0, 'driver program succeeded?!' |
|
274 |
- assert result.stdout == '''\ |
|
275 |
-[1] baked beans |
|
276 |
-Will replace with spam, okay? (Please say "y" or "n".): |
|
277 |
-Boo. |
|
278 |
-''', 'driver program produced unexpected output' |
|
279 |
- assert isinstance(result.exception, IndexError), ( |
|
280 |
- 'driver program did not raise IndexError?!' |
|
281 |
- ) |
|
282 |
- |
|
283 |
- |
|
284 |
-def test_103_prompt_for_passphrase(monkeypatch: Any) -> None: |
|
285 |
- monkeypatch.setattr(click, 'prompt', |
|
286 |
- lambda *a, **kw: json.dumps({'args': a, 'kwargs': kw})) |
|
287 |
- res = json.loads(cli._prompt_for_passphrase()) |
|
288 |
- assert 'args' in res and 'kwargs' in res, ( |
|
289 |
- 'missing arguments to passphrase prompt' |
|
290 |
- ) |
|
291 |
- assert res['args'][:1] == ['Passphrase'], ( |
|
292 |
- 'missing arguments to passphrase prompt' |
|
293 |
- ) |
|
294 |
- assert (res['kwargs'].get('default') == '' |
|
295 |
- and not res['kwargs'].get('show_default', True)), ( |
|
296 |
- 'missing arguments to passphrase prompt' |
|
297 |
- ) |
|
298 |
- assert res['kwargs'].get('err') and res['kwargs'].get('hide_input'), ( |
|
299 |
- 'missing arguments to passphrase prompt' |
|
300 |
- ) |
|
301 |
- |
|
302 | 152 |
|
303 |
-def test_200_help_output(): |
|
153 |
+class TestCLI: |
|
154 |
+ def test_200_help_output(self): |
|
304 | 155 |
runner = click.testing.CliRunner(mix_stderr=False) |
305 | 156 |
result = runner.invoke(cli.derivepassphrase, ['--help'], |
306 | 157 |
catch_exceptions=False) |
... | ... |
@@ -316,10 +167,9 @@ def test_200_help_output(): |
316 | 167 |
[('lower',), ('upper',), ('number',), ('space',), |
317 | 168 |
('dash',), ('symbol',)]) |
318 | 169 |
def test_201_disable_character_set( |
319 |
- monkeypatch: Any, charset_name: str |
|
170 |
+ self, monkeypatch: Any, charset_name: str |
|
320 | 171 |
) -> None: |
321 |
- monkeypatch.setattr(cli, '_prompt_for_passphrase', |
|
322 |
- lambda *a, **kw: DUMMY_PASSPHRASE.decode('UTF-8')) |
|
172 |
+ monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt) |
|
323 | 173 |
option = f'--{charset_name}' |
324 | 174 |
charset = dpp.Vault._CHARSETS[charset_name].decode('ascii') |
325 | 175 |
runner = click.testing.CliRunner(mix_stderr=False) |
... | ... |
@@ -338,9 +188,8 @@ def test_201_disable_character_set( |
338 | 188 |
f'{result.stdout!r}' |
339 | 189 |
) |
340 | 190 |
|
341 |
-def test_202_disable_repetition(monkeypatch: Any) -> None: |
|
342 |
- monkeypatch.setattr(cli, '_prompt_for_passphrase', |
|
343 |
- lambda *a, **kw: DUMMY_PASSPHRASE.decode('UTF-8')) |
|
191 |
+ def test_202_disable_repetition(self, monkeypatch: Any) -> None: |
|
192 |
+ monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt) |
|
344 | 193 |
runner = click.testing.CliRunner(mix_stderr=False) |
345 | 194 |
result = runner.invoke(cli.derivepassphrase, |
346 | 195 |
['--repeat', '0', '-p', DUMMY_SERVICE], |
... | ... |
@@ -358,92 +207,47 @@ def test_202_disable_repetition(monkeypatch: Any) -> None: |
358 | 207 |
f'at position {i}: {result.stdout!r}' |
359 | 208 |
) |
360 | 209 |
|
361 |
-@pytest.mark.parametrize(['command_line', 'config', 'result_config'], [ |
|
362 |
- (['--delete-globals'], |
|
363 |
- {'global': {'phrase': 'abc'}, 'services': {}}, {'services': {}}), |
|
364 |
- (['--delete', DUMMY_SERVICE], |
|
365 |
- {'global': {'phrase': 'abc'}, |
|
366 |
- 'services': {DUMMY_SERVICE: {'notes': '...'}}}, |
|
367 |
- {'global': {'phrase': 'abc'}, 'services': {}}), |
|
368 |
- (['--clear'], |
|
369 |
- {'global': {'phrase': 'abc'}, |
|
370 |
- 'services': {DUMMY_SERVICE: {'notes': '...'}}}, |
|
371 |
- {'services': {}}), |
|
372 |
-]) |
|
373 |
-def test_203_repeated_config_deletion( |
|
374 |
- monkeypatch: Any, command_line: list[str], |
|
375 |
- config: dpp.types.VaultConfig, result_config: dpp.types.VaultConfig, |
|
376 |
-) -> None: |
|
377 |
- runner = click.testing.CliRunner(mix_stderr=False) |
|
378 |
- for start_config in [config, result_config]: |
|
379 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
380 |
- config=start_config): |
|
381 |
- result = runner.invoke(cli.derivepassphrase, command_line, |
|
382 |
- catch_exceptions=False) |
|
383 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
384 |
- 'program exited with failure' |
|
385 |
- ) |
|
386 |
- with open(cli._config_filename(), 'rt') as infile: |
|
387 |
- config_readback = json.load(infile) |
|
388 |
- assert config_readback == result_config |
|
389 |
- |
|
390 |
-def test_204_phrase_from_key_manually() -> None: |
|
391 |
- assert ( |
|
392 |
- dpp.Vault(phrase=DUMMY_PHRASE_FROM_KEY1, **DUMMY_CONFIG_SETTINGS) |
|
393 |
- .generate(DUMMY_SERVICE) == DUMMY_RESULT_KEY1 |
|
394 |
- ) |
|
395 |
- |
|
396 | 210 |
@pytest.mark.parametrize(['config'], [ |
397 |
- pytest.param({'global': {'key': DUMMY_KEY1}, |
|
211 |
+ pytest.param({'global': {'key': DUMMY_KEY1_B64}, |
|
398 | 212 |
'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}}, |
399 | 213 |
id='global'), |
400 |
- pytest.param({'global': {'phrase': DUMMY_PASSPHRASE.rstrip(b'\n').decode('ASCII')}, |
|
401 |
- 'services': {DUMMY_SERVICE: {'key': DUMMY_KEY1, |
|
214 |
+ pytest.param({'global': {'phrase': DUMMY_PASSPHRASE.rstrip(b'\n') |
|
215 |
+ .decode('ASCII')}, |
|
216 |
+ 'services': {DUMMY_SERVICE: {'key': DUMMY_KEY1_B64, |
|
402 | 217 |
**DUMMY_CONFIG_SETTINGS}}}, |
403 | 218 |
id='service'), |
404 | 219 |
]) |
405 | 220 |
def test_204a_key_from_config( |
406 |
- monkeypatch: Any, config: dpp.types.VaultConfig, |
|
221 |
+ self, monkeypatch: Any, config: dpp.types.VaultConfig, |
|
407 | 222 |
) -> None: |
408 |
- def phrase_from_key(key: bytes) -> bytes: |
|
409 |
- if key == base64.standard_b64decode(DUMMY_KEY1): # pragma: no branch |
|
410 |
- return DUMMY_PHRASE_FROM_KEY1 |
|
411 |
- raise KeyError(key) # pragma: no cover |
|
412 | 223 |
runner = click.testing.CliRunner(mix_stderr=False) |
413 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
224 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
414 | 225 |
config=config): |
415 | 226 |
monkeypatch.setattr(dpp.Vault, 'phrase_from_key', |
416 |
- phrase_from_key) |
|
227 |
+ tests.phrase_from_key) |
|
417 | 228 |
result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE], |
418 | 229 |
catch_exceptions=False) |
419 | 230 |
assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
420 | 231 |
'program exited with failure' |
421 | 232 |
) |
422 |
- assert result.stdout_bytes.rstrip(b'\n') != DUMMY_RESULT_PASSPHRASE, ( |
|
233 |
+ assert ( |
|
234 |
+ result.stdout_bytes.rstrip(b'\n') != DUMMY_RESULT_PASSPHRASE |
|
235 |
+ ), ( |
|
423 | 236 |
'program generated unexpected result (phrase instead of key)' |
424 | 237 |
) |
425 | 238 |
assert result.stdout_bytes.rstrip(b'\n') == DUMMY_RESULT_KEY1, ( |
426 | 239 |
'program generated unexpected result (wrong settings?)' |
427 | 240 |
) |
428 | 241 |
|
429 |
-def test_204b_key_from_command_line(monkeypatch: Any) -> None: |
|
430 |
- KeyCommentPair = ssh_agent_client.types.KeyCommentPair |
|
431 |
- key_list = [ |
|
432 |
- KeyCommentPair(base64.standard_b64decode(DUMMY_KEY1), b'no comment'), |
|
433 |
- KeyCommentPair(base64.standard_b64decode(DUMMY_KEY2), b'a comment'), |
|
434 |
- ] |
|
435 |
- def _suitable_ssh_keys(conn: Any) -> Iterator[KeyCommentPair]: |
|
436 |
- yield from key_list |
|
437 |
- def phrase_from_key(key: bytes) -> bytes: |
|
438 |
- if key == base64.standard_b64decode(DUMMY_KEY1): # pragma: no branch |
|
439 |
- return DUMMY_PHRASE_FROM_KEY1 |
|
440 |
- raise KeyError(key) # pragma: no cover |
|
242 |
+ def test_204b_key_from_command_line(self, monkeypatch: Any) -> None: |
|
441 | 243 |
runner = click.testing.CliRunner(mix_stderr=False) |
442 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
443 |
- config={'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}}): |
|
444 |
- monkeypatch.setattr(cli, '_get_suitable_ssh_keys', _suitable_ssh_keys) |
|
244 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
245 |
+ config={'services': {DUMMY_SERVICE: |
|
246 |
+ DUMMY_CONFIG_SETTINGS}}): |
|
247 |
+ monkeypatch.setattr(cli, '_get_suitable_ssh_keys', |
|
248 |
+ tests.suitable_ssh_keys) |
|
445 | 249 |
monkeypatch.setattr(dpp.Vault, 'phrase_from_key', |
446 |
- phrase_from_key) |
|
250 |
+ tests.phrase_from_key) |
|
447 | 251 |
result = runner.invoke(cli.derivepassphrase, |
448 | 252 |
['-k', DUMMY_SERVICE], |
449 | 253 |
input=b'1\n', catch_exceptions=False) |
... | ... |
@@ -457,15 +261,18 @@ def test_204b_key_from_command_line(monkeypatch: Any) -> None: |
457 | 261 |
'program generated unexpected result (wrong settings?)' |
458 | 262 |
) |
459 | 263 |
|
460 |
-def test_205_service_phrase_if_key_in_global_config(monkeypatch: Any) -> None: |
|
264 |
+ def test_205_service_phrase_if_key_in_global_config( |
|
265 |
+ self, monkeypatch: Any, |
|
266 |
+ ) -> None: |
|
461 | 267 |
runner = click.testing.CliRunner(mix_stderr=False) |
462 |
- with isolated_config( |
|
268 |
+ with tests.isolated_config( |
|
463 | 269 |
monkeypatch=monkeypatch, runner=runner, |
464 | 270 |
config={ |
465 |
- 'global': {'key': DUMMY_KEY1}, |
|
271 |
+ 'global': {'key': DUMMY_KEY1_B64}, |
|
466 | 272 |
'services': { |
467 | 273 |
DUMMY_SERVICE: { |
468 |
- 'phrase': DUMMY_PASSPHRASE.rstrip(b'\n').decode('ASCII'), |
|
274 |
+ 'phrase': DUMMY_PASSPHRASE.rstrip(b'\n') |
|
275 |
+ .decode('ASCII'), |
|
469 | 276 |
**DUMMY_CONFIG_SETTINGS}}} |
470 | 277 |
): |
471 | 278 |
result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE], |
... | ... |
@@ -484,13 +291,15 @@ def test_205_service_phrase_if_key_in_global_config(monkeypatch: Any) -> None: |
484 | 291 |
[('--lower',), ('--upper',), ('--number',), |
485 | 292 |
('--space',), ('--dash',), ('--symbol',), |
486 | 293 |
('--repeat',), ('--length',)]) |
487 |
-def test_210_invalid_argument_range(option: str) -> None: |
|
294 |
+ def test_210_invalid_argument_range(self, option: str) -> None: |
|
488 | 295 |
runner = click.testing.CliRunner(mix_stderr=False) |
489 | 296 |
value: str | int |
490 | 297 |
for value in '-42', 'invalid': |
491 | 298 |
result = runner.invoke(cli.derivepassphrase, |
492 |
- [option, cast(str, value), '-p', DUMMY_SERVICE], |
|
493 |
- input=DUMMY_PASSPHRASE, catch_exceptions=False) |
|
299 |
+ [option, cast(str, value), '-p', |
|
300 |
+ DUMMY_SERVICE], |
|
301 |
+ input=DUMMY_PASSPHRASE, |
|
302 |
+ catch_exceptions=False) |
|
494 | 303 |
assert result.exit_code > 0, ( |
495 | 304 |
'program unexpectedly succeeded' |
496 | 305 |
) |
... | ... |
@@ -501,33 +310,18 @@ def test_210_invalid_argument_range(option: str) -> None: |
501 | 310 |
'program did not print the expected error message' |
502 | 311 |
) |
503 | 312 |
|
504 |
-@pytest.mark.parametrize(['vfunc', 'input'], [ |
|
505 |
- (cli._validate_occurrence_constraint, 20), |
|
506 |
- (cli._validate_length, 20), |
|
507 |
-]) |
|
508 |
-def test_210a_validate_constraints_manually( |
|
509 |
- vfunc: Callable[[click.Context, click.Parameter, Any], int | None], |
|
510 |
- input: int, |
|
511 |
-) -> None: |
|
512 |
- ctx = cli.derivepassphrase.make_context(cli.prog_name, []) |
|
513 |
- param = cli.derivepassphrase.params[0] |
|
514 |
- assert vfunc(ctx, param, input) == input |
|
515 |
- |
|
516 | 313 |
@pytest.mark.parametrize( |
517 | 314 |
['options', 'service', 'input', 'check_success'], |
518 | 315 |
[(o.options, o.needs_service, o.input, o.check_success) |
519 | 316 |
for o in INTERESTING_OPTION_COMBINATIONS if not o.incompatible], |
520 | 317 |
) |
521 | 318 |
def test_211_service_needed( |
522 |
- monkeypatch: Any, options: list[str], |
|
319 |
+ self, monkeypatch: Any, options: list[str], |
|
523 | 320 |
service: bool | None, input: bytes | None, check_success: bool, |
524 | 321 |
) -> None: |
525 |
- def _prompt1(*args, **kwargs): |
|
526 |
- """Needed for --config handling.""" |
|
527 |
- return DUMMY_PASSPHRASE.decode('UTF-8') |
|
528 |
- monkeypatch.setattr(cli, '_prompt_for_passphrase', _prompt1) |
|
322 |
+ monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt) |
|
529 | 323 |
runner = click.testing.CliRunner(mix_stderr=False) |
530 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
324 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
531 | 325 |
config={'global': {'phrase': 'abc'}, |
532 | 326 |
'services': {}}): |
533 | 327 |
result = runner.invoke(cli.derivepassphrase, |
... | ... |
@@ -551,12 +345,11 @@ def test_211_service_needed( |
551 | 345 |
'program unexpectedly failed' |
552 | 346 |
) |
553 | 347 |
if check_success: |
554 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
348 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
555 | 349 |
config={'global': {'phrase': 'abc'}, |
556 | 350 |
'services': {}}): |
557 |
- def _prompt2(*args, **kwargs): |
|
558 |
- return DUMMY_PASSPHRASE.decode('UTF-8') |
|
559 |
- monkeypatch.setattr(cli, '_prompt_for_passphrase', _prompt2) |
|
351 |
+ monkeypatch.setattr(cli, '_prompt_for_passphrase', |
|
352 |
+ tests.auto_prompt) |
|
560 | 353 |
result = runner.invoke(cli.derivepassphrase, |
561 | 354 |
options + [DUMMY_SERVICE] |
562 | 355 |
if service else options, |
... | ... |
@@ -571,11 +364,12 @@ def test_211_service_needed( |
571 | 364 |
for o in INTERESTING_OPTION_COMBINATIONS if o.incompatible], |
572 | 365 |
) |
573 | 366 |
def test_212_incompatible_options( |
574 |
- options: list[str], service: bool | None, input: bytes | None, |
|
367 |
+ self, options: list[str], service: bool | None, input: bytes | None, |
|
575 | 368 |
) -> None: |
576 | 369 |
runner = click.testing.CliRunner(mix_stderr=False) |
577 | 370 |
result = runner.invoke(cli.derivepassphrase, |
578 |
- options + [DUMMY_SERVICE] if service else options, |
|
371 |
+ options + [DUMMY_SERVICE] if service |
|
372 |
+ else options, |
|
579 | 373 |
input=DUMMY_PASSPHRASE, catch_exceptions=False) |
580 | 374 |
assert result.exit_code > 0, ( |
581 | 375 |
'program unexpectedly succeeded' |
... | ... |
@@ -587,9 +381,11 @@ def test_212_incompatible_options( |
587 | 381 |
'program did not print the expected error message' |
588 | 382 |
) |
589 | 383 |
|
590 |
-def test_213_import_bad_config_not_vault_config(monkeypatch: Any) -> None: |
|
384 |
+ def test_213_import_bad_config_not_vault_config( |
|
385 |
+ self, monkeypatch: Any, |
|
386 |
+ ) -> None: |
|
591 | 387 |
runner = click.testing.CliRunner(mix_stderr=False) |
592 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
388 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
593 | 389 |
config={'services': {}}): |
594 | 390 |
result = runner.invoke(cli.derivepassphrase, ['--import', '-'], |
595 | 391 |
input=b'null', catch_exceptions=False) |
... | ... |
@@ -603,9 +399,11 @@ def test_213_import_bad_config_not_vault_config(monkeypatch: Any) -> None: |
603 | 399 |
'program did not print the expected error message' |
604 | 400 |
) |
605 | 401 |
|
606 |
-def test_213a_import_bad_config_not_json_data(monkeypatch: Any) -> None: |
|
402 |
+ def test_213a_import_bad_config_not_json_data( |
|
403 |
+ self, monkeypatch: Any, |
|
404 |
+ ) -> None: |
|
607 | 405 |
runner = click.testing.CliRunner(mix_stderr=False) |
608 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
406 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
609 | 407 |
config={'services': {}}): |
610 | 408 |
result = runner.invoke(cli.derivepassphrase, ['--import', '-'], |
611 | 409 |
input=b'This string is not valid JSON.', |
... | ... |
@@ -620,9 +418,11 @@ def test_213a_import_bad_config_not_json_data(monkeypatch: Any) -> None: |
620 | 418 |
'program did not print the expected error message' |
621 | 419 |
) |
622 | 420 |
|
623 |
-def test_213b_import_bad_config_not_a_file(monkeypatch: Any) -> None: |
|
421 |
+ def test_213b_import_bad_config_not_a_file( |
|
422 |
+ self, monkeypatch: Any, |
|
423 |
+ ) -> None: |
|
624 | 424 |
runner = click.testing.CliRunner(mix_stderr=False) |
625 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
425 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
626 | 426 |
config={'services': {}}): |
627 | 427 |
with open(cli._config_filename(), 'wt') as outfile: |
628 | 428 |
print('This string is not valid JSON.', file=outfile) |
... | ... |
@@ -639,9 +439,11 @@ def test_213b_import_bad_config_not_a_file(monkeypatch: Any) -> None: |
639 | 439 |
# Don't test the actual error message, because it is subject to |
640 | 440 |
# locale settings. TODO: find a way anyway. |
641 | 441 |
|
642 |
-def test_214_export_settings_no_stored_settings(monkeypatch: Any) -> None: |
|
442 |
+ def test_214_export_settings_no_stored_settings( |
|
443 |
+ self, monkeypatch: Any, |
|
444 |
+ ) -> None: |
|
643 | 445 |
runner = click.testing.CliRunner(mix_stderr=False) |
644 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
446 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
645 | 447 |
config={'services': {}}): |
646 | 448 |
try: |
647 | 449 |
os.remove(cli._config_filename()) |
... | ... |
@@ -653,9 +455,11 @@ def test_214_export_settings_no_stored_settings(monkeypatch: Any) -> None: |
653 | 455 |
'program exited with failure' |
654 | 456 |
) |
655 | 457 |
|
656 |
-def test_214a_export_settings_bad_stored_config(monkeypatch: Any) -> None: |
|
458 |
+ def test_214a_export_settings_bad_stored_config( |
|
459 |
+ self, monkeypatch: Any, |
|
460 |
+ ) -> None: |
|
657 | 461 |
runner = click.testing.CliRunner(mix_stderr=False) |
658 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
462 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
659 | 463 |
config={}): |
660 | 464 |
result = runner.invoke(cli.derivepassphrase, ['--export', '-'], |
661 | 465 |
input=b'null', catch_exceptions=False) |
... | ... |
@@ -669,9 +473,11 @@ def test_214a_export_settings_bad_stored_config(monkeypatch: Any) -> None: |
669 | 473 |
'program did not print the expected error message' |
670 | 474 |
) |
671 | 475 |
|
672 |
-def test_214b_export_settings_not_a_file(monkeypatch: Any) -> None: |
|
476 |
+ def test_214b_export_settings_not_a_file( |
|
477 |
+ self, monkeypatch: Any, |
|
478 |
+ ) -> None: |
|
673 | 479 |
runner = click.testing.CliRunner(mix_stderr=False) |
674 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
480 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
675 | 481 |
config={'services': {}}): |
676 | 482 |
try: |
677 | 483 |
os.remove(cli._config_filename()) |
... | ... |
@@ -690,9 +496,11 @@ def test_214b_export_settings_not_a_file(monkeypatch: Any) -> None: |
690 | 496 |
'program did not print the expected error message' |
691 | 497 |
) |
692 | 498 |
|
693 |
-def test_214c_export_settings_target_not_a_file(monkeypatch: Any) -> None: |
|
499 |
+ def test_214c_export_settings_target_not_a_file( |
|
500 |
+ self, monkeypatch: Any, |
|
501 |
+ ) -> None: |
|
694 | 502 |
runner = click.testing.CliRunner(mix_stderr=False) |
695 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
503 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
696 | 504 |
config={'services': {}}): |
697 | 505 |
dname = os.path.dirname(cli._config_filename()) |
698 | 506 |
result = runner.invoke(cli.derivepassphrase, |
... | ... |
@@ -708,14 +516,14 @@ def test_214c_export_settings_target_not_a_file(monkeypatch: Any) -> None: |
708 | 516 |
'program did not print the expected error message' |
709 | 517 |
) |
710 | 518 |
|
711 |
-def test_220_edit_notes_successfully(monkeypatch: Any) -> None: |
|
519 |
+ def test_220_edit_notes_successfully(self, monkeypatch: Any) -> None: |
|
712 | 520 |
edit_result = ''' |
713 | 521 |
|
714 | 522 |
# - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - - |
715 | 523 |
contents go here |
716 | 524 |
''' |
717 | 525 |
runner = click.testing.CliRunner(mix_stderr=False) |
718 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
526 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
719 | 527 |
config={'global': {'phrase': 'abc'}, |
720 | 528 |
'services': {}}): |
721 | 529 |
monkeypatch.setattr(click, 'edit', |
... | ... |
@@ -728,11 +536,12 @@ contents go here |
728 | 536 |
with open(cli._config_filename(), 'rt') as infile: |
729 | 537 |
config = json.load(infile) |
730 | 538 |
assert config == {'global': {'phrase': 'abc'}, |
731 |
- 'services': {'sv': {'notes': 'contents go here'}}} |
|
539 |
+ 'services': {'sv': {'notes': |
|
540 |
+ 'contents go here'}}} |
|
732 | 541 |
|
733 |
-def test_221_edit_notes_noop(monkeypatch: Any) -> None: |
|
542 |
+ def test_221_edit_notes_noop(self, monkeypatch: Any) -> None: |
|
734 | 543 |
runner = click.testing.CliRunner(mix_stderr=False) |
735 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
544 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
736 | 545 |
config={'global': {'phrase': 'abc'}, |
737 | 546 |
'services': {}}): |
738 | 547 |
monkeypatch.setattr(click, 'edit', lambda *a, **kw: None) |
... | ... |
@@ -745,9 +554,9 @@ def test_221_edit_notes_noop(monkeypatch: Any) -> None: |
745 | 554 |
config = json.load(infile) |
746 | 555 |
assert config == {'global': {'phrase': 'abc'}, 'services': {}} |
747 | 556 |
|
748 |
-def test_222_edit_notes_marker_removed(monkeypatch: Any) -> None: |
|
557 |
+ def test_222_edit_notes_marker_removed(self, monkeypatch: Any) -> None: |
|
749 | 558 |
runner = click.testing.CliRunner(mix_stderr=False) |
750 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
559 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
751 | 560 |
config={'global': {'phrase': 'abc'}, |
752 | 561 |
'services': {}}): |
753 | 562 |
monkeypatch.setattr(click, 'edit', lambda *a, **kw: 'long\ntext') |
... | ... |
@@ -761,9 +570,9 @@ def test_222_edit_notes_marker_removed(monkeypatch: Any) -> None: |
761 | 570 |
assert config == {'global': {'phrase': 'abc'}, |
762 | 571 |
'services': {'sv': {'notes': 'long\ntext'}}} |
763 | 572 |
|
764 |
-def test_223_edit_notes_abort(monkeypatch: Any) -> None: |
|
573 |
+ def test_223_edit_notes_abort(self, monkeypatch: Any) -> None: |
|
765 | 574 |
runner = click.testing.CliRunner(mix_stderr=False) |
766 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
575 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
767 | 576 |
config={'global': {'phrase': 'abc'}, |
768 | 577 |
'services': {}}): |
769 | 578 |
monkeypatch.setattr(click, 'edit', lambda *a, **kw: '\n\n') |
... | ... |
@@ -786,7 +595,7 @@ def test_223_edit_notes_abort(monkeypatch: Any) -> None: |
786 | 595 |
( |
787 | 596 |
['--key'], |
788 | 597 |
b'1\n', |
789 |
- {'global': {'key': DUMMY_KEY1}, 'services': {}}, |
|
598 |
+ {'global': {'key': DUMMY_KEY1_B64}, 'services': {}}, |
|
790 | 599 |
), |
791 | 600 |
( |
792 | 601 |
['--phrase', 'sv'], |
... | ... |
@@ -798,31 +607,25 @@ def test_223_edit_notes_abort(monkeypatch: Any) -> None: |
798 | 607 |
['--key', 'sv'], |
799 | 608 |
b'1\n', |
800 | 609 |
{'global': {'phrase': 'abc'}, |
801 |
- 'services': {'sv': {'key': DUMMY_KEY1}}}, |
|
610 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64}}}, |
|
802 | 611 |
), |
803 | 612 |
( |
804 | 613 |
['--key', '--length', '15', 'sv'], |
805 | 614 |
b'1\n', |
806 | 615 |
{'global': {'phrase': 'abc'}, |
807 |
- 'services': {'sv': {'key': DUMMY_KEY1, 'length': 15}}}, |
|
616 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}}}, |
|
808 | 617 |
), |
809 | 618 |
]) |
810 | 619 |
def test_224_store_config_good( |
811 |
- monkeypatch: Any, command_line: list[str], input: bytes, |
|
620 |
+ self, monkeypatch: Any, command_line: list[str], input: bytes, |
|
812 | 621 |
result_config: Any, |
813 | 622 |
) -> None: |
814 |
- KeyCommentPair = ssh_agent_client.types.KeyCommentPair |
|
815 |
- key_list = [ |
|
816 |
- KeyCommentPair(base64.standard_b64decode(DUMMY_KEY1), b'no comment'), |
|
817 |
- KeyCommentPair(base64.standard_b64decode(DUMMY_KEY2), b'a comment'), |
|
818 |
- ] |
|
819 |
- def _suitable_ssh_keys(conn: Any) -> Iterator[KeyCommentPair]: |
|
820 |
- yield from key_list |
|
821 | 623 |
runner = click.testing.CliRunner(mix_stderr=False) |
822 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
624 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
823 | 625 |
config={'global': {'phrase': 'abc'}, |
824 | 626 |
'services': {}}): |
825 |
- monkeypatch.setattr(cli, '_get_suitable_ssh_keys', _suitable_ssh_keys) |
|
627 |
+ monkeypatch.setattr(cli, '_get_suitable_ssh_keys', |
|
628 |
+ tests.suitable_ssh_keys) |
|
826 | 629 |
result = runner.invoke(cli.derivepassphrase, |
827 | 630 |
['--config'] + command_line, |
828 | 631 |
catch_exceptions=False, input=input) |
... | ... |
@@ -835,25 +638,24 @@ def test_224_store_config_good( |
835 | 638 |
|
836 | 639 |
@pytest.mark.parametrize(['command_line', 'input', 'err_text'], [ |
837 | 640 |
([], b'', b'cannot update global settings without actual settings'), |
838 |
- (['sv'], b'', b'cannot update service settings without actual settings'), |
|
641 |
+ ( |
|
642 |
+ ['sv'], |
|
643 |
+ b'', |
|
644 |
+ b'cannot update service settings without actual settings', |
|
645 |
+ ), |
|
839 | 646 |
(['--phrase', 'sv'], b'', b'no passphrase given'), |
840 | 647 |
(['--key'], b'', b'no valid SSH key selected'), |
841 | 648 |
]) |
842 | 649 |
def test_225_store_config_fail( |
843 |
- monkeypatch: Any, command_line: list[str], input: bytes, err_text: str, |
|
650 |
+ self, monkeypatch: Any, command_line: list[str], |
|
651 |
+ input: bytes, err_text: str, |
|
844 | 652 |
) -> None: |
845 |
- KeyCommentPair = ssh_agent_client.types.KeyCommentPair |
|
846 |
- key_list = [ |
|
847 |
- KeyCommentPair(base64.standard_b64decode(DUMMY_KEY1), b'no comment'), |
|
848 |
- KeyCommentPair(base64.standard_b64decode(DUMMY_KEY2), b'a comment'), |
|
849 |
- ] |
|
850 |
- def _suitable_ssh_keys(conn: Any) -> Iterator[KeyCommentPair]: |
|
851 |
- yield from key_list |
|
852 | 653 |
runner = click.testing.CliRunner(mix_stderr=False) |
853 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
654 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
854 | 655 |
config={'global': {'phrase': 'abc'}, |
855 | 656 |
'services': {}}): |
856 |
- monkeypatch.setattr(cli, '_get_suitable_ssh_keys', _suitable_ssh_keys) |
|
657 |
+ monkeypatch.setattr(cli, '_get_suitable_ssh_keys', |
|
658 |
+ tests.suitable_ssh_keys) |
|
857 | 659 |
result = runner.invoke(cli.derivepassphrase, |
858 | 660 |
['--config'] + command_line, |
859 | 661 |
catch_exceptions=False, input=input) |
... | ... |
@@ -863,31 +665,33 @@ def test_225_store_config_fail( |
863 | 665 |
) |
864 | 666 |
|
865 | 667 |
def test_225a_store_config_fail_manual_no_ssh_key_selection( |
866 |
- monkeypatch: Any, |
|
668 |
+ self, monkeypatch: Any, |
|
867 | 669 |
) -> None: |
868 | 670 |
runner = click.testing.CliRunner(mix_stderr=False) |
869 |
- with isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
671 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
870 | 672 |
config={'global': {'phrase': 'abc'}, |
871 | 673 |
'services': {}}): |
872 | 674 |
def raiser(): |
873 | 675 |
raise RuntimeError('custom error message') |
874 | 676 |
monkeypatch.setattr(cli, '_select_ssh_key', raiser) |
875 |
- result = runner.invoke(cli.derivepassphrase, ['--key', '--config'], |
|
677 |
+ result = runner.invoke(cli.derivepassphrase, |
|
678 |
+ ['--key', '--config'], |
|
876 | 679 |
catch_exceptions=False) |
877 | 680 |
assert result.exit_code != 0, 'program unexpectedly succeeded' |
878 | 681 |
assert b'custom error message' in result.stderr_bytes, ( |
879 | 682 |
'expected error message missing' |
880 | 683 |
) |
881 | 684 |
|
882 |
-def test_226_no_arguments() -> None: |
|
685 |
+ def test_226_no_arguments(self) -> None: |
|
883 | 686 |
runner = click.testing.CliRunner(mix_stderr=False) |
884 |
- result = runner.invoke(cli.derivepassphrase, [], catch_exceptions=False) |
|
687 |
+ result = runner.invoke(cli.derivepassphrase, [], |
|
688 |
+ catch_exceptions=False) |
|
885 | 689 |
assert result.exit_code != 0, 'program unexpectedly succeeded' |
886 | 690 |
assert b'SERVICE is required' in result.stderr_bytes, ( |
887 | 691 |
'expected error message missing' |
888 | 692 |
) |
889 | 693 |
|
890 |
-def test_226a_no_passphrase_or_key() -> None: |
|
694 |
+ def test_226a_no_passphrase_or_key(self) -> None: |
|
891 | 695 |
runner = click.testing.CliRunner(mix_stderr=False) |
892 | 696 |
result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE], |
893 | 697 |
catch_exceptions=False) |
... | ... |
@@ -895,3 +699,213 @@ def test_226a_no_passphrase_or_key() -> None: |
895 | 699 |
assert b'no passphrase or key given' in result.stderr_bytes, ( |
896 | 700 |
'expected error message missing' |
897 | 701 |
) |
702 |
+ |
|
703 |
+ |
|
704 |
+class TestCLIUtils: |
|
705 |
+ |
|
706 |
+ def test_100_save_bad_config(self, monkeypatch: Any) -> None: |
|
707 |
+ runner = click.testing.CliRunner() |
|
708 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
709 |
+ config={}): |
|
710 |
+ with pytest.raises(ValueError, match='Invalid vault config'): |
|
711 |
+ cli._save_config(None) # type: ignore |
|
712 |
+ |
|
713 |
+ |
|
714 |
+ def test_101_prompt_for_selection_multiple(self, monkeypatch: Any) -> None: |
|
715 |
+ @click.command() |
|
716 |
+ @click.option('--heading', default='Our menu:') |
|
717 |
+ @click.argument('items', nargs=-1) |
|
718 |
+ def driver(heading, items): |
|
719 |
+ # from https://montypython.fandom.com/wiki/Spam#The_menu |
|
720 |
+ items = items or [ |
|
721 |
+ 'Egg and bacon', |
|
722 |
+ 'Egg, sausage and bacon', |
|
723 |
+ 'Egg and spam', |
|
724 |
+ 'Egg, bacon and spam', |
|
725 |
+ 'Egg, bacon, sausage and spam', |
|
726 |
+ 'Spam, bacon, sausage and spam', |
|
727 |
+ 'Spam, egg, spam, spam, bacon and spam', |
|
728 |
+ 'Spam, spam, spam, egg and spam', |
|
729 |
+ ('Spam, spam, spam, spam, spam, spam, baked beans, ' |
|
730 |
+ 'spam, spam, spam and spam'), |
|
731 |
+ ('Lobster thermidor aux crevettes with a mornay sauce ' |
|
732 |
+ 'garnished with truffle paté, brandy ' |
|
733 |
+ 'and a fried egg on top and spam'), |
|
734 |
+ ] |
|
735 |
+ index = cli._prompt_for_selection(items, heading=heading) |
|
736 |
+ click.echo('A fine choice: ', nl=False) |
|
737 |
+ click.echo(items[index]) |
|
738 |
+ click.echo('(Note: Vikings strictly optional.)') |
|
739 |
+ runner = click.testing.CliRunner(mix_stderr=True) |
|
740 |
+ result = runner.invoke(driver, [], input='9') |
|
741 |
+ assert result.exit_code == 0, 'driver program failed' |
|
742 |
+ assert result.stdout == '''\ |
|
743 |
+Our menu: |
|
744 |
+[1] Egg and bacon |
|
745 |
+[2] Egg, sausage and bacon |
|
746 |
+[3] Egg and spam |
|
747 |
+[4] Egg, bacon and spam |
|
748 |
+[5] Egg, bacon, sausage and spam |
|
749 |
+[6] Spam, bacon, sausage and spam |
|
750 |
+[7] Spam, egg, spam, spam, bacon and spam |
|
751 |
+[8] Spam, spam, spam, egg and spam |
|
752 |
+[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam |
|
753 |
+[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam |
|
754 |
+Your selection? (1-10, leave empty to abort): 9 |
|
755 |
+A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam |
|
756 |
+(Note: Vikings strictly optional.) |
|
757 |
+''', 'driver program produced unexpected output' |
|
758 |
+ result = runner.invoke(driver, ['--heading='], input='', |
|
759 |
+ catch_exceptions=True) |
|
760 |
+ assert result.exit_code > 0, 'driver program succeeded?!' |
|
761 |
+ assert result.stdout == '''\ |
|
762 |
+[1] Egg and bacon |
|
763 |
+[2] Egg, sausage and bacon |
|
764 |
+[3] Egg and spam |
|
765 |
+[4] Egg, bacon and spam |
|
766 |
+[5] Egg, bacon, sausage and spam |
|
767 |
+[6] Spam, bacon, sausage and spam |
|
768 |
+[7] Spam, egg, spam, spam, bacon and spam |
|
769 |
+[8] Spam, spam, spam, egg and spam |
|
770 |
+[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam |
|
771 |
+[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam |
|
772 |
+Your selection? (1-10, leave empty to abort): \n''', ( |
|
773 |
+ 'driver program produced unexpected output' |
|
774 |
+ ) |
|
775 |
+ assert isinstance(result.exception, IndexError), ( |
|
776 |
+ 'driver program did not raise IndexError?!' |
|
777 |
+ ) |
|
778 |
+ |
|
779 |
+ |
|
780 |
+ def test_102_prompt_for_selection_single(self, monkeypatch: Any) -> None: |
|
781 |
+ @click.command() |
|
782 |
+ @click.option('--item', default='baked beans') |
|
783 |
+ @click.argument('prompt') |
|
784 |
+ def driver(item, prompt): |
|
785 |
+ try: |
|
786 |
+ cli._prompt_for_selection([item], heading='', |
|
787 |
+ single_choice_prompt=prompt) |
|
788 |
+ except IndexError as e: |
|
789 |
+ click.echo('Boo.') |
|
790 |
+ raise e |
|
791 |
+ else: |
|
792 |
+ click.echo('Great!') |
|
793 |
+ runner = click.testing.CliRunner(mix_stderr=True) |
|
794 |
+ result = runner.invoke(driver, ['Will replace with spam. Confirm, y/n?'], |
|
795 |
+ input='y') |
|
796 |
+ assert result.exit_code == 0, 'driver program failed' |
|
797 |
+ assert result.stdout == '''\ |
|
798 |
+[1] baked beans |
|
799 |
+Will replace with spam. Confirm, y/n? y |
|
800 |
+Great! |
|
801 |
+''', 'driver program produced unexpected output' |
|
802 |
+ result = runner.invoke(driver, |
|
803 |
+ ['Will replace with spam, okay? ' + |
|
804 |
+ '(Please say "y" or "n".)'], |
|
805 |
+ input='') |
|
806 |
+ assert result.exit_code > 0, 'driver program succeeded?!' |
|
807 |
+ assert result.stdout == '''\ |
|
808 |
+[1] baked beans |
|
809 |
+Will replace with spam, okay? (Please say "y" or "n".): |
|
810 |
+Boo. |
|
811 |
+''', 'driver program produced unexpected output' |
|
812 |
+ assert isinstance(result.exception, IndexError), ( |
|
813 |
+ 'driver program did not raise IndexError?!' |
|
814 |
+ ) |
|
815 |
+ |
|
816 |
+ |
|
817 |
+ def test_103_prompt_for_passphrase(self, monkeypatch: Any) -> None: |
|
818 |
+ monkeypatch.setattr(click, 'prompt', |
|
819 |
+ lambda *a, **kw: json.dumps({'args': a, 'kwargs': kw})) |
|
820 |
+ res = json.loads(cli._prompt_for_passphrase()) |
|
821 |
+ assert 'args' in res and 'kwargs' in res, ( |
|
822 |
+ 'missing arguments to passphrase prompt' |
|
823 |
+ ) |
|
824 |
+ assert res['args'][:1] == ['Passphrase'], ( |
|
825 |
+ 'missing arguments to passphrase prompt' |
|
826 |
+ ) |
|
827 |
+ assert (res['kwargs'].get('default') == '' |
|
828 |
+ and not res['kwargs'].get('show_default', True)), ( |
|
829 |
+ 'missing arguments to passphrase prompt' |
|
830 |
+ ) |
|
831 |
+ assert res['kwargs'].get('err') and res['kwargs'].get('hide_input'), ( |
|
832 |
+ 'missing arguments to passphrase prompt' |
|
833 |
+ ) |
|
834 |
+ |
|
835 |
+ |
|
836 |
+ @pytest.mark.parametrize(['command_line', 'config', 'result_config'], [ |
|
837 |
+ (['--delete-globals'], |
|
838 |
+ {'global': {'phrase': 'abc'}, 'services': {}}, {'services': {}}), |
|
839 |
+ (['--delete', DUMMY_SERVICE], |
|
840 |
+ {'global': {'phrase': 'abc'}, |
|
841 |
+ 'services': {DUMMY_SERVICE: {'notes': '...'}}}, |
|
842 |
+ {'global': {'phrase': 'abc'}, 'services': {}}), |
|
843 |
+ (['--clear'], |
|
844 |
+ {'global': {'phrase': 'abc'}, |
|
845 |
+ 'services': {DUMMY_SERVICE: {'notes': '...'}}}, |
|
846 |
+ {'services': {}}), |
|
847 |
+ ]) |
|
848 |
+ def test_203_repeated_config_deletion( |
|
849 |
+ self, monkeypatch: Any, command_line: list[str], |
|
850 |
+ config: dpp.types.VaultConfig, result_config: dpp.types.VaultConfig, |
|
851 |
+ ) -> None: |
|
852 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
853 |
+ for start_config in [config, result_config]: |
|
854 |
+ with tests.isolated_config(monkeypatch=monkeypatch, |
|
855 |
+ runner=runner, config=start_config): |
|
856 |
+ result = runner.invoke(cli.derivepassphrase, command_line, |
|
857 |
+ catch_exceptions=False) |
|
858 |
+ assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
859 |
+ 'program exited with failure' |
|
860 |
+ ) |
|
861 |
+ with open(cli._config_filename(), 'rt') as infile: |
|
862 |
+ config_readback = json.load(infile) |
|
863 |
+ assert config_readback == result_config |
|
864 |
+ |
|
865 |
+ |
|
866 |
+ def test_204_phrase_from_key_manually(self) -> None: |
|
867 |
+ assert ( |
|
868 |
+ dpp.Vault(phrase=DUMMY_PHRASE_FROM_KEY1, **DUMMY_CONFIG_SETTINGS) |
|
869 |
+ .generate(DUMMY_SERVICE) == DUMMY_RESULT_KEY1 |
|
870 |
+ ) |
|
871 |
+ |
|
872 |
+ |
|
873 |
+ @pytest.mark.parametrize(['vfunc', 'input'], [ |
|
874 |
+ (cli._validate_occurrence_constraint, 20), |
|
875 |
+ (cli._validate_length, 20), |
|
876 |
+ ]) |
|
877 |
+ def test_210a_validate_constraints_manually( |
|
878 |
+ self, |
|
879 |
+ vfunc: Callable[[click.Context, click.Parameter, Any], int | None], |
|
880 |
+ input: int, |
|
881 |
+ ) -> None: |
|
882 |
+ ctx = cli.derivepassphrase.make_context(cli.prog_name, []) |
|
883 |
+ param = cli.derivepassphrase.params[0] |
|
884 |
+ assert vfunc(ctx, param, input) == input |
|
885 |
+ |
|
886 |
+ |
|
887 |
+ @tests.skip_if_no_agent |
|
888 |
+ @pytest.mark.parametrize(['conn_hint'], |
|
889 |
+ [('none',), ('socket',), ('client',)]) |
|
890 |
+ def test_227_get_suitable_ssh_keys(self, monkeypatch, conn_hint): |
|
891 |
+ monkeypatch.setattr(ssh_agent_client.SSHAgentClient, |
|
892 |
+ 'list_keys', tests.list_keys) |
|
893 |
+ hint: ssh_agent_client.SSHAgentClient | socket.socket | None |
|
894 |
+ match conn_hint: |
|
895 |
+ case 'client': |
|
896 |
+ hint = ssh_agent_client.SSHAgentClient() |
|
897 |
+ case 'socket': |
|
898 |
+ hint = socket.socket(family=socket.AF_UNIX) |
|
899 |
+ hint.connect(os.environ['SSH_AUTH_SOCK']) |
|
900 |
+ case _: |
|
901 |
+ assert conn_hint == 'none' |
|
902 |
+ hint = None |
|
903 |
+ exception: Exception | None = None |
|
904 |
+ try: |
|
905 |
+ list(cli._get_suitable_ssh_keys(hint)) |
|
906 |
+ except RuntimeError: # pragma: no cover |
|
907 |
+ pass |
|
908 |
+ except Exception as e: # pragma: no cover |
|
909 |
+ exception = e |
|
910 |
+ finally: |
|
911 |
+ assert exception == None, 'exception querying suitable SSH keys' |
... | ... |
@@ -6,7 +6,7 @@ from __future__ import annotations |
6 | 6 |
|
7 | 7 |
from typing import Any, cast, TYPE_CHECKING, NamedTuple |
8 | 8 |
|
9 |
-import derivepassphrase as dpp |
|
9 |
+import derivepassphrase.types |
|
10 | 10 |
import pytest |
11 | 11 |
|
12 | 12 |
@pytest.mark.parametrize(['obj', 'comment'], [ |
... | ... |
@@ -43,9 +43,13 @@ import pytest |
43 | 43 |
'services': {'sv': {'phrase': 'abc', 'length': 10}}}, ''), |
44 | 44 |
({'global': {'key': '...'}, |
45 | 45 |
'services': {'sv': {'phrase': 'abc', 'length': 10}}}, ''), |
46 |
+ ({'global': {'key': '...'}, |
|
47 |
+ 'services': {'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
48 |
+ 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}}}, ''), |
|
46 | 49 |
]) |
47 | 50 |
def test_200_is_vault_config(obj: Any, comment: str) -> None: |
48 |
- assert dpp.types.is_vault_config(obj) == (not comment), ( |
|
51 |
+ is_vault_config = derivepassphrase.types.is_vault_config |
|
52 |
+ assert is_vault_config(obj) == (not comment), ( |
|
49 | 53 |
'failed to complain about: ' + comment if comment |
50 | 54 |
else 'failed on valid example' |
51 | 55 |
) |
... | ... |
@@ -1,197 +0,0 @@ |
1 |
-# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info> |
|
2 |
-# |
|
3 |
-# SPDX-License-Identifier: MIT |
|
4 |
- |
|
5 |
-"""Test passphrase generation via derivepassphrase.Vault.""" |
|
6 |
- |
|
7 |
-from __future__ import annotations |
|
8 |
- |
|
9 |
-import math |
|
10 |
- |
|
11 |
-import derivepassphrase |
|
12 |
-import sequin |
|
13 |
-import pytest |
|
14 |
- |
|
15 |
-Vault = derivepassphrase.Vault |
|
16 |
-phrase = b'She cells C shells bye the sea shoars' |
|
17 |
-google_phrase = rb': 4TVH#5:aZl8LueOT\{' |
|
18 |
-twitter_phrase = rb"[ (HN_N:lI&<ro=)3'g9" |
|
19 |
- |
|
20 |
-@pytest.mark.parametrize(['service', 'expected'], [ |
|
21 |
- (b'google', google_phrase), |
|
22 |
- ('twitter', twitter_phrase), |
|
23 |
-]) |
|
24 |
-def test_200_basic_configuration(service, expected): |
|
25 |
- assert Vault(phrase=phrase).generate(service) == expected |
|
26 |
- |
|
27 |
-def test_201_phrase_dependence(): |
|
28 |
- assert ( |
|
29 |
- Vault(phrase=(phrase + b'X')).generate('google') == |
|
30 |
- b'n+oIz6sL>K*lTEWYRO%7' |
|
31 |
- ) |
|
32 |
- |
|
33 |
-def test_202_reproducibility_and_bytes_service_name(): |
|
34 |
- assert ( |
|
35 |
- Vault(phrase=phrase).generate(b'google') == |
|
36 |
- Vault(phrase=phrase).generate('google') |
|
37 |
- ) |
|
38 |
- |
|
39 |
-def test_203_reproducibility_and_bytearray_service_name(): |
|
40 |
- assert ( |
|
41 |
- Vault(phrase=phrase).generate(b'google') == |
|
42 |
- Vault(phrase=phrase).generate(bytearray(b'google')) |
|
43 |
- ) |
|
44 |
- |
|
45 |
-def test_210_nonstandard_length(): |
|
46 |
- assert Vault(phrase=phrase, length=4).generate('google') == b'xDFu' |
|
47 |
- |
|
48 |
-def test_211_repetition_limit(): |
|
49 |
- assert ( |
|
50 |
- Vault(phrase=b'', length=24, symbol=0, number=0, |
|
51 |
- repeat=1).generate('asd') == |
|
52 |
- b'IVTDzACftqopUXqDHPkuCIhV' |
|
53 |
- ) |
|
54 |
- |
|
55 |
-def test_212_without_symbols(): |
|
56 |
- assert ( |
|
57 |
- Vault(phrase=phrase, symbol=0).generate('google') == |
|
58 |
- b'XZ4wRe0bZCazbljCaMqR' |
|
59 |
- ) |
|
60 |
- |
|
61 |
-def test_213_too_many_symbols(): |
|
62 |
- with pytest.raises(ValueError, |
|
63 |
- match='requested passphrase length too short'): |
|
64 |
- Vault(phrase=phrase, symbol=100) |
|
65 |
- |
|
66 |
-def test_214_no_numbers(): |
|
67 |
- assert ( |
|
68 |
- Vault(phrase=phrase, number=0).generate('google') == |
|
69 |
- b'_*$TVH.%^aZl(LUeOT?>' |
|
70 |
- ) |
|
71 |
- |
|
72 |
-def test_214_no_lowercase_letters(): |
|
73 |
- assert ( |
|
74 |
- Vault(phrase=phrase, lower=0).generate('google') == |
|
75 |
- b':{?)+7~@OA:L]!0E$)(+' |
|
76 |
- ) |
|
77 |
- |
|
78 |
-def test_215_at_least_5_digits(): |
|
79 |
- assert ( |
|
80 |
- Vault(phrase=phrase, length=8, number=5).generate('songkick') == |
|
81 |
- b'i0908.7[' |
|
82 |
- ) |
|
83 |
- |
|
84 |
-def test_216_lots_of_spaces(): |
|
85 |
- assert ( |
|
86 |
- Vault(phrase=phrase, space=12).generate('songkick') == |
|
87 |
- b' c 6 Bq % 5fR ' |
|
88 |
- ) |
|
89 |
- |
|
90 |
-def test_217_no_viable_characters(): |
|
91 |
- with pytest.raises(ValueError, |
|
92 |
- match='no allowed characters left'): |
|
93 |
- Vault(phrase=phrase, lower=0, upper=0, number=0, |
|
94 |
- space=0, dash=0, symbol=0) |
|
95 |
- |
|
96 |
-def test_218_all_character_classes(): |
|
97 |
- assert ( |
|
98 |
- Vault(phrase=phrase, lower=2, upper=2, number=1, |
|
99 |
- space=3, dash=2, symbol=1).generate('google') == |
|
100 |
- b': : fv_wqt>a-4w1S R' |
|
101 |
- ) |
|
102 |
- |
|
103 |
-def test_219_only_numbers_and_very_high_repetition_limit(): |
|
104 |
- generated = Vault(phrase=b'', length=40, lower=0, upper=0, space=0, |
|
105 |
- dash=0, symbol=0, repeat=4).generate('abcdef') |
|
106 |
- assert b'0000' not in generated |
|
107 |
- assert b'1111' not in generated |
|
108 |
- assert b'2222' not in generated |
|
109 |
- assert b'3333' not in generated |
|
110 |
- assert b'4444' not in generated |
|
111 |
- assert b'5555' not in generated |
|
112 |
- assert b'6666' not in generated |
|
113 |
- assert b'7777' not in generated |
|
114 |
- assert b'8888' not in generated |
|
115 |
- assert b'9999' not in generated |
|
116 |
- |
|
117 |
-def test_220_very_limited_character_set(): |
|
118 |
- generated = Vault(phrase=b'', length=24, lower=0, upper=0, |
|
119 |
- space=0, symbol=0).generate('testing') |
|
120 |
- assert b'763252593304946694588866' == generated |
|
121 |
- |
|
122 |
-def test_300_character_set_subtraction(): |
|
123 |
- assert Vault._subtract(b'be', b'abcdef') == bytearray(b'acdf') |
|
124 |
- |
|
125 |
-def test_301_character_set_subtraction_duplicate(): |
|
126 |
- with pytest.raises(ValueError, match='duplicate characters'): |
|
127 |
- Vault._subtract(b'abcdef', b'aabbccddeeff') |
|
128 |
- with pytest.raises(ValueError, match='duplicate characters'): |
|
129 |
- Vault._subtract(b'aabbccddeeff', b'abcdef') |
|
130 |
- |
|
131 |
-@pytest.mark.parametrize(['length', 'settings', 'entropy'], [ |
|
132 |
- (20, {}, math.log2(math.factorial(20)) + 20 * math.log2(94)), |
|
133 |
- ( |
|
134 |
- 20, |
|
135 |
- {'upper': 0, 'number': 0, 'space': 0, 'symbol': 0}, |
|
136 |
- math.log2(math.factorial(20)) + 20 * math.log2(26) |
|
137 |
- ), |
|
138 |
- (0, {}, float('-inf')), |
|
139 |
- (0, {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0}, float('-inf')), |
|
140 |
- (1, {}, math.log2(94)), |
|
141 |
- (1, {'upper': 0, 'lower': 0, 'number': 0, 'symbol': 0}, 0.0), |
|
142 |
-]) |
|
143 |
-def test_400_entropy( |
|
144 |
- length: int, settings: dict[str, int], entropy: int |
|
145 |
-) -> None: |
|
146 |
- v = Vault(length=length, **settings) |
|
147 |
- assert math.isclose(v._entropy(), entropy) |
|
148 |
- assert v._estimate_sufficient_hash_length() > 0 |
|
149 |
- if math.isfinite(entropy) and entropy: |
|
150 |
- assert v._estimate_sufficient_hash_length(1.0) == math.ceil(entropy / 8) |
|
151 |
- assert v._estimate_sufficient_hash_length(8.0) >= entropy |
|
152 |
- |
|
153 |
-def test_401_hash_length_estimation( |
|
154 |
-) -> None: |
|
155 |
- v = Vault(phrase=phrase) |
|
156 |
- with pytest.raises(ValueError, |
|
157 |
- match='invalid safety factor'): |
|
158 |
- assert v._estimate_sufficient_hash_length(-1.0) |
|
159 |
- with pytest.raises(TypeError, |
|
160 |
- match='invalid safety factor: not a float'): |
|
161 |
- assert v._estimate_sufficient_hash_length(None) # type: ignore |
|
162 |
- v2 = Vault(phrase=phrase, lower=0, upper=0, number=0, symbol=0, |
|
163 |
- space=1, length=1) |
|
164 |
- assert v2._entropy() == 0.0 |
|
165 |
- assert v2._estimate_sufficient_hash_length() > 0 |
|
166 |
- |
|
167 |
-@pytest.mark.parametrize(['service', 'expected'], [ |
|
168 |
- (b'google', google_phrase), |
|
169 |
- ('twitter', twitter_phrase), |
|
170 |
-]) |
|
171 |
-def test_402_hash_length_expansion( |
|
172 |
- monkeypatch: Any, service: str | bytes, expected: bytes |
|
173 |
-) -> None: |
|
174 |
- v = Vault(phrase=phrase) |
|
175 |
- monkeypatch.setattr(v, |
|
176 |
- '_estimate_sufficient_hash_length', |
|
177 |
- lambda *args, **kwargs: 1) |
|
178 |
- assert v._estimate_sufficient_hash_length |
|
179 |
- assert v.generate(service) == expected |
|
180 |
- |
|
181 |
-@pytest.mark.parametrize(['s', 'raises'], [ |
|
182 |
- ('ñ', True), ('Düsseldorf', True), |
|
183 |
- ('liberté, egalité, fraternité', True), ('ASCII', False), |
|
184 |
- ('Düsseldorf'.encode('UTF-8'), False), |
|
185 |
- (bytearray([2, 3, 5, 7, 11, 13]), False), |
|
186 |
-]) |
|
187 |
-def test_403_binary_strings(s: str | bytes | bytearray, raises: bool) -> None: |
|
188 |
- binstr = derivepassphrase.Vault._get_binary_string |
|
189 |
- if raises: |
|
190 |
- with pytest.raises(derivepassphrase.AmbiguousByteRepresentationError): |
|
191 |
- binstr(s) |
|
192 |
- elif isinstance(s, str): |
|
193 |
- assert binstr(s) == s.encode('UTF-8') |
|
194 |
- assert binstr(binstr(s)) == s.encode('UTF-8') |
|
195 |
- else: |
|
196 |
- assert binstr(s) == bytes(s) |
|
197 |
- assert binstr(binstr(s)) == bytes(s) |
... | ... |
@@ -1,598 +0,0 @@ |
1 |
-# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info> |
|
2 |
-# |
|
3 |
-# SPDX-License-Identifier: MIT |
|
4 |
- |
|
5 |
-"""Test OpenSSH key loading and signing.""" |
|
6 |
- |
|
7 |
-from __future__ import annotations |
|
8 |
- |
|
9 |
-import click |
|
10 |
-import pytest |
|
11 |
- |
|
12 |
-import derivepassphrase |
|
13 |
-import derivepassphrase.cli |
|
14 |
-import ssh_agent_client |
|
15 |
- |
|
16 |
-import base64 |
|
17 |
-import errno |
|
18 |
-import io |
|
19 |
-import os |
|
20 |
-import socket |
|
21 |
-import subprocess |
|
22 |
- |
|
23 |
-if not os.environ.get('SSH_AUTH_SOCK'): # pragma: no cover |
|
24 |
- pytest.skip('no running SSH agent detected', allow_module_level=True) |
|
25 |
- |
|
26 |
-SUPPORTED = { |
|
27 |
- 'ed25519': { |
|
28 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
29 |
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW |
|
30 |
-QyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdgAAAKDweO7H8Hju |
|
31 |
-xwAAAAtzc2gtZWQyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdg |
|
32 |
-AAAEAbM/A869nkWZbe2tp3Dm/L6gitvmpH/aRZt8sBII3ExYF4gWgm1gJIXw//Mkhv5MEw |
|
33 |
-idwcakUGCekJD/vCEml2AAAAG3Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQEC |
|
34 |
------END OPENSSH PRIVATE KEY----- |
|
35 |
-''', |
|
36 |
- 'public_key': rb'''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2 test key without passphrase |
|
37 |
-''', |
|
38 |
- 'public_key_data': bytes.fromhex(''' |
|
39 |
- 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39 |
|
40 |
- 00 00 00 20 |
|
41 |
- 81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1 |
|
42 |
- 30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76 |
|
43 |
-'''), |
|
44 |
- 'expected_signature': bytes.fromhex(''' |
|
45 |
- 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39 |
|
46 |
- 00 00 00 40 |
|
47 |
- f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86 |
|
48 |
- 66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd |
|
49 |
- 0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c |
|
50 |
- 1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02 |
|
51 |
- '''), |
|
52 |
- 'derived_passphrase': rb'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==', |
|
53 |
- }, |
|
54 |
- # Currently only supported by PuTTY (which is deficient in other |
|
55 |
- # niceties of the SSH agent and the agent's client). |
|
56 |
- 'ed448': { |
|
57 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
58 |
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz |
|
59 |
-c2gtZWQ0NDgAAAA54vZy009Wu8wExjvEb3hqtLz1GO/+d5vmGUbErWQ4AUO9mYLT |
|
60 |
-zHJHc2m4s+yWzP29Cc3EcxizLG8AAAAA8BdhfCcXYXwnAAAACXNzaC1lZDQ0OAAA |
|
61 |
-ADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM |
|
62 |
-/b0JzcRzGLMsbwAAAAByM7GIMRvWJB3YD6SIpAF2uudX4ozZe0X917wPwiBrs373 |
|
63 |
-9TM1n94Nib6hrxGNmCk2iBQDe2KALPgA4vZy009Wu8wExjvEb3hqtLz1GO/+d5vm |
|
64 |
-GUbErWQ4AUO9mYLTzHJHc2m4s+yWzP29Cc3EcxizLG8AAAAAG3Rlc3Qga2V5IHdp |
|
65 |
-dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ== |
|
66 |
------END OPENSSH PRIVATE KEY----- |
|
67 |
-''', |
|
68 |
- 'public_key': rb'''ssh-ed448 AAAACXNzaC1lZDQ0OAAAADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM/b0JzcRzGLMsbwA= test key without passphrase |
|
69 |
-''', |
|
70 |
- 'public_key_data': bytes.fromhex(''' |
|
71 |
- 00 00 00 09 73 73 68 2d 65 64 34 34 38 |
|
72 |
- 00 00 00 39 |
|
73 |
- e2 f6 72 d3 4f 56 bb cc 04 c6 3b c4 6f 78 6a b4 |
|
74 |
- bc f5 18 ef fe 77 9b e6 19 46 c4 ad 64 38 01 43 |
|
75 |
- bd 99 82 d3 cc 72 47 73 69 b8 b3 ec 96 cc fd bd |
|
76 |
- 09 cd c4 73 18 b3 2c 6f 00 |
|
77 |
- '''), |
|
78 |
- |
|
79 |
- 'expected_signature': bytes.fromhex(''' |
|
80 |
- 00 00 00 09 73 73 68 2d 65 64 34 34 38 |
|
81 |
- 00 00 00 72 06 86 |
|
82 |
- f4 64 a4 a6 ba d9 c3 22 c4 93 49 99 fc 11 de 67 |
|
83 |
- 97 08 f2 d8 b7 3c 2c 13 e7 c5 1c 1e 92 a6 0e d8 |
|
84 |
- 2f 6d 81 03 82 00 e3 72 e4 32 6d 72 d2 6d 32 84 |
|
85 |
- 3f cc a9 1e 57 2c 00 9a b3 99 de 45 da ce 2e d1 |
|
86 |
- db e5 89 f3 35 be 24 58 90 c6 ca 04 f0 db 88 80 |
|
87 |
- db bd 77 7c 80 20 7f 3a 48 61 f6 1f ae a9 5e 53 |
|
88 |
- 7b e0 9d 93 1e ea dc eb b5 cd 56 4c ea 8f 08 00 |
|
89 |
- '''), |
|
90 |
- 'derived_passphrase': rb'Bob0ZKSmutnDIsSTSZn8Ed5nlwjy2Lc8LBPnxRwekqYO2C9tgQOCAONy5DJtctJtMoQ/zKkeVywAmrOZ3kXazi7R2+WJ8zW+JFiQxsoE8NuIgNu9d3yAIH86SGH2H66pXlN74J2THurc67XNVkzqjwgA', |
|
91 |
- }, |
|
92 |
- 'rsa': { |
|
93 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
94 |
-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
-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
-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
-G4wYDuqxshwelraB/L3e0zhD7fjYHF8IbFsqGlFHWEwOtlfhhfbxJsTGguLm4A8/gdEJD5 |
|
98 |
-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
-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
-mTEpNgQLKoe8ePIEPq4WHhFUovBdwlrOByUKKqxreyvWt5gkpTARz+9Lt8OjBO3rpqK8sZ |
|
101 |
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-yWUH8f9rRjnr5ILuwabIDgvFaVG+xM1O1hWADqzMnSEcknHRkTYEsqYPykAtxFvjOFEh70 |
|
112 |
-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
-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
-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
-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
-R5YSy3lpZ35594ZMOLR8KqVvhgJGF6i9019BiF91SDxjE+sp6dNGfN8W+64tHdDv2a0Mso |
|
117 |
-+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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-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
-Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB |
|
130 |
------END OPENSSH PRIVATE KEY----- |
|
131 |
-''', |
|
132 |
- '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 |
|
133 |
-''', |
|
134 |
- 'public_key_data': bytes.fromhex(''' |
|
135 |
- 00 00 00 07 73 73 68 2d 72 73 61 |
|
136 |
- 00 00 00 03 01 00 01 |
|
137 |
- 00 00 01 81 00 |
|
138 |
- b1 a1 ee e9 7b 38 71 5b 2e 0d 23 49 94 c0 aa a0 |
|
139 |
- f5 60 98 38 04 be 22 3c 4d f5 e6 1c aa 97 dc 99 |
|
140 |
- 08 a8 8d cb b2 f5 7b 22 37 fc 58 27 53 bd e1 2a |
|
141 |
- ec 29 2e 66 12 26 b6 c4 79 d8 03 84 54 0a 14 f2 |
|
142 |
- 60 d7 0c da fe 9c 9e 70 50 fc df d5 42 1a 46 30 |
|
143 |
- de d2 57 17 5b 84 a5 17 4d 61 42 c1 b8 c1 80 ee |
|
144 |
- ab 1b 21 c1 e9 6b 68 1f cb dd ed 33 84 3e df 8d |
|
145 |
- 81 c5 f0 86 c5 b2 a1 a5 14 75 84 c0 eb 65 7e 18 |
|
146 |
- 5f 6f 12 6c 4c 68 2e 2e 6e 00 f3 f8 1d 10 90 f9 |
|
147 |
- da b9 2a 0d c6 69 21 70 87 b4 96 c2 cd 6f 5a 42 |
|
148 |
- 95 9c b3 f3 c3 c3 9c a5 c2 5f f7 74 1e e3 7f 1c |
|
149 |
- 9f ac 6c f8 3b 74 2b ce ca a1 58 4b 22 a7 de e6 |
|
150 |
- 3a 6c 25 ea a0 87 6e af 15 f3 1f 73 bb cf 43 fc |
|
151 |
- 7f 9b 1b 98 a3 ab 18 62 8d b8 dc 55 45 b7 95 97 |
|
152 |
- f0 ff de e0 cb d2 7a ac 6d f5 1c d6 ab e4 7f 06 |
|
153 |
- c9 c2 d3 17 0b 82 15 ea 43 99 31 29 36 04 0b 2a |
|
154 |
- 87 bc 78 f2 04 3e ae 16 1e 11 54 a2 f0 5d c2 5a |
|
155 |
- ce 07 25 0a 2a ac 6b 7b 2b d6 b7 98 24 a5 30 11 |
|
156 |
- cf ef 4b b7 c3 a3 04 ed eb a6 a2 bc b1 95 4a 1f |
|
157 |
- 7b 04 dd d7 b6 44 93 37 57 d3 c9 76 66 52 b3 66 |
|
158 |
- fc 10 52 b7 3e c5 06 76 53 0f 33 da 67 d6 e9 38 |
|
159 |
- b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f |
|
160 |
- 0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31 |
|
161 |
- d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f |
|
162 |
-'''), |
|
163 |
- 'expected_signature': bytes.fromhex(''' |
|
164 |
- 00 00 00 07 73 73 68 2d 72 73 61 |
|
165 |
- 00 00 01 80 |
|
166 |
- a2 10 7c 2e f6 bb 53 a8 74 2a a1 19 99 ad 81 be |
|
167 |
- 79 9c ed d6 9d 09 4e 6e c5 18 48 33 90 77 99 68 |
|
168 |
- f7 9e 03 5a cd 4e 18 eb 89 7d 85 a2 ee ae 4a 92 |
|
169 |
- f6 6f ce b9 fe 86 7f 2a 6b 31 da 6e 1a fe a2 a5 |
|
170 |
- 88 b8 44 7f a1 76 73 b3 ec 75 b5 d0 a6 b9 15 97 |
|
171 |
- 65 09 13 7d 94 21 d1 fb 5d 0f 8b 23 04 77 c2 c3 |
|
172 |
- 55 22 b1 a0 09 8a f5 38 2a d6 7f 1b 87 29 a0 25 |
|
173 |
- d3 25 6f cb 64 61 07 98 dc 14 c5 84 f8 92 24 5e |
|
174 |
- 50 11 6b 49 e5 f0 cc 29 cb 29 a9 19 d8 a7 71 1f |
|
175 |
- 91 0b 05 b1 01 4b c2 5f 00 a5 b6 21 bf f8 2c 9d |
|
176 |
- 67 9b 47 3b 0a 49 6b 79 2d fc 1d ec 0c b0 e5 27 |
|
177 |
- 22 d5 a9 f8 d3 c3 f9 df 48 68 e9 fb ef 3c dc 26 |
|
178 |
- bf cf ea 29 43 01 a6 e3 c5 51 95 f4 66 6d 8a 55 |
|
179 |
- e2 47 ec e8 30 45 4c ae 47 e7 c9 a4 21 8b 64 ba |
|
180 |
- b6 88 f6 21 f8 73 b9 cb 11 a1 78 75 92 c6 5a e5 |
|
181 |
- 64 fe ed 42 d9 95 99 e6 2b 6f 3c 16 3c 28 74 a4 |
|
182 |
- 72 2f 0d 3f 2c 33 67 aa 35 19 8e e7 b5 11 2f b3 |
|
183 |
- f7 6a c5 02 e2 6f a3 42 e3 62 19 99 03 ea a5 20 |
|
184 |
- e7 a1 e3 bc c8 06 a3 b5 7c d6 76 5d df 6f 60 46 |
|
185 |
- 83 2a 08 00 d6 d3 d9 a4 c1 41 8c f8 60 56 45 81 |
|
186 |
- da 3b a2 16 1f 9e 4e 75 83 17 da c3 53 c3 3e 19 |
|
187 |
- a4 1b bc d2 29 b8 78 61 2b 78 e6 b1 52 b0 d5 ec |
|
188 |
- de 69 2c 48 62 d9 fd d1 9b 6b b0 49 db d3 ff 38 |
|
189 |
- e7 10 d9 2d ce 9f 0d 5e 09 7b 37 d2 7b c3 bf ce |
|
190 |
-'''), |
|
191 |
- 'derived_passphrase': rb'ohB8Lva7U6h0KqEZma2Bvnmc7dadCU5uxRhIM5B3mWj3ngNazU4Y64l9haLurkqS9m/Ouf6GfyprMdpuGv6ipYi4RH+hdnOz7HW10Ka5FZdlCRN9lCHR+10PiyMEd8LDVSKxoAmK9Tgq1n8bhymgJdMlb8tkYQeY3BTFhPiSJF5QEWtJ5fDMKcspqRnYp3EfkQsFsQFLwl8ApbYhv/gsnWebRzsKSWt5Lfwd7Ayw5Sci1an408P530ho6fvvPNwmv8/qKUMBpuPFUZX0Zm2KVeJH7OgwRUyuR+fJpCGLZLq2iPYh+HO5yxGheHWSxlrlZP7tQtmVmeYrbzwWPCh0pHIvDT8sM2eqNRmO57URL7P3asUC4m+jQuNiGZkD6qUg56HjvMgGo7V81nZd329gRoMqCADW09mkwUGM+GBWRYHaO6IWH55OdYMX2sNTwz4ZpBu80im4eGEreOaxUrDV7N5pLEhi2f3Rm2uwSdvT/zjnENktzp8NXgl7N9J7w7/O', |
|
192 |
- }, |
|
193 |
-} |
|
194 |
- |
|
195 |
-UNSUITABLE = { |
|
196 |
- 'dsa1024': { |
|
197 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
198 |
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH |
|
199 |
-NzAAAAgQC7KAZXqBGNVLBQPrcMYAoNW54BhD8aIhe7BDWYzJcsaMt72VKSkguZ8+XR7nRa |
|
200 |
-0C/ZsBi+uJp0dpxy9ZMTOWX4u5YPMeQcXEdGExZIfimGqSOAsy6fCld2IfJZJZExcCmhe9 |
|
201 |
-Ssjsd3YSAPJRluOXFQc95MZoR5hMwlIDD8QzrE7QAAABUA99nOZOgd7aHMVGoXpUEBcn7H |
|
202 |
-ossAAACALr2Ag3hxM3rKdxzVUw8fX0VVPXO+3+Kr8hGe0Kc/7NwVaBVL1GQ8fenBuWynpA |
|
203 |
-UbH0wo3h1wkB/8hX6p+S8cnu5rIBlUuVNwLw/bIYohK98LfqTYK/V+g6KD+8m34wvEiXZm |
|
204 |
-qywY54n2bksch1Nqvj/tNpLzExSx/XS0kSM1aigAAACAbQNRPcVEuGDrEcf+xg5tgAejPX |
|
205 |
-BPXr/Jss+Chk64km3mirMYjAWyWYtVcgT+7hOYxtYRin8LyMLqKRmqa0Q5UrvDfChgLhvs |
|
206 |
-G9YSb/Mpw5qm8PiHSafwhkaz/te3+8hKogqoe7sd+tCF06IpJr5k70ACiNtRGqssNF8Elr |
|
207 |
-l1efYAAAH4swlfVrMJX1YAAAAHc3NoLWRzcwAAAIEAuygGV6gRjVSwUD63DGAKDVueAYQ/ |
|
208 |
-GiIXuwQ1mMyXLGjLe9lSkpILmfPl0e50WtAv2bAYvriadHaccvWTEzll+LuWDzHkHFxHRh |
|
209 |
-MWSH4phqkjgLMunwpXdiHyWSWRMXApoXvUrI7Hd2EgDyUZbjlxUHPeTGaEeYTMJSAw/EM6 |
|
210 |
-xO0AAAAVAPfZzmToHe2hzFRqF6VBAXJ+x6LLAAAAgC69gIN4cTN6yncc1VMPH19FVT1zvt |
|
211 |
-/iq/IRntCnP+zcFWgVS9RkPH3pwblsp6QFGx9MKN4dcJAf/IV+qfkvHJ7uayAZVLlTcC8P |
|
212 |
-2yGKISvfC36k2Cv1foOig/vJt+MLxIl2ZqssGOeJ9m5LHIdTar4/7TaS8xMUsf10tJEjNW |
|
213 |
-ooAAAAgG0DUT3FRLhg6xHH/sYObYAHoz1wT16/ybLPgoZOuJJt5oqzGIwFslmLVXIE/u4T |
|
214 |
-mMbWEYp/C8jC6ikZqmtEOVK7w3woYC4b7BvWEm/zKcOapvD4h0mn8IZGs/7Xt/vISqIKqH |
|
215 |
-u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW |
|
216 |
-0gAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UBAgMEBQYH |
|
217 |
------END OPENSSH PRIVATE KEY----- |
|
218 |
-''', |
|
219 |
- 'public_key': rb'''ssh-dss AAAAB3NzaC1kc3MAAACBALsoBleoEY1UsFA+twxgCg1bngGEPxoiF7sENZjMlyxoy3vZUpKSC5nz5dHudFrQL9mwGL64mnR2nHL1kxM5Zfi7lg8x5BxcR0YTFkh+KYapI4CzLp8KV3Yh8lklkTFwKaF71KyOx3dhIA8lGW45cVBz3kxmhHmEzCUgMPxDOsTtAAAAFQD32c5k6B3tocxUahelQQFyfseiywAAAIAuvYCDeHEzesp3HNVTDx9fRVU9c77f4qvyEZ7Qpz/s3BVoFUvUZDx96cG5bKekBRsfTCjeHXCQH/yFfqn5Lxye7msgGVS5U3AvD9shiiEr3wt+pNgr9X6DooP7ybfjC8SJdmarLBjnifZuSxyHU2q+P+02kvMTFLH9dLSRIzVqKAAAAIBtA1E9xUS4YOsRx/7GDm2AB6M9cE9ev8myz4KGTriSbeaKsxiMBbJZi1VyBP7uE5jG1hGKfwvIwuopGaprRDlSu8N8KGAuG+wb1hJv8ynDmqbw+IdJp/CGRrP+17f7yEqiCqh7ux360IXToikmvmTvQAKI21Eaqyw0XwSWuXV59g== test key without passphrase |
|
220 |
-''', |
|
221 |
- 'public_key_data': bytes.fromhex(''' |
|
222 |
- 00 00 00 07 73 73 68 2d 64 73 73 |
|
223 |
- 00 00 00 81 00 |
|
224 |
- bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d |
|
225 |
- 5b 9e 01 84 3f 1a 22 17 bb 04 35 98 cc 97 2c 68 |
|
226 |
- cb 7b d9 52 92 92 0b 99 f3 e5 d1 ee 74 5a d0 2f |
|
227 |
- d9 b0 18 be b8 9a 74 76 9c 72 f5 93 13 39 65 f8 |
|
228 |
- bb 96 0f 31 e4 1c 5c 47 46 13 16 48 7e 29 86 a9 |
|
229 |
- 23 80 b3 2e 9f 0a 57 76 21 f2 59 25 91 31 70 29 |
|
230 |
- a1 7b d4 ac 8e c7 77 61 20 0f 25 19 6e 39 71 50 |
|
231 |
- 73 de 4c 66 84 79 84 cc 25 20 30 fc 43 3a c4 ed |
|
232 |
- 00 00 00 15 00 f7 d9 ce 64 |
|
233 |
- e8 1d ed a1 cc 54 6a 17 a5 41 01 72 7e c7 a2 cb |
|
234 |
- 00 00 00 80 |
|
235 |
- 2e bd 80 83 78 71 33 7a ca 77 1c d5 53 0f 1f 5f |
|
236 |
- 45 55 3d 73 be df e2 ab f2 11 9e d0 a7 3f ec dc |
|
237 |
- 15 68 15 4b d4 64 3c 7d e9 c1 b9 6c a7 a4 05 1b |
|
238 |
- 1f 4c 28 de 1d 70 90 1f fc 85 7e a9 f9 2f 1c 9e |
|
239 |
- ee 6b 20 19 54 b9 53 70 2f 0f db 21 8a 21 2b df |
|
240 |
- 0b 7e a4 d8 2b f5 7e 83 a2 83 fb c9 b7 e3 0b c4 |
|
241 |
- 89 76 66 ab 2c 18 e7 89 f6 6e 4b 1c 87 53 6a be |
|
242 |
- 3f ed 36 92 f3 13 14 b1 fd 74 b4 91 23 35 6a 28 |
|
243 |
- 00 00 00 80 |
|
244 |
- 6d 03 51 3d c5 44 b8 60 eb 11 c7 fe c6 0e 6d 80 |
|
245 |
- 07 a3 3d 70 4f 5e bf c9 b2 cf 82 86 4e b8 92 6d |
|
246 |
- e6 8a b3 18 8c 05 b2 59 8b 55 72 04 fe ee 13 98 |
|
247 |
- c6 d6 11 8a 7f 0b c8 c2 ea 29 19 aa 6b 44 39 52 |
|
248 |
- bb c3 7c 28 60 2e 1b ec 1b d6 12 6f f3 29 c3 9a |
|
249 |
- a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a |
|
250 |
- a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef |
|
251 |
- 40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6 |
|
252 |
-'''), |
|
253 |
- 'expected_signature': None, |
|
254 |
- 'derived_passphrase': None, |
|
255 |
- }, |
|
256 |
- 'ecdsa256': { |
|
257 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
258 |
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS |
|
259 |
-1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTLbU0zDwsk2Dvp+VYIrsNVf5gWwz2S |
|
260 |
-3SZ8TbxiQRkpnGSVqyIoHJOJc+NQItAa7xlJ/8Z6gfz57Z3apUkaMJm6AAAAuKeY+YinmP |
|
261 |
-mIAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5 |
|
262 |
-Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmb |
|
263 |
-oAAAAhAKIl/3n0pKVIxpZkXTGtii782Qr4yIcvHdpxjO/QsIqKAAAAG3Rlc3Qga2V5IHdp |
|
264 |
-dGhvdXQgcGFzc3BocmFzZQECAwQ= |
|
265 |
------END OPENSSH PRIVATE KEY----- |
|
266 |
-''', |
|
267 |
- 'public_key': rb'''ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmbo= test key without passphrase |
|
268 |
-''', |
|
269 |
- 'public_key_data': bytes.fromhex(''' |
|
270 |
- 00 00 00 13 65 63 64 73 61 2d 73 68 61 32 2d 6e |
|
271 |
- 69 73 74 70 32 35 36 |
|
272 |
- 00 00 00 08 6e 69 73 74 70 32 35 36 |
|
273 |
- 00 00 00 41 04 |
|
274 |
- cb 6d 4d 33 0f 0b 24 d8 3b e9 f9 56 08 ae c3 55 |
|
275 |
- 7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c |
|
276 |
- 64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19 |
|
277 |
- 49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba |
|
278 |
-'''), |
|
279 |
- 'expected_signature': None, |
|
280 |
- 'derived_passphrase': None, |
|
281 |
- }, |
|
282 |
- 'ecdsa384': { |
|
283 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
284 |
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS |
|
285 |
-1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSgkOjkAvq7v5vHuj3KBL4/EAWcn5hZ |
|
286 |
-DyKcbyV0eBMGFq7hKXQlZqIahLVqeMR0QqmkxNJ2rly2VHcXneq3vZ+9fIsWCOdYk5WP3N |
|
287 |
-ZPzv911Xn7wbEkC7QndD5zKlm4pBUAAADomhj+IZoY/iEAAAATZWNkc2Etc2hhMi1uaXN0 |
|
288 |
-cDM4NAAAAAhuaXN0cDM4NAAAAGEEoJDo5AL6u7+bx7o9ygS+PxAFnJ+YWQ8inG8ldHgTBh |
|
289 |
-au4Sl0JWaiGoS1anjEdEKppMTSdq5ctlR3F53qt72fvXyLFgjnWJOVj9zWT87/ddV5+8Gx |
|
290 |
-JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B |
|
291 |
-2OdqVOVRkwZWRROz0AAAAbdGVzdCBrZXkgd2l0aG91dCBwYXNzcGhyYXNlAQIDBA== |
|
292 |
------END OPENSSH PRIVATE KEY----- |
|
293 |
-''', |
|
294 |
- 'public_key': rb'''ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKCQ6OQC+ru/m8e6PcoEvj8QBZyfmFkPIpxvJXR4EwYWruEpdCVmohqEtWp4xHRCqaTE0nauXLZUdxed6re9n718ixYI51iTlY/c1k/O/3XVefvBsSQLtCd0PnMqWbikFQ== test key without passphrase |
|
295 |
-''', |
|
296 |
- 'public_key_data': bytes.fromhex(''' |
|
297 |
- 00 00 00 13 |
|
298 |
- 65 63 64 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 |
|
299 |
- 33 38 34 |
|
300 |
- 00 00 00 08 6e 69 73 74 70 33 38 34 |
|
301 |
- 00 00 00 61 04 |
|
302 |
- a0 90 e8 e4 02 fa bb bf 9b c7 ba 3d ca 04 be 3f |
|
303 |
- 10 05 9c 9f 98 59 0f 22 9c 6f 25 74 78 13 06 16 |
|
304 |
- ae e1 29 74 25 66 a2 1a 84 b5 6a 78 c4 74 42 a9 |
|
305 |
- a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd |
|
306 |
- 7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5 |
|
307 |
- 79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15 |
|
308 |
-'''), |
|
309 |
- 'expected_signature': None, |
|
310 |
- 'derived_passphrase': None, |
|
311 |
- }, |
|
312 |
-} |
|
313 |
- |
|
314 |
-def test_client_uint32(): |
|
315 |
- uint32 = ssh_agent_client.SSHAgentClient.uint32 |
|
316 |
- assert uint32(16777216) == b'\x01\x00\x00\x00' |
|
317 |
- |
|
318 |
-@pytest.mark.parametrize(['value', 'exc_type', 'exc_pattern'], [ |
|
319 |
- (10000000000000000, OverflowError, 'int too big to convert'), |
|
320 |
- (-1, OverflowError, "can't convert negative int to unsigned"), |
|
321 |
-]) |
|
322 |
-def test_client_uint32_exceptions(value, exc_type, exc_pattern): |
|
323 |
- uint32 = ssh_agent_client.SSHAgentClient.uint32 |
|
324 |
- with pytest.raises(exc_type, match=exc_pattern): |
|
325 |
- uint32(value) |
|
326 |
- |
|
327 |
-@pytest.mark.parametrize(['input', 'expected'], [ |
|
328 |
- (b'ssh-rsa', b'\x00\x00\x00\x07ssh-rsa'), |
|
329 |
- (b'ssh-ed25519', b'\x00\x00\x00\x0bssh-ed25519'), |
|
330 |
- ( |
|
331 |
- ssh_agent_client.SSHAgentClient.string(b'ssh-ed25519'), |
|
332 |
- b'\x00\x00\x00\x0f\x00\x00\x00\x0bssh-ed25519', |
|
333 |
- ), |
|
334 |
-]) |
|
335 |
-def test_client_string(input, expected): |
|
336 |
- string = ssh_agent_client.SSHAgentClient.string |
|
337 |
- assert bytes(string(input)) == expected |
|
338 |
- |
|
339 |
-@pytest.mark.parametrize(['input', 'exc_type', 'exc_pattern'], [ |
|
340 |
- ('some string', TypeError, 'invalid payload type'), |
|
341 |
-]) |
|
342 |
-def test_client_string_exceptions(input, exc_type, exc_pattern): |
|
343 |
- string = ssh_agent_client.SSHAgentClient.string |
|
344 |
- with pytest.raises(exc_type, match=exc_pattern): |
|
345 |
- string(input) |
|
346 |
- |
|
347 |
-@pytest.mark.parametrize(['input', 'expected'], [ |
|
348 |
- (b'\x00\x00\x00\x07ssh-rsa', b'ssh-rsa'), |
|
349 |
- ( |
|
350 |
- ssh_agent_client.SSHAgentClient.string(b'ssh-ed25519'), |
|
351 |
- b'ssh-ed25519', |
|
352 |
- ), |
|
353 |
-]) |
|
354 |
-def test_client_unstring(input, expected): |
|
355 |
- unstring = ssh_agent_client.SSHAgentClient.unstring |
|
356 |
- unstring_prefix = ssh_agent_client.SSHAgentClient.unstring_prefix |
|
357 |
- assert bytes(unstring(input)) == expected |
|
358 |
- assert tuple(bytes(x) for x in unstring_prefix(input)) == (expected, b'') |
|
359 |
- |
|
360 |
-@pytest.mark.parametrize( |
|
361 |
- ['input', 'exc_type', 'exc_pattern', 'has_trailer', 'parts'], [ |
|
362 |
- (b'ssh', ValueError, 'malformed SSH byte string', False, None), |
|
363 |
- ( |
|
364 |
- b'\x00\x00\x00\x08ssh-rsa', |
|
365 |
- ValueError, 'malformed SSH byte string', |
|
366 |
- False, None, |
|
367 |
- ), |
|
368 |
- ( |
|
369 |
- b'\x00\x00\x00\x04XXX trailing text', |
|
370 |
- ValueError, 'malformed SSH byte string', |
|
371 |
- True, (b'XXX ', b'trailing text'), |
|
372 |
- ), |
|
373 |
-]) |
|
374 |
-def test_client_unstring_exceptions(input, exc_type, exc_pattern, |
|
375 |
- has_trailer, parts): |
|
376 |
- unstring = ssh_agent_client.SSHAgentClient.unstring |
|
377 |
- unstring_prefix = ssh_agent_client.SSHAgentClient.unstring_prefix |
|
378 |
- with pytest.raises(exc_type, match=exc_pattern): |
|
379 |
- unstring(input) |
|
380 |
- if has_trailer: |
|
381 |
- assert tuple(bytes(x) for x in unstring_prefix(input)) == parts |
|
382 |
- else: |
|
383 |
- with pytest.raises(exc_type, match=exc_pattern): |
|
384 |
- unstring_prefix(input) |
|
385 |
- |
|
386 |
-def test_key_decoding(): |
|
387 |
- public_key = SUPPORTED['ed25519']['public_key'] |
|
388 |
- public_key_data = SUPPORTED['ed25519']['public_key_data'] |
|
389 |
- keydata = base64.b64decode(public_key.split(None, 2)[1]) |
|
390 |
- assert ( |
|
391 |
- keydata == public_key_data |
|
392 |
- ), "recorded public key data doesn't match" |
|
393 |
- |
|
394 |
-@pytest.mark.parametrize(['keytype', 'data_dict'], list(SUPPORTED.items())) |
|
395 |
-def test_sign_data_via_agent(keytype, data_dict): |
|
396 |
- private_key = data_dict['private_key'] |
|
397 |
- try: |
|
398 |
- result = subprocess.run(['ssh-add', '-t', '30', '-q', '-'], |
|
399 |
- input=private_key, check=True, |
|
400 |
- capture_output=True) |
|
401 |
- except subprocess.CalledProcessError as e: |
|
402 |
- pytest.skip( |
|
403 |
- f"uploading test key: {e!r}, stdout={e.stdout!r}, " |
|
404 |
- f"stderr={e.stderr!r}" |
|
405 |
- ) |
|
406 |
- else: |
|
407 |
- try: |
|
408 |
- client = ssh_agent_client.SSHAgentClient() |
|
409 |
- except OSError: # pragma: no cover |
|
410 |
- pytest.skip('communication error with the SSH agent') |
|
411 |
- with client: |
|
412 |
- key_comment_pairs = {bytes(k): bytes(c) |
|
413 |
- for k, c in client.list_keys()} |
|
414 |
- public_key_data = data_dict['public_key_data'] |
|
415 |
- expected_signature = data_dict['expected_signature'] |
|
416 |
- derived_passphrase = data_dict['derived_passphrase'] |
|
417 |
- if public_key_data not in key_comment_pairs: # pragma: no cover |
|
418 |
- pytest.skip('prerequisite SSH key not loaded') |
|
419 |
- signature = bytes(client.sign( |
|
420 |
- payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
421 |
- assert signature == expected_signature, 'SSH signature mismatch' |
|
422 |
- signature2 = bytes(client.sign( |
|
423 |
- payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
424 |
- assert signature2 == expected_signature, 'SSH signature mismatch' |
|
425 |
- assert ( |
|
426 |
- derivepassphrase.Vault.phrase_from_key(public_key_data) == |
|
427 |
- derived_passphrase |
|
428 |
- ), 'SSH signature mismatch' |
|
429 |
- |
|
430 |
-@pytest.mark.parametrize(['keytype', 'data_dict'], list(UNSUITABLE.items())) |
|
431 |
-def test_sign_data_via_agent_unsupported(keytype, data_dict): |
|
432 |
- private_key = data_dict['private_key'] |
|
433 |
- try: |
|
434 |
- result = subprocess.run(['ssh-add', '-t', '30', '-q', '-'], |
|
435 |
- input=private_key, check=True, |
|
436 |
- capture_output=True) |
|
437 |
- except subprocess.CalledProcessError as e: # pragma: no cover |
|
438 |
- pytest.skip( |
|
439 |
- f"uploading test key: {e!r}, stdout={e.stdout!r}, " |
|
440 |
- f"stderr={e.stderr!r}" |
|
441 |
- ) |
|
442 |
- else: |
|
443 |
- try: |
|
444 |
- client = ssh_agent_client.SSHAgentClient() |
|
445 |
- except OSError: # pragma: no cover |
|
446 |
- pytest.skip('communication error with the SSH agent') |
|
447 |
- with client: |
|
448 |
- key_comment_pairs = {bytes(k): bytes(c) |
|
449 |
- for k, c in client.list_keys()} |
|
450 |
- public_key_data = data_dict['public_key_data'] |
|
451 |
- expected_signature = data_dict['expected_signature'] |
|
452 |
- if public_key_data not in key_comment_pairs: # pragma: no cover |
|
453 |
- pytest.skip('prerequisite SSH key not loaded') |
|
454 |
- signature = bytes(client.sign( |
|
455 |
- payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
456 |
- signature2 = bytes(client.sign( |
|
457 |
- payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
458 |
- assert signature != signature2, 'SSH signature repeatable?!' |
|
459 |
- with pytest.raises(ValueError, match='unsuitable SSH key'): |
|
460 |
- derivepassphrase.Vault.phrase_from_key(public_key_data) |
|
461 |
- |
|
462 |
-@pytest.mark.parametrize(['this_data', 'all_data'], |
|
463 |
- [(v, tuple(SUPPORTED.values())) |
|
464 |
- for v in SUPPORTED.values()]) |
|
465 |
-def test_ssh_key_selector(monkeypatch, this_data, all_data): |
|
466 |
- successfully_uploaded_keys: list[bytes] = [] |
|
467 |
- for data in all_data: |
|
468 |
- private_key = data['private_key'] |
|
469 |
- try: |
|
470 |
- result = subprocess.run(['ssh-add', '-t', '60', '-q', '-'], |
|
471 |
- input=private_key, check=True, |
|
472 |
- capture_output=True) |
|
473 |
- except subprocess.CalledProcessError as e: |
|
474 |
- if data == this_data: |
|
475 |
- pytest.skip( |
|
476 |
- f"uploading non-optional test key: {e!r}, " |
|
477 |
- f"stdout={e.stdout!r}, stderr={e.stderr!r}" |
|
478 |
- ) |
|
479 |
- else: |
|
480 |
- successfully_uploaded_keys.append(data['public_key_data']) |
|
481 |
- index = 1 + successfully_uploaded_keys.index(this_data['public_key_data']) |
|
482 |
- b64_key = base64.standard_b64encode( |
|
483 |
- this_data['public_key_data']).decode('ASCII') |
|
484 |
- n = len(successfully_uploaded_keys) |
|
485 |
- text = (f'Your selection? (1-{n}, leave empty to abort): {index}\n' |
|
486 |
- if n > 1 else 'Use this key? yes\n') |
|
487 |
- |
|
488 |
- @click.command() |
|
489 |
- def driver(): |
|
490 |
- key = derivepassphrase.cli._select_ssh_key() |
|
491 |
- click.echo(base64.standard_b64encode(key).decode('ASCII')) |
|
492 |
- |
|
493 |
- runner = click.testing.CliRunner(mix_stderr=True) |
|
494 |
- result = runner.invoke(driver, [], |
|
495 |
- input=(f'{index}\n' if n > 1 else f'yes\n'), |
|
496 |
- catch_exceptions=True) |
|
497 |
- assert result.exit_code == 0, 'driver program failed?!' |
|
498 |
- assert result.stdout.startswith('Suitable SSH keys:\n'), ( |
|
499 |
- 'missing expected output' |
|
500 |
- ) |
|
501 |
- assert text in result.stdout, 'missing expected output' |
|
502 |
- assert result.stdout.endswith(f'\n{b64_key}\n'), 'missing expected output' |
|
503 |
- |
|
504 |
-@pytest.mark.parametrize(['conn_hint'], [('none',), ('socket',), ('client',)]) |
|
505 |
-def test_get_suitable_ssh_keys(conn_hint): |
|
506 |
- hint: ssh_agent_client.SSHAgentClient | socket.socket | None |
|
507 |
- match conn_hint: |
|
508 |
- case 'client': |
|
509 |
- hint = ssh_agent_client.SSHAgentClient() |
|
510 |
- case 'socket': |
|
511 |
- hint = socket.socket(family=socket.AF_UNIX) |
|
512 |
- hint.connect(os.environ['SSH_AUTH_SOCK']) |
|
513 |
- case _: |
|
514 |
- assert conn_hint == 'none' |
|
515 |
- hint = None |
|
516 |
- exception: type[Exception] | None = None |
|
517 |
- try: |
|
518 |
- list(derivepassphrase.cli._get_suitable_ssh_keys(hint)) |
|
519 |
- except RuntimeError: # pragma: no cover |
|
520 |
- pass |
|
521 |
- except Exception as e: # pragma: no cover |
|
522 |
- exception = e |
|
523 |
- finally: |
|
524 |
- assert exception == None, 'exception querying suitable SSH keys' |
|
525 |
- |
|
526 |
-def test_constructor_no_running_agent(monkeypatch): |
|
527 |
- monkeypatch.delenv('SSH_AUTH_SOCK', raising=False) |
|
528 |
- sock = socket.socket(family=socket.AF_UNIX) |
|
529 |
- with pytest.raises(RuntimeError, match='missing SSH_AUTH_SOCK'): |
|
530 |
- ssh_agent_client.SSHAgentClient(socket=sock) |
|
531 |
- |
|
532 |
-def test_constructor_bad_running_agent(monkeypatch): |
|
533 |
- monkeypatch.setenv('SSH_AUTH_SOCK', os.environ['SSH_AUTH_SOCK'] + '~') |
|
534 |
- sock = socket.socket(family=socket.AF_UNIX) |
|
535 |
- with pytest.raises(RuntimeError, match='unusable SSH_AUTH_SOCK'): |
|
536 |
- ssh_agent_client.SSHAgentClient(socket=sock) |
|
537 |
- |
|
538 |
-@pytest.mark.parametrize(['response'], [ |
|
539 |
- (b'\x00\x00',), |
|
540 |
- (b'\x00\x00\x00\x1f some bytes missing',), |
|
541 |
-]) |
|
542 |
-def test_truncated_server_response(monkeypatch, response): |
|
543 |
- client = ssh_agent_client.SSHAgentClient() |
|
544 |
- response_stream = io.BytesIO(response) |
|
545 |
- class PseudoSocket(object): |
|
546 |
- pass |
|
547 |
- pseudo_socket = PseudoSocket() |
|
548 |
- pseudo_socket.sendall = lambda *a, **kw: None |
|
549 |
- pseudo_socket.recv = response_stream.read |
|
550 |
- monkeypatch.setattr(client, '_connection', pseudo_socket) |
|
551 |
- with pytest.raises(EOFError): |
|
552 |
- client.request(255, b'') |
|
553 |
- |
|
554 |
-@pytest.mark.parametrize( |
|
555 |
- ['response_code', 'response', 'exc_type', 'exc_pattern'], |
|
556 |
- [ |
|
557 |
- (255, b'', RuntimeError, 'error return from SSH agent:'), |
|
558 |
- (12, b'\x00\x00\x00\x01', EOFError, 'truncated response'), |
|
559 |
- (12, b'\x00\x00\x00\x00abc', RuntimeError, 'overlong response'), |
|
560 |
- ] |
|
561 |
-) |
|
562 |
-def test_list_keys_error_responses(monkeypatch, response_code, response, |
|
563 |
- exc_type, exc_pattern): |
|
564 |
- client = ssh_agent_client.SSHAgentClient() |
|
565 |
- monkeypatch.setattr(client, 'request', |
|
566 |
- lambda *a, **kw: (response_code, response)) |
|
567 |
- with pytest.raises(exc_type, match=exc_pattern): |
|
568 |
- client.list_keys() |
|
569 |
- |
|
570 |
-@pytest.mark.parametrize( |
|
571 |
- ['key', 'check', 'response', 'exc_type', 'exc_pattern'], |
|
572 |
- [ |
|
573 |
- ( |
|
574 |
- b'invalid-key', |
|
575 |
- True, |
|
576 |
- (255, b''), |
|
577 |
- RuntimeError, |
|
578 |
- 'target SSH key not loaded into agent', |
|
579 |
- ), |
|
580 |
- ( |
|
581 |
- SUPPORTED['ed25519']['public_key_data'], |
|
582 |
- True, |
|
583 |
- (255, b''), |
|
584 |
- RuntimeError, |
|
585 |
- 'signing data failed:', |
|
586 |
- ) |
|
587 |
- ] |
|
588 |
-) |
|
589 |
-def test_sign_error_responses(monkeypatch, key, check, response, exc_type, |
|
590 |
- exc_pattern): |
|
591 |
- client = ssh_agent_client.SSHAgentClient() |
|
592 |
- monkeypatch.setattr(client, 'request', lambda a, b: response) |
|
593 |
- KeyCommentPair = ssh_agent_client.types.KeyCommentPair |
|
594 |
- loaded_keys = [KeyCommentPair(v['public_key_data'], b'no comment') |
|
595 |
- for v in SUPPORTED.values()] |
|
596 |
- monkeypatch.setattr(client, 'list_keys', lambda: loaded_keys) |
|
597 |
- with pytest.raises(exc_type, match=exc_pattern): |
|
598 |
- client.sign(key, b'abc', check_if_key_loaded=check) |
... | ... |
@@ -4,13 +4,12 @@ |
4 | 4 |
|
5 | 5 |
"""Test sequin.Sequin.""" |
6 | 6 |
|
7 |
-import pytest |
|
7 |
+import collections |
|
8 | 8 |
|
9 |
+import pytest |
|
9 | 10 |
import sequin |
10 | 11 |
|
11 |
-import collections |
|
12 |
- |
|
13 |
-benum = sequin.Sequin._big_endian_number |
|
12 |
+class TestStaticFunctionality: |
|
14 | 13 |
|
15 | 14 |
@pytest.mark.parametrize(['sequence', 'base', 'expected'], [ |
16 | 15 |
([1, 2, 3, 4, 5, 6], 10, 123456), |
... | ... |
@@ -19,17 +18,24 @@ benum = sequin.Sequin._big_endian_number |
19 | 18 |
([1, 0, 0, 1, 0, 0, 0, 0], 2, 144), |
20 | 19 |
([1, 7, 5, 5], 8, 0o1755), |
21 | 20 |
]) |
22 |
-def test_big_endian_number(sequence, base, expected): |
|
23 |
- assert benum(sequence, base=base) == expected |
|
21 |
+ def test_200_big_endian_number(self, sequence, base, expected): |
|
22 |
+ assert ( |
|
23 |
+ sequin.Sequin._big_endian_number(sequence, base=base) |
|
24 |
+ ) == expected |
|
24 | 25 |
|
25 |
-@pytest.mark.parametrize(['exc_type', 'exc_pattern', 'sequence' , 'base'], [ |
|
26 |
+ @pytest.mark.parametrize( |
|
27 |
+ ['exc_type', 'exc_pattern', 'sequence' , 'base'], [ |
|
26 | 28 |
(ValueError, 'invalid base 3 digit:', [-1], 3), |
27 | 29 |
(ValueError, 'invalid base:', [0], 1), |
28 | 30 |
(TypeError, 'not an integer:', [0.0, 1.0, 0.0, 1.0], 2), |
29 |
-]) |
|
30 |
-def test_big_endian_number_exceptions(exc_type, exc_pattern, sequence, base): |
|
31 |
+ ] |
|
32 |
+ ) |
|
33 |
+ def test_300_big_endian_number_exceptions(self, exc_type, exc_pattern, |
|
34 |
+ sequence, base): |
|
31 | 35 |
with pytest.raises(exc_type, match=exc_pattern): |
32 |
- benum(sequence, base=base) |
|
36 |
+ sequin.Sequin._big_endian_number(sequence, base=base) |
|
37 |
+ |
|
38 |
+class TestSequin: |
|
33 | 39 |
|
34 | 40 |
@pytest.mark.parametrize(['sequence', 'is_bitstring', 'expected'], [ |
35 | 41 |
([1, 0, 0, 1, 0, 1], False, [0, 0, 0, 0, 0, 0, 0, 1, |
... | ... |
@@ -44,38 +50,11 @@ def test_big_endian_number_exceptions(exc_type, exc_pattern, sequence, base): |
44 | 50 |
('OK', False, [0, 1, 0, 0, 1, 1, 1, 1, |
45 | 51 |
0, 1, 0, 0, 1, 0, 1, 1]), |
46 | 52 |
]) |
47 |
-def test_constructor(sequence, is_bitstring, expected): |
|
53 |
+ def test_200_constructor(self, sequence, is_bitstring, expected): |
|
48 | 54 |
seq = sequin.Sequin(sequence, is_bitstring=is_bitstring) |
49 | 55 |
assert seq.bases == {2: collections.deque(expected)} |
50 | 56 |
|
51 |
-@pytest.mark.parametrize( |
|
52 |
- ['sequence', 'is_bitstring', 'exc_type', 'exc_pattern'], |
|
53 |
- [ |
|
54 |
- ([0, 1, 2, 3, 4, 5, 6, 7], True, |
|
55 |
- ValueError, 'sequence item out of range'), |
|
56 |
- (u'こんにちは。', False, |
|
57 |
- ValueError, 'sequence item out of range'), |
|
58 |
- ] |
|
59 |
-) |
|
60 |
-def test_constructor_exceptions(sequence, is_bitstring, exc_type, exc_pattern): |
|
61 |
- with pytest.raises(exc_type, match=exc_pattern): |
|
62 |
- sequin.Sequin(sequence, is_bitstring=is_bitstring) |
|
63 |
- |
|
64 |
-def test_shifting(): |
|
65 |
- seq = sequin.Sequin([1, 0, 1, 0, 0, 1, 0, 0, 0, 1], is_bitstring=True) |
|
66 |
- assert seq.bases == {2: collections.deque([1, 0, 1, 0, 0, 1, 0, 0, 0, 1])} |
|
67 |
- # |
|
68 |
- assert seq._all_or_nothing_shift(3) == (1, 0, 1) |
|
69 |
- assert seq._all_or_nothing_shift(3) == (0, 0, 1) |
|
70 |
- assert seq.bases[2] == collections.deque([0, 0, 0, 1]) |
|
71 |
- # |
|
72 |
- assert seq._all_or_nothing_shift(5) == () |
|
73 |
- assert seq.bases[2] == collections.deque([0, 0, 0, 1]) |
|
74 |
- # |
|
75 |
- assert seq._all_or_nothing_shift(4), (0, 0, 0, 1) |
|
76 |
- assert 2 not in seq.bases |
|
77 |
- |
|
78 |
-def test_generating(): |
|
57 |
+ def test_201_generating(self): |
|
79 | 58 |
seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
80 | 59 |
is_bitstring=True) |
81 | 60 |
assert seq.generate(1) == 0 |
... | ... |
@@ -91,7 +70,7 @@ def test_generating(): |
91 | 70 |
with pytest.raises(ValueError, match='invalid target range'): |
92 | 71 |
seq.generate(0) |
93 | 72 |
|
94 |
-def test_internal_generating(): |
|
73 |
+ def test_210_internal_generating(self): |
|
95 | 74 |
seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
96 | 75 |
is_bitstring=True) |
97 | 76 |
assert seq._generate_inner(5) == 3 |
... | ... |
@@ -106,3 +85,31 @@ def test_internal_generating(): |
106 | 85 |
seq._generate_inner(0) |
107 | 86 |
with pytest.raises(ValueError, match='invalid base:'): |
108 | 87 |
seq._generate_inner(16, base=1) |
88 |
+ |
|
89 |
+ def test_211_shifting(self): |
|
90 |
+ seq = sequin.Sequin([1, 0, 1, 0, 0, 1, 0, 0, 0, 1], is_bitstring=True) |
|
91 |
+ assert seq.bases == {2: collections.deque([1, 0, 1, 0, 0, 1, 0, 0, 0, 1])} |
|
92 |
+ # |
|
93 |
+ assert seq._all_or_nothing_shift(3) == (1, 0, 1) |
|
94 |
+ assert seq._all_or_nothing_shift(3) == (0, 0, 1) |
|
95 |
+ assert seq.bases[2] == collections.deque([0, 0, 0, 1]) |
|
96 |
+ # |
|
97 |
+ assert seq._all_or_nothing_shift(5) == () |
|
98 |
+ assert seq.bases[2] == collections.deque([0, 0, 0, 1]) |
|
99 |
+ # |
|
100 |
+ assert seq._all_or_nothing_shift(4), (0, 0, 0, 1) |
|
101 |
+ assert 2 not in seq.bases |
|
102 |
+ |
|
103 |
+ @pytest.mark.parametrize( |
|
104 |
+ ['sequence', 'is_bitstring', 'exc_type', 'exc_pattern'], |
|
105 |
+ [ |
|
106 |
+ ([0, 1, 2, 3, 4, 5, 6, 7], True, |
|
107 |
+ ValueError, 'sequence item out of range'), |
|
108 |
+ (u'こんにちは。', False, |
|
109 |
+ ValueError, 'sequence item out of range'), |
|
110 |
+ ] |
|
111 |
+ ) |
|
112 |
+ def test_300_constructor_exceptions(self, sequence, is_bitstring, |
|
113 |
+ exc_type, exc_pattern): |
|
114 |
+ with pytest.raises(exc_type, match=exc_pattern): |
|
115 |
+ sequin.Sequin(sequence, is_bitstring=is_bitstring) |
... | ... |
@@ -0,0 +1,316 @@ |
1 |
+# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info> |
|
2 |
+# |
|
3 |
+# SPDX-License-Identifier: MIT |
|
4 |
+ |
|
5 |
+"""Test OpenSSH key loading and signing.""" |
|
6 |
+ |
|
7 |
+from __future__ import annotations |
|
8 |
+ |
|
9 |
+import base64 |
|
10 |
+import errno |
|
11 |
+import io |
|
12 |
+import os |
|
13 |
+import socket |
|
14 |
+import subprocess |
|
15 |
+from typing import Any |
|
16 |
+ |
|
17 |
+import click |
|
18 |
+import click.testing |
|
19 |
+import derivepassphrase |
|
20 |
+import derivepassphrase.cli |
|
21 |
+import pytest |
|
22 |
+import ssh_agent_client |
|
23 |
+import tests |
|
24 |
+ |
|
25 |
+class TestStaticFunctionality: |
|
26 |
+ |
|
27 |
+ @pytest.mark.parametrize(['public_key', 'public_key_data'], |
|
28 |
+ [(val['public_key'], val['public_key_data']) |
|
29 |
+ for val in tests.SUPPORTED_KEYS.values()]) |
|
30 |
+ def test_100_key_decoding(self, public_key, public_key_data): |
|
31 |
+ keydata = base64.b64decode(public_key.split(None, 2)[1]) |
|
32 |
+ assert ( |
|
33 |
+ keydata == public_key_data |
|
34 |
+ ), "recorded public key data doesn't match" |
|
35 |
+ |
|
36 |
+ def test_200_constructor_no_running_agent(self, monkeypatch): |
|
37 |
+ monkeypatch.delenv('SSH_AUTH_SOCK', raising=False) |
|
38 |
+ sock = socket.socket(family=socket.AF_UNIX) |
|
39 |
+ with pytest.raises(RuntimeError, |
|
40 |
+ match='SSH_AUTH_SOCK environment variable'): |
|
41 |
+ ssh_agent_client.SSHAgentClient(socket=sock) |
|
42 |
+ |
|
43 |
+ @pytest.mark.parametrize(['input', 'expected'], [ |
|
44 |
+ (16777216, b'\x01\x00\x00\x00'), |
|
45 |
+ ]) |
|
46 |
+ def test_210_uint32(self, input, expected): |
|
47 |
+ uint32 = ssh_agent_client.SSHAgentClient.uint32 |
|
48 |
+ assert uint32(input) == expected |
|
49 |
+ |
|
50 |
+ @pytest.mark.parametrize(['input', 'expected'], [ |
|
51 |
+ (b'ssh-rsa', b'\x00\x00\x00\x07ssh-rsa'), |
|
52 |
+ (b'ssh-ed25519', b'\x00\x00\x00\x0bssh-ed25519'), |
|
53 |
+ ( |
|
54 |
+ ssh_agent_client.SSHAgentClient.string(b'ssh-ed25519'), |
|
55 |
+ b'\x00\x00\x00\x0f\x00\x00\x00\x0bssh-ed25519', |
|
56 |
+ ), |
|
57 |
+ ]) |
|
58 |
+ def test_211_string(self, input, expected): |
|
59 |
+ string = ssh_agent_client.SSHAgentClient.string |
|
60 |
+ assert bytes(string(input)) == expected |
|
61 |
+ |
|
62 |
+ @pytest.mark.parametrize(['input', 'expected'], [ |
|
63 |
+ (b'\x00\x00\x00\x07ssh-rsa', b'ssh-rsa'), |
|
64 |
+ ( |
|
65 |
+ ssh_agent_client.SSHAgentClient.string(b'ssh-ed25519'), |
|
66 |
+ b'ssh-ed25519', |
|
67 |
+ ), |
|
68 |
+ ]) |
|
69 |
+ def test_212_unstring(self, input, expected): |
|
70 |
+ unstring = ssh_agent_client.SSHAgentClient.unstring |
|
71 |
+ unstring_prefix = ssh_agent_client.SSHAgentClient.unstring_prefix |
|
72 |
+ assert bytes(unstring(input)) == expected |
|
73 |
+ assert tuple( |
|
74 |
+ bytes(x) for x in unstring_prefix(input) |
|
75 |
+ ) == (expected, b'') |
|
76 |
+ |
|
77 |
+ @pytest.mark.parametrize(['value', 'exc_type', 'exc_pattern'], [ |
|
78 |
+ (10000000000000000, OverflowError, 'int too big to convert'), |
|
79 |
+ (-1, OverflowError, "can't convert negative int to unsigned"), |
|
80 |
+ ]) |
|
81 |
+ def test_310_uint32_exceptions(self, value, exc_type, exc_pattern): |
|
82 |
+ uint32 = ssh_agent_client.SSHAgentClient.uint32 |
|
83 |
+ with pytest.raises(exc_type, match=exc_pattern): |
|
84 |
+ uint32(value) |
|
85 |
+ |
|
86 |
+ @pytest.mark.parametrize(['input', 'exc_type', 'exc_pattern'], [ |
|
87 |
+ ('some string', TypeError, 'invalid payload type'), |
|
88 |
+ ]) |
|
89 |
+ def test_311_string_exceptions(self, input, exc_type, exc_pattern): |
|
90 |
+ string = ssh_agent_client.SSHAgentClient.string |
|
91 |
+ with pytest.raises(exc_type, match=exc_pattern): |
|
92 |
+ string(input) |
|
93 |
+ |
|
94 |
+ @pytest.mark.parametrize( |
|
95 |
+ ['input', 'exc_type', 'exc_pattern', 'has_trailer', 'parts'], [ |
|
96 |
+ (b'ssh', ValueError, 'malformed SSH byte string', False, None), |
|
97 |
+ ( |
|
98 |
+ b'\x00\x00\x00\x08ssh-rsa', |
|
99 |
+ ValueError, 'malformed SSH byte string', |
|
100 |
+ False, None, |
|
101 |
+ ), |
|
102 |
+ ( |
|
103 |
+ b'\x00\x00\x00\x04XXX trailing text', |
|
104 |
+ ValueError, 'malformed SSH byte string', |
|
105 |
+ True, (b'XXX ', b'trailing text'), |
|
106 |
+ ), |
|
107 |
+ ]) |
|
108 |
+ def test_312_unstring_exceptions(self, input, exc_type, exc_pattern, |
|
109 |
+ has_trailer, parts): |
|
110 |
+ unstring = ssh_agent_client.SSHAgentClient.unstring |
|
111 |
+ unstring_prefix = ssh_agent_client.SSHAgentClient.unstring_prefix |
|
112 |
+ with pytest.raises(exc_type, match=exc_pattern): |
|
113 |
+ unstring(input) |
|
114 |
+ if has_trailer: |
|
115 |
+ assert tuple(bytes(x) for x in unstring_prefix(input)) == parts |
|
116 |
+ else: |
|
117 |
+ with pytest.raises(exc_type, match=exc_pattern): |
|
118 |
+ unstring_prefix(input) |
|
119 |
+ |
|
120 |
+@tests.skip_if_no_agent |
|
121 |
+class TestAgentInteraction: |
|
122 |
+ |
|
123 |
+ @pytest.mark.parametrize(['keytype', 'data_dict'], |
|
124 |
+ list(tests.SUPPORTED_KEYS.items())) |
|
125 |
+ def test_200_sign_data_via_agent(self, keytype, data_dict): |
|
126 |
+ private_key = data_dict['private_key'] |
|
127 |
+ try: |
|
128 |
+ result = subprocess.run(['ssh-add', '-t', '30', '-q', '-'], |
|
129 |
+ input=private_key, check=True, |
|
130 |
+ capture_output=True) |
|
131 |
+ except subprocess.CalledProcessError as e: |
|
132 |
+ pytest.skip( |
|
133 |
+ f"uploading test key: {e!r}, stdout={e.stdout!r}, " |
|
134 |
+ f"stderr={e.stderr!r}" |
|
135 |
+ ) |
|
136 |
+ else: |
|
137 |
+ try: |
|
138 |
+ client = ssh_agent_client.SSHAgentClient() |
|
139 |
+ except OSError: # pragma: no cover |
|
140 |
+ pytest.skip('communication error with the SSH agent') |
|
141 |
+ with client: |
|
142 |
+ key_comment_pairs = {bytes(k): bytes(c) |
|
143 |
+ for k, c in client.list_keys()} |
|
144 |
+ public_key_data = data_dict['public_key_data'] |
|
145 |
+ expected_signature = data_dict['expected_signature'] |
|
146 |
+ derived_passphrase = data_dict['derived_passphrase'] |
|
147 |
+ if public_key_data not in key_comment_pairs: # pragma: no cover |
|
148 |
+ pytest.skip('prerequisite SSH key not loaded') |
|
149 |
+ signature = bytes(client.sign( |
|
150 |
+ payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
151 |
+ assert signature == expected_signature, 'SSH signature mismatch' |
|
152 |
+ signature2 = bytes(client.sign( |
|
153 |
+ payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
154 |
+ assert signature2 == expected_signature, 'SSH signature mismatch' |
|
155 |
+ assert ( |
|
156 |
+ derivepassphrase.Vault.phrase_from_key(public_key_data) == |
|
157 |
+ derived_passphrase |
|
158 |
+ ), 'SSH signature mismatch' |
|
159 |
+ |
|
160 |
+ @pytest.mark.parametrize(['keytype', 'data_dict'], |
|
161 |
+ list(tests.UNSUITABLE_KEYS.items())) |
|
162 |
+ def test_201_sign_data_via_agent_unsupported(self, keytype, data_dict): |
|
163 |
+ private_key = data_dict['private_key'] |
|
164 |
+ try: |
|
165 |
+ result = subprocess.run(['ssh-add', '-t', '30', '-q', '-'], |
|
166 |
+ input=private_key, check=True, |
|
167 |
+ capture_output=True) |
|
168 |
+ except subprocess.CalledProcessError as e: # pragma: no cover |
|
169 |
+ pytest.skip( |
|
170 |
+ f"uploading test key: {e!r}, stdout={e.stdout!r}, " |
|
171 |
+ f"stderr={e.stderr!r}" |
|
172 |
+ ) |
|
173 |
+ else: |
|
174 |
+ try: |
|
175 |
+ client = ssh_agent_client.SSHAgentClient() |
|
176 |
+ except OSError: # pragma: no cover |
|
177 |
+ pytest.skip('communication error with the SSH agent') |
|
178 |
+ with client: |
|
179 |
+ key_comment_pairs = {bytes(k): bytes(c) |
|
180 |
+ for k, c in client.list_keys()} |
|
181 |
+ public_key_data = data_dict['public_key_data'] |
|
182 |
+ expected_signature = data_dict['expected_signature'] |
|
183 |
+ if public_key_data not in key_comment_pairs: # pragma: no cover |
|
184 |
+ pytest.skip('prerequisite SSH key not loaded') |
|
185 |
+ signature = bytes(client.sign( |
|
186 |
+ payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
187 |
+ signature2 = bytes(client.sign( |
|
188 |
+ payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
189 |
+ assert signature != signature2, 'SSH signature repeatable?!' |
|
190 |
+ with pytest.raises(ValueError, match='unsuitable SSH key'): |
|
191 |
+ derivepassphrase.Vault.phrase_from_key(public_key_data) |
|
192 |
+ |
|
193 |
+ @staticmethod |
|
194 |
+ def _params(): |
|
195 |
+ for value in tests.SUPPORTED_KEYS.values(): |
|
196 |
+ key = value['public_key_data'] |
|
197 |
+ yield (key, False) |
|
198 |
+ singleton_key = tests.list_keys_singleton()[0].key |
|
199 |
+ for value in tests.SUPPORTED_KEYS.values(): |
|
200 |
+ key = value['public_key_data'] |
|
201 |
+ if key == singleton_key: |
|
202 |
+ yield (key, True) |
|
203 |
+ |
|
204 |
+ @pytest.mark.parametrize(['key', 'single'], list(_params())) |
|
205 |
+ def test_210_ssh_key_selector(self, monkeypatch, key, single): |
|
206 |
+ def key_is_suitable(key: bytes): |
|
207 |
+ return key in {v['public_key_data'] |
|
208 |
+ for v in tests.SUPPORTED_KEYS.values()} |
|
209 |
+ if single: |
|
210 |
+ monkeypatch.setattr(ssh_agent_client.SSHAgentClient, |
|
211 |
+ 'list_keys', tests.list_keys_singleton) |
|
212 |
+ keys = [pair.key for pair in tests.list_keys_singleton() |
|
213 |
+ if key_is_suitable(pair.key)] |
|
214 |
+ index = '1' |
|
215 |
+ text = f'Use this key? yes\n' |
|
216 |
+ else: |
|
217 |
+ monkeypatch.setattr(ssh_agent_client.SSHAgentClient, |
|
218 |
+ 'list_keys', tests.list_keys) |
|
219 |
+ keys = [pair.key for pair in tests.list_keys() |
|
220 |
+ if key_is_suitable(pair.key)] |
|
221 |
+ index = str(1 + keys.index(key)) |
|
222 |
+ n = len(keys) |
|
223 |
+ text = f'Your selection? (1-{n}, leave empty to abort): {index}\n' |
|
224 |
+ b64_key = base64.standard_b64encode(key).decode('ASCII') |
|
225 |
+ |
|
226 |
+ @click.command() |
|
227 |
+ def driver(): |
|
228 |
+ key = derivepassphrase.cli._select_ssh_key() |
|
229 |
+ click.echo(base64.standard_b64encode(key).decode('ASCII')) |
|
230 |
+ |
|
231 |
+ runner = click.testing.CliRunner(mix_stderr=True) |
|
232 |
+ result = runner.invoke(driver, [], |
|
233 |
+ input=('yes\n' if single else f'{index}\n'), |
|
234 |
+ catch_exceptions=True) |
|
235 |
+ assert result.stdout.startswith('Suitable SSH keys:\n'), ( |
|
236 |
+ 'missing expected output' |
|
237 |
+ ) |
|
238 |
+ assert text in result.stdout, 'missing expected output' |
|
239 |
+ assert ( |
|
240 |
+ result.stdout.endswith(f'\n{b64_key}\n') |
|
241 |
+ ), 'missing expected output' |
|
242 |
+ assert result.exit_code == 0, 'driver program failed?!' |
|
243 |
+ |
|
244 |
+ del _params |
|
245 |
+ |
|
246 |
+ def test_300_constructor_bad_running_agent(self, monkeypatch): |
|
247 |
+ monkeypatch.setenv('SSH_AUTH_SOCK', |
|
248 |
+ os.environ['SSH_AUTH_SOCK'] + '~') |
|
249 |
+ sock = socket.socket(family=socket.AF_UNIX) |
|
250 |
+ with pytest.raises(OSError): |
|
251 |
+ ssh_agent_client.SSHAgentClient(socket=sock) |
|
252 |
+ |
|
253 |
+ @pytest.mark.parametrize(['response'], [ |
|
254 |
+ (b'\x00\x00',), |
|
255 |
+ (b'\x00\x00\x00\x1f some bytes missing',), |
|
256 |
+ ]) |
|
257 |
+ def test_310_truncated_server_response(self, monkeypatch, response): |
|
258 |
+ client = ssh_agent_client.SSHAgentClient() |
|
259 |
+ response_stream = io.BytesIO(response) |
|
260 |
+ class PseudoSocket(object): |
|
261 |
+ def sendall(self, *args: Any, **kwargs: Any) -> Any: |
|
262 |
+ return None |
|
263 |
+ def recv(self, *args: Any, **kwargs: Any) -> Any: |
|
264 |
+ return response_stream.read(*args, **kwargs) |
|
265 |
+ pseudo_socket = PseudoSocket() |
|
266 |
+ monkeypatch.setattr(client, '_connection', pseudo_socket) |
|
267 |
+ with pytest.raises(EOFError): |
|
268 |
+ client.request(255, b'') |
|
269 |
+ |
|
270 |
+ @tests.skip_if_no_agent |
|
271 |
+ @pytest.mark.parametrize( |
|
272 |
+ ['response_code', 'response', 'exc_type', 'exc_pattern'], |
|
273 |
+ [ |
|
274 |
+ (255, b'', RuntimeError, 'error return from SSH agent:'), |
|
275 |
+ (12, b'\x00\x00\x00\x01', EOFError, 'truncated response'), |
|
276 |
+ (12, b'\x00\x00\x00\x00abc', RuntimeError, 'overlong response'), |
|
277 |
+ ] |
|
278 |
+ ) |
|
279 |
+ def test_320_list_keys_error_responses(self, monkeypatch, response_code, |
|
280 |
+ response, exc_type, exc_pattern): |
|
281 |
+ client = ssh_agent_client.SSHAgentClient() |
|
282 |
+ monkeypatch.setattr(client, 'request', |
|
283 |
+ lambda *a, **kw: (response_code, response)) |
|
284 |
+ with pytest.raises(exc_type, match=exc_pattern): |
|
285 |
+ client.list_keys() |
|
286 |
+ |
|
287 |
+ @tests.skip_if_no_agent |
|
288 |
+ @pytest.mark.parametrize( |
|
289 |
+ ['key', 'check', 'response', 'exc_type', 'exc_pattern'], |
|
290 |
+ [ |
|
291 |
+ ( |
|
292 |
+ b'invalid-key', |
|
293 |
+ True, |
|
294 |
+ (255, b''), |
|
295 |
+ KeyError, |
|
296 |
+ 'target SSH key not loaded into agent', |
|
297 |
+ ), |
|
298 |
+ ( |
|
299 |
+ tests.SUPPORTED_KEYS['ed25519']['public_key_data'], |
|
300 |
+ True, |
|
301 |
+ (255, b''), |
|
302 |
+ RuntimeError, |
|
303 |
+ 'signing data failed:', |
|
304 |
+ ) |
|
305 |
+ ] |
|
306 |
+ ) |
|
307 |
+ def test_330_sign_error_responses(self, monkeypatch, key, check, |
|
308 |
+ response, exc_type, exc_pattern): |
|
309 |
+ client = ssh_agent_client.SSHAgentClient() |
|
310 |
+ monkeypatch.setattr(client, 'request', lambda a, b: response) |
|
311 |
+ KeyCommentPair = ssh_agent_client.types.KeyCommentPair |
|
312 |
+ loaded_keys = [KeyCommentPair(v['public_key_data'], b'no comment') |
|
313 |
+ for v in tests.SUPPORTED_KEYS.values()] |
|
314 |
+ monkeypatch.setattr(client, 'list_keys', lambda: loaded_keys) |
|
315 |
+ with pytest.raises(exc_type, match=exc_pattern): |
|
316 |
+ client.sign(key, b'abc', check_if_key_loaded=check) |
|
0 | 317 |