Repository: wbond/asn1crypto

Issue:
Can asn1crypto support pickle!
Whether asn1crypto supports serialization and deserialization with files. I found that it could not work normally after being serialized and saved to a file. It was not in the same memory twice.
[https://github.com/MatthiasValvekens/pyHanko/discussions/154](interrupted_signing)

Discussion:
MatthiasValvekens: OK, now only the macOS seem to be failing, but fortunately not because of anything in this commit (looks like they got cancelled out of the gate).

Patch:
diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py
index fc25e4d..b7d406c 100644
--- a/asn1crypto/algos.py
+++ b/asn1crypto/algos.py
@@ -245,17 +245,29 @@ class SignedDigestAlgorithmId(ObjectIdentifier):
         '1.2.840.10040.4.3': 'sha1_dsa',
         '1.3.14.3.2.13': 'sha1_dsa',
         '1.3.14.3.2.27': 'sha1_dsa',
+        # Source: NIST CSOR Algorithm Registrations
         '2.16.840.1.101.3.4.3.1': 'sha224_dsa',
         '2.16.840.1.101.3.4.3.2': 'sha256_dsa',
+        '2.16.840.1.101.3.4.3.3': 'sha384_dsa',
+        '2.16.840.1.101.3.4.3.4': 'sha512_dsa',
         '1.2.840.10045.4.1': 'sha1_ecdsa',
         '1.2.840.10045.4.3.1': 'sha224_ecdsa',
         '1.2.840.10045.4.3.2': 'sha256_ecdsa',
         '1.2.840.10045.4.3.3': 'sha384_ecdsa',
         '1.2.840.10045.4.3.4': 'sha512_ecdsa',
+        # Source: NIST CSOR Algorithm Registrations
+        '2.16.840.1.101.3.4.3.5': 'sha3_224_dsa',
+        '2.16.840.1.101.3.4.3.6': 'sha3_256_dsa',
+        '2.16.840.1.101.3.4.3.7': 'sha3_384_dsa',
+        '2.16.840.1.101.3.4.3.8': 'sha3_512_dsa',
         '2.16.840.1.101.3.4.3.9': 'sha3_224_ecdsa',
         '2.16.840.1.101.3.4.3.10': 'sha3_256_ecdsa',
         '2.16.840.1.101.3.4.3.11': 'sha3_384_ecdsa',
         '2.16.840.1.101.3.4.3.12': 'sha3_512_ecdsa',
+        '2.16.840.1.101.3.4.3.13': 'sha3_224_rsa',
+        '2.16.840.1.101.3.4.3.14': 'sha3_256_rsa',
+        '2.16.840.1.101.3.4.3.15': 'sha3_384_rsa',
+        '2.16.840.1.101.3.4.3.16': 'sha3_512_rsa',
         # For when the digest is specified elsewhere in a Sequence
         '1.2.840.113549.1.1.1': 'rsassa_pkcs1v15',
         '1.2.840.10040.4.1': 'dsa',
@@ -281,14 +293,25 @@ class SignedDigestAlgorithmId(ObjectIdentifier):
         'sha256_dsa': '2.16.840.1.101.3.4.3.2',
         'sha256_ecdsa': '1.2.840.10045.4.3.2',
         'sha256_rsa': '1.2.840.113549.1.1.11',
+        'sha384_dsa': '2.16.840.1.101.3.4.3.3',
         'sha384_ecdsa': '1.2.840.10045.4.3.3',
         'sha384_rsa': '1.2.840.113549.1.1.12',
+        'sha512_dsa': '2.16.840.1.101.3.4.3.4',
         'sha512_ecdsa': '1.2.840.10045.4.3.4',
         'sha512_rsa': '1.2.840.113549.1.1.13',
+        # Source: NIST CSOR Algorithm Registrations
+        'sha3_224_dsa': '2.16.840.1.101.3.4.3.5',
+        'sha3_256_dsa': '2.16.840.1.101.3.4.3.6',
+        'sha3_384_dsa': '2.16.840.1.101.3.4.3.7',
+        'sha3_512_dsa': '2.16.840.1.101.3.4.3.8',
         'sha3_224_ecdsa': '2.16.840.1.101.3.4.3.9',
         'sha3_256_ecdsa': '2.16.840.1.101.3.4.3.10',
         'sha3_384_ecdsa': '2.16.840.1.101.3.4.3.11',
         'sha3_512_ecdsa': '2.16.840.1.101.3.4.3.12',
+        'sha3_224_rsa': '2.16.840.1.101.3.4.3.13',
+        'sha3_256_rsa': '2.16.840.1.101.3.4.3.14',
+        'sha3_384_rsa': '2.16.840.1.101.3.4.3.15',
+        'sha3_512_rsa': '2.16.840.1.101.3.4.3.16',
         'ed25519': '1.3.101.112',
         'ed448': '1.3.101.113',
     }
@@ -323,11 +346,21 @@ class SignedDigestAlgorithm(_ForceNullParameters, Sequence):
             'sha256_rsa': 'rsassa_pkcs1v15',
             'sha384_rsa': 'rsassa_pkcs1v15',
             'sha512_rsa': 'rsassa_pkcs1v15',
+            'sha3_224_rsa': 'rsassa_pkcs1v15',
+            'sha3_256_rsa': 'rsassa_pkcs1v15',
+            'sha3_384_rsa': 'rsassa_pkcs1v15',
+            'sha3_512_rsa': 'rsassa_pkcs1v15',
             'rsassa_pkcs1v15': 'rsassa_pkcs1v15',
             'rsassa_pss': 'rsassa_pss',
             'sha1_dsa': 'dsa',
             'sha224_dsa': 'dsa',
             'sha256_dsa': 'dsa',
+            'sha384_dsa': 'dsa',
+            'sha512_dsa': 'dsa',
+            'sha3_224_dsa': 'dsa',
+            'sha3_256_dsa': 'dsa',
+            'sha3_384_dsa': 'dsa',
+            'sha3_512_dsa': 'dsa',
             'dsa': 'dsa',
             'sha1_ecdsa': 'ecdsa',
             'sha224_ecdsa': 'ecdsa',
@@ -373,11 +406,25 @@ class SignedDigestAlgorithm(_ForceNullParameters, Sequence):
             'sha1_dsa': 'sha1',
             'sha224_dsa': 'sha224',
             'sha256_dsa': 'sha256',
+            'sha384_dsa': 'sha384',
+            'sha512_dsa': 'sha512',
             'sha1_ecdsa': 'sha1',
             'sha224_ecdsa': 'sha224',
             'sha256_ecdsa': 'sha256',
             'sha384_ecdsa': 'sha384',
             'sha512_ecdsa': 'sha512',
+            'sha3_224_dsa': 'sha3_224',
+            'sha3_256_dsa': 'sha3_256',
+            'sha3_384_dsa': 'sha3_384',
+            'sha3_512_dsa': 'sha3_512',
+            'sha3_224_ecdsa': 'sha3_224',
+            'sha3_256_ecdsa': 'sha3_256',
+            'sha3_384_ecdsa': 'sha3_384',
+            'sha3_512_ecdsa': 'sha3_512',
+            'sha3_224_rsa': 'sha3_224',
+            'sha3_256_rsa': 'sha3_256',
+            'sha3_384_rsa': 'sha3_384',
+            'sha3_512_rsa': 'sha3_512',
             'ed25519': 'sha512',
             'ed448': 'shake256',
         }
diff --git a/asn1crypto/core.py b/asn1crypto/core.py
index 364c6b5..2edd4f3 100644
--- a/asn1crypto/core.py
+++ b/asn1crypto/core.py
@@ -166,6 +166,15 @@ def load(encoded_data, strict=False):
     return Asn1Value.load(encoded_data, strict=strict)
 
 
+def unpickle_helper(asn1crypto_cls, der_bytes):
+    """
+    Helper function to integrate with pickle.
+
+    Note that this must be an importable top-level function.
+    """
+    return asn1crypto_cls.load(der_bytes)
+
+
 class Asn1Value(object):
     """
     The basis of all ASN.1 values
@@ -481,6 +490,12 @@ class Asn1Value(object):
 
         return self.__repr__()
 
+    def __reduce__(self):
+        """
+        Permits pickling Asn1Value objects using their DER representation.
+        """
+        return unpickle_helper, (self.__class__, self.dump())
+
     def _new_instance(self):
         """
         Constructs a new copy of the current object, preserving any tagging


Test patch:
diff --git a/tests/test_algos.py b/tests/test_algos.py
index 88e8cbf..064aad5 100644
--- a/tests/test_algos.py
+++ b/tests/test_algos.py
@@ -6,6 +6,8 @@ import sys
 import os
 
 from asn1crypto import algos, core
+
+from .unittest_data import data_decorator, data
 from ._unittest_compat import patch
 
 patch()
@@ -22,6 +24,7 @@ tests_root = os.path.dirname(__file__)
 fixtures_dir = os.path.join(tests_root, 'fixtures')
 
 
+@data_decorator
 class AlgoTests(unittest.TestCase):
 
     def test_signed_digest_parameters(self):
@@ -78,3 +81,31 @@ class AlgoTests(unittest.TestCase):
         params = algo["parameters"]
         self.assertEqual(params["version"].native, 'v1-0')
         self.assertEqual(params["rounds"].native, 42)
+
+    @staticmethod
+    def sha3_algo_pairs():
+        return [
+            ('sha3_224_dsa', 'sha3_224', 'dsa'),
+            ('sha3_256_dsa', 'sha3_256', 'dsa'),
+            ('sha3_384_dsa', 'sha3_384', 'dsa'),
+            ('sha3_512_dsa', 'sha3_512', 'dsa'),
+            ('sha3_224_ecdsa', 'sha3_224', 'ecdsa'),
+            ('sha3_256_ecdsa', 'sha3_256', 'ecdsa'),
+            ('sha3_384_ecdsa', 'sha3_384', 'ecdsa'),
+            ('sha3_512_ecdsa', 'sha3_512', 'ecdsa'),
+            ('sha3_224_rsa', 'sha3_224', 'rsa'),
+            ('sha3_256_rsa', 'sha3_256', 'rsa'),
+            ('sha3_384_rsa', 'sha3_384', 'rsa'),
+            ('sha3_512_rsa', 'sha3_512', 'rsa'),
+        ]
+
+    @data('sha3_algo_pairs', True)
+    def sha3_algos_round_trip(self, digest_alg, sig_alg):
+        alg_name = "%s_%s" % (digest_alg, sig_alg)
+        original = algos.SignedDigestAlgorithm({'algorithm': alg_name})
+        parsed = algos.SignedDigestAlgorithm.load(original.dump())
+        self.assertEqual(parsed.hash_algo, digest_alg)
+        self.assertEqual(
+            parsed.signature_algo,
+            'rsassa_pkcs1v15' if sig_alg == 'rsa' else sig_alg
+        )
diff --git a/tests/test_core.py b/tests/test_core.py
index 7ac9196..fabb675 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -1,6 +1,7 @@
 # coding: utf-8
 from __future__ import unicode_literals, division, absolute_import, print_function
 
+import pickle
 import unittest
 import os
 from datetime import datetime, timedelta
@@ -1375,3 +1376,11 @@ class CoreTests(unittest.TestCase):
 
         with self.assertRaisesRegex(ValueError, "Second arc must be "):
             core.ObjectIdentifier("0.40")
+
+    def test_pickle_integration(self):
+        orig = Seq({'id': '2.3.4', 'value': b"\xde\xad\xbe\xef"})
+        pickled_bytes = pickle.dumps(orig)
+        # ensure that our custom pickling implementation was used
+        self.assertIn(b"unpickle_helper", pickled_bytes)
+        unpickled = pickle.loads(pickled_bytes)
+        self.assertEqual(orig.native, unpickled.native)