Repository: certbot/certbot

Issue:
Ignore unknown ACME challenge types
As ACME provides general framework for challenges, instead of throwing

`Deserialization error: Could not decode 'challenges' ... http-00 was not recognized`

unrecognized challenge types should be (silently) ignored.

The idea is to deserialize them to:

``` python
class UnrecognizedChallenge(Challenge):
    """Unrecognized challenge.

    :ivar jobj: Original JSON decoded object.

    """
```



Discussion:

Resolving patch:
diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py
--- a/acme/acme/challenges.py
+++ b/acme/acme/challenges.py
@@ -25,6 +25,14 @@ class Challenge(jose.TypedJSONObjectWithFields):
     """ACME challenge."""
     TYPES = {}
 
+    @classmethod
+    def from_json(cls, jobj):
+        try:
+            return super(Challenge, cls).from_json(jobj)
+        except jose.UnrecognizedTypeError as error:
+            logger.debug(error)
+            return UnrecognizedChallenge.from_json(jobj)
+
 
 class ContinuityChallenge(Challenge):  # pylint: disable=abstract-method
     """Client validation challenges."""
@@ -34,11 +42,6 @@ class DVChallenge(Challenge):  # pylint: disable=abstract-method
     """Domain validation challenges."""
 
 
-class UnrecognizedChallenge(Challenge):
-    """Unrecognized challenge."""
-    typ = "unknown"
-
-
 class ChallengeResponse(jose.TypedJSONObjectWithFields):
     # _fields_to_partial_json | pylint: disable=abstract-method
     """ACME challenge response."""
@@ -47,6 +50,32 @@ class ChallengeResponse(jose.TypedJSONObjectWithFields):
     resource = fields.Resource(resource_type)
 
 
+class UnrecognizedChallenge(Challenge):
+    """Unrecognized challenge.
+
+    ACME specification defines a generic framework for challenges and
+    defines some standard challenges that are implemented in this
+    module. However, other implementations (including peers) might
+    define additional challenge types, which should be ignored if
+    unrecognized.
+
+    :ivar jobj: Original JSON decoded object.
+
+    """
+
+    def __init__(self, jobj):
+        super(UnrecognizedChallenge, self).__init__()
+        object.__setattr__(self, "jobj", jobj)
+
+    def to_partial_json(self):
+        # pylint: disable=no-member
+        return self.jobj
+
+    @classmethod
+    def from_json(cls, jobj):
+        return cls(jobj)
+
+
 @Challenge.register
 class SimpleHTTP(DVChallenge):
     """ACME "simpleHttp" challenge.
diff --git a/acme/acme/messages.py b/acme/acme/messages.py
--- a/acme/acme/messages.py
+++ b/acme/acme/messages.py
@@ -373,19 +373,7 @@ class Authorization(ResourceBody):
 
     @challenges.decoder
     def challenges(value):  # pylint: disable=missing-docstring,no-self-argument
-        # The from_json method raises errors.UnrecognizedTypeError when a
-        # challenge of unknown type is encountered.  We want to ignore this
-        # case.  This forces us to do an explicit iteration, since list
-        # comprehensions can't handle exceptions.
-        challs = []
-        for chall in value:
-            try:
-                challs.append(ChallengeBody.from_json(chall))
-            except jose.UnrecognizedTypeError:
-                challs.append(ChallengeBody(
-                    uri="UNKNOWN", chall=challenges.UnrecognizedChallenge,
-                    status=STATUS_UNKNOWN))
-        return tuple(challs)
+        return tuple(ChallengeBody.from_json(chall) for chall in value)
 
     @property
     def resolved_combinations(self):


Test patch:
diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py
--- a/acme/acme/challenges_test.py
+++ b/acme/acme/challenges_test.py
@@ -17,6 +17,32 @@
 KEY = test_util.load_rsa_private_key('rsa512_key.pem')
 
 
+class ChallengeTest(unittest.TestCase):
+
+    def test_from_json_unrecognized(self):
+        from acme.challenges import Challenge
+        from acme.challenges import UnrecognizedChallenge
+        chall = UnrecognizedChallenge({"type": "foo"})
+        # pylint: disable=no-member
+        self.assertEqual(chall, Challenge.from_json(chall.jobj))
+
+
+class UnrecognizedChallengeTest(unittest.TestCase):
+
+    def setUp(self):
+        from acme.challenges import UnrecognizedChallenge
+        self.jobj = {"type": "foo"}
+        self.chall = UnrecognizedChallenge(self.jobj)
+
+    def test_to_partial_json(self):
+        self.assertEqual(self.jobj, self.chall.to_partial_json())
+
+    def test_from_json(self):
+        from acme.challenges import UnrecognizedChallenge
+        self.assertEqual(
+            self.chall, UnrecognizedChallenge.from_json(self.jobj))
+
+
 class SimpleHTTPTest(unittest.TestCase):
 
     def setUp(self):
diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py
--- a/acme/acme/messages_test.py
+++ b/acme/acme/messages_test.py
@@ -301,19 +301,6 @@ def setUp(self):
             'combinations': combinations,
         }
 
-        # For unknown challenge types
-        self.jmsg_unknown_chall = {
-            'resource': 'challenge',
-            'uri': 'random_uri',
-            'type': 'unknown',
-            'tls': True,
-        }
-
-        self.jobj_from_unknown = {
-            'identifier': identifier.to_json(),
-            'challenges': [self.jmsg_unknown_chall],
-        }
-
     def test_from_json(self):
         from acme.messages import Authorization
         Authorization.from_json(self.jobj_from)
@@ -328,11 +315,6 @@ def test_resolved_combinations(self):
             (self.challbs[1], self.challbs[2]),
         ))
 
-    def test_unknown_chall_type(self):
-        """Just make sure an error isn't thrown."""
-        from acme.messages import Authorization
-        Authorization.from_json(self.jobj_from_unknown)
-
 
 class AuthorizationResourceTest(unittest.TestCase):
     """Tests for acme.messages.AuthorizationResource."""