Add more hypothesis tests for the `ssh_agent` module
Marco Ricci

Marco Ricci commited on 2025-01-26 16:21:45
Zeige 1 geänderte Dateien mit 64 Einfügungen und 10 Löschungen.


Add more hypothesis tests for SSH string encoding and decoding, based on
David R. MacIver's articles.  Also document the explicit examples in the
proper way.
... ...
@@ -819,17 +819,33 @@ class TestAgentInteraction:
819 819
 class TestHypotheses:
820 820
     """Test properties via hypothesis."""
821 821
 
822
+    @staticmethod
823
+    def as_ssh_string(bytestring: bytes) -> bytes:
824
+        return int.to_bytes(len(bytestring), 4, 'big') + bytestring
825
+
826
+    @staticmethod
827
+    def canonicalize1(data: bytes) -> bytes:
828
+        return ssh_agent.SSHAgentClient.string(
829
+            ssh_agent.SSHAgentClient.unstring(data)
830
+        )
831
+
832
+    @staticmethod
833
+    def canonicalize2(data: bytes) -> bytes:
834
+        unstringed, trailer = ssh_agent.SSHAgentClient.unstring_prefix(data)
835
+        assert not trailer
836
+        return ssh_agent.SSHAgentClient.string(unstringed)
837
+
822 838
     @hypothesis.given(strategies.integers(min_value=0, max_value=0xFFFFFFFF))
823
-    # standard example value
824
-    @hypothesis.example(0xDEADBEEF)
839
+    @hypothesis.example(0xDEADBEEF).via('manual, pre-hypothesis example')
825 840
     def test_210a_uint32_from_number(self, num: int) -> None:
826 841
         """`uint32` encoding works, starting from numbers."""
827 842
         uint32 = ssh_agent.SSHAgentClient.uint32
828 843
         assert int.from_bytes(uint32(num), 'big', signed=False) == num
829 844
 
830 845
     @hypothesis.given(strategies.binary(min_size=4, max_size=4))
831
-    # standard example value
832
-    @hypothesis.example(b'\xde\xad\xbe\xef')
846
+    @hypothesis.example(b'\xde\xad\xbe\xef').via(
847
+        'manual, pre-hypothesis example'
848
+    )
833 849
     def test_210b_uint32_from_bytestring(self, bytestring: bytes) -> None:
834 850
         """`uint32` encoding works, starting from length four byte strings."""
835 851
         uint32 = ssh_agent.SSHAgentClient.uint32
... ...
@@ -839,8 +855,9 @@ class TestHypotheses:
839 855
         )
840 856
 
841 857
     @hypothesis.given(strategies.binary(max_size=0x0001FFFF))
842
-    # example: highest order bit is set
843
-    @hypothesis.example(b'DEADBEEF' * 10000)
858
+    @hypothesis.example(b'DEADBEEF' * 10000).via(
859
+        'manual, pre-hypothesis example with highest order bit set'
860
+    )
844 861
     def test_211a_string_from_bytestring(self, bytestring: bytes) -> None:
845 862
         """SSH string encoding works, starting from a byte string."""
846 863
         res = ssh_agent.SSHAgentClient.string(bytestring)
... ...
@@ -849,10 +866,22 @@ class TestHypotheses:
849 866
         assert res[4:] == bytestring
850 867
 
851 868
     @hypothesis.given(strategies.binary(max_size=0x00FFFFFF))
852
-    # example: check for double-deserialization
853
-    @hypothesis.example(b'\x00\x00\x00\x07ssh-rsa')
854
-    def test_212_string_unstring(self, bytestring: bytes) -> None:
855
-        """SSH string decoding of encoded SSH strings works."""
869
+    @hypothesis.example(b'\x00\x00\x00\x07ssh-rsa').via(
870
+        'manual, pre-hypothesis example to attempt to detect double-decoding'
871
+    )
872
+    @hypothesis.example(b'\x00\x00\x00\x01').via(
873
+        'detect no-op encoding via ill-formed SSH string'
874
+    )
875
+    def test_212a_unstring_of_string_of_data(self, bytestring: bytes) -> None:
876
+        """SSH string decoding of encoded SSH strings works.
877
+
878
+        References:
879
+
880
+          * [David R. MacIver: The Encode/Decode invariant][ENCODE_DECODE]
881
+
882
+        [ENCODE_DECODE]: https://hypothesis.works/articles/encode-decode-invariant/
883
+
884
+        """
856 885
         string = ssh_agent.SSHAgentClient.string
857 886
         unstring = ssh_agent.SSHAgentClient.unstring
858 887
         unstring_prefix = ssh_agent.SSHAgentClient.unstring_prefix
... ...
@@ -862,3 +891,28 @@ class TestHypotheses:
862 891
         trailing_data = b'  trailing data'
863 892
         encoded2 = string(bytestring) + trailing_data
864 893
         assert unstring_prefix(encoded2) == (bytestring, trailing_data)
894
+
895
+    @hypothesis.given(
896
+        strategies.binary(max_size=0x00FFFFFF).map(
897
+            # Scoping issues, and the fact that staticmethod objects
898
+            # (before class finalization) are not callable, necessitate
899
+            # wrapping this staticmethod call in a lambda.
900
+            lambda x: TestHypotheses.as_ssh_string(x)  # noqa: PLW0108
901
+        ),
902
+    )
903
+    def test_212b_string_of_unstring_of_data(self, encoded: bytes) -> None:
904
+        """SSH string decoding of encoded SSH strings works.
905
+
906
+        References:
907
+
908
+          * [David R. MacIver: Another invariant to test for
909
+            encoders][DECODE_ENCODE]
910
+
911
+        [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/
912
+
913
+        """
914
+        canonical_functions = [self.canonicalize1, self.canonicalize2]
915
+        for canon1 in canonical_functions:
916
+            for canon2 in canonical_functions:
917
+                assert canon1(encoded) == canon2(encoded)
918
+                assert canon1(canon2(encoded)) == canon1(encoded)
865 919