Пример #1
0
class Registration(ResourceBody):
    """Registration Resource Body.

    :ivar acme.jose.jwk.JWK key: Public key.
    :ivar tuple contact: Contact information following ACME spec,
        `tuple` of `unicode`.
    :ivar unicode 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)
    contact = jose.Field('contact', omitempty=True, default=())
    agreement = jose.Field('agreement', omitempty=True)
    status = jose.Field('status', omitempty=True)

    phone_prefix = 'tel:'
    email_prefix = 'mailto:'

    @classmethod
    def from_data(cls, phone=None, email=None, **kwargs):
        """Create registration resource from contact details."""
        details = list(kwargs.pop('contact', ()))
        if phone is not None:
            details.append(cls.phone_prefix + phone)
        if email is not None:
            details.append(cls.email_prefix + email)
        kwargs['contact'] = tuple(details)
        return cls(**kwargs)

    def _filter_contact(self, prefix):
        return tuple(detail[len(prefix):] for detail in self.contact
                     if detail.startswith(prefix))

    @property
    def phones(self):
        """All phones found in the ``contact`` field."""
        return self._filter_contact(self.phone_prefix)

    @property
    def emails(self):
        """All emails found in the ``contact`` field."""
        return self._filter_contact(self.email_prefix)
class CertificateRequest(jose.JSONObjectWithFields):
    """ACME new-cert request.

    :ivar acme.jose.util.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)
Пример #3
0
class ChallengeBody(ResourceBody):
    """Challenge Resource Body.

    .. todo::
       Confusingly, this has a similar name to `.challenges.Challenge`,
       as well as `.achallenges.AnnotatedChallenge`. Please use names
       such as ``challb`` to distinguish instances of this class from
       ``achall``.

    :ivar acme.challenges.Challenge: Wrapped challenge.
        Conveniently, all challenge fields are proxied, i.e. you can
        call ``challb.x`` to get ``challb.chall.x`` contents.
    :ivar acme.messages.Status status:
    :ivar datetime.datetime validated:
    :ivar messages.Error error:

    """
    __slots__ = ('chall', )
    uri = jose.Field('uri')
    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,
                       omitempty=True,
                       default=None)

    def to_partial_json(self):
        jobj = super(ChallengeBody, self).to_partial_json()
        jobj.update(self.chall.to_partial_json())
        return jobj

    @classmethod
    def fields_from_json(cls, jobj):
        jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj)
        jobj_fields['chall'] = challenges.Challenge.from_json(jobj)
        return jobj_fields

    def __getattr__(self, name):
        return getattr(self.chall, name)
Пример #4
0
class Revocation(jose.JSONObjectWithFields):
    """Revocation message.

    :ivar .ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in
        `.ComparableX509`

    """
    resource_type = 'revoke-cert'
    resource = fields.Resource(resource_type)
    certificate = jose.Field(
        'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
Пример #5
0
class Signature(jose.Signature):
    """ACME-specific Signature. Uses ACME-specific Header for customer fields."""
    __slots__ = jose.Signature._orig_slots  # pylint: disable=no-member

    # TODO: decoder/encoder should accept cls? Otherwise, subclassing
    # JSONObjectWithFields is tricky...
    header_cls = Header
    header = jose.Field('header',
                        omitempty=True,
                        default=header_cls(),
                        decoder=header_cls.from_json)
Пример #6
0
class ProofOfPossessionResponse(ChallengeResponse):
    """ACME "proofOfPossession" challenge response.

    :ivar bytes nonce: Random data, **not** base64-encoded.
    :ivar acme.other.Signature signature: Sugnature of this message.

    """
    typ = "proofOfPossession"

    NONCE_SIZE = ProofOfPossession.NONCE_SIZE

    nonce = jose.Field(
        "nonce", encoder=jose.encode_b64jose, decoder=functools.partial(
            jose.decode_b64jose, size=NONCE_SIZE))
    signature = jose.Field("signature", decoder=other.Signature.from_json)

    def verify(self):
        """Verify the challenge."""
        # self.signature is not Field | pylint: disable=no-member
        return self.signature.verify(self.nonce)
Пример #7
0
class SimpleHTTPResponse(ChallengeResponse):
    """ACME "simpleHttp" challenge response."""
    typ = "simpleHttp"
    path = jose.Field("path")
    tls = jose.Field("tls", default=True, omitempty=True)

    URI_ROOT_PATH = ".well-known/acme-challenge"
    """URI root path for the server provisioned resource."""

    _URI_TEMPLATE = "{scheme}://{domain}/" + URI_ROOT_PATH + "/{path}"

    MAX_PATH_LEN = 25
    """Maximum allowed `path` length."""
    @property
    def good_path(self):
        """Is `path` good?

        .. todo:: acme-spec: "The value MUST be comprised entirely of
           characters from the URL-safe alphabet for Base64 encoding
           [RFC4648]", base64.b64decode ignores those characters

        """
        return len(self.path) <= 25

    @property
    def scheme(self):
        """URL scheme for the provisioned resource."""
        return "https" if self.tls else "http"

    def uri(self, domain):
        """Create an URI to the provisioned resource.

        Forms an URI to the HTTPS server provisioned resource
        (containing :attr:`~SimpleHTTP.token`).

        :param str domain: Domain name being verified.

        """
        return self._URI_TEMPLATE.format(scheme=self.scheme,
                                         domain=domain,
                                         path=self.path)
Пример #8
0
    class Meta(jose.JSONObjectWithFields):
        """Account metadata

        :ivar datetime.datetime creation_dt: Creation date and time (UTC).
        :ivar str creation_host: FQDN of host, where account has been created.

        .. note:: ``creation_dt`` and ``creation_host`` are useful in
            cross-machine migration scenarios.

        """
        creation_dt = acme_fields.RFC3339Field("creation_dt")
        creation_host = jose.Field("creation_host")
Пример #9
0
class Header(jose.Header):
    """ACME JOSE Header.

    .. todo:: Implement ``acmePath``.

    """
    nonce = jose.Field('nonce', omitempty=True, encoder=jose.encode_b64jose)

    @nonce.decoder
    def nonce(value):  # pylint: disable=missing-docstring,no-self-argument
        try:
            return jose.decode_b64jose(value)
        except jose.DeserializationError as error:
            # TODO: custom error
            raise jose.DeserializationError("Invalid nonce: {0}".format(error))
class SimpleHTTPResponse(ChallengeResponse):
    """ACME "simpleHttp" challenge response."""
    typ = "simpleHttp"
    path = jose.Field("path")

    URI_TEMPLATE = "https://{domain}/.well-known/acme-challenge/{path}"
    """URI template for HTTPS server provisioned resource."""
    def uri(self, domain):
        """Create an URI to the provisioned resource.

        Forms an URI to the HTTPS server provisioned resource (containing
        :attr:`~SimpleHTTP.token`) by populating the :attr:`URI_TEMPLATE`.

        :param str domain: Domain name being verified.

        """
        return self.URI_TEMPLATE.format(domain=domain, path=self.path)
Пример #11
0
class KeyAuthorizationChallengeResponse(ChallengeResponse):
    """Response to Challenges based on Key Authorization.

    :param unicode key_authorization:

    """
    key_authorization = jose.Field("keyAuthorization")
    thumbprint_hash_function = hashes.SHA256

    def verify(self, chall, account_public_key):
        """Verify the key authorization.

        :param KeyAuthorization chall: Challenge that corresponds to
            this response.
        :param JWK account_public_key:

        :return: ``True`` iff verification of the key authorization was
            successful.
        :rtype: bool

        """
        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)
            return False

        if parts[0] != chall.encode("token"):
            logger.debug(
                "Mismatching token in key authorization: "
                "%r instead of %r", parts[0], chall.encode("token"))
            return False

        thumbprint = jose.b64encode(
            account_public_key.thumbprint(
                hash_function=self.thumbprint_hash_function)).decode()
        if parts[1] != thumbprint:
            logger.debug(
                "Mismatching thumbprint in key authorization: "
                "%r instead of %r", parts[0], thumbprint)
            return False

        return True
Пример #12
0
class DNSResponse(ChallengeResponse):
    """ACME "dns" challenge response.

    :param JWS validation:

    """
    typ = "dns"

    validation = jose.Field("validation", decoder=jose.JWS.from_json)

    def check_validation(self, chall, account_public_key):
        """Check validation.

        :param challenges.DNS chall:
        :param JWK account_public_key:

        :rtype: bool

        """
        return chall.check_validation(self.validation, account_public_key)
Пример #13
0
class Revocation(jose.JSONObjectWithFields):
    """Revocation message.

    :ivar .ComparableX509 certificate: `M2Crypto.X509.X509` wrapped in
        `.ComparableX509`

    """
    certificate = jose.Field('certificate',
                             decoder=jose.decode_cert,
                             encoder=jose.encode_cert)

    # TODO: acme-spec#138, this allows only one ACME server instance per domain
    PATH = '/acme/revoke-cert'
    """Path to revocation URL, see `url`"""
    @classmethod
    def url(cls, base):
        """Get revocation URL.

        :param str base: New Registration Resource or server (root) URL.

        """
        return urlparse.urljoin(base, cls.PATH)
Пример #14
0
class SimpleHTTP(DVChallenge):
    """ACME "simpleHttp" challenge.

    :ivar unicode token:

    """
    typ = "simpleHttp"

    TOKEN_SIZE = 128 / 8  # Based on the entropy value from the spec
    """Minimum size of the :attr:`token` in bytes."""

    URI_ROOT_PATH = ".well-known/acme-challenge"
    """URI root path for the server provisioned resource."""

    # TODO: acme-spec doesn't specify token as base64-encoded value
    token = jose.Field("token",
                       encoder=jose.encode_b64jose,
                       decoder=functools.partial(jose.decode_b64jose,
                                                 size=TOKEN_SIZE,
                                                 minimum=True))

    @property
    def good_token(self):  # XXX: @token.decoder
        """Is `token` good?

        .. todo:: acme-spec wants "It MUST NOT contain any non-ASCII
           characters", but it should also warrant that it doesn't
           contain ".." or "/"...

        """
        # TODO: check that path combined with uri does not go above
        # URI_ROOT_PATH!
        return b'..' not in self.token and b'/' not in self.token

    @property
    def path(self):
        """Path (starting with '/') for provisioned resource."""
        return '/' + self.URI_ROOT_PATH + '/' + self.encode('token')
Пример #15
0
class DVSNIResponse(ChallengeResponse):
    """ACME "dvsni" challenge response.

    :param str s: Random data, **not** base64-encoded.

    """
    typ = "dvsni"

    DOMAIN_SUFFIX = DVSNI.DOMAIN_SUFFIX
    """Domain name suffix."""

    S_SIZE = 32
    """Required size of the :attr:`s` in bytes."""

    s = jose.Field(
        "s",
        encoder=jose.b64encode,  # pylint: disable=invalid-name
        decoder=functools.partial(jose.decode_b64jose, size=S_SIZE))

    def __init__(self, s=None, *args, **kwargs):
        s = Crypto.Random.get_random_bytes(self.S_SIZE) if s is None else s
        super(DVSNIResponse, self).__init__(s=s, *args, **kwargs)

    def z(self, chall):  # pylint: disable=invalid-name
        """Compute the parameter ``z``.

        :param challenge: Corresponding challenge.
        :type challenge: :class:`DVSNI`

        """
        z = hashlib.new("sha256")  # pylint: disable=invalid-name
        z.update(chall.r)
        z.update(self.s)
        return z.hexdigest()

    def z_domain(self, chall):
        """Domain name for certificate subjectAltName."""
        return self.z(chall) + self.DOMAIN_SUFFIX
Пример #16
0
class DNS(DVChallenge):
    """ACME "dns" challenge.

    :ivar unicode token:

    """
    typ = "dns"

    LABEL = "_acme-challenge"
    """Label clients prepend to the domain name being validated."""

    TOKEN_SIZE = 128 / 8  # Based on the entropy value from the spec
    """Minimum size of the :attr:`token` in bytes."""

    token = jose.Field("token",
                       encoder=jose.encode_b64jose,
                       decoder=functools.partial(jose.decode_b64jose,
                                                 size=TOKEN_SIZE,
                                                 minimum=True))

    def gen_validation(self, account_key, alg=jose.RS256, **kwargs):
        """Generate validation.

        :param .JWK account_key: Private account key.
        :param .JWA alg:

        :returns: This challenge wrapped in `.JWS`
        :rtype: .JWS

        """
        return jose.JWS.sign(
            payload=self.json_dumps(sort_keys=True).encode('utf-8'),
            key=account_key,
            alg=alg,
            **kwargs)

    def check_validation(self, validation, account_public_key):
        """Check validation.

        :param JWS validation:
        :type account_public_key:
            `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
            or
            `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
            or
            `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
            wrapped in `.ComparableKey`

        :rtype: bool

        """
        if not validation.verify(key=account_public_key):
            return False
        try:
            return self == self.json_loads(validation.payload.decode('utf-8'))
        except jose.DeserializationError as error:
            logger.debug("Checking validation for DNS failed: %s", error)
            return False

    def gen_response(self, account_key, **kwargs):
        """Generate response.

        :param .JWK account_key: Private account key.
        :param .JWA alg:

        :rtype: DNSResponse

        """
        return DNSResponse(
            validation=self.gen_validation(self, account_key, **kwargs))

    def validation_domain_name(self, name):
        """Domain name for TXT validation record.

        :param unicode name: Domain name being validated.

        """
        return "{0}.{1}".format(self.LABEL, name)
Пример #17
0
 class Meta(jose.JSONObjectWithFields):
     """Directory Meta."""
     terms_of_service = jose.Field('terms-of-service', omitempty=True)
     website = jose.Field('website', omitempty=True)
     caa_identities = jose.Field('caa-identities', omitempty=True)
Пример #18
0
class Signature(jose.JSONObjectWithFields):
    """ACME signature.

    :ivar .JWASignature alg: Signature algorithm.
    :ivar bytes sig: Signature.
    :ivar bytes nonce: Nonce.
    :ivar .JWK jwk: JWK.

    """
    NONCE_SIZE = 16
    """Minimum size of nonce in bytes."""

    alg = jose.Field('alg', decoder=jose.JWASignature.from_json)
    sig = jose.Field('sig',
                     encoder=jose.encode_b64jose,
                     decoder=jose.decode_b64jose)
    nonce = jose.Field('nonce',
                       encoder=jose.encode_b64jose,
                       decoder=functools.partial(jose.decode_b64jose,
                                                 size=NONCE_SIZE,
                                                 minimum=True))
    jwk = jose.Field('jwk', decoder=jose.JWK.from_json)

    @classmethod
    def from_msg(cls, msg, key, nonce=None, nonce_size=None, alg=jose.RS256):
        """Create signature with nonce prepended to the message.

        :param bytes msg: Message to be signed.

        :param key: Key used for signing.
        :type key: `cryptography.hazmat.primitives.assymetric.rsa.RSAPrivateKey`
            (optionally wrapped in `.ComparableRSAKey`).

        :param bytes nonce: Nonce to be used. If None, nonce of
            ``nonce_size`` will be randomly generated.
        :param int nonce_size: Size of the automatically generated nonce.
            Defaults to :const:`NONCE_SIZE`.

        :param .JWASignature alg:

        """
        nonce_size = cls.NONCE_SIZE if nonce_size is None else nonce_size
        nonce = os.urandom(nonce_size) if nonce is None else nonce

        msg_with_nonce = nonce + msg
        sig = alg.sign(key, nonce + msg)
        logger.debug('%r signed as %r', msg_with_nonce, sig)

        return cls(alg=alg,
                   sig=sig,
                   nonce=nonce,
                   jwk=alg.kty(key=key.public_key()))

    def verify(self, msg):
        """Verify the signature.

        :param bytes msg: Message that was used in signing.

        """
        # self.alg is not Field, but JWA | pylint: disable=no-member
        return self.alg.verify(self.jwk.key, self.nonce + msg, self.sig)
class StatusRequest(Message):
    """ACME "statusRequest" message."""
    typ = "statusRequest"
    schema = util.load_schema(typ)
    token = jose.Field("token")
Пример #20
0
class Signature(jose.JSONObjectWithFields):
    """ACME signature.

    :ivar str alg: Signature algorithm.
    :ivar str sig: Signature.
    :ivar str nonce: Nonce.

    :ivar jwk: JWK.
    :type jwk: :class:`JWK`

    """
    NONCE_SIZE = 16
    """Minimum size of nonce in bytes."""

    alg = jose.Field('alg', decoder=jose.JWASignature.from_json)
    sig = jose.Field('sig',
                     encoder=jose.b64encode,
                     decoder=jose.decode_b64jose)
    nonce = jose.Field('nonce',
                       encoder=jose.b64encode,
                       decoder=functools.partial(jose.decode_b64jose,
                                                 size=NONCE_SIZE,
                                                 minimum=True))
    jwk = jose.Field('jwk', decoder=jose.JWK.from_json)

    @classmethod
    def from_msg(cls, msg, key, nonce=None, nonce_size=None, alg=jose.RS256):
        """Create signature with nonce prepended to the message.

        .. todo:: Protect against crypto unicode errors... is this sufficient?
            Do I need to escape?

        :param str msg: Message to be signed.

        :param key: Key used for signing.
        :type key: :class:`Crypto.PublicKey.RSA`

        :param str nonce: Nonce to be used. If None, nonce of
            ``nonce_size`` will be randomly generated.
        :param int nonce_size: Size of the automatically generated nonce.
            Defaults to :const:`NONCE_SIZE`.

        """
        nonce_size = cls.NONCE_SIZE if nonce_size is None else nonce_size
        if nonce is None:
            nonce = Crypto.Random.get_random_bytes(nonce_size)

        msg_with_nonce = nonce + msg
        sig = alg.sign(key, nonce + msg)
        logging.debug('%s signed as %s', msg_with_nonce, sig)

        return cls(alg=alg,
                   sig=sig,
                   nonce=nonce,
                   jwk=alg.kty(key=key.publickey()))

    def verify(self, msg):
        """Verify the signature.

        :param str msg: Message that was used in signing.

        """
        # self.alg is not Field, but JWA | pylint: disable=no-member
        return self.alg.verify(self.jwk.key, self.nonce + msg, self.sig)
Пример #21
0
class Registration(ResourceBody):
    """Registration Resource Body.

    :ivar acme.jose.jwk.JWK key: Public key.
    :ivar tuple contact: Contact information following ACME spec,
        `tuple` of `unicode`.
    :ivar unicode agreement:
    :ivar unicode authorizations: URI where
        `messages.Registration.Authorizations` can be found.
    :ivar unicode certificates: URI where
        `messages.Registration.Certificates` can be found.

    """
    # 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)
    contact = jose.Field('contact', omitempty=True, default=())
    agreement = jose.Field('agreement', omitempty=True)
    authorizations = jose.Field('authorizations', omitempty=True)
    certificates = jose.Field('certificates', omitempty=True)

    class Authorizations(jose.JSONObjectWithFields):
        """Authorizations granted to Account in the process of registration.

        :ivar tuple authorizations: URIs to Authorization Resources.

        """
        authorizations = jose.Field('authorizations')

    class Certificates(jose.JSONObjectWithFields):
        """Certificates granted to Account in the process of registration.

        :ivar tuple certificates: URIs to Certificate Resources.

        """
        certificates = jose.Field('certificates')

    phone_prefix = 'tel:'
    email_prefix = 'mailto:'

    @classmethod
    def from_data(cls, phone=None, email=None, **kwargs):
        """Create registration resource from contact details."""
        details = list(kwargs.pop('contact', ()))
        if phone is not None:
            details.append(cls.phone_prefix + phone)
        if email is not None:
            details.append(cls.email_prefix + email)
        kwargs['contact'] = tuple(details)
        return cls(**kwargs)

    def _filter_contact(self, prefix):
        return tuple(detail[len(prefix):] for detail in self.contact
                     if detail.startswith(prefix))

    @property
    def phones(self):
        """All phones found in the ``contact`` field."""
        return self._filter_contact(self.phone_prefix)

    @property
    def emails(self):
        """All emails found in the ``contact`` field."""
        return self._filter_contact(self.email_prefix)
Пример #22
0
class SimpleHTTPResponse(ChallengeResponse):
    """ACME "simpleHttp" challenge response.

    :ivar bool tls:

    """
    typ = "simpleHttp"
    tls = jose.Field("tls", default=True, omitempty=True)

    URI_ROOT_PATH = ".well-known/acme-challenge"
    """URI root path for the server provisioned resource."""

    _URI_TEMPLATE = "{scheme}://{domain}/" + URI_ROOT_PATH + "/{token}"

    CONTENT_TYPE = "application/jose+json"

    @property
    def scheme(self):
        """URL scheme for the provisioned resource."""
        return "https" if self.tls else "http"

    @property
    def port(self):
        """Port that the ACME client should be listening for validation."""
        return 443 if self.tls else 80

    def uri(self, domain, chall):
        """Create an URI to the provisioned resource.

        Forms an URI to the HTTPS server provisioned resource
        (containing :attr:`~SimpleHTTP.token`).

        :param unicode domain: Domain name being verified.
        :param challenges.SimpleHTTP chall:

        """
        return self._URI_TEMPLATE.format(
            scheme=self.scheme, domain=domain, token=chall.encode("token"))

    def gen_resource(self, chall):
        """Generate provisioned resource.

        :param challenges.SimpleHTTP chall:
        :rtype: SimpleHTTPProvisionedResource

        """
        return SimpleHTTPProvisionedResource(token=chall.token, tls=self.tls)

    def gen_validation(self, chall, account_key, alg=jose.RS256, **kwargs):
        """Generate validation.

        :param challenges.SimpleHTTP chall:
        :param .JWK account_key: Private account key.
        :param .JWA alg:

        :returns: `.SimpleHTTPProvisionedResource` signed in `.JWS`
        :rtype: .JWS

        """
        return jose.JWS.sign(
            payload=self.gen_resource(chall).json_dumps(
                sort_keys=True).encode('utf-8'),
            key=account_key, alg=alg, **kwargs)

    def check_validation(self, validation, chall, account_public_key):
        """Check validation.

        :param .JWS validation:
        :param challenges.SimpleHTTP chall:
        :type account_public_key:
            `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
            or
            `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
            or
            `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
            wrapped in `.ComparableKey`

        :rtype: bool

        """
        if not validation.verify(key=account_public_key):
            return False

        try:
            resource = SimpleHTTPProvisionedResource.json_loads(
                validation.payload.decode('utf-8'))
        except jose.DeserializationError as error:
            logger.debug(error)
            return False

        return resource.token == chall.token and resource.tls == self.tls

    def simple_verify(self, chall, domain, account_public_key, port=None):
        """Simple verify.

        According to the ACME specification, "the ACME server MUST
        ignore the certificate provided by the HTTPS server", so
        ``requests.get`` is called with ``verify=False``.

        :param challenges.SimpleHTTP chall: Corresponding challenge.
        :param unicode domain: Domain name being verified.
        :param account_public_key: Public key for the key pair
            being authorized. If ``None`` key verification is not
            performed!
        :type account_public_key:
            `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
            or
            `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
            or
            `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
            wrapped in `.ComparableKey`
        :param int port: Port used in the validation.

        :returns: ``True`` iff validation is successful, ``False``
            otherwise.
        :rtype: bool

        """
        # TODO: ACME specification defines URI template that doesn't
        # allow to use a custom port... Make sure port is not in the
        # request URI, if it's standard.
        if port is not None and port != self.port:
            logger.warn(
                "Using non-standard port for SimpleHTTP verification: %s", port)
            domain += ":{0}".format(port)

        uri = self.uri(domain, chall)
        logger.debug("Verifying %s at %s...", chall.typ, uri)
        try:
            http_response = requests.get(uri, verify=False)
        except requests.exceptions.RequestException as error:
            logger.error("Unable to reach %s: %s", uri, error)
            return False
        logger.debug("Received %s: %s. Headers: %s", http_response,
                     http_response.text, http_response.headers)

        if self.CONTENT_TYPE != http_response.headers.get(
                "Content-Type", self.CONTENT_TYPE):
            return False

        try:
            validation = jose.JWS.json_loads(http_response.text)
        except jose.DeserializationError as error:
            logger.debug(error)
            return False

        return self.check_validation(validation, chall, account_public_key)
Пример #23
0
class DVSNIResponse(ChallengeResponse):
    """ACME "dvsni" challenge response.

    :param bytes s: Random data, **not** base64-encoded.

    """
    typ = "dvsni"

    DOMAIN_SUFFIX = DVSNI.DOMAIN_SUFFIX
    """Domain name suffix."""

    S_SIZE = 32
    """Required size of the :attr:`s` in bytes."""

    s = jose.Field(
        "s",
        encoder=jose.encode_b64jose,  # pylint: disable=invalid-name
        decoder=functools.partial(jose.decode_b64jose, size=S_SIZE))

    def __init__(self, s=None, *args, **kwargs):
        s = os.urandom(self.S_SIZE) if s is None else s
        super(DVSNIResponse, self).__init__(s=s, *args, **kwargs)

    def z(self, chall):  # pylint: disable=invalid-name
        """Compute the parameter ``z``.

        :param challenge: Corresponding challenge.
        :type challenge: :class:`DVSNI`

        :rtype: bytes

        """
        z = hashlib.new("sha256")  # pylint: disable=invalid-name
        z.update(chall.r)
        z.update(self.s)
        return z.hexdigest().encode()

    def z_domain(self, chall):
        """Domain name for certificate subjectAltName.

        :rtype bytes:

        """
        return self.z(chall) + self.DOMAIN_SUFFIX

    def gen_cert(self, chall, domain, key):
        """Generate DVSNI certificate.

        :param .DVSNI chall: Corresponding challenge.
        :param unicode domain:
        :param OpenSSL.crypto.PKey

        """
        return crypto_util.gen_ss_cert(key, [
            domain,
            chall.nonce_domain.decode(),
            self.z_domain(chall).decode()
        ])

    def simple_verify(self, chall, domain, public_key, **kwargs):
        """Simple verify.

        Probes DVSNI certificate and checks it using `verify_cert`;
        hence all arguments documented in `verify_cert`.

        """
        try:
            cert = chall.probe_cert(domain=domain, **kwargs)
        except errors.Error as error:
            logger.debug(error, exc_info=True)
            return False
        return self.verify_cert(chall, domain, public_key, cert)

    def verify_cert(self, chall, domain, public_key, cert):
        """Verify DVSNI certificate.

        :param .challenges.DVSNI chall: Corresponding challenge.
        :param str domain: Domain name being validated.
        :param public_key: Public key for the key pair
            being authorized. If ``None`` key verification is not
            performed!
        :type public_key:
            `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
            or
            `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
            or
            `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
            wrapped in `.ComparableKey
        :param OpenSSL.crypto.X509 cert:

        :returns: ``True`` iff client's control of the domain has been
            verified, ``False`` otherwise.
        :rtype: bool

        """
        # TODO: check "It is a valid self-signed certificate" and
        # return False if not

        # pylint: disable=protected-access
        sans = crypto_util._pyopenssl_cert_or_req_san(cert)
        logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans)

        cert = x509.load_der_x509_certificate(
            OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1,
                                            cert), default_backend())

        if public_key is None:
            logging.warn('No key verification is performed')
        elif public_key != jose.ComparableKey(cert.public_key()):
            return False

        return domain in sans and self.z_domain(chall).decode() in sans
class AuthorizationRequest(Message):
    """ACME "authorizationRequest" message.

    :ivar str nonce: Random data from the corresponding
        :attr:`Challenge.nonce`, **not** base64-encoded.
    :ivar list responses: List of completed challenges (
        :class:`acme.challenges.ChallengeResponse`).
    :ivar signature: Signature (:class:`acme.other.Signature`).

    """
    typ = "authorizationRequest"
    schema = util.load_schema(typ)

    session_id = jose.Field("sessionID")
    nonce = jose.Field("nonce",
                       encoder=jose.b64encode,
                       decoder=jose.decode_b64jose)
    responses = jose.Field("responses")
    signature = jose.Field("signature", decoder=other.Signature.from_json)
    contact = jose.Field("contact", omitempty=True, default=())

    @responses.decoder
    def responses(value):  # pylint: disable=missing-docstring,no-self-argument
        return tuple(
            challenges.ChallengeResponse.from_json(chall) for chall in value)

    @classmethod
    def create(cls, name, key, sig_nonce=None, **kwargs):
        """Create signed "authorizationRequest".

        :param str name: Hostname

        :param key: Key used for signing.
        :type key: :class:`Crypto.PublicKey.RSA`

        :param str sig_nonce: Nonce used for signature. Useful for testing.
        :kwargs: Any other arguments accepted by the class constructor.

        :returns: Signed "authorizationRequest" ACME message.
        :rtype: :class:`AuthorizationRequest`

        """
        # pylint: disable=too-many-arguments
        signature = other.Signature.from_msg(name + kwargs["nonce"], key,
                                             sig_nonce)
        return cls(signature=signature,
                   contact=kwargs.pop("contact", ()),
                   **kwargs)

    def verify(self, name):
        """Verify signature.

        .. warning:: Caller must check that the public key encoded in the
            :attr:`signature`'s :class:`acme.jose.JWK` object
            is the correct key for a given context.

        :param str name: Hostname

        :returns: True iff ``signature`` can be verified, False otherwise.
        :rtype: bool

        """
        # self.signature is not Field | pylint: disable=no-member
        return self.signature.verify(name + self.nonce)
Пример #25
0
class RecoveryTokenResponse(ChallengeResponse):
    """ACME "recoveryToken" challenge response."""
    typ = "recoveryToken"
    token = jose.Field("token", omitempty=True)
Пример #26
0
class SimpleHTTPResponse(ChallengeResponse):
    """ACME "simpleHttp" challenge response."""
    typ = "simpleHttp"
    path = jose.Field("path")
    tls = jose.Field("tls", default=True, omitempty=True)

    URI_ROOT_PATH = ".well-known/acme-challenge"
    """URI root path for the server provisioned resource."""

    _URI_TEMPLATE = "{scheme}://{domain}/" + URI_ROOT_PATH + "/{path}"

    MAX_PATH_LEN = 25
    """Maximum allowed `path` length."""

    CONTENT_TYPE = "text/plain"

    @property
    def good_path(self):
        """Is `path` good?

        .. todo:: acme-spec: "The value MUST be comprised entirely of
           characters from the URL-safe alphabet for Base64 encoding
           [RFC4648]", base64.b64decode ignores those characters

        """
        # TODO: check that path combined with uri does not go above
        # URI_ROOT_PATH!
        return len(self.path) <= 25

    @property
    def scheme(self):
        """URL scheme for the provisioned resource."""
        return "https" if self.tls else "http"

    @property
    def port(self):
        """Port that the ACME client should be listening for validation."""
        return 443 if self.tls else 80

    def uri(self, domain):
        """Create an URI to the provisioned resource.

        Forms an URI to the HTTPS server provisioned resource
        (containing :attr:`~SimpleHTTP.token`).

        :param str domain: Domain name being verified.

        """
        return self._URI_TEMPLATE.format(scheme=self.scheme,
                                         domain=domain,
                                         path=self.path)

    def simple_verify(self, chall, domain, port=None):
        """Simple verify.

        According to the ACME specification, "the ACME server MUST
        ignore the certificate provided by the HTTPS server", so
        ``requests.get`` is called with ``verify=False``.

        :param .SimpleHTTP chall: Corresponding challenge.
        :param str domain: Domain name being verified.
        :param int port: Port used in the validation.

        :returns: ``True`` iff validation is successful, ``False``
            otherwise.
        :rtype: bool

        """
        # TODO: ACME specification defines URI template that doesn't
        # allow to use a custom port... Make sure port is not in the
        # request URI, if it's standard.
        if port is not None and port != self.port:
            logger.warn(
                "Using non-standard port for SimpleHTTP verification: %s",
                port)
            domain += ":{0}".format(port)

        uri = self.uri(domain)
        logger.debug("Verifying %s at %s...", chall.typ, uri)
        try:
            http_response = requests.get(uri, verify=False)
        except requests.exceptions.RequestException as error:
            logger.error("Unable to reach %s: %s", uri, error)
            return False
        logger.debug("Received %s. Headers: %s", http_response,
                     http_response.headers)

        good_token = http_response.text == chall.token
        if not good_token:
            logger.error("Unable to verify %s! Expected: %r, returned: %r.",
                         uri, chall.token, http_response.text)
        # TODO: spec contradicts itself, c.f.
        # https://github.com/letsencrypt/acme-spec/pull/156/files#r33136438
        good_ct = self.CONTENT_TYPE == http_response.headers.get(
            "Content-Type", self.CONTENT_TYPE)
        return self.good_path and good_ct and good_token
Пример #27
0
class SimpleHTTP(DVChallenge):
    """ACME "simpleHttp" challenge."""
    typ = "simpleHttp"
    token = jose.Field("token")
Пример #28
0
class DNS(DVChallenge):
    """ACME "dns" challenge."""
    typ = "dns"
    token = jose.Field("token")
Пример #29
0
class DVSNIResponse(ChallengeResponse):
    """ACME "dvsni" challenge response.

    :param bytes s: Random data, **not** base64-encoded.

    """
    typ = "dvsni"

    DOMAIN_SUFFIX = b".acme.invalid"
    """Domain name suffix."""

    PORT = DVSNI.PORT
    """Port to perform DVSNI challenge."""

    validation = jose.Field("validation", decoder=jose.JWS.from_json)

    @property
    def z(self):  # pylint: disable=invalid-name
        """The ``z``  parameter.

        :rtype: bytes

        """
        # Instance of 'Field' has no 'signature' member
        # pylint: disable=no-member
        return hashlib.sha256(self.validation.signature.encode(
            "signature").encode("utf-8")).hexdigest().encode()

    @property
    def z_domain(self):
        """Domain name for certificate subjectAltName.

        :rtype: bytes

        """
        z = self.z  # pylint: disable=invalid-name
        return z[:32] + b'.' + z[32:] + self.DOMAIN_SUFFIX

    @property
    def chall(self):
        """Get challenge encoded in the `validation` payload.

        :rtype: challenges.DVSNI

        """
        # pylint: disable=no-member
        return DVSNI.json_loads(self.validation.payload.decode('utf-8'))

    def gen_cert(self, key=None, bits=2048):
        """Generate DVSNI certificate.

        :param OpenSSL.crypto.PKey key: Optional private key used in
            certificate generation. If not provided (``None``), then
            fresh key will be generated.
        :param int bits: Number of bits for newly generated key.

        :rtype: `tuple` of `OpenSSL.crypto.X509` and
            `OpenSSL.crypto.PKey`

        """
        if key is None:
            key = OpenSSL.crypto.PKey()
            key.generate_key(OpenSSL.crypto.TYPE_RSA, bits)
        return crypto_util.gen_ss_cert(key, [
            # z_domain is too big to fit into CN, hence first dummy domain
            'dummy', self.z_domain.decode()], force_san=True), key

    def probe_cert(self, domain, **kwargs):
        """Probe DVSNI challenge certificate.

        :param unicode domain:

        """
        if "host" not in kwargs:
            host = socket.gethostbyname(domain)
            logging.debug('%s resolved to %s', domain, host)
            kwargs["host"] = host

        kwargs.setdefault("port", self.PORT)
        kwargs["name"] = self.z_domain
        # TODO: try different methods?
        # pylint: disable=protected-access
        return crypto_util.probe_sni(**kwargs)

    def verify_cert(self, cert):
        """Verify DVSNI challenge certificate."""
        # pylint: disable=protected-access
        sans = crypto_util._pyopenssl_cert_or_req_san(cert)
        logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans)
        return self.z_domain.decode() in sans

    def simple_verify(self, chall, domain, account_public_key,
                      cert=None, **kwargs):
        """Simple verify.

        Verify ``validation`` using ``account_public_key``, optionally
        probe DVSNI certificate and check using `verify_cert`.

        :param .challenges.DVSNI chall: Corresponding challenge.
        :param str domain: Domain name being validated.
        :type account_public_key:
            `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
            or
            `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
            or
            `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
            wrapped in `.ComparableKey`
        :param OpenSSL.crypto.X509 cert: Optional certificate. If not
            provided (``None``) certificate will be retrieved using
            `probe_cert`.

        :returns: ``True`` iff client's control of the domain has been
            verified, ``False`` otherwise.
        :rtype: bool

        """
        # pylint: disable=no-member
        if not self.validation.verify(key=account_public_key):
            return False

        # TODO: it's not checked that payload has exectly 2 fields!
        try:
            decoded_chall = self.chall
        except jose.DeserializationError as error:
            logger.debug(error, exc_info=True)
            return False

        if decoded_chall.token != chall.token:
            logger.debug("Wrong token: expected %r, found %r",
                         chall.token, decoded_chall.token)
            return False

        if cert is None:
            try:
                cert = self.probe_cert(domain=domain, **kwargs)
            except errors.Error as error:
                logger.debug(error, exc_info=True)
                return False

        return self.verify_cert(cert)
class ChallengeRequest(Message):
    """ACME "challengeRequest" message."""
    typ = "challengeRequest"
    schema = util.load_schema(typ)
    identifier = jose.Field("identifier")