Marco Ricci commited on 2025-07-26 16:45:41
Zeige 2 geänderte Dateien mit 168 Einfügungen und 29 Löschungen.
Record and provide more deterministic signatures for the test keys, where feasible. In particular, for DSA and ECDSA keys, support recording the RFC 6979 deterministic DSA signatures, as implemented by Pageant. In particular, this entails that the `expected_signature` and `derived_passphrase` fields of the test keys need to change to accomodate the new shape of the data. For the existing DSA and ECDSA keys, RFC 6979 signatures and expected master passphrases are also provided. Providing the Pageant 0.68–0.80 deterministic signatures and expected master passphrases however is not quite so straight-forward: because the signature class is the subject of CVE-2024-31497, Pageant < 0.81 is considered to have a security hole, and thus it is hard to obtain pre-compiled, unpatched installations of Pageant to compute the signatures/master passphrases against.
... | ... |
@@ -42,6 +42,66 @@ if TYPE_CHECKING: |
42 | 42 |
from typing_extensions import Any |
43 | 43 |
|
44 | 44 |
|
45 |
+class SSHTestKeyDeterministicSignatureClass(str, enum.Enum): |
|
46 |
+ """The class of a deterministic signature from an SSH test key. |
|
47 |
+ |
|
48 |
+ Attributes: |
|
49 |
+ SPEC: |
|
50 |
+ A deterministic signature directly implied by the |
|
51 |
+ specification of the signature algorithm. |
|
52 |
+ RFC_6979: |
|
53 |
+ A deterministic signature as specified by RFC 6979. Only |
|
54 |
+ used with DSA and ECDSA keys (that aren't also EdDSA keys). |
|
55 |
+ Pageant_068_080: |
|
56 |
+ A deterministic signature as specified by Pageant 0.68. |
|
57 |
+ Only used with DSA and ECDSA keys (that aren't also EdDSA |
|
58 |
+ keys), and only used with Pageant from 0.68 up to and |
|
59 |
+ including 0.80. |
|
60 |
+ |
|
61 |
+ Usage of this signature class together with an ECDSA NIST |
|
62 |
+ P-521 key [turned out to leak enough information per |
|
63 |
+ signature to quickly compromise the entire private key |
|
64 |
+ (CVE-2024-31497)][PUTTY_CVE_2024_31497], so newer Pageant |
|
65 |
+ versions abandon this signature class in favor of RFC 6979. |
|
66 |
+ |
|
67 |
+ [PUTTY_CVE_2024_31497]: https://www.chiark.greenend.org.uk/~sgtatham/putty/wishlist/vuln-p521-bias.html |
|
68 |
+ |
|
69 |
+ """ |
|
70 |
+ |
|
71 |
+ SPEC = enum.auto() |
|
72 |
+ """""" |
|
73 |
+ RFC_6979 = enum.auto() |
|
74 |
+ """""" |
|
75 |
+ Pageant_068_080 = enum.auto() |
|
76 |
+ """""" |
|
77 |
+ |
|
78 |
+ |
|
79 |
+class SSHTestKeyDeterministicSignature(NamedTuple): |
|
80 |
+ """An SSH test key deterministic signature. |
|
81 |
+ |
|
82 |
+ Attributes: |
|
83 |
+ signature: |
|
84 |
+ The binary signature of the [vault UUID][vault.Vault.UUID] |
|
85 |
+ under this signature class. |
|
86 |
+ derived_passphrase: |
|
87 |
+ The equivalent master passphrase derived from this |
|
88 |
+ signature. |
|
89 |
+ signature_class: |
|
90 |
+ The [signature |
|
91 |
+ class][SSHTestKeyDeterministicSignatureClass]. |
|
92 |
+ |
|
93 |
+ """ |
|
94 |
+ |
|
95 |
+ signature: bytes |
|
96 |
+ """""" |
|
97 |
+ derived_passphrase: bytes |
|
98 |
+ """""" |
|
99 |
+ signature_class: SSHTestKeyDeterministicSignatureClass = ( |
|
100 |
+ SSHTestKeyDeterministicSignatureClass.SPEC |
|
101 |
+ ) |
|
102 |
+ """""" |
|
103 |
+ |
|
104 |
+ |
|
45 | 105 |
class SSHTestKey(NamedTuple): |
46 | 106 |
"""An SSH test key. |
47 | 107 |
|
... | ... |
@@ -56,15 +116,11 @@ class SSHTestKey(NamedTuple): |
56 | 116 |
OpenSSH's v1 private key format. |
57 | 117 |
private_key_blob: |
58 | 118 |
The SSH protocol wire format of the private key. |
59 |
- expected_signature: |
|
60 |
- For deterministic signature types, this is the expected |
|
61 |
- signature of the vault UUID. For other types this is |
|
62 |
- `None`. |
|
63 |
- derived_passphrase: |
|
64 |
- For deterministic signature types, this is the "equivalent |
|
65 |
- master passphrase" derived from this key (a transformation |
|
66 |
- of [`expected_signature`][]). For other types this is |
|
67 |
- `None`. |
|
119 |
+ expected_signatures: |
|
120 |
+ A mapping of deterministic signature classes to the |
|
121 |
+ expected, deterministic signature (of that class) of the |
|
122 |
+ vault UUID for this key, together with the respective |
|
123 |
+ "equivalent master passphrase" derived from this signature. |
|
68 | 124 |
|
69 | 125 |
""" |
70 | 126 |
|
... | ... |
@@ -76,9 +132,9 @@ class SSHTestKey(NamedTuple): |
76 | 132 |
"""""" |
77 | 133 |
private_key_blob: bytes |
78 | 134 |
"""""" |
79 |
- expected_signature: bytes | None = None |
|
80 |
- """""" |
|
81 |
- derived_passphrase: bytes | str | None = None |
|
135 |
+ expected_signatures: Mapping[ |
|
136 |
+ SSHTestKeyDeterministicSignatureClass, SSHTestKeyDeterministicSignature |
|
137 |
+ ] |
|
82 | 138 |
"""""" |
83 | 139 |
|
84 | 140 |
def is_suitable( |
... | ... |
@@ -644,7 +700,9 @@ idwcakUGCekJD/vCEml2AAAAG3Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQEC |
644 | 700 |
81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1 |
645 | 701 |
30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76 |
646 | 702 |
"""), |
647 |
- expected_signature=bytes.fromhex(""" |
|
703 |
+ expected_signatures={ |
|
704 |
+ SSHTestKeyDeterministicSignatureClass.SPEC: SSHTestKeyDeterministicSignature( |
|
705 |
+ signature=bytes.fromhex(""" |
|
648 | 706 |
00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39 |
649 | 707 |
00 00 00 40 |
650 | 708 |
f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86 |
... | ... |
@@ -654,6 +712,8 @@ idwcakUGCekJD/vCEml2AAAAG3Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQEC |
654 | 712 |
"""), |
655 | 713 |
derived_passphrase=rb'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==', |
656 | 714 |
), |
715 |
+ }, |
|
716 |
+ ), |
|
657 | 717 |
# Currently only supported by PuTTY (which is deficient in other |
658 | 718 |
# niceties of the SSH agent and the agent's client). |
659 | 719 |
'ed448': SSHTestKey( |
... | ... |
@@ -694,7 +754,9 @@ dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ== |
694 | 754 |
46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69 |
695 | 755 |
b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00 |
696 | 756 |
"""), |
697 |
- expected_signature=bytes.fromhex(""" |
|
757 |
+ expected_signatures={ |
|
758 |
+ SSHTestKeyDeterministicSignatureClass.SPEC: SSHTestKeyDeterministicSignature( |
|
759 |
+ signature=bytes.fromhex(""" |
|
698 | 760 |
00 00 00 09 73 73 68 2d 65 64 34 34 38 |
699 | 761 |
00 00 00 72 06 86 |
700 | 762 |
f4 64 a4 a6 ba d9 c3 22 c4 93 49 99 fc 11 de 67 |
... | ... |
@@ -707,6 +769,8 @@ dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ== |
707 | 769 |
"""), |
708 | 770 |
derived_passphrase=rb'Bob0ZKSmutnDIsSTSZn8Ed5nlwjy2Lc8LBPnxRwekqYO2C9tgQOCAONy5DJtctJtMoQ/zKkeVywAmrOZ3kXazi7R2+WJ8zW+JFiQxsoE8NuIgNu9d3yAIH86SGH2H66pXlN74J2THurc67XNVkzqjwgA', |
709 | 771 |
), |
772 |
+ }, |
|
773 |
+ ), |
|
710 | 774 |
'rsa': SSHTestKey( |
711 | 775 |
private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
712 | 776 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn |
... | ... |
@@ -873,7 +937,9 @@ Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB |
873 | 937 |
0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31 |
874 | 938 |
d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f |
875 | 939 |
"""), |
876 |
- expected_signature=bytes.fromhex(""" |
|
940 |
+ expected_signatures={ |
|
941 |
+ SSHTestKeyDeterministicSignatureClass.SPEC: SSHTestKeyDeterministicSignature( |
|
942 |
+ signature=bytes.fromhex(""" |
|
877 | 943 |
00 00 00 07 73 73 68 2d 72 73 61 |
878 | 944 |
00 00 01 80 |
879 | 945 |
a2 10 7c 2e f6 bb 53 a8 74 2a a1 19 99 ad 81 be |
... | ... |
@@ -903,6 +969,8 @@ Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB |
903 | 969 |
"""), |
904 | 970 |
derived_passphrase=rb'ohB8Lva7U6h0KqEZma2Bvnmc7dadCU5uxRhIM5B3mWj3ngNazU4Y64l9haLurkqS9m/Ouf6GfyprMdpuGv6ipYi4RH+hdnOz7HW10Ka5FZdlCRN9lCHR+10PiyMEd8LDVSKxoAmK9Tgq1n8bhymgJdMlb8tkYQeY3BTFhPiSJF5QEWtJ5fDMKcspqRnYp3EfkQsFsQFLwl8ApbYhv/gsnWebRzsKSWt5Lfwd7Ayw5Sci1an408P530ho6fvvPNwmv8/qKUMBpuPFUZX0Zm2KVeJH7OgwRUyuR+fJpCGLZLq2iPYh+HO5yxGheHWSxlrlZP7tQtmVmeYrbzwWPCh0pHIvDT8sM2eqNRmO57URL7P3asUC4m+jQuNiGZkD6qUg56HjvMgGo7V81nZd329gRoMqCADW09mkwUGM+GBWRYHaO6IWH55OdYMX2sNTwz4ZpBu80im4eGEreOaxUrDV7N5pLEhi2f3Rm2uwSdvT/zjnENktzp8NXgl7N9J7w7/O', |
905 | 971 |
), |
972 |
+ }, |
|
973 |
+ ), |
|
906 | 974 |
'dsa1024': SSHTestKey( |
907 | 975 |
private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
908 | 976 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH |
... | ... |
@@ -996,8 +1064,19 @@ u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW |
996 | 1064 |
a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef |
997 | 1065 |
40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6 |
998 | 1066 |
"""), |
999 |
- expected_signature=None, |
|
1000 |
- derived_passphrase=None, |
|
1067 |
+ expected_signatures={ |
|
1068 |
+ SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature( |
|
1069 |
+ signature=bytes.fromhex(""" |
|
1070 |
+ 00 00 00 07 73 73 68 2d 64 73 73 |
|
1071 |
+ 00 00 00 28 11 5f 4d 13 c2 ee 61 97 |
|
1072 |
+ 1e f6 23 14 3b 2b dd cf 06 c0 71 13 cc ac 34 19 |
|
1073 |
+ ad 36 8d 79 aa 25 fb 5e 4f ea fe 6b 5b fa 57 42 |
|
1074 |
+"""), |
|
1075 |
+ derived_passphrase=rb"""EV9NE8LuYZce9iMUOyvdzwbAcRPMrDQZrTaNeaol+15P6v5rW/pXQg== |
|
1076 |
+""", |
|
1077 |
+ signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979, |
|
1078 |
+ ), |
|
1079 |
+ }, |
|
1001 | 1080 |
), |
1002 | 1081 |
'ecdsa256': SSHTestKey( |
1003 | 1082 |
private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
... | ... |
@@ -1037,8 +1116,24 @@ dGhvdXQgcGFzc3BocmFzZQECAwQ= |
1037 | 1116 |
64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19 |
1038 | 1117 |
49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba |
1039 | 1118 |
"""), |
1040 |
- expected_signature=None, |
|
1041 |
- derived_passphrase=None, |
|
1119 |
+ expected_signatures={ |
|
1120 |
+ SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature( |
|
1121 |
+ signature=bytes.fromhex(""" |
|
1122 |
+ 00 00 00 13 65 63 64 |
|
1123 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36 |
|
1124 |
+ 00 00 00 49 |
|
1125 |
+ 00 00 00 20 |
|
1126 |
+ 22 ad 23 8a 9c 5d ca 4e ea 73 e7 29 77 ab a8 b2 |
|
1127 |
+ 2e 01 d8 de 11 ae c9 b3 57 ce d5 84 9c 85 73 eb |
|
1128 |
+ 00 00 00 21 00 |
|
1129 |
+ 9b 1a cb dd 45 89 f0 37 95 9c a2 d8 ac c3 f7 71 |
|
1130 |
+ 55 33 50 86 9e cb 3a 95 e4 68 80 1a 9d d6 d5 bc |
|
1131 |
+"""), |
|
1132 |
+ derived_passphrase=rb"""AAAAICKtI4qcXcpO6nPnKXerqLIuAdjeEa7Js1fO1YSchXPrAAAAIQCbGsvdRYnwN5Wcotisw/dxVTNQhp7LOpXkaIAandbVvA== |
|
1133 |
+""", |
|
1134 |
+ signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979, |
|
1135 |
+ ), |
|
1136 |
+ }, |
|
1042 | 1137 |
), |
1043 | 1138 |
'ecdsa384': SSHTestKey( |
1044 | 1139 |
private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
... | ... |
@@ -1084,8 +1179,26 @@ JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B |
1084 | 1179 |
7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5 |
1085 | 1180 |
79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15 |
1086 | 1181 |
"""), |
1087 |
- expected_signature=None, |
|
1088 |
- derived_passphrase=None, |
|
1182 |
+ expected_signatures={ |
|
1183 |
+ SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature( |
|
1184 |
+ signature=bytes.fromhex(""" |
|
1185 |
+ 00 00 00 13 65 63 64 |
|
1186 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34 |
|
1187 |
+ 00 00 00 68 |
|
1188 |
+ 00 00 00 30 |
|
1189 |
+ 78 e1 a8 f5 8c d2 7a 21 e5 a2 ca e6 d0 1a 19 f8 |
|
1190 |
+ 3a 1c 39 7e 71 a0 e6 7e 93 83 49 95 05 01 d0 3e |
|
1191 |
+ 23 22 cd 09 63 7f 7c 6c b0 97 44 6d 7e 48 39 87 |
|
1192 |
+ 00 00 00 30 |
|
1193 |
+ 10 ee 85 51 77 2b 91 2c e9 42 79 66 59 8a a2 c0 |
|
1194 |
+ d2 c8 8a 8f 2f 8f 33 87 9e 12 54 e4 da 02 f9 e7 |
|
1195 |
+ 95 f5 82 6f 82 2b 38 6d 6e 5d 17 15 ac 12 e7 62 |
|
1196 |
+"""), |
|
1197 |
+ derived_passphrase=rb"""AAAAMHjhqPWM0noh5aLK5tAaGfg6HDl+caDmfpODSZUFAdA+IyLNCWN/fGywl0Rtfkg5hwAAADAQ7oVRdyuRLOlCeWZZiqLA0siKjy+PM4eeElTk2gL555X1gm+CKzhtbl0XFawS52I= |
|
1198 |
+""", |
|
1199 |
+ signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979, |
|
1200 |
+ ), |
|
1201 |
+ }, |
|
1089 | 1202 |
), |
1090 | 1203 |
'ecdsa521': SSHTestKey( |
1091 | 1204 |
private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
... | ... |
@@ -1138,8 +1251,28 @@ Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQ== |
1138 | 1251 |
23 a0 a4 b7 fc 27 43 54 c4 a6 3f 33 0c a8 91 12 |
1139 | 1252 |
bc 6c f5 ee 90 2c 61 25 35 19 33 2c f3 2c fa 63 |
1140 | 1253 |
"""), |
1141 |
- expected_signature=None, |
|
1142 |
- derived_passphrase=None, |
|
1254 |
+ expected_signatures={ |
|
1255 |
+ SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature( |
|
1256 |
+ signature=bytes.fromhex(""" |
|
1257 |
+ 00 00 00 13 65 63 64 |
|
1258 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31 |
|
1259 |
+ 00 00 00 8b |
|
1260 |
+ 00 00 00 42 01 d8 |
|
1261 |
+ ea c2 1e 55 c6 9e dd 4b 00 ed 1b 93 19 cc 9b 74 |
|
1262 |
+ 27 44 c0 c0 e3 5b 3d 81 15 00 12 cc 07 89 54 97 |
|
1263 |
+ ec 60 42 ad e6 40 c1 c6 5f c0 1b c3 0a 8e 58 6e |
|
1264 |
+ da 3f a9 57 90 04 79 46 1d 48 bb 19 67 e9 65 19 |
|
1265 |
+ 00 00 00 41 7d |
|
1266 |
+ 58 e0 2e d7 86 2e 36 8c 1a 44 23 af 19 e7 51 97 |
|
1267 |
+ bb fb 32 90 a1 35 bb 88 d7 b5 22 37 b3 99 ba e4 |
|
1268 |
+ a7 9d 2d 56 14 0a f5 68 f5 cc 38 84 e9 b6 c6 71 |
|
1269 |
+ 7a 3b 87 e7 7a b1 37 e7 1d e6 80 96 d1 a6 1e bc |
|
1270 |
+"""), |
|
1271 |
+ derived_passphrase=rb"""AAAAQgHY6sIeVcae3UsA7RuTGcybdCdEwMDjWz2BFQASzAeJVJfsYEKt5kDBxl/AG8MKjlhu2j+pV5AEeUYdSLsZZ+llGQAAAEF9WOAu14YuNowaRCOvGedRl7v7MpChNbuI17UiN7OZuuSnnS1WFAr1aPXMOITptsZxejuH53qxN+cd5oCW0aYevA== |
|
1272 |
+""", |
|
1273 |
+ signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979, |
|
1274 |
+ ), |
|
1275 |
+ }, |
|
1143 | 1276 |
), |
1144 | 1277 |
} |
1145 | 1278 |
"""The master list of SSH test keys.""" |
... | ... |
@@ -1749,8 +1882,9 @@ def sign( |
1749 | 1882 |
assert message == vault.Vault.UUID |
1750 | 1883 |
for value in SUPPORTED_KEYS.values(): |
1751 | 1884 |
if value.public_key_data == key: # pragma: no branch |
1752 |
- assert value.expected_signature is not None |
|
1753 |
- return value.expected_signature |
|
1885 |
+ return value.expected_signatures[ |
|
1886 |
+ SSHTestKeyDeterministicSignatureClass.SPEC |
|
1887 |
+ ].signature |
|
1754 | 1888 |
raise AssertionError |
1755 | 1889 |
|
1756 | 1890 |
|
... | ... |
@@ -572,10 +572,15 @@ class TestAgentInteraction: |
572 | 572 |
client = ssh_agent_client_with_test_keys_loaded |
573 | 573 |
key_comment_pairs = {bytes(k): bytes(c) for k, c in client.list_keys()} |
574 | 574 |
public_key_data = ssh_test_key.public_key_data |
575 |
- expected_signature = ssh_test_key.expected_signature |
|
576 |
- derived_passphrase = ssh_test_key.derived_passphrase |
|
577 |
- assert expected_signature is not None |
|
578 |
- assert derived_passphrase is not None |
|
575 |
+ assert ( |
|
576 |
+ tests.SSHTestKeyDeterministicSignatureClass.SPEC |
|
577 |
+ in ssh_test_key.expected_signatures |
|
578 |
+ ) |
|
579 |
+ sig = ssh_test_key.expected_signatures[ |
|
580 |
+ tests.SSHTestKeyDeterministicSignatureClass.SPEC |
|
581 |
+ ] |
|
582 |
+ expected_signature = sig.signature |
|
583 |
+ derived_passphrase = sig.derived_passphrase |
|
579 | 584 |
if public_key_data not in key_comment_pairs: # pragma: no cover |
580 | 585 |
pytest.skip(f'prerequisite {ssh_test_key_type} SSH key not loaded') |
581 | 586 |
signature = bytes( |
582 | 587 |