1
0
mirror of https://github.com/certbot/certbot.git synced 2026-01-21 19:01:07 +03:00

Initial impl. of v02, works with Boulder

This commit is contained in:
Jakub Warmuz
2015-03-20 23:58:23 +00:00
parent 7def7df897
commit 7e820b093d
2 changed files with 205 additions and 0 deletions

66
examples/restified.py Normal file
View File

@@ -0,0 +1,66 @@
import httplib
import logging
import os
import pkg_resources
import requests
from letsencrypt.acme import messages2
from letsencrypt.acme import jose
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
URL_ROOT = 'https://www.letsencrypt-demo.org'
NEW_AUTHZ_URL = URL_ROOT + '/acme/new-authz'
NEW_CERT_URL = URL_ROOT + '/acme/new-certz'
class Resource(jose.ImmutableMap):
__slots__ = ('body', 'location')
def send(resource, key, alg=jose.RS256):
dumps = resource.body.json_dumps()
logging.debug('Serialized JSON: %s', dumps)
sig = jose.JWS.sign(payload=dumps, key=key, alg=alg).json_dumps()
logging.debug('Serialized JWS: %s', sig)
response = requests.post(resource.location, sig)
logging.debug('Received response %s: %s', response, response.text)
if (response.status_code == httplib.OK or
response.status_code == httplib.CREATED):
pass
# TODO: server might override NEW_AUTHZ_URI (after new-reg) or
# NEW_CERTZ_URI (after new-authz) and we should use it
# instead. Below code only prints the link.
if 'next' in response.links:
logging.debug('Link (next): %s', response.links['next']['url'])
if 'up' in response.links:
logging.debug('Link (up): %s', response.links['up']['url'])
# TODO: new-cert response is not JSON
return Resource(
body=type(resource.body).from_json(response.json()),
location=response.headers['location'])
registration = messages2.Registration(contact=(
'mailto:cert-admin@example.com', 'tel:+12025551212'))
key = jose.JWKRSA.load(pkg_resources.resource_string(
'letsencrypt.acme.jose', os.path.join('testdata', 'rsa512_key.pem')))
authz = Resource(body=messages2.Authorization(identifier=messages2.Identifier(
typ=messages2.Identifier.FQDN, value="example1.com")),
location=NEW_AUTHZ_URL)
authz2 = send(authz, key)
assert authz2.body.key == key.public()
assert authz2.body.identifier == authz.body.identifier
assert authz2.body.challenges is not None
print authz2
print
print requests.get(authz2.location).json()

View File

@@ -0,0 +1,139 @@
"""ACME protocol v02 messages."""
import jsonschema
from letsencrypt.acme import challenges
from letsencrypt.acme import errors
from letsencrypt.acme import jose
from letsencrypt.acme import other
from letsencrypt.acme import util
class Resource(jose.JSONObjectWithFields):
"""ACME Resource."""
class Error(object):
"""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)",
}
typ = jose.Field('type')
title = jose.Field('title', omitempty=True)
detail = jose.Field('detail')
instance = jose.Field('instance')
@typ.encoder
def typ(value):
return ERROR_TYPE_NAMESPACE + value
@typ.decoder
def typ(value):
if not value.startswith(ERROR_TYPE_NAMESPACE):
raise errors.DeserializationError('Unrecognized error type')
return value[len(ERROR_TYPE_NAMESPACE):]
@property
def description(self):
return self.ERROR_TYPE_DESCRIPTIONS[self.typ]
class Registration(Resource):
"""Registration resource."""
# key will be ignored by server and taken from JWS instead
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)
class Identifier(jose.JSONObjectWithFields):
typ = jose.Field('type')
value = jose.Field('value')
FQDN = 'dns' # TODO: acme-spec uses 'domain' in some examples,
# Boulder uses 'dns' though
class ChallengeWithMeta(jose.JSONObjectWithFields):
__slots__ = ('body',)
status = jose.Field('status')
validated = jose.Field('validated', omitempty=True)
uri = jose.Field('uri')
def to_json(self):
jobj = super(ChallengeWithMeta, self).to_json()
jobj.update(self.body.to_json())
return jobj
@classmethod
def fields_from_json(cls, jobj):
fields = super(ChallengeWithMeta, cls).fields_from_json(jobj)
fields['body'] = challenges.Challenge.from_json(jobj)
return fields
class Authorization(Resource):
class Status(object):
VALID = frozenset(['pending', 'valid', 'invalid'])
identifier = jose.Field('identifier', decoder=Identifier.from_json)
# acme-spec marks 'key' as 'required', but new-authz does not need
# to carry it, server will take 'key' from the 'jwk' found in the
# JWS
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
status = jose.Field('status', omitempty=True)
challenges = jose.Field('challenges', omitempty=True)
combinations = jose.Field('combinations', omitempty=True)
# TODO: 'The client MAY provide contact information in the
# "contact" field in this or any subsequent request.' ???
# TODO: 'expires' is allowed for Authorization Resources in
# general, but for Authorization '[t]he "expires" field MUST be
# absent'... then acme-spec gives example with 'expires'
# present... That's confusing!
#expires = jose.Field('expires', omitempty=True)
@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)
@challenges.decoder
def challenges(value): # pylint: disable=missing-docstring,no-self-argument
# TODO: acme-spec examples use hybrid between a list and a
# dict: "challenges": [ "simpleHttps": {}, ... ], while
# Boulder uses (more sane): "challenges": [{"type":
# "simpleHttps", ...}, ...]
# TODO: Server also returns the follwing:
# u'status': u'pending', u'completed': u'0001-01-01T00:00:00Z'
# "uri":"http://0.0.0.0:4000/acme/authz/vI_H5tJroyaGhappi8xBtpGYSYBvuIo3JIvakORaEJo?challenge=0"
tuple((chall['status'], chall.get('validated'), chall['uri'])
for chall in value)
return tuple(ChallengeWithMeta.from_json(chall) for chall in value)
class NewCertificate(Resource):
"""ACME new certificate resource request."""
csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
authorizations = jose.Field('authorizations', decoder=tuple)
class Revocation(Resource):
revoke = jose.Field('revoke')
authorizations = NewCertificate.authorizations