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 letsencrypt.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 letsencrypt.acme.messages2.Status status: :ivar datetime.datetime validated: """ __slots__ = ('chall',) uri = jose.Field('uri') status = jose.Field('status', decoder=Status.from_json) validated = fields.RFC3339Field('validated', omitempty=True) 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)
class ChallengeBody(ResourceBody): """Challenge Resource Body. .. todo:: Confusingly, this has a similar name to `.challenges.Challenge`, as well as `.achallenges.AnnotatedChallenge` or `.achallenges.Indexed`... Once `messages2` and `network2` is integrated with the rest of the client, this class functionality will be merged with `.challenges.Challenge`. Meanwhile, separation allows the ``master`` to be still interoperable with Node.js server (protocol v00). For the time being use names such as ``challb`` to distinguish instances of this class from ``achall`` or ``ichall``. :ivar letsencrypt.acme.messages2.Status status: :ivar datetime.datetime validated: """ __slots__ = ('chall', ) uri = jose.Field('uri') status = jose.Field('status', decoder=Status.from_json) validated = fields.RFC3339Field('validated', omitempty=True) 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
class RecoveryContact(ContinuityChallenge): """ACME "recoveryContact" challenge.""" typ = "recoveryContact" activation_url = jose.Field("activationURL", omitempty=True) success_url = jose.Field("successURL", omitempty=True) contact = jose.Field("contact", omitempty=True)
class Certificate(Message): """ACME "certificate" message. :ivar certificate: The certificate (:class:`M2Crypto.X509.X509` wrapped in :class:`letsencrypt.acme.util.ComparableX509`). :ivar list chain: Chain of certificates (:class:`M2Crypto.X509.X509` wrapped in :class:`letsencrypt.acme.util.ComparableX509` ). """ typ = "certificate" schema = util.load_schema(typ) certificate = jose.Field("certificate", encoder=jose.encode_cert, decoder=jose.decode_cert) chain = jose.Field("chain", omitempty=True, default=()) refresh = jose.Field("refresh", omitempty=True) @chain.decoder def chain(value): # pylint: disable=missing-docstring,no-self-argument return tuple(jose.decode_cert(cert) for cert in value) @chain.encoder def chain(value): # pylint: disable=missing-docstring,no-self-argument return tuple(jose.encode_cert(cert) for cert in value)
class Challenge(Message): """ACME "challenge" message. :ivar str nonce: Random data, **not** base64-encoded. :ivar list challenges: List of :class:`~letsencrypt.acme.challenges.Challenge` objects. .. todo:: 1. can challenges contain two challenges of the same type? 2. can challenges contain duplicates? 3. check "combinations" indices are in valid range 4. turn "combinations" elements into sets? 5. turn "combinations" into set? """ typ = "challenge" schema = util.load_schema(typ) session_id = jose.Field("sessionID") nonce = jose.Field("nonce", encoder=jose.b64encode, decoder=jose.decode_b64jose) challenges = jose.Field("challenges") combinations = jose.Field("combinations", omitempty=True, default=()) @challenges.decoder def challenges(value): # pylint: disable=missing-docstring,no-self-argument return tuple(challenges.Challenge.from_json(chall) for chall in value) @property def resolved_combinations(self): """Combinations with challenges instead of indices.""" return tuple( tuple(self.challenges[idx] for idx in combo) for combo in self.combinations)
class DVSNI(DVChallenge): """ACME "dvsni" challenge. :ivar str r: Random data, **not** base64-encoded. :ivar str nonce: Random data, **not** hex-encoded. """ typ = "dvsni" DOMAIN_SUFFIX = ".acme.invalid" """Domain name suffix.""" R_SIZE = 32 """Required size of the :attr:`r` in bytes.""" NONCE_SIZE = 16 """Required size of the :attr:`nonce` in bytes.""" r = jose.Field( "r", encoder=jose.b64encode, # pylint: disable=invalid-name decoder=functools.partial(jose.decode_b64jose, size=R_SIZE)) nonce = jose.Field("nonce", encoder=binascii.hexlify, decoder=functools.partial( functools.partial(jose.decode_hex16, size=NONCE_SIZE))) @property def nonce_domain(self): """Domain name used in SNI.""" return binascii.hexlify(self.nonce) + self.DOMAIN_SUFFIX
class Defer(Message): """ACME "defer" message.""" typ = "defer" schema = util.load_schema(typ) token = jose.Field("token") interval = jose.Field("interval", omitempty=True) message = jose.Field("message", omitempty=True)
class Identifier(jose.JSONObjectWithFields): """ACME identifier. :ivar letsencrypt.acme.messages2.IdentifierType typ: """ typ = jose.Field('type', decoder=IdentifierType.from_json) value = jose.Field('value')
class CertificateRequest(jose.JSONObjectWithFields): """ACME new-cert request. :ivar letsencrypt.acme.jose.util.ComparableX509 csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` :ivar tuple authorizations: `tuple` of URIs (`str`) """ csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) authorizations = jose.Field('authorizations', decoder=tuple)
class MockMessage(MockParentMessage): typ = 'test' schema = { 'type': 'object', 'properties': { 'price': {'type': 'number'}, 'name': {'type': 'string'}, }, } price = jose.Field('price') name = jose.Field('name')
class Authorization(Message): """ACME "authorization" message. :ivar jwk: :class:`letsencrypt.acme.jose.JWK` """ typ = "authorization" schema = util.load_schema(typ) recovery_token = jose.Field("recoveryToken", omitempty=True) identifier = jose.Field("identifier", omitempty=True) jwk = jose.Field("jwk", decoder=jose.JWK.from_json, omitempty=True)
class Registration(ResourceBody): """Registration Resource Body. :ivar letsencrypt.acme.jose.jwk.JWK key: Public key. :ivar tuple contact: Contact information following ACME spec """ # 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=()) recovery_token = jose.Field('recoveryToken', omitempty=True) agreement = jose.Field('agreement', omitempty=True)
class ProofOfPossession(ContinuityChallenge): """ACME "proofOfPossession" challenge. :ivar str nonce: Random data, **not** base64-encoded. :ivar hints: Various clues for the client (:class:`Hints`). """ typ = "proofOfPossession" NONCE_SIZE = 16 class Hints(jose.JSONObjectWithFields): """Hints for "proofOfPossession" challenge. :ivar jwk: JSON Web Key (:class:`letsencrypt.acme.jose.JWK`) :ivar list certs: List of :class:`letsencrypt.acme.jose.ComparableX509` certificates. """ jwk = jose.Field("jwk", decoder=jose.JWK.from_json) cert_fingerprints = jose.Field("certFingerprints", omitempty=True, default=()) certs = jose.Field("certs", omitempty=True, default=()) subject_key_identifiers = jose.Field("subjectKeyIdentifiers", omitempty=True, default=()) serial_numbers = jose.Field("serialNumbers", omitempty=True, default=()) issuers = jose.Field("issuers", omitempty=True, default=()) authorized_for = jose.Field("authorizedFor", omitempty=True, default=()) @certs.encoder def certs(value): # pylint: disable=missing-docstring,no-self-argument return tuple(jose.encode_cert(cert) for cert in value) @certs.decoder def certs(value): # pylint: disable=missing-docstring,no-self-argument return tuple(jose.decode_cert(cert) for cert in value) alg = jose.Field("alg", decoder=jose.JWASignature.from_json) nonce = jose.Field("nonce", encoder=jose.b64encode, decoder=functools.partial(jose.decode_b64jose, size=NONCE_SIZE)) hints = jose.Field("hints", decoder=Hints.from_json)
class Revocation(jose.JSONObjectWithFields): """Revocation message. :ivar revoke: Either a `datetime.datetime` or `Revocation.NOW`. :ivar tuple authorizations: Same as `CertificateRequest.authorizations` """ NOW = 'now' """A possible value for `revoke`, denoting that certificate should be revoked now.""" revoke = jose.Field('revoke') authorizations = CertificateRequest._fields['authorizations'] @revoke.decoder def revoke(value): # pylint: disable=missing-docstring,no-self-argument if value == Revocation.NOW: return value else: return fields.RFC3339Field.default_decoder(value) @revoke.encoder def revoke(value): # pylint: disable=missing-docstring,no-self-argument if value == Revocation.NOW: return value else: return fields.RFC3339Field.default_encoder(value)
class RevocationRequest(Message): """ACME "revocationRequest" message. :ivar certificate: Certificate (:class:`M2Crypto.X509.X509` wrapped in :class:`letsencrypt.acme.util.ComparableX509`). :ivar signature: Signature (:class:`letsencrypt.acme.other.Signature`). """ typ = "revocationRequest" schema = util.load_schema(typ) certificate = jose.Field("certificate", decoder=jose.decode_cert, encoder=jose.encode_cert) signature = jose.Field("signature", decoder=other.Signature.from_json) @classmethod def create(cls, key, sig_nonce=None, **kwargs): """Create signed "revocationRequest". :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 "revocationRequest" ACME message. :rtype: :class:`RevocationRequest` """ return cls(signature=other.Signature.from_msg( kwargs["certificate"].as_der(), key, sig_nonce), **kwargs) def verify(self): """Verify signature. .. warning:: Caller must check that the public key encoded in the :attr:`signature`'s :class:`letsencrypt.acme.jose.JWK` object is the correct key for a given context. :returns: True iff ``signature`` can be verified, False otherwise. :rtype: bool """ # self.signature is not Field | pylint: disable=no-member return self.signature.verify(self.certificate.as_der())
class Error(jose.JSONObjectWithFields, Exception): """ACME error. https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00 """ ERROR_TYPE_NAMESPACE = 'urn:acme:error:' ERROR_TYPE_DESCRIPTIONS = { 'malformed': 'The request message was malformed', 'unauthorized': 'The client lacks sufficient authorization', 'serverInternal': 'The server experienced an internal error', 'badCSR': 'The CSR is unacceptable (e.g., due to a short key)', } # TODO: Boulder omits 'type' and 'instance', spec requires, boulder#128 typ = jose.Field('type', omitempty=True) title = jose.Field('title', omitempty=True) detail = jose.Field('detail') instance = jose.Field('instance', omitempty=True) @typ.encoder def typ(value): # pylint: disable=missing-docstring,no-self-argument return Error.ERROR_TYPE_NAMESPACE + value @typ.decoder def typ(value): # pylint: disable=missing-docstring,no-self-argument # pylint thinks isinstance(value, Error), so startswith is not found # pylint: disable=no-member if not value.startswith(Error.ERROR_TYPE_NAMESPACE): raise jose.DeserializationError('Missing error type prefix') without_prefix = value[len(Error.ERROR_TYPE_NAMESPACE):] if without_prefix not in Error.ERROR_TYPE_DESCRIPTIONS: raise jose.DeserializationError('Error type not recognized') return without_prefix @property def description(self): """Hardcoded error description based on its type.""" return self.ERROR_TYPE_DESCRIPTIONS[self.typ] def __str__(self): if self.typ is not None: return ' :: '.join([self.typ, self.description, self.detail]) else: return str(self.detail)
class Error(Message): """ACME "error" message.""" typ = "error" schema = util.load_schema(typ) error = jose.Field("error") message = jose.Field("message", omitempty=True) more_info = jose.Field("moreInfo", omitempty=True) MESSAGE_CODES = { "malformed": "The request message was malformed", "unauthorized": "The client lacks sufficient authorization", "serverInternal": "The server experienced an internal error", "notSupported": "The request type is not supported", "unknown": "The server does not recognize an ID/token in the request", "badCSR": "The CSR is unacceptable (e.g., due to a short key)", }
class Hints(jose.JSONObjectWithFields): """Hints for "proofOfPossession" challenge. :ivar jwk: JSON Web Key (:class:`letsencrypt.acme.jose.JWK`) :ivar list certs: List of :class:`letsencrypt.acme.jose.ComparableX509` certificates. """ jwk = jose.Field("jwk", decoder=jose.JWK.from_json) cert_fingerprints = jose.Field("certFingerprints", omitempty=True, default=()) certs = jose.Field("certs", omitempty=True, default=()) subject_key_identifiers = jose.Field("subjectKeyIdentifiers", omitempty=True, default=()) serial_numbers = jose.Field("serialNumbers", omitempty=True, default=()) issuers = jose.Field("issuers", omitempty=True, default=()) authorized_for = jose.Field("authorizedFor", omitempty=True, default=()) @certs.encoder def certs(value): # pylint: disable=missing-docstring,no-self-argument return tuple(jose.encode_cert(cert) for cert in value) @certs.decoder def certs(value): # pylint: disable=missing-docstring,no-self-argument return tuple(jose.decode_cert(cert) for cert in value)
class ProofOfPossessionResponse(ChallengeResponse): """ACME "proofOfPossession" challenge response. :ivar str nonce: Random data, **not** base64-encoded. :ivar signature: :class:`~letsencrypt.acme.other.Signature` of this message. """ typ = "proofOfPossession" NONCE_SIZE = ProofOfPossession.NONCE_SIZE nonce = jose.Field( "nonce", encoder=jose.b64encode, 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)
class Authorization(ResourceBody): """Authorization Resource Body. :ivar letsencrypt.acme.messages2.Identifier identifier: :ivar list challenges: `list` of `Challenge` :ivar tuple combinations: Challenge combinations (`tuple` of `tuple` of `int`, as opposed to `list` of `list` from the spec). :ivar letsencrypt.acme.jose.jwk.JWK key: Public key. :ivar tuple contact: :ivar letsencrypt.acme.messages2.Status status: :ivar datetime.datetime expires: """ identifier = jose.Field('identifier', decoder=Identifier.from_json) challenges = jose.Field('challenges', omitempty=True) combinations = jose.Field('combinations', omitempty=True) # TODO: acme-spec #92, #98 key = Registration._fields['key'] contact = Registration._fields['contact'] 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) @challenges.decoder def challenges(value): # pylint: disable=missing-docstring,no-self-argument return tuple(ChallengeBody.from_json(chall) for chall in value) @property def resolved_combinations(self): """Combinations with challenges instead of indices.""" return tuple( tuple(self.challenges[idx] for idx in combo) for combo in self.combinations)
class SimpleHTTPSResponse(ChallengeResponse): """ACME "simpleHttps" challenge response.""" typ = "simpleHttps" 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:`~SimpleHTTPS.token`) by populating the :attr:`URI_TEMPLATE`. :param str domain: Domain name being verified. """ return self.URI_TEMPLATE.format(domain=domain, path=self.path)
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
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:`letsencrypt.acme.challenges.ChallengeResponse`). :ivar signature: Signature (:class:`letsencrypt.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:`letsencrypt.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)
class ChallengeRequest(Message): """ACME "challengeRequest" message.""" typ = "challengeRequest" schema = util.load_schema(typ) identifier = jose.Field("identifier")
class SimpleHTTPS(DVChallenge): """ACME "simpleHttps" challenge.""" typ = "simpleHttps" token = jose.Field("token")
class DNS(DVChallenge): """ACME "dns" challenge.""" typ = "dns" token = jose.Field("token")
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)
class StatusRequest(Message): """ACME "statusRequest" message.""" typ = "statusRequest" schema = util.load_schema(typ) token = jose.Field("token")
class RecoveryTokenResponse(ChallengeResponse): """ACME "recoveryToken" challenge response.""" typ = "recoveryToken" token = jose.Field("token", omitempty=True)