mirror of
https://github.com/certbot/certbot.git
synced 2026-01-26 07:41:33 +03:00
Typed jose fields (#9073)
* Add generic methods to save some casts, and fix lint * Update current and oldest pinning * Fix classes * Remove some todos thanks to josepy 1.11.0 * Cleanup some useless pylint disable * Finish complete typing * Better TypeVar names * Upgrade pinning and fix some typing errors * Use protocol * Fix types in apache Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
This commit is contained in:
@@ -12,6 +12,8 @@ from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
import josepy as jose
|
||||
@@ -27,6 +29,8 @@ from acme.mixins import TypeMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
GenericChallenge = TypeVar('GenericChallenge', bound='Challenge')
|
||||
|
||||
|
||||
class Challenge(jose.TypedJSONObjectWithFields):
|
||||
# _fields_to_partial_json
|
||||
@@ -34,9 +38,10 @@ class Challenge(jose.TypedJSONObjectWithFields):
|
||||
TYPES: Dict[str, Type['Challenge']] = {}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj: Mapping[str, Any]) -> 'Challenge':
|
||||
def from_json(cls: Type[GenericChallenge],
|
||||
jobj: Mapping[str, Any]) -> Union[GenericChallenge, 'UnrecognizedChallenge']:
|
||||
try:
|
||||
return super().from_json(jobj)
|
||||
return cast(GenericChallenge, super().from_json(jobj))
|
||||
except jose.UnrecognizedTypeError as error:
|
||||
logger.debug(error)
|
||||
return UnrecognizedChallenge.from_json(jobj)
|
||||
@@ -47,7 +52,7 @@ class ChallengeResponse(ResourceMixin, TypeMixin, jose.TypedJSONObjectWithFields
|
||||
"""ACME challenge response."""
|
||||
TYPES: Dict[str, Type['ChallengeResponse']] = {}
|
||||
resource_type = 'challenge'
|
||||
resource = fields.Resource(resource_type)
|
||||
resource: str = fields.resource(resource_type)
|
||||
|
||||
|
||||
class UnrecognizedChallenge(Challenge):
|
||||
@@ -62,6 +67,7 @@ class UnrecognizedChallenge(Challenge):
|
||||
:ivar jobj: Original JSON decoded object.
|
||||
|
||||
"""
|
||||
jobj: Dict[str, Any]
|
||||
|
||||
def __init__(self, jobj: Mapping[str, Any]) -> None:
|
||||
super().__init__()
|
||||
@@ -85,7 +91,7 @@ class _TokenChallenge(Challenge):
|
||||
"""Minimum size of the :attr:`token` in bytes."""
|
||||
|
||||
# TODO: acme-spec doesn't specify token as base64-encoded value
|
||||
token: bytes = jose.Field(
|
||||
token: bytes = jose.field(
|
||||
"token", encoder=jose.encode_b64jose, decoder=functools.partial(
|
||||
jose.decode_b64jose, size=TOKEN_SIZE, minimum=True))
|
||||
|
||||
@@ -108,10 +114,10 @@ class _TokenChallenge(Challenge):
|
||||
class KeyAuthorizationChallengeResponse(ChallengeResponse):
|
||||
"""Response to Challenges based on Key Authorization.
|
||||
|
||||
:param unicode key_authorization:
|
||||
:param str key_authorization:
|
||||
|
||||
"""
|
||||
key_authorization = jose.Field("keyAuthorization")
|
||||
key_authorization: str = jose.field("keyAuthorization")
|
||||
thumbprint_hash_function = hashes.SHA256
|
||||
|
||||
def verify(self, chall: 'KeyAuthorizationChallenge', account_public_key: jose.JWK) -> bool:
|
||||
@@ -126,7 +132,7 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse):
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
parts = self.key_authorization.split('.')
|
||||
parts = self.key_authorization.split('.') # pylint: disable=no-member
|
||||
if len(parts) != 2:
|
||||
logger.debug("Key authorization (%r) is not well formed",
|
||||
self.key_authorization)
|
||||
@@ -152,6 +158,9 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse):
|
||||
return jobj
|
||||
|
||||
|
||||
# TODO: Make this method a generic of K (bound=KeyAuthorizationChallenge), response_cls of type
|
||||
# Type[K] and use it in response/response_and_validation return types once Python 3.6 support is
|
||||
# dropped (do not support generic ABC classes, see https://github.com/python/typing/issues/449).
|
||||
class KeyAuthorizationChallenge(_TokenChallenge, metaclass=abc.ABCMeta):
|
||||
"""Challenge based on Key Authorization.
|
||||
|
||||
@@ -168,7 +177,7 @@ class KeyAuthorizationChallenge(_TokenChallenge, metaclass=abc.ABCMeta):
|
||||
"""Generate Key Authorization.
|
||||
|
||||
:param JWK account_key:
|
||||
:rtype unicode:
|
||||
:rtype str:
|
||||
|
||||
"""
|
||||
return self.encode("token") + "." + jose.b64encode(
|
||||
@@ -229,7 +238,7 @@ class DNS01Response(KeyAuthorizationChallengeResponse):
|
||||
around `KeyAuthorizationChallengeResponse.verify`.
|
||||
|
||||
:param challenges.DNS01 chall: Corresponding challenge.
|
||||
:param unicode domain: Domain name being verified.
|
||||
:param str domain: Domain name being verified.
|
||||
:param JWK account_public_key: Public key for the key pair
|
||||
being authorized.
|
||||
|
||||
@@ -257,7 +266,7 @@ class DNS01(KeyAuthorizationChallenge):
|
||||
"""Generate validation.
|
||||
|
||||
:param JWK account_key:
|
||||
:rtype: unicode
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return jose.b64encode(hashlib.sha256(self.key_authorization(
|
||||
@@ -266,7 +275,8 @@ class DNS01(KeyAuthorizationChallenge):
|
||||
def validation_domain_name(self, name: str) -> str:
|
||||
"""Domain name for TXT validation record.
|
||||
|
||||
:param unicode name: Domain name being validated.
|
||||
:param str name: Domain name being validated.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return "{0}.{1}".format(self.LABEL, name)
|
||||
@@ -293,7 +303,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse):
|
||||
"""Simple verify.
|
||||
|
||||
:param challenges.SimpleHTTP chall: Corresponding challenge.
|
||||
:param unicode domain: Domain name being verified.
|
||||
:param str domain: Domain name being verified.
|
||||
:param JWK account_public_key: Public key for the key pair
|
||||
being authorized.
|
||||
:param int port: Port used in the validation.
|
||||
@@ -357,7 +367,7 @@ class HTTP01(KeyAuthorizationChallenge):
|
||||
def path(self) -> str:
|
||||
"""Path (starting with '/') for provisioned resource.
|
||||
|
||||
:rtype: string
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return '/' + self.URI_ROOT_PATH + '/' + self.encode('token')
|
||||
@@ -368,8 +378,8 @@ class HTTP01(KeyAuthorizationChallenge):
|
||||
Forms an URI to the HTTPS server provisioned resource
|
||||
(containing :attr:`~SimpleHTTP.token`).
|
||||
|
||||
:param unicode domain: Domain name being verified.
|
||||
:rtype: string
|
||||
:param str domain: Domain name being verified.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return "http://" + domain + self.path
|
||||
@@ -378,7 +388,7 @@ class HTTP01(KeyAuthorizationChallenge):
|
||||
"""Generate validation.
|
||||
|
||||
:param JWK account_key:
|
||||
:rtype: unicode
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return self.key_authorization(account_key)
|
||||
@@ -409,7 +419,7 @@ class TLSALPN01Response(KeyAuthorizationChallengeResponse):
|
||||
) -> Tuple[crypto.X509, crypto.PKey]:
|
||||
"""Generate tls-alpn-01 certificate.
|
||||
|
||||
:param unicode domain: Domain verified by the challenge.
|
||||
:param str domain: Domain verified by the challenge.
|
||||
:param OpenSSL.crypto.PKey key: Optional private key used in
|
||||
certificate generation. If not provided (``None``), then
|
||||
fresh key will be generated.
|
||||
@@ -433,8 +443,8 @@ class TLSALPN01Response(KeyAuthorizationChallengeResponse):
|
||||
port: Optional[int] = None) -> crypto.X509:
|
||||
"""Probe tls-alpn-01 challenge certificate.
|
||||
|
||||
:param unicode domain: domain being validated, required.
|
||||
:param string host: IP address used to probe the certificate.
|
||||
:param str domain: domain being validated, required.
|
||||
:param str host: IP address used to probe the certificate.
|
||||
:param int port: Port used to probe the certificate.
|
||||
|
||||
"""
|
||||
@@ -450,7 +460,7 @@ class TLSALPN01Response(KeyAuthorizationChallengeResponse):
|
||||
def verify_cert(self, domain: str, cert: crypto.X509) -> bool:
|
||||
"""Verify tls-alpn-01 challenge certificate.
|
||||
|
||||
:param unicode domain: Domain name being validated.
|
||||
:param str domain: Domain name being validated.
|
||||
:param OpensSSL.crypto.X509 cert: Challenge certificate.
|
||||
|
||||
:returns: Whether the certificate was successfully verified.
|
||||
@@ -523,7 +533,7 @@ class TLSALPN01(KeyAuthorizationChallenge):
|
||||
"""Generate validation.
|
||||
|
||||
:param JWK account_key:
|
||||
:param unicode domain: Domain verified by the challenge.
|
||||
:param str domain: Domain verified by the challenge.
|
||||
:param OpenSSL.crypto.PKey cert_key: Optional private key used
|
||||
in certificate generation. If not provided (``None``), then
|
||||
fresh key will be generated.
|
||||
@@ -531,9 +541,10 @@ class TLSALPN01(KeyAuthorizationChallenge):
|
||||
:rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
|
||||
|
||||
"""
|
||||
return self.response(account_key).gen_cert(
|
||||
# TODO: Remove cast when response() is generic.
|
||||
return cast(TLSALPN01Response, self.response(account_key)).gen_cert(
|
||||
key=kwargs.get('cert_key'),
|
||||
domain=kwargs.get('domain'))
|
||||
domain=cast(str, kwargs.get('domain')))
|
||||
|
||||
@staticmethod
|
||||
def is_supported() -> bool:
|
||||
@@ -599,13 +610,12 @@ class DNS(_TokenChallenge):
|
||||
:rtype: DNSResponse
|
||||
|
||||
"""
|
||||
return DNSResponse(validation=self.gen_validation(
|
||||
account_key, **kwargs))
|
||||
return DNSResponse(validation=self.gen_validation(account_key, **kwargs))
|
||||
|
||||
def validation_domain_name(self, name: str) -> str:
|
||||
"""Domain name for TXT validation record.
|
||||
|
||||
:param unicode name: Domain name being validated.
|
||||
:param str name: Domain name being validated.
|
||||
|
||||
"""
|
||||
return "{0}.{1}".format(self.LABEL, name)
|
||||
@@ -620,7 +630,7 @@ class DNSResponse(ChallengeResponse):
|
||||
"""
|
||||
typ = "dns"
|
||||
|
||||
validation = jose.Field("validation", decoder=jose.JWS.from_json)
|
||||
validation: jose.JWS = jose.field("validation", decoder=jose.JWS.from_json)
|
||||
|
||||
def check_validation(self, chall: 'DNS', account_public_key: jose.JWK) -> bool:
|
||||
"""Check validation.
|
||||
@@ -631,4 +641,4 @@ class DNSResponse(ChallengeResponse):
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return chall.check_validation(cast(jose.JWS, self.validation), account_public_key)
|
||||
return chall.check_validation(self.validation, account_public_key)
|
||||
|
||||
@@ -19,6 +19,7 @@ from typing import cast
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Text
|
||||
@@ -33,6 +34,7 @@ from requests.adapters import HTTPAdapter
|
||||
from requests.utils import parse_header_links
|
||||
from requests_toolbelt.adapters.source import SourceAddressAdapter
|
||||
|
||||
from acme import challenges
|
||||
from acme import crypto_util
|
||||
from acme import errors
|
||||
from acme import jws
|
||||
@@ -156,12 +158,12 @@ class ClientBase:
|
||||
authzr = messages.AuthorizationResource(
|
||||
body=messages.Authorization.from_json(response.json()),
|
||||
uri=response.headers.get('Location', uri))
|
||||
if identifier is not None and authzr.body.identifier != identifier:
|
||||
if identifier is not None and authzr.body.identifier != identifier: # pylint: disable=no-member
|
||||
raise errors.UnexpectedUpdate(authzr)
|
||||
return authzr
|
||||
|
||||
def answer_challenge(self, challb: messages.ChallengeBody, response: requests.Response
|
||||
) -> messages.ChallengeResource:
|
||||
def answer_challenge(self, challb: messages.ChallengeBody,
|
||||
response: challenges.ChallengeResponse) -> messages.ChallengeResource:
|
||||
"""Answer challenge.
|
||||
|
||||
:param challb: Challenge Resource body.
|
||||
@@ -176,15 +178,15 @@ class ClientBase:
|
||||
:raises .UnexpectedUpdate:
|
||||
|
||||
"""
|
||||
response = self._post(challb.uri, response)
|
||||
resp = self._post(challb.uri, response)
|
||||
try:
|
||||
authzr_uri = response.links['up']['url']
|
||||
authzr_uri = resp.links['up']['url']
|
||||
except KeyError:
|
||||
raise errors.ClientError('"up" Link header missing')
|
||||
challr = messages.ChallengeResource(
|
||||
authzr_uri=authzr_uri,
|
||||
body=messages.ChallengeBody.from_json(response.json()))
|
||||
# TODO: check that challr.uri == response.headers['Location']?
|
||||
body=messages.ChallengeBody.from_json(resp.json()))
|
||||
# TODO: check that challr.uri == resp.headers['Location']?
|
||||
if challr.uri != challb.uri:
|
||||
raise errors.UnexpectedUpdate(challr.uri)
|
||||
return challr
|
||||
@@ -492,7 +494,7 @@ class Client(ClientBase):
|
||||
updated[authzr] = updated_authzr
|
||||
|
||||
attempts[authzr] += 1
|
||||
if updated_authzr.body.status not in (
|
||||
if updated_authzr.body.status not in ( # pylint: disable=no-member
|
||||
messages.STATUS_VALID, messages.STATUS_INVALID):
|
||||
if attempts[authzr] < max_attempts:
|
||||
# push back to the priority queue, with updated retry_after
|
||||
@@ -599,7 +601,7 @@ class Client(ClientBase):
|
||||
:raises .ClientError: If revocation is unsuccessful.
|
||||
|
||||
"""
|
||||
self._revoke(cert, rsn, self.directory[cast(str, messages.Revocation)])
|
||||
self._revoke(cert, rsn, self.directory[messages.Revocation])
|
||||
|
||||
|
||||
class ClientV2(ClientBase):
|
||||
@@ -756,7 +758,7 @@ class ClientV2(ClientBase):
|
||||
for url in orderr.body.authorizations:
|
||||
while datetime.datetime.now() < deadline:
|
||||
authzr = self._authzr_from_response(self._post_as_get(url), uri=url)
|
||||
if authzr.body.status != messages.STATUS_PENDING:
|
||||
if authzr.body.status != messages.STATUS_PENDING: # pylint: disable=no-member
|
||||
responses.append(authzr)
|
||||
break
|
||||
time.sleep(1)
|
||||
@@ -897,11 +899,11 @@ class BackwardsCompatibleClientV2:
|
||||
check_tos_cb(tos)
|
||||
if self.acme_version == 1:
|
||||
client_v1 = cast(Client, self.client)
|
||||
regr = client_v1.register(regr)
|
||||
if regr.terms_of_service is not None:
|
||||
_assess_tos(regr.terms_of_service)
|
||||
return client_v1.agree_to_tos(regr)
|
||||
return regr
|
||||
regr_res = client_v1.register(regr)
|
||||
if regr_res.terms_of_service is not None:
|
||||
_assess_tos(regr_res.terms_of_service)
|
||||
return client_v1.agree_to_tos(regr_res)
|
||||
return regr_res
|
||||
else:
|
||||
client_v2 = cast(ClientV2, self.client)
|
||||
if "terms_of_service" in client_v2.directory.meta:
|
||||
@@ -970,7 +972,8 @@ class BackwardsCompatibleClientV2:
|
||||
'certificate, please rerun the command for a new one.')
|
||||
|
||||
cert = OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped).decode()
|
||||
OpenSSL.crypto.FILETYPE_PEM,
|
||||
cast(OpenSSL.crypto.X509, cast(jose.ComparableX509, certr.body).wrapped)).decode()
|
||||
chain_str = crypto_util.dump_pyopenssl_chain(chain).decode()
|
||||
|
||||
return orderr.update(fullchain_pem=(cert + chain_str))
|
||||
@@ -1056,7 +1059,7 @@ class ClientNetwork:
|
||||
pass
|
||||
|
||||
def _wrap_in_jws(self, obj: jose.JSONDeSerializable, nonce: str, url: str,
|
||||
acme_version: int) -> jose.JWS:
|
||||
acme_version: int) -> str:
|
||||
"""Wrap `JSONDeSerializable` object in JWS.
|
||||
|
||||
.. todo:: Implement ``acmePath``.
|
||||
@@ -1064,7 +1067,7 @@ class ClientNetwork:
|
||||
:param josepy.JSONDeSerializable obj:
|
||||
:param str url: The URL to which this object will be POSTed
|
||||
:param str nonce:
|
||||
:rtype: `josepy.JWS`
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
if isinstance(obj, VersionedLEACMEMixin):
|
||||
@@ -1082,7 +1085,7 @@ class ClientNetwork:
|
||||
if self.account is not None:
|
||||
kwargs["kid"] = self.account["uri"]
|
||||
kwargs["key"] = self.key
|
||||
return jws.JWS.sign(jobj, **kwargs).json_dumps(indent=2)
|
||||
return jws.JWS.sign(jobj, **cast(Mapping[str, Any], kwargs)).json_dumps(indent=2)
|
||||
|
||||
@classmethod
|
||||
def _check_response(cls, response: requests.Response,
|
||||
|
||||
@@ -278,7 +278,7 @@ def _pyopenssl_cert_or_req_san(cert_or_req: Union[crypto.X509, crypto.X509Req])
|
||||
:type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
|
||||
|
||||
:returns: A list of Subject Alternative Names that is DNS.
|
||||
:rtype: `list` of `unicode`
|
||||
:rtype: `list` of `str`
|
||||
|
||||
"""
|
||||
# This function finds SANs with dns name
|
||||
@@ -300,7 +300,7 @@ def _pyopenssl_cert_or_req_san_ip(cert_or_req: Union[crypto.X509, crypto.X509Req
|
||||
:type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
|
||||
|
||||
:returns: A list of Subject Alternative Names that are IP Addresses.
|
||||
:rtype: `list` of `unicode`. note that this returns as string, not IPaddress object
|
||||
:rtype: `list` of `str`. note that this returns as string, not IPaddress object
|
||||
|
||||
"""
|
||||
|
||||
@@ -320,7 +320,7 @@ def _pyopenssl_extract_san_list_raw(cert_or_req: Union[crypto.X509, crypto.X509R
|
||||
:type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
|
||||
|
||||
:returns: raw san strings, parsed byte as utf-8
|
||||
:rtype: `list` of `unicode`
|
||||
:rtype: `list` of `str`
|
||||
|
||||
"""
|
||||
# This function finds SANs by dumping the certificate/CSR to text and
|
||||
@@ -352,7 +352,7 @@ def gen_ss_cert(key: crypto.PKey, domains: Optional[List[str]] = None,
|
||||
) -> crypto.X509:
|
||||
"""Generate new self-signed certificate.
|
||||
|
||||
:type domains: `list` of `unicode`
|
||||
:type domains: `list` of `str`
|
||||
:param OpenSSL.crypto.PKey key:
|
||||
:param bool force_san:
|
||||
:param extensions: List of additional extensions to include in the cert.
|
||||
@@ -410,7 +410,8 @@ def gen_ss_cert(key: crypto.PKey, domains: Optional[List[str]] = None,
|
||||
return cert
|
||||
|
||||
|
||||
def dump_pyopenssl_chain(chain: List[crypto.X509], filetype: int = crypto.FILETYPE_PEM) -> bytes:
|
||||
def dump_pyopenssl_chain(chain: Union[List[jose.ComparableX509], List[crypto.X509]],
|
||||
filetype: int = crypto.FILETYPE_PEM) -> bytes:
|
||||
"""Dump certificate chain into a bundle.
|
||||
|
||||
:param list chain: List of `OpenSSL.crypto.X509` (or wrapped in
|
||||
@@ -425,6 +426,8 @@ def dump_pyopenssl_chain(chain: List[crypto.X509], filetype: int = crypto.FILETY
|
||||
|
||||
def _dump_cert(cert: Union[jose.ComparableX509, crypto.X509]) -> bytes:
|
||||
if isinstance(cert, jose.ComparableX509):
|
||||
if isinstance(cert.wrapped, crypto.X509Req):
|
||||
raise errors.Error("Unexpected CSR provided.") # pragma: no cover
|
||||
cert = cert.wrapped
|
||||
return crypto.dump_certificate(filetype, cert)
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ class Resource(jose.Field):
|
||||
|
||||
def __init__(self, resource_type: str, *args: Any, **kwargs: Any) -> None:
|
||||
self.resource_type = resource_type
|
||||
super().__init__(
|
||||
'resource', default=resource_type, *args, **kwargs)
|
||||
kwargs['default'] = resource_type
|
||||
super().__init__('resource', *args, **kwargs)
|
||||
|
||||
def decode(self, value: Any) -> Any:
|
||||
if value != self.resource_type:
|
||||
@@ -65,3 +65,18 @@ class Resource(jose.Field):
|
||||
'Wrong resource type: {0} instead of {1}'.format(
|
||||
value, self.resource_type))
|
||||
return value
|
||||
|
||||
|
||||
def fixed(json_name: str, value: Any) -> Any:
|
||||
"""Generates a type-friendly Fixed field."""
|
||||
return Fixed(json_name, value)
|
||||
|
||||
|
||||
def rfc3339(json_name: str, omitempty: bool = False) -> Any:
|
||||
"""Generates a type-friendly RFC3339 field."""
|
||||
return RFC3339Field(json_name, omitempty=omitempty)
|
||||
|
||||
|
||||
def resource(resource_type: str) -> Any:
|
||||
"""Generates a type-friendly Resource field."""
|
||||
return Resource(resource_type)
|
||||
|
||||
@@ -12,14 +12,14 @@ import josepy as jose
|
||||
class Header(jose.Header):
|
||||
"""ACME-specific JOSE Header. Implements nonce, kid, and url.
|
||||
"""
|
||||
nonce = jose.Field('nonce', omitempty=True, encoder=jose.encode_b64jose)
|
||||
kid = jose.Field('kid', omitempty=True)
|
||||
url = jose.Field('url', omitempty=True)
|
||||
nonce: Optional[bytes] = jose.field('nonce', omitempty=True, encoder=jose.encode_b64jose)
|
||||
kid: Optional[str] = jose.field('kid', omitempty=True)
|
||||
url: Optional[str] = jose.field('url', omitempty=True)
|
||||
|
||||
# Mypy does not understand the josepy magic happening here, and falsely claims
|
||||
# that nonce is redefined. Let's ignore the type check here.
|
||||
@nonce.decoder # type: ignore
|
||||
def nonce(value: str) -> bytes: # pylint: disable=no-self-argument,missing-function-docstring
|
||||
@nonce.decoder # type: ignore[no-redef,union-attr]
|
||||
def nonce(value: str) -> bytes: # type: ignore[misc] # pylint: disable=no-self-argument,missing-function-docstring
|
||||
try:
|
||||
return jose.decode_b64jose(value)
|
||||
except jose.DeserializationError as error:
|
||||
@@ -29,12 +29,12 @@ class Header(jose.Header):
|
||||
|
||||
class Signature(jose.Signature):
|
||||
"""ACME-specific Signature. Uses ACME-specific Header for customer fields."""
|
||||
__slots__ = jose.Signature._orig_slots # pylint: disable=no-member
|
||||
__slots__ = jose.Signature._orig_slots # type: ignore[attr-defined] # pylint: disable=protected-access,no-member
|
||||
|
||||
# TODO: decoder/encoder should accept cls? Otherwise, subclassing
|
||||
# JSONObjectWithFields is tricky...
|
||||
header_cls = Header
|
||||
header = jose.Field(
|
||||
header: Header = jose.field(
|
||||
'header', omitempty=True, default=header_cls(),
|
||||
decoder=header_cls.from_json)
|
||||
|
||||
@@ -44,10 +44,10 @@ class Signature(jose.Signature):
|
||||
class JWS(jose.JWS):
|
||||
"""ACME-specific JWS. Includes none, url, and kid in protected header."""
|
||||
signature_cls = Signature
|
||||
__slots__ = jose.JWS._orig_slots
|
||||
__slots__ = jose.JWS._orig_slots # type: ignore[attr-defined] # pylint: disable=protected-access
|
||||
|
||||
@classmethod
|
||||
# pylint: disable=arguments-differ
|
||||
# type: ignore[override] # pylint: disable=arguments-differ
|
||||
def sign(cls, payload: bytes, key: jose.JWK, alg: jose.JWASignature, nonce: Optional[bytes],
|
||||
url: Optional[str] = None, kid: Optional[str] = None) -> jose.JWS:
|
||||
# Per ACME spec, jwk and kid are mutually exclusive, so only include a
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""ACME protocol messages."""
|
||||
import datetime
|
||||
from collections.abc import Hashable
|
||||
import json
|
||||
from typing import Any
|
||||
@@ -7,9 +8,12 @@ from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import MutableMapping
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
import josepy as jose
|
||||
|
||||
@@ -20,6 +24,11 @@ from acme import jws
|
||||
from acme import util
|
||||
from acme.mixins import ResourceMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Protocol # pragma: no cover
|
||||
else:
|
||||
Protocol = object
|
||||
|
||||
OLD_ERROR_PREFIX = "urn:acme:error:"
|
||||
ERROR_PREFIX = "urn:ietf:params:acme:error:"
|
||||
|
||||
@@ -75,20 +84,20 @@ class Error(jose.JSONObjectWithFields, errors.Error):
|
||||
|
||||
https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
|
||||
|
||||
:ivar unicode typ:
|
||||
:ivar unicode title:
|
||||
:ivar unicode detail:
|
||||
:ivar str typ:
|
||||
:ivar str title:
|
||||
:ivar str detail:
|
||||
|
||||
"""
|
||||
typ = jose.Field('type', omitempty=True, default='about:blank')
|
||||
title = jose.Field('title', omitempty=True)
|
||||
detail = jose.Field('detail', omitempty=True)
|
||||
typ: str = jose.field('type', omitempty=True, default='about:blank')
|
||||
title: str = jose.field('title', omitempty=True)
|
||||
detail: str = jose.field('detail', omitempty=True)
|
||||
|
||||
@classmethod
|
||||
def with_code(cls, code: str, **kwargs: Any) -> 'Error':
|
||||
"""Create an Error instance with an ACME Error code.
|
||||
|
||||
:unicode code: An ACME error code, like 'dnssec'.
|
||||
:str code: An ACME error code, like 'dnssec'.
|
||||
:kwargs: kwargs to pass to Error.
|
||||
|
||||
"""
|
||||
@@ -98,14 +107,14 @@ class Error(jose.JSONObjectWithFields, errors.Error):
|
||||
typ = ERROR_PREFIX + code
|
||||
# Mypy will not understand that the Error constructor accepts a named argument
|
||||
# "typ" because of josepy magic. Let's ignore the type check here.
|
||||
return cls(typ=typ, **kwargs) # type: ignore
|
||||
return cls(typ=typ, **kwargs)
|
||||
|
||||
@property
|
||||
def description(self) -> Optional[str]:
|
||||
"""Hardcoded error description based on its type.
|
||||
|
||||
:returns: Description if standard ACME error or ``None``.
|
||||
:rtype: unicode
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return ERROR_TYPE_DESCRIPTIONS.get(self.typ)
|
||||
@@ -117,7 +126,7 @@ class Error(jose.JSONObjectWithFields, errors.Error):
|
||||
Basically self.typ without the ERROR_PREFIX.
|
||||
|
||||
:returns: error code if standard ACME code or ``None``.
|
||||
:rtype: unicode
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
code = str(self.typ).rsplit(':', maxsplit=1)[-1]
|
||||
@@ -164,7 +173,7 @@ class _Constant(jose.JSONDeSerializable, Hashable):
|
||||
|
||||
class Status(_Constant):
|
||||
"""ACME "status" field."""
|
||||
POSSIBLE_NAMES: Dict[str, 'Status'] = {}
|
||||
POSSIBLE_NAMES: Dict[str, _Constant] = {}
|
||||
|
||||
|
||||
STATUS_UNKNOWN = Status('unknown')
|
||||
@@ -179,7 +188,7 @@ STATUS_DEACTIVATED = Status('deactivated')
|
||||
|
||||
class IdentifierType(_Constant):
|
||||
"""ACME identifier type."""
|
||||
POSSIBLE_NAMES: Dict[str, 'IdentifierType'] = {}
|
||||
POSSIBLE_NAMES: Dict[str, _Constant] = {}
|
||||
|
||||
|
||||
IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder
|
||||
@@ -190,25 +199,35 @@ class Identifier(jose.JSONObjectWithFields):
|
||||
"""ACME identifier.
|
||||
|
||||
:ivar IdentifierType typ:
|
||||
:ivar unicode value:
|
||||
:ivar str value:
|
||||
|
||||
"""
|
||||
typ = jose.Field('type', decoder=IdentifierType.from_json)
|
||||
value = jose.Field('value')
|
||||
typ: IdentifierType = jose.field('type', decoder=IdentifierType.from_json)
|
||||
value: str = jose.field('value')
|
||||
|
||||
|
||||
class HasResourceType(Protocol):
|
||||
"""
|
||||
Represents a class with a resource_type class parameter of type string.
|
||||
"""
|
||||
resource_type: str = NotImplemented
|
||||
|
||||
|
||||
GenericHasResourceType = TypeVar("GenericHasResourceType", bound=HasResourceType)
|
||||
|
||||
|
||||
class Directory(jose.JSONDeSerializable):
|
||||
"""Directory."""
|
||||
|
||||
_REGISTERED_TYPES: Dict[str, Type['Directory']] = {}
|
||||
_REGISTERED_TYPES: Dict[str, Type[HasResourceType]] = {}
|
||||
|
||||
class Meta(jose.JSONObjectWithFields):
|
||||
"""Directory Meta."""
|
||||
_terms_of_service = jose.Field('terms-of-service', omitempty=True)
|
||||
_terms_of_service_v2 = jose.Field('termsOfService', omitempty=True)
|
||||
website = jose.Field('website', omitempty=True)
|
||||
caa_identities = jose.Field('caaIdentities', omitempty=True)
|
||||
external_account_required = jose.Field('externalAccountRequired', omitempty=True)
|
||||
_terms_of_service: str = jose.field('terms-of-service', omitempty=True)
|
||||
_terms_of_service_v2: str = jose.field('termsOfService', omitempty=True)
|
||||
website: str = jose.field('website', omitempty=True)
|
||||
caa_identities: List[str] = jose.field('caaIdentities', omitempty=True)
|
||||
external_account_required: bool = jose.field('externalAccountRequired', omitempty=True)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
kwargs = {self._internal_name(k): v for k, v in kwargs.items()}
|
||||
@@ -229,11 +248,14 @@ class Directory(jose.JSONDeSerializable):
|
||||
return '_' + name if name == 'terms_of_service' else name
|
||||
|
||||
@classmethod
|
||||
def _canon_key(cls, key: str) -> str:
|
||||
return getattr(key, 'resource_type', key)
|
||||
def _canon_key(cls, key: Union[str, HasResourceType, Type[HasResourceType]]) -> str:
|
||||
if isinstance(key, str):
|
||||
return key
|
||||
return key.resource_type
|
||||
|
||||
@classmethod
|
||||
def register(cls, resource_body_cls: Type['Directory']) -> Type['Directory']:
|
||||
def register(cls,
|
||||
resource_body_cls: Type[GenericHasResourceType]) -> Type[GenericHasResourceType]:
|
||||
"""Register resource."""
|
||||
resource_type = resource_body_cls.resource_type
|
||||
assert resource_type not in cls._REGISTERED_TYPES
|
||||
@@ -252,7 +274,7 @@ class Directory(jose.JSONDeSerializable):
|
||||
except KeyError as error:
|
||||
raise AttributeError(str(error))
|
||||
|
||||
def __getitem__(self, name: str) -> Any:
|
||||
def __getitem__(self, name: Union[str, HasResourceType, Type[HasResourceType]]) -> Any:
|
||||
try:
|
||||
return self._jobj[self._canon_key(name)]
|
||||
except KeyError:
|
||||
@@ -273,16 +295,16 @@ class Resource(jose.JSONObjectWithFields):
|
||||
:ivar acme.messages.ResourceBody body: Resource body.
|
||||
|
||||
"""
|
||||
body = jose.Field('body')
|
||||
body: "ResourceBody" = jose.field('body')
|
||||
|
||||
|
||||
class ResourceWithURI(Resource):
|
||||
"""ACME Resource with URI.
|
||||
|
||||
:ivar unicode ~.uri: Location of the resource.
|
||||
:ivar str uri: Location of the resource.
|
||||
|
||||
"""
|
||||
uri = jose.Field('uri') # no ChallengeResource.uri
|
||||
uri: str = jose.field('uri') # no ChallengeResource.uri
|
||||
|
||||
|
||||
class ResourceBody(jose.JSONObjectWithFields):
|
||||
@@ -308,35 +330,40 @@ class ExternalAccountBinding:
|
||||
return eab.to_partial_json()
|
||||
|
||||
|
||||
GenericRegistration = TypeVar('GenericRegistration', bound='Registration')
|
||||
|
||||
|
||||
class Registration(ResourceBody):
|
||||
"""Registration Resource Body.
|
||||
|
||||
:ivar josepy.jwk.JWK key: Public key.
|
||||
:ivar jose.JWK key: Public key.
|
||||
:ivar tuple contact: Contact information following ACME spec,
|
||||
`tuple` of `unicode`.
|
||||
:ivar unicode agreement:
|
||||
`tuple` of `str`.
|
||||
:ivar str agreement:
|
||||
|
||||
"""
|
||||
# on new-reg key server ignores 'key' and populates it based on
|
||||
# JWS.signature.combined.jwk
|
||||
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
|
||||
key: jose.JWK = jose.field('key', omitempty=True, decoder=jose.JWK.from_json)
|
||||
# Contact field implements special behavior to allow messages that clear existing
|
||||
# contacts while not expecting the `contact` field when loading from json.
|
||||
# This is implemented in the constructor and *_json methods.
|
||||
contact = jose.Field('contact', omitempty=True, default=())
|
||||
agreement = jose.Field('agreement', omitempty=True)
|
||||
status = jose.Field('status', omitempty=True)
|
||||
terms_of_service_agreed = jose.Field('termsOfServiceAgreed', omitempty=True)
|
||||
only_return_existing = jose.Field('onlyReturnExisting', omitempty=True)
|
||||
external_account_binding = jose.Field('externalAccountBinding', omitempty=True)
|
||||
contact: Tuple[str, ...] = jose.field('contact', omitempty=True, default=())
|
||||
agreement: str = jose.field('agreement', omitempty=True)
|
||||
status: Status = jose.field('status', omitempty=True)
|
||||
terms_of_service_agreed: bool = jose.field('termsOfServiceAgreed', omitempty=True)
|
||||
only_return_existing: bool = jose.field('onlyReturnExisting', omitempty=True)
|
||||
external_account_binding: Dict[str, Any] = jose.field('externalAccountBinding',
|
||||
omitempty=True)
|
||||
|
||||
phone_prefix = 'tel:'
|
||||
email_prefix = 'mailto:'
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, phone: Optional[str] = None, email: Optional[str] = None,
|
||||
def from_data(cls: Type[GenericRegistration], phone: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
external_account_binding: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any) -> 'Registration':
|
||||
**kwargs: Any) -> GenericRegistration:
|
||||
"""
|
||||
Create registration resource from contact details.
|
||||
|
||||
@@ -419,26 +446,26 @@ class Registration(ResourceBody):
|
||||
class NewRegistration(ResourceMixin, Registration):
|
||||
"""New registration."""
|
||||
resource_type = 'new-reg'
|
||||
resource = fields.Resource(resource_type)
|
||||
resource: str = fields.resource(resource_type)
|
||||
|
||||
|
||||
class UpdateRegistration(ResourceMixin, Registration):
|
||||
"""Update registration."""
|
||||
resource_type = 'reg'
|
||||
resource = fields.Resource(resource_type)
|
||||
resource: str = fields.resource(resource_type)
|
||||
|
||||
|
||||
class RegistrationResource(ResourceWithURI):
|
||||
"""Registration Resource.
|
||||
|
||||
:ivar acme.messages.Registration body:
|
||||
:ivar unicode new_authzr_uri: Deprecated. Do not use.
|
||||
:ivar unicode terms_of_service: URL for the CA TOS.
|
||||
:ivar str new_authzr_uri: Deprecated. Do not use.
|
||||
:ivar str terms_of_service: URL for the CA TOS.
|
||||
|
||||
"""
|
||||
body = jose.Field('body', decoder=Registration.from_json)
|
||||
new_authzr_uri = jose.Field('new_authzr_uri', omitempty=True)
|
||||
terms_of_service = jose.Field('terms_of_service', omitempty=True)
|
||||
body: Registration = jose.field('body', decoder=Registration.from_json)
|
||||
new_authzr_uri: str = jose.field('new_authzr_uri', omitempty=True)
|
||||
terms_of_service: str = jose.field('terms_of_service', omitempty=True)
|
||||
|
||||
|
||||
class ChallengeBody(ResourceBody):
|
||||
@@ -463,12 +490,12 @@ class ChallengeBody(ResourceBody):
|
||||
# challenge object supports either one, but should be accessed through the
|
||||
# name "uri". In Client.answer_challenge, whichever one is set will be
|
||||
# used.
|
||||
_uri = jose.Field('uri', omitempty=True, default=None)
|
||||
_url = jose.Field('url', omitempty=True, default=None)
|
||||
status = jose.Field('status', decoder=Status.from_json,
|
||||
_uri: str = jose.field('uri', omitempty=True, default=None)
|
||||
_url: str = jose.field('url', omitempty=True, default=None)
|
||||
status: Status = jose.field('status', decoder=Status.from_json,
|
||||
omitempty=True, default=STATUS_PENDING)
|
||||
validated = fields.RFC3339Field('validated', omitempty=True)
|
||||
error = jose.Field('error', decoder=Error.from_json,
|
||||
validated: datetime.datetime = fields.rfc3339('validated', omitempty=True)
|
||||
error: Error = jose.field('error', decoder=Error.from_json,
|
||||
omitempty=True, default=None)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
@@ -511,16 +538,16 @@ class ChallengeResource(Resource):
|
||||
"""Challenge Resource.
|
||||
|
||||
:ivar acme.messages.ChallengeBody body:
|
||||
:ivar unicode authzr_uri: URI found in the 'up' ``Link`` header.
|
||||
:ivar str authzr_uri: URI found in the 'up' ``Link`` header.
|
||||
|
||||
"""
|
||||
body = jose.Field('body', decoder=ChallengeBody.from_json)
|
||||
authzr_uri = jose.Field('authzr_uri')
|
||||
body: ChallengeBody = jose.field('body', decoder=ChallengeBody.from_json)
|
||||
authzr_uri: str = jose.field('authzr_uri')
|
||||
|
||||
@property
|
||||
def uri(self) -> str:
|
||||
"""The URL of the challenge body."""
|
||||
return self.body.uri
|
||||
return self.body.uri # pylint: disable=no-member
|
||||
|
||||
|
||||
class Authorization(ResourceBody):
|
||||
@@ -534,26 +561,26 @@ class Authorization(ResourceBody):
|
||||
:ivar datetime.datetime expires:
|
||||
|
||||
"""
|
||||
identifier = jose.Field('identifier', decoder=Identifier.from_json, omitempty=True)
|
||||
challenges = jose.Field('challenges', omitempty=True)
|
||||
combinations = jose.Field('combinations', omitempty=True)
|
||||
identifier: Identifier = jose.field('identifier', decoder=Identifier.from_json, omitempty=True)
|
||||
challenges: List[ChallengeBody] = jose.field('challenges', omitempty=True)
|
||||
combinations: Tuple[Tuple[int, ...], ...] = jose.field('combinations', omitempty=True)
|
||||
|
||||
status = jose.Field('status', omitempty=True, decoder=Status.from_json)
|
||||
status: Status = jose.field('status', omitempty=True, decoder=Status.from_json)
|
||||
# TODO: 'expires' is allowed for Authorization Resources in
|
||||
# general, but for Key Authorization '[t]he "expires" field MUST
|
||||
# be absent'... then acme-spec gives example with 'expires'
|
||||
# present... That's confusing!
|
||||
expires = fields.RFC3339Field('expires', omitempty=True)
|
||||
wildcard = jose.Field('wildcard', omitempty=True)
|
||||
expires: datetime.datetime = fields.rfc3339('expires', omitempty=True)
|
||||
wildcard: bool = jose.field('wildcard', omitempty=True)
|
||||
|
||||
# Mypy does not understand the josepy magic happening here, and falsely claims
|
||||
# that challenge is redefined. Let's ignore the type check here.
|
||||
@challenges.decoder # type: ignore
|
||||
def challenges(value: List[Mapping[str, Any]]) -> Tuple[ChallengeBody, ...]: # pylint: disable=no-self-argument,missing-function-docstring
|
||||
def challenges(value: List[Dict[str, Any]]) -> Tuple[ChallengeBody, ...]: # type: ignore[misc] # pylint: disable=no-self-argument,missing-function-docstring
|
||||
return tuple(ChallengeBody.from_json(chall) for chall in value)
|
||||
|
||||
@property
|
||||
def resolved_combinations(self) -> Tuple[Tuple[Dict[str, Any], ...], ...]:
|
||||
def resolved_combinations(self) -> Tuple[Tuple[ChallengeBody, ...], ...]:
|
||||
"""Combinations with challenges instead of indices."""
|
||||
return tuple(tuple(self.challenges[idx] for idx in combo)
|
||||
for combo in self.combinations) # pylint: disable=not-an-iterable
|
||||
@@ -563,37 +590,37 @@ class Authorization(ResourceBody):
|
||||
class NewAuthorization(ResourceMixin, Authorization):
|
||||
"""New authorization."""
|
||||
resource_type = 'new-authz'
|
||||
resource = fields.Resource(resource_type)
|
||||
resource: str = fields.resource(resource_type)
|
||||
|
||||
|
||||
class UpdateAuthorization(ResourceMixin, Authorization):
|
||||
"""Update authorization."""
|
||||
resource_type = 'authz'
|
||||
resource = fields.Resource(resource_type)
|
||||
resource: str = fields.resource(resource_type)
|
||||
|
||||
|
||||
class AuthorizationResource(ResourceWithURI):
|
||||
"""Authorization Resource.
|
||||
|
||||
:ivar acme.messages.Authorization body:
|
||||
:ivar unicode new_cert_uri: Deprecated. Do not use.
|
||||
:ivar str new_cert_uri: Deprecated. Do not use.
|
||||
|
||||
"""
|
||||
body = jose.Field('body', decoder=Authorization.from_json)
|
||||
new_cert_uri = jose.Field('new_cert_uri', omitempty=True)
|
||||
body: Authorization = jose.field('body', decoder=Authorization.from_json)
|
||||
new_cert_uri: str = jose.field('new_cert_uri', omitempty=True)
|
||||
|
||||
|
||||
@Directory.register
|
||||
class CertificateRequest(ResourceMixin, jose.JSONObjectWithFields):
|
||||
"""ACME new-cert request.
|
||||
|
||||
:ivar josepy.util.ComparableX509 csr:
|
||||
:ivar jose.ComparableX509 csr:
|
||||
`OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
|
||||
|
||||
"""
|
||||
resource_type = 'new-cert'
|
||||
resource = fields.Resource(resource_type)
|
||||
csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
|
||||
resource: str = fields.resource(resource_type)
|
||||
csr: jose.ComparableX509 = jose.field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
|
||||
|
||||
|
||||
class CertificateResource(ResourceWithURI):
|
||||
@@ -601,27 +628,27 @@ class CertificateResource(ResourceWithURI):
|
||||
|
||||
:ivar josepy.util.ComparableX509 body:
|
||||
`OpenSSL.crypto.X509` wrapped in `.ComparableX509`
|
||||
:ivar unicode cert_chain_uri: URI found in the 'up' ``Link`` header
|
||||
:ivar str cert_chain_uri: URI found in the 'up' ``Link`` header
|
||||
:ivar tuple authzrs: `tuple` of `AuthorizationResource`.
|
||||
|
||||
"""
|
||||
cert_chain_uri = jose.Field('cert_chain_uri')
|
||||
authzrs = jose.Field('authzrs')
|
||||
cert_chain_uri: str = jose.field('cert_chain_uri')
|
||||
authzrs: Tuple[AuthorizationResource, ...] = jose.field('authzrs')
|
||||
|
||||
|
||||
@Directory.register
|
||||
class Revocation(ResourceMixin, jose.JSONObjectWithFields):
|
||||
"""Revocation message.
|
||||
|
||||
:ivar .ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in
|
||||
`.ComparableX509`
|
||||
:ivar jose.ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in
|
||||
`jose.ComparableX509`
|
||||
|
||||
"""
|
||||
resource_type = 'revoke-cert'
|
||||
resource = fields.Resource(resource_type)
|
||||
certificate = jose.Field(
|
||||
resource: str = fields.resource(resource_type)
|
||||
certificate: jose.ComparableX509 = jose.field(
|
||||
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
|
||||
reason = jose.Field('reason')
|
||||
reason: int = jose.field('reason')
|
||||
|
||||
|
||||
class Order(ResourceBody):
|
||||
@@ -638,19 +665,18 @@ class Order(ResourceBody):
|
||||
:ivar datetime.datetime expires: When the order expires.
|
||||
:ivar ~.Error error: Any error that occurred during finalization, if applicable.
|
||||
"""
|
||||
identifiers = jose.Field('identifiers', omitempty=True)
|
||||
status = jose.Field('status', decoder=Status.from_json,
|
||||
omitempty=True)
|
||||
authorizations = jose.Field('authorizations', omitempty=True)
|
||||
certificate = jose.Field('certificate', omitempty=True)
|
||||
finalize = jose.Field('finalize', omitempty=True)
|
||||
expires = fields.RFC3339Field('expires', omitempty=True)
|
||||
error = jose.Field('error', omitempty=True, decoder=Error.from_json)
|
||||
identifiers: List[Identifier] = jose.field('identifiers', omitempty=True)
|
||||
status: Status = jose.field('status', decoder=Status.from_json, omitempty=True)
|
||||
authorizations: List[str] = jose.field('authorizations', omitempty=True)
|
||||
certificate: str = jose.field('certificate', omitempty=True)
|
||||
finalize: str = jose.field('finalize', omitempty=True)
|
||||
expires: datetime.datetime = fields.rfc3339('expires', omitempty=True)
|
||||
error: Error = jose.field('error', omitempty=True, decoder=Error.from_json)
|
||||
|
||||
# Mypy does not understand the josepy magic happening here, and falsely claims
|
||||
# that identifiers is redefined. Let's ignore the type check here.
|
||||
@identifiers.decoder # type: ignore
|
||||
def identifiers(value: List[Mapping[str, Any]]) -> Tuple[Identifier, ...]: # pylint: disable=no-self-argument,missing-function-docstring
|
||||
def identifiers(value: List[Dict[str, Any]]) -> Tuple[Identifier, ...]: # type: ignore[misc] # pylint: disable=no-self-argument,missing-function-docstring
|
||||
return tuple(Identifier.from_json(identifier) for identifier in value)
|
||||
|
||||
|
||||
@@ -658,7 +684,7 @@ class OrderResource(ResourceWithURI):
|
||||
"""Order Resource.
|
||||
|
||||
:ivar acme.messages.Order body:
|
||||
:ivar str csr_pem: The CSR this Order will be finalized with.
|
||||
:ivar bytes csr_pem: The CSR this Order will be finalized with.
|
||||
:ivar authorizations: Fully-fetched AuthorizationResource objects.
|
||||
:vartype authorizations: `list` of `acme.messages.AuthorizationResource`
|
||||
:ivar str fullchain_pem: The fetched contents of the certificate URL
|
||||
@@ -668,11 +694,13 @@ class OrderResource(ResourceWithURI):
|
||||
finalization.
|
||||
:vartype alternative_fullchains_pem: `list` of `str`
|
||||
"""
|
||||
body = jose.Field('body', decoder=Order.from_json)
|
||||
csr_pem = jose.Field('csr_pem', omitempty=True)
|
||||
authorizations = jose.Field('authorizations')
|
||||
fullchain_pem = jose.Field('fullchain_pem', omitempty=True)
|
||||
alternative_fullchains_pem = jose.Field('alternative_fullchains_pem', omitempty=True)
|
||||
body: Order = jose.field('body', decoder=Order.from_json)
|
||||
csr_pem: bytes = jose.field('csr_pem', omitempty=True)
|
||||
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)
|
||||
|
||||
|
||||
@Directory.register
|
||||
class NewOrder(Order):
|
||||
|
||||
@@ -8,6 +8,7 @@ import socket
|
||||
import socketserver
|
||||
import threading
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
@@ -39,10 +40,10 @@ class TLSServer(socketserver.TCPServer):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _wrap_sock(self) -> None:
|
||||
self.socket = crypto_util.SSLSocket(
|
||||
self.socket = cast(socket.socket, crypto_util.SSLSocket(
|
||||
self.socket, cert_selection=self._cert_selection,
|
||||
alpn_selection=getattr(self, '_alpn_selection', None),
|
||||
method=self.method)
|
||||
method=self.method))
|
||||
|
||||
def _cert_selection(self, connection: SSL.Connection
|
||||
) -> Tuple[crypto.PKey, crypto.X509]: # pragma: no cover
|
||||
|
||||
@@ -7,7 +7,7 @@ version = '1.23.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'cryptography>=2.5.0',
|
||||
'josepy>=1.9.0',
|
||||
'josepy>=1.10.0',
|
||||
'PyOpenSSL>=17.3.0',
|
||||
'pyrfc3339',
|
||||
'pytz',
|
||||
@@ -24,6 +24,7 @@ docs_extras = [
|
||||
test_extras = [
|
||||
'pytest',
|
||||
'pytest-xdist',
|
||||
'typing-extensions',
|
||||
]
|
||||
|
||||
setup(
|
||||
|
||||
@@ -10,8 +10,8 @@ class FixedTest(unittest.TestCase):
|
||||
"""Tests for acme.fields.Fixed."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.fields import Fixed
|
||||
self.field = Fixed('name', 'x')
|
||||
from acme.fields import fixed
|
||||
self.field = fixed('name', 'x')
|
||||
|
||||
def test_decode(self):
|
||||
self.assertEqual('x', self.field.decode('x'))
|
||||
|
||||
Reference in New Issue
Block a user