Provide more deterministic signatures for the test keys
Marco Ricci

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