Blame sign-cert.py

Ivan Mahonin 7a50dc
#!/usr/bin/python3
Ivan Mahonin 7a50dc
Ivan Mahonin cc2683
import sys
Ivan Mahonin 7a50dc
import time
Ivan Mahonin 7a50dc
import json
Ivan Mahonin 7a50dc
import requests
Ivan Mahonin cc2683
from cryptography import x509
Ivan Mahonin b6b0ba
from cryptography.hazmat.backends import default_backend
Ivan Mahonin 7bd19b
from cryptography.hazmat.primitives.serialization import Encoding
Ivan Mahonin cc2683
from jwcrypto import jwk, jws
Ivan Mahonin 7bd19b
from jwcrypto.common import base64url_encode
Ivan Mahonin 7a50dc
Ivan Mahonin 7a50dc
Ivan Mahonin 96bebd
#api_url             = 'https://acme-staging-v02.api.letsencrypt.org/directory'
Ivan Mahonin 96bebd
api_url             = 'https://acme-v02.api.letsencrypt.org/directory'
Ivan Mahonin 96bebd
api_client_key_file = '/home/acmeclient/acmeclient/api-client-key.json'
Ivan Mahonin 96bebd
answers_prefix      = '/home/acmeclient/acmeclient/challenge/'
Ivan Mahonin 7a50dc
Ivan Mahonin 7a50dc
Ivan Mahonin 7a50dc
class Session:
Ivan Mahonin b6b0ba
  def __init__(self, url, key, csr, answers_prefix, out_cert_file):
Ivan Mahonin b6b0ba
    self.url = str(url)
Ivan Mahonin 7a50dc
    self.key = key
Ivan Mahonin b6b0ba
    self.csr = csr
Ivan Mahonin b6b0ba
    self.answers_prefix = str(answers_prefix)
Ivan Mahonin b6b0ba
    self.out_cert_file = str(out_cert_file)
Ivan Mahonin 7a50dc
Ivan Mahonin 7a50dc
Ivan Mahonin 7bd19b
  # common method uses in many steps to send unsigned requests
Ivan Mahonin cc2683
  # returns: requests.Response
Ivan Mahonin 7bd19b
  def get_request(self, url):
Ivan Mahonin 7bd19b
    print('  get ' + url)
Ivan Mahonin 7bd19b
    r = requests.get(url)
Ivan Mahonin 7bd19b
    if r.status_code != 200:
Ivan Mahonin 7bd19b
      print('response status code: ' + str(r.status_code))
Ivan Mahonin cc2683
      print('response headers')
Ivan Mahonin cc2683
      print(r.headers)
Ivan Mahonin cc2683
      print('response body')
Ivan Mahonin cc2683
      print(r.text)
Ivan Mahonin cc2683
      raise Exception('unexpected server answer')
Ivan Mahonin cc2683
    return r
Ivan Mahonin cc2683
Ivan Mahonin cc2683
Ivan Mahonin 7bd19b
  # common method uses in many steps to send signed requests
Ivan Mahonin cc2683
  # in: self.nonce, self.key, self.kid
Ivan Mahonin cc2683
  # out: self.nonce
Ivan Mahonin cc2683
  # returns: requests.Response
Ivan Mahonin 7bd19b
  def post_signed_request(self, url, claims):
Ivan Mahonin cc2683
    print('  post signed ' + url)
Ivan Mahonin cc2683
    
Ivan Mahonin cc2683
    header = {
Ivan Mahonin cc2683
      'alg': 'RS256',
Ivan Mahonin cc2683
      'nonce': self.nonce,
Ivan Mahonin cc2683
      'url': url }
Ivan Mahonin cc2683
    if self.kid is None:
Ivan Mahonin cc2683
      header['jwk'] = json.loads(self.key.export_public())
Ivan Mahonin cc2683
    else:
Ivan Mahonin cc2683
      header['kid'] = self.kid
Ivan Mahonin cc2683
    
Ivan Mahonin cc2683
    signer = jws.JWS(json.dumps(claims))
Ivan Mahonin cc2683
    signer.add_signature(self.key, protected = json.dumps(header))
Ivan Mahonin cc2683
    data = signer.serialize()
Ivan Mahonin cc2683
    
Ivan Mahonin cc2683
    headers = { 'Content-Type': 'application/jose+json' }
Ivan Mahonin cc2683
    r = requests.post(url, headers = headers, data = data)
Ivan Mahonin 7bd19b
    if r.status_code != 200 and r.status_code != 201:
Ivan Mahonin 7bd19b
      print('response status code ' + str(r.status_code))
Ivan Mahonin cc2683
      print('response headers')
Ivan Mahonin cc2683
      print(r.headers)
Ivan Mahonin cc2683
      print('response body')
Ivan Mahonin cc2683
      print(r.text)
Ivan Mahonin cc2683
      raise Exception('unexpected server answer')
Ivan Mahonin cc2683
    self.nonce = str(r.headers['Replay-Nonce'])
Ivan Mahonin cc2683
    print('  nonce: ' + self.nonce)
Ivan Mahonin cc2683
    return r
Ivan Mahonin cc2683
Ivan Mahonin cc2683
Ivan Mahonin b6b0ba
  # step 1
Ivan Mahonin 7bd19b
  # uses: get_request
Ivan Mahonin b6b0ba
  # in: self.url
Ivan Mahonin b6b0ba
  # out: self.url_newnonce, self.url_newaccount, self.url_neworder
Ivan Mahonin 7a50dc
  def fetch_directory(self):
Ivan Mahonin 7a50dc
    print('fetch directory')
Ivan Mahonin 7bd19b
    r = self.get_request(self.url)
Ivan Mahonin 7a50dc
    json = r.json()
Ivan Mahonin 7a50dc
    self.url_newnonce = str(json['newNonce'])
Ivan Mahonin 7a50dc
    self.url_newaccount = str(json['newAccount'])
Ivan Mahonin 7a50dc
    self.url_neworder = str(json['newOrder'])
Ivan Mahonin 7a50dc
    print('  url for new nonce: ' + self.url_newnonce)
Ivan Mahonin 7a50dc
    print('  url for new account: ' + self.url_newaccount)
Ivan Mahonin 7a50dc
    print('  url for new order: ' + self.url_neworder)
Ivan Mahonin 7a50dc
  
Ivan Mahonin b6b0ba
  
Ivan Mahonin b6b0ba
  # step 2
Ivan Mahonin b6b0ba
  # in: self.url_newnonce
Ivan Mahonin b6b0ba
  # out: self.nonce
Ivan Mahonin 7a50dc
  def fetch_nonce(self):
Ivan Mahonin 7a50dc
    print('fetch nonce')
Ivan Mahonin b6b0ba
    print('  head ' + self.url_newnonce)
Ivan Mahonin b6b0ba
    r = requests.head(self.url_newnonce)
Ivan Mahonin 7a50dc
    if r.status_code != 200:
Ivan Mahonin cc2683
      print('response status code: ' + r.status_code)
Ivan Mahonin cc2683
      print('response headers')
Ivan Mahonin cc2683
      print(r.headers)
Ivan Mahonin cc2683
      print('response body')
Ivan Mahonin cc2683
      print(r.text)
Ivan Mahonin cc2683
      raise Exception('unexpected server answer')
Ivan Mahonin 7a50dc
    self.nonce = str(r.headers['Replay-Nonce'])
Ivan Mahonin 7a50dc
    print('  nonce: ' + self.nonce)
Ivan Mahonin 7a50dc
  
Ivan Mahonin b6b0ba
  
Ivan Mahonin b6b0ba
  # step 3
Ivan Mahonin 7bd19b
  # uses: post_signed_request (also see 'in' and 'out' there)
Ivan Mahonin b6b0ba
  # in: self.url_newaccount
Ivan Mahonin b6b0ba
  # out: self.kid
Ivan Mahonin b6b0ba
  def fetch_account(self):
Ivan Mahonin b6b0ba
    print('fetch account')
Ivan Mahonin b6b0ba
    self.kid = None
Ivan Mahonin b6b0ba
    claims = { 'termsOfServiceAgreed': True }
Ivan Mahonin 7bd19b
    r = self.post_signed_request(self.url_newaccount, claims)
Ivan Mahonin b6b0ba
    self.kid = str(r.headers['Location'])
Ivan Mahonin 7a50dc
    print('  kid: ' + self.kid)
Ivan Mahonin b6b0ba
Ivan Mahonin 7a50dc
  
Ivan Mahonin b6b0ba
  # step 4
Ivan Mahonin 7bd19b
  # uses: post_signed_request (also see 'in' and 'out' there)
Ivan Mahonin 7bd19b
  # in: self.csr, self.url_neworder
Ivan Mahonin cc2683
  # out: self.url_order, self.url_authorizations, self.url_finalize
Ivan Mahonin b6b0ba
  def create_order(self):
Ivan Mahonin 7a50dc
    print('create order')
Ivan Mahonin 7bd19b
Ivan Mahonin 7bd19b
    common_name = str( self.csr.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value )
Ivan Mahonin 7bd19b
    print('  csr common name: ' + common_name)
Ivan Mahonin b6b0ba
    
Ivan Mahonin 7bd19b
    names = { common_name }
Ivan Mahonin 7bd19b
    try:
Ivan Mahonin 7bd19b
      extension = self.csr.extensions.get_extension_for_oid(x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
Ivan Mahonin 7bd19b
      for x in extension.value.get_values_for_type(x509.DNSName):
Ivan Mahonin 7bd19b
        names.add(str(x))
Ivan Mahonin 7bd19b
    except x509.ExtensionNotFound:
Ivan Mahonin 7bd19b
        pass
Ivan Mahonin 7a50dc
    
Ivan Mahonin 7bd19b
    identifiers = []
Ivan Mahonin 7bd19b
    for x in names:
Ivan Mahonin 7bd19b
      identifiers.append({
Ivan Mahonin 7bd19b
        'type': 'dns',
Ivan Mahonin 7bd19b
        'value': x })
Ivan Mahonin 7bd19b
      print('  csr name: ' + x)
Ivan Mahonin b6b0ba
    claims = { 'identifiers': identifiers }
Ivan Mahonin 7bd19b
    
Ivan Mahonin 7bd19b
    r = self.post_signed_request(self.url_neworder, claims)
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
    self.url_order = str(r.headers['Location'])
Ivan Mahonin 7a50dc
    
Ivan Mahonin 7a50dc
    json = r.json()
Ivan Mahonin b6b0ba
    self.url_authorizations = []
Ivan Mahonin cc2683
    for x in json['authorizations']:
Ivan Mahonin b6b0ba
      self.url_authorizations.append(str(x))
Ivan Mahonin 7a50dc
      print('  url for authorization: ' + str(x))
Ivan Mahonin b6b0ba
    self.url_finalize = str(json['finalize'])
Ivan Mahonin b6b0ba
    print('  url for finalize: ' + self.url_finalize)
Ivan Mahonin b6b0ba
  
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
  # step 5.1
Ivan Mahonin 7bd19b
  # uses: get_request
Ivan Mahonin b6b0ba
  # returns: url_chall (str), token (str)
Ivan Mahonin b6b0ba
  def fetch_authorization(self, url_authorization):
Ivan Mahonin b6b0ba
    print('fetch authorization')
Ivan Mahonin 7bd19b
    r = self.get_request(url_authorization)
Ivan Mahonin b6b0ba
    json = r.json()
Ivan Mahonin b6b0ba
    print('  identifier type: ' + str(json['identifier']['type']))
Ivan Mahonin b6b0ba
    print('  identifier value: ' + str(json['identifier']['value']))
Ivan Mahonin b6b0ba
    
Ivan Mahonin b6b0ba
    url_chall = None
Ivan Mahonin b6b0ba
    token = None
Ivan Mahonin cc2683
    for x in json['challenges']:
Ivan Mahonin b6b0ba
      challenge = str(x['type'])
Ivan Mahonin b6b0ba
      if challenge == 'http-01':
Ivan Mahonin b6b0ba
        print('  suitable challenge: ' + challenge)
Ivan Mahonin b6b0ba
        url_chall = str(x['url'])
Ivan Mahonin b6b0ba
        token = str(x['token'])
Ivan Mahonin b6b0ba
        break
Ivan Mahonin b6b0ba
    assert(not url_chall is None)
Ivan Mahonin b6b0ba
    assert(not token is None)
Ivan Mahonin 7a50dc
    
Ivan Mahonin b6b0ba
    print('  url for challenge: ' + url_chall)
Ivan Mahonin b6b0ba
    print('  url for challenge: ' + url_chall)
Ivan Mahonin b6b0ba
    print('  challenge token: ' + token)
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
    print('  verify token')
Ivan Mahonin b6b0ba
    if len(token) > 256:
Ivan Mahonin b6b0ba
        raise Exception('token too long, maximum length is 256')
Ivan Mahonin b6b0ba
    valid_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
Ivan Mahonin b6b0ba
    for char in token:
Ivan Mahonin b6b0ba
      if not char in valid_chars:
Ivan Mahonin b6b0ba
        raise Exception('wrong token, allowed following chars only: ' + valid_chars)
Ivan Mahonin b6b0ba
    
Ivan Mahonin b6b0ba
    return url_chall, token
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
  # step 5.2
Ivan Mahonin b6b0ba
  # in: self.answers_prefix
Ivan Mahonin b6b0ba
  def prepare_challenge_answer(self, token):
Ivan Mahonin b6b0ba
    print('prepare challenge answer')
Ivan Mahonin b6b0ba
    answer = token + '.' + str(key.thumbprint(jwk.hashes.SHA256()))
Ivan Mahonin b6b0ba
    print('  answer: ' + answer)
Ivan Mahonin b6b0ba
    filename = answers_prefix + token
Ivan Mahonin b6b0ba
    print('  write answer to file: ' + filename)
Ivan Mahonin b6b0ba
    with open(filename, 'w') as f:
Ivan Mahonin b6b0ba
      f.write( answer )
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
  # step 5.3
Ivan Mahonin 7bd19b
  # uses: post_signed_request (also see 'in' and 'out' there)
Ivan Mahonin b6b0ba
  def notify_challenge_ready(self, url_chall):
Ivan Mahonin b6b0ba
    print('notify that challenge is ready')
Ivan Mahonin 7bd19b
    self.post_signed_request(url_chall, dict())
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
  # step 5.4
Ivan Mahonin 7bd19b
  # uses: get_request
Ivan Mahonin b6b0ba
  def wait_authorization(self, url_authorization):
Ivan Mahonin b6b0ba
    for i in range(0, 10):
Ivan Mahonin b6b0ba
      print('wait 5 seconds')
Ivan Mahonin b6b0ba
      time.sleep(5)
Ivan Mahonin b6b0ba
      print('check authorization')
Ivan Mahonin 7bd19b
      r = self.get_request(url_authorization)
Ivan Mahonin b6b0ba
      json = r.json()
Ivan Mahonin b6b0ba
      status = str(json['status'])
Ivan Mahonin b6b0ba
      print('  authorization status: ' + status)
Ivan Mahonin b6b0ba
      if status == 'valid':
Ivan Mahonin b6b0ba
        print('authorization success')
Ivan Mahonin b6b0ba
        return
Ivan Mahonin b6b0ba
      assert(status == 'pending')
Ivan Mahonin b6b0ba
    raise Exception('authorization was not happened')
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
  # step 5 all
Ivan Mahonin 7bd19b
  # uses: get_request, post_signed_request (also see 'in' and 'out' there)
Ivan Mahonin b6b0ba
  # in: self.url_authorizations
Ivan Mahonin b6b0ba
  def process_authorizations(self):
Ivan Mahonin b6b0ba
    print('process authorizations')
Ivan Mahonin b6b0ba
    for url_authorization in self.url_authorizations:
Ivan Mahonin b6b0ba
      url_chall, token = self.fetch_authorization(url_authorization)
Ivan Mahonin b6b0ba
      self.prepare_challenge_answer(token)
Ivan Mahonin b6b0ba
      self.notify_challenge_ready(url_chall)
Ivan Mahonin b6b0ba
      self.wait_authorization(url_authorization)
Ivan Mahonin b6b0ba
    print('all authorizations success')
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
  # step 6
Ivan Mahonin 7bd19b
  # uses: get_request
Ivan Mahonin b6b0ba
  # in: self.url_order
Ivan Mahonin b6b0ba
  def wait_order_ready(self):
Ivan Mahonin 7a50dc
    for i in range(0, 10):
Ivan Mahonin 7a50dc
      print('wait 5 seconds')
Ivan Mahonin 7a50dc
      time.sleep(5)
Ivan Mahonin b6b0ba
      print('check order status')
Ivan Mahonin 7bd19b
      r = self.get_request(self.url_order)
Ivan Mahonin 7a50dc
      json = r.json()
Ivan Mahonin 7a50dc
      status = str(json['status'])
Ivan Mahonin 7a50dc
      print('  order status: ' + status)
Ivan Mahonin 7a50dc
      if status == 'ready':
Ivan Mahonin b6b0ba
        print('order is ready')
Ivan Mahonin b6b0ba
        return
Ivan Mahonin 7a50dc
      assert(status == 'pending')
Ivan Mahonin b6b0ba
    raise Exception('order was not became ready')
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
  # step 7
Ivan Mahonin 7bd19b
  # uses: post_signed_request (also see 'in' and 'out' there)
Ivan Mahonin b6b0ba
  # in: self.csr, self.url_finalize
Ivan Mahonin b6b0ba
  def finalize_order(self):
Ivan Mahonin 7a50dc
    print('finalize order (send CSR)')
Ivan Mahonin 7bd19b
    csr_data = base64url_encode(self.csr.public_bytes(Encoding.DER))
Ivan Mahonin 7bd19b
    print('  csr data: ' + csr_data)
Ivan Mahonin 7a50dc
    claims = { 'csr': csr_data }
Ivan Mahonin 7bd19b
    self.post_signed_request(self.url_finalize, claims)
Ivan Mahonin b6b0ba
  
Ivan Mahonin b6b0ba
  
Ivan Mahonin b6b0ba
  # step 8
Ivan Mahonin 7bd19b
  # uses: get_request
Ivan Mahonin b6b0ba
  # in: self.url_order
Ivan Mahonin cc2683
  # out: self.url_cert
Ivan Mahonin b6b0ba
  def wait_order_valid(self):
Ivan Mahonin 7a50dc
    for i in range(0, 10):
Ivan Mahonin 7a50dc
      print('wait 5 seconds')
Ivan Mahonin 7a50dc
      time.sleep(5)
Ivan Mahonin b6b0ba
      print('check order status')
Ivan Mahonin 7bd19b
      r = self.get_request(self.url_order)
Ivan Mahonin 7a50dc
      json = r.json()
Ivan Mahonin 7a50dc
      status = str(json['status'])
Ivan Mahonin 7a50dc
      print('  order status: ' + status)
Ivan Mahonin 7a50dc
      if status == 'valid':
Ivan Mahonin 7bd19b
        self.url_cert = str(json['certificate'])
Ivan Mahonin 7bd19b
        print('  url for cert: ' + self.url_cert)
Ivan Mahonin 7a50dc
        print('order success')
Ivan Mahonin b6b0ba
        return
Ivan Mahonin 7a50dc
      assert(status == 'processing')
Ivan Mahonin b6b0ba
    raise Exception('order was not became valid')
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
  # step 9
Ivan Mahonin 7bd19b
  # uses: get_request
Ivan Mahonin b6b0ba
  # in: self.url_cert
Ivan Mahonin b6b0ba
  def fetch_certificate(self):
Ivan Mahonin b6b0ba
    print('fetch certificate')
Ivan Mahonin 7bd19b
    r = self.get_request(self.url_cert)
Ivan Mahonin 7bd19b
    cert = r.text
Ivan Mahonin b6b0ba
    
Ivan Mahonin 7bd19b
    if len(cert) > 10000000:
Ivan Mahonin 7bd19b
      raise Exception('certificate too long, max length is 10000000')
Ivan Mahonin 7bd19b
      
Ivan Mahonin cc2683
    print('downloaded certificate:')
Ivan Mahonin 7bd19b
    print(cert)
Ivan Mahonin 7a50dc
    print('write certificate to file: ' + out_cert_file)
Ivan Mahonin 7a50dc
    with open(out_cert_file, 'w') as f:
Ivan Mahonin 7bd19b
      f.write(cert)
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
Ivan Mahonin b6b0ba
  # all steps
Ivan Mahonin b6b0ba
  def run(self):
Ivan Mahonin b6b0ba
    print('run')
Ivan Mahonin b6b0ba
    self.fetch_directory()
Ivan Mahonin b6b0ba
    self.fetch_nonce()
Ivan Mahonin b6b0ba
    self.fetch_account()
Ivan Mahonin b6b0ba
    self.create_order()
Ivan Mahonin b6b0ba
    self.process_authorizations()
Ivan Mahonin b6b0ba
    self.wait_order_ready()
Ivan Mahonin b6b0ba
    self.finalize_order()
Ivan Mahonin b6b0ba
    self.wait_order_valid()
Ivan Mahonin b6b0ba
    self.fetch_certificate()
Ivan Mahonin 7a50dc
    print('done')
Ivan Mahonin 7a50dc
Ivan Mahonin 7a50dc
Ivan Mahonin 7a50dc
Ivan Mahonin cc2683
if len(sys.argv) != 3 or (len(sys.argv) == 2 and sys.argv[1] == '--help'):
Ivan Mahonin cc2683
  print('Usage: ' + sys.argv[0] + ' /path/to/certificate/sign/request.csr /path/to/out/certificate.crt')
Ivan Mahonin cc2683
  quit()
Ivan Mahonin cc2683
Ivan Mahonin cc2683
Ivan Mahonin cc2683
csr_file = str(sys.argv[1])
Ivan Mahonin cc2683
out_cert_file = str(sys.argv[2])
Ivan Mahonin cc2683
Ivan Mahonin cc2683
print('hello')
Ivan Mahonin 7a50dc
Ivan Mahonin 7a50dc
print('load api client key from file: ' + api_client_key_file)
Ivan Mahonin 7a50dc
with open(api_client_key_file, 'r') as f:
Ivan Mahonin 7a50dc
  key = jwk.JWK( **json.loads(f.read()) )
Ivan Mahonin b6b0ba
print('  loaded fingerprint: ' + str(key.thumbprint()))
Ivan Mahonin b6b0ba
Ivan Mahonin 7a50dc
Ivan Mahonin b6b0ba
print('load CSF from file: ' + csr_file)
Ivan Mahonin b6b0ba
with open(csr_file, 'rb') as f:
Ivan Mahonin b6b0ba
  csr = x509.load_pem_x509_csr(f.read(), default_backend())
Ivan Mahonin 7a50dc
Ivan Mahonin b6b0ba
session = Session(api_url, key, csr, answers_prefix, out_cert_file)
Ivan Mahonin b6b0ba
session.run()
Ivan Mahonin 7a50dc