From 5d5dc429c477a308a23764a3bd19659b1c5b169e Mon Sep 17 00:00:00 2001 From: Anna Glasgall Date: Tue, 21 Mar 2023 13:49:39 -0400 Subject: [PATCH] acme.messages.OrderResource: Make roundtrippable through JSON (#9617) Right now if you to_json() an `OrderResource` and later deserialize it, the `AuthorizationResource` objects don't come back through the round-trip (they just get de-jsonified as frozendicts and worse, they can't even be passed to `AuthorizationResource.from_json` because frozendicts aren't dicts). In addition, the `csr_pem` field gets encoded as an array of integers, which definitely does not get de-jsonified into what we want. Fix these by adding an encoder to `authorizations` and encoder and decoder to `csr_pem`. --- AUTHORS.md | 1 + acme/acme/messages.py | 18 +++++++++++++++++- acme/tests/messages_test.py | 30 ++++++++++++++++++++++++++++++ certbot/CHANGELOG.md | 3 ++- 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 410e72030..3337859a2 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -23,6 +23,7 @@ Authors * [amplifi](https://github.com/amplifi) * [Andrew Murray](https://github.com/radarhere) * [Andrzej Górski](https://github.com/andrzej3393) +* [Anna Glasgall](https://github.com/aglasgall) * [Anselm Levskaya](https://github.com/levskaya) * [Antoine Jacoutot](https://github.com/ajacoutot) * [April King](https://github.com/april) diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 07a6f4ec5..07016ea69 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -654,12 +654,28 @@ class OrderResource(ResourceWithURI): :vartype alternative_fullchains_pem: `list` of `str` """ body: Order = jose.field('body', decoder=Order.from_json) - csr_pem: bytes = jose.field('csr_pem', omitempty=True) + csr_pem: bytes = jose.field('csr_pem', omitempty=True, + # This looks backwards, but it's not - + # we want the deserialized value to be + # `bytes`, but anything we put into + # JSON needs to be `str`, so we encode + # to decode and decode to + # encode. Otherwise we end up with an + # array of ints on serialization + decoder=lambda s: s.encode("utf-8"), + encoder=lambda b: b.decode("utf-8")) + authorizations: List[AuthorizationResource] = jose.field('authorizations') fullchain_pem: str = jose.field('fullchain_pem', omitempty=True) alternative_fullchains_pem: List[str] = jose.field('alternative_fullchains_pem', omitempty=True) + # Mypy does not understand the josepy magic happening here, and falsely claims + # that authorizations is redefined. Let's ignore the type check here. + @authorizations.decoder # type: ignore + def authorizations(value: List[Dict[str, Any]]) -> Tuple[AuthorizationResource, ...]: # pylint: disable=no-self-argument,missing-function-docstring + return tuple(AuthorizationResource.from_json(authz) for authz in value) + class NewOrder(Order): """New order.""" diff --git a/acme/tests/messages_test.py b/acme/tests/messages_test.py index 781a1b1ac..ea7e2feaf 100644 --- a/acme/tests/messages_test.py +++ b/acme/tests/messages_test.py @@ -492,6 +492,36 @@ class OrderResourceTest(unittest.TestCase): 'authorizations': None, } + def test_json_de_serializable(self): + from acme.messages import ChallengeBody + from acme.messages import STATUS_PENDING + challbs = ( + ChallengeBody( + uri='http://challb1', status=STATUS_PENDING, + chall=challenges.HTTP01(token=b'IlirfxKKXAsHtmzK29Pj8A')), + ChallengeBody(uri='http://challb2', status=STATUS_PENDING, + chall=challenges.DNS( + token=b'DGyRejmCefe7v4NfDGDKfA')), + ) + + from acme.messages import Authorization + from acme.messages import AuthorizationResource + from acme.messages import Identifier + from acme.messages import IDENTIFIER_FQDN + identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com') + authz = AuthorizationResource(uri="http://authz1", + body=Authorization( + identifier=identifier, + challenges=challbs)) + from acme.messages import Order + body = Order(identifiers=(identifier,), status=STATUS_PENDING, + authorizations=tuple(challb.uri for challb in challbs)) + from acme.messages import OrderResource + orderr = OrderResource(uri="http://order1", body=body, + csr_pem=b'test blob', + authorizations=(authz,)) + self.assertEqual(orderr, + OrderResource.from_json(orderr.to_json())) class NewOrderTest(unittest.TestCase): """Tests for acme.messages.NewOrder.""" diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 4829a65bd..6d7e46715 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -6,7 +6,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### Added -* +* `acme.messages.OrderResource` now supports being round-tripped + through JSON ### Changed