From 7bd19b96694e0aec3d6dd84b596e7ecefd502a80 Mon Sep 17 00:00:00 2001 From: Ivan Mahonin Date: Feb 08 2019 16:42:16 +0000 Subject: works --- diff --git a/example.com.cron-task.sh b/example.com.cron-task.sh index b1fffdb..0f0e1cd 100755 --- a/example.com.cron-task.sh +++ b/example.com.cron-task.sh @@ -5,7 +5,8 @@ set -e ./generate-and-sign-cert.sh \ "acmeclient" \ "/etc/nginx/certs/example.com" \ - "/CN=example.com/CN=www.example.com" \ + "/CN=example.com" \ + "subjectAltName=DNS:example.com,DNS:www.example.com" \ &>> "/var/log/acmeclient/example.com.log" service nginx reload diff --git a/generate-and-sign-cert.sh b/generate-and-sign-cert.sh index f138340..d79c563 100755 --- a/generate-and-sign-cert.sh +++ b/generate-and-sign-cert.sh @@ -5,9 +5,15 @@ set -e ACMEUSER="$1" CERTS_DIR="$2" SUBJ="$3" +EXT="$4" if [ -z "$ACMEUSER" ] || [ -z "$CERTS_DIR" ] || [ -z "$SUBJ" ]; then - echo "Usage: $0 acmeuser /mysite/certs/dir/ /CN=mysite.com/CN=www.mysite.com" + echo "Usage:" + echo "$0 \\" + echo " acmeuser \\" + echo " /mysite/certs/dir/ \\" + echo " /CN=mysite.com \\" + echo " subjectAltName=DNS:mysite.com,DNS:www.mysite.com" exit 0 fi @@ -20,13 +26,17 @@ echo " begin $PREFIX " echo " -------------------------------------------- " mkdir -p "$CERTS_DIR" -openssl genrsa -out "$PREFIX.key" 4096 -openssl req -new -sha512 -key "$PREFIX.key" -out "$PREFIX.csr" -subj "$SUBJ" +openssl req -newkey rsa:4096 -sha512 -nodes \ + -keyout "$PREFIX.key" \ + -out "$PREFIX.csr" \ + -subj "$SUBJ" \ + -reqexts san \ + -config <(echo '[req]'; echo 'distinguished_name=req'; echo '[san]'; echo $EXT) sudo -u "$ACMEUSER" "$BASE_DIR/sign-cert.py" "$PREFIX.csr" "$PREFIX.crt" echo "compare modulus" -$MUDULUS_CRT=`openssl x509 -noout -modulus -in "$PREFIX.csr"` -$MUDULUS_KEY=`openssl rsa -noout -modulus -in "$PREFIX.key"` +MUDULUS_CRT=`openssl x509 -noout -modulus -in "$PREFIX.csr"` +MUDULUS_KEY=`openssl rsa -noout -modulus -in "$PREFIX.key"` if [ "$MODULUS_CRT" != "$MUDULUS_KEY" ]; then echo "ERROR: modulus of certificate do not matches modulus of key" exit 1 diff --git a/sign-cert.py b/sign-cert.py index c9db408..c9dc8ca 100755 --- a/sign-cert.py +++ b/sign-cert.py @@ -3,12 +3,12 @@ import sys import time import json -import base64 import requests from cryptography import x509 from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.serialization import Encoding from jwcrypto import jwk, jws +from jwcrypto.common import base64url_encode api_url = 'https://acme-staging-v02.api.letsencrypt.org/directory' @@ -25,46 +25,26 @@ class Session: self.out_cert_file = str(out_cert_file) - # common method using in many steps to send unsigned requests - # in: self.nonce, self.key, self.kid - # out: self.nonce + # common method uses in many steps to send unsigned requests # returns: requests.Response - def get_request(self, url, expected_status_codes): - print(' post signed ' + url) - - header = { - 'alg': 'RS256', - 'nonce': self.nonce, - 'url': url } - if self.kid is None: - header['jwk'] = json.loads(self.key.export_public()) - else: - header['kid'] = self.kid - - signer = jws.JWS(json.dumps(claims)) - signer.add_signature(self.key, protected = json.dumps(header)) - data = signer.serialize() - - headers = { 'Content-Type': 'application/jose+json' } - r = requests.post(url, headers = headers, data = data) - if not r.status_code in expected_status_codes: - print(' wrong status code ' + str(r.status_code)) - print(' expected ' + str(expected_status_codes)) + def get_request(self, url): + print(' get ' + url) + r = requests.get(url) + if r.status_code != 200: + print('response status code: ' + str(r.status_code)) print('response headers') print(r.headers) print('response body') print(r.text) raise Exception('unexpected server answer') - self.nonce = str(r.headers['Replay-Nonce']) - print(' nonce: ' + self.nonce) return r - # common method using in many steps to send signed requests + # common method uses in many steps to send signed requests # in: self.nonce, self.key, self.kid # out: self.nonce # returns: requests.Response - def post_signed_request(self, url, claims, expected_status_codes): + def post_signed_request(self, url, claims): print(' post signed ' + url) header = { @@ -82,9 +62,8 @@ class Session: headers = { 'Content-Type': 'application/jose+json' } r = requests.post(url, headers = headers, data = data) - if not r.status_code in expected_status_codes: - print(' wrong status code ' + str(r.status_code)) - print(' expected ' + str(expected_status_codes)) + if r.status_code != 200 and r.status_code != 201: + print('response status code ' + str(r.status_code)) print('response headers') print(r.headers) print('response body') @@ -96,19 +75,12 @@ class Session: # step 1 + # uses: get_request # in: self.url # out: self.url_newnonce, self.url_newaccount, self.url_neworder def fetch_directory(self): print('fetch directory') - print(' get ' + self.url) - r = requests.get(self.url) - if r.status_code != 200: - print('response status code: ' + r.status_code) - print('response headers') - print(r.headers) - print('response body') - print(r.text) - raise Exception('unexpected server answer') + r = self.get_request(self.url) json = r.json() self.url_newnonce = str(json['newNonce']) self.url_newaccount = str(json['newAccount']) @@ -137,37 +109,45 @@ class Session: # step 3 - # uses: post_jws_request (also see 'in', 'out' and 'raises' there) + # uses: post_signed_request (also see 'in' and 'out' there) # in: self.url_newaccount # out: self.kid def fetch_account(self): print('fetch account') self.kid = None claims = { 'termsOfServiceAgreed': True } - r = self.post_jws_request(self.url_newaccount, claims, [200, 201]) + r = self.post_signed_request(self.url_newaccount, claims) self.kid = str(r.headers['Location']) print(' kid: ' + self.kid) # step 4 - # uses: post_jws_request (also see 'in', 'out' and 'raises' there) - # in: self.url_neworder + # uses: post_signed_request (also see 'in' and 'out' there) + # in: self.csr, self.url_neworder # out: self.url_order, self.url_authorizations, self.url_finalize def create_order(self): print('create order') + + common_name = str( self.csr.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value ) + print(' csr common name: ' + common_name) - identifiers = [] - for attribute in csr.subject: - if attribute.oid == x509.NameOID.COMMON_NAME: - identifiers.append({ - 'type': 'dns', - 'value': str(attribute.value) }) - print(' csr name: ' + str(attribute.value)) - else: - print(' WARNING: not supported attribute in csr: ' + str(attribute.rfc4514_string())) + names = { common_name } + try: + extension = self.csr.extensions.get_extension_for_oid(x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME) + for x in extension.value.get_values_for_type(x509.DNSName): + names.add(str(x)) + except x509.ExtensionNotFound: + pass + identifiers = [] + for x in names: + identifiers.append({ + 'type': 'dns', + 'value': x }) + print(' csr name: ' + x) claims = { 'identifiers': identifiers } - r = self.post_jws_request(self.url_neworder, claims, [201]) + + r = self.post_signed_request(self.url_neworder, claims) self.url_order = str(r.headers['Location']) @@ -181,11 +161,11 @@ class Session: # step 5.1 - # uses: post_jws_request (also see 'in', 'out' and 'raises' there) + # uses: get_request # returns: url_chall (str), token (str) def fetch_authorization(self, url_authorization): print('fetch authorization') - r = self.post_jws_request(url_authorization, dict(), [200]) + r = self.get_request(url_authorization) json = r.json() print(' identifier type: ' + str(json['identifier']['type'])) print(' identifier value: ' + str(json['identifier']['value'])) @@ -230,20 +210,20 @@ class Session: # step 5.3 - # uses: post_jws_request (also see 'in', 'out' and 'raises' there) + # uses: post_signed_request (also see 'in' and 'out' there) def notify_challenge_ready(self, url_chall): print('notify that challenge is ready') - self.post_jws_request(url_chall, dict(), [200]) + self.post_signed_request(url_chall, dict()) # step 5.4 - # uses: post_jws_request (also see 'in', 'out' and 'raises' there) + # uses: get_request def wait_authorization(self, url_authorization): for i in range(0, 10): print('wait 5 seconds') time.sleep(5) print('check authorization') - r = self.post_jws_request(url_authorization, '', [200]) + r = self.get_request(url_authorization) json = r.json() status = str(json['status']) print(' authorization status: ' + status) @@ -255,7 +235,7 @@ class Session: # step 5 all - # uses: post_jws_request (also see 'in', 'out' and 'raises' there) + # uses: get_request, post_signed_request (also see 'in' and 'out' there) # in: self.url_authorizations def process_authorizations(self): print('process authorizations') @@ -268,20 +248,18 @@ class Session: # step 6 - # uses: post_jws_request (also see 'in', 'out' and 'raises' there) + # uses: get_request # in: self.url_order def wait_order_ready(self): for i in range(0, 10): print('wait 5 seconds') time.sleep(5) print('check order status') - r = self.post_jws_request(self.url_order, '', [200]) + r = self.get_request(self.url_order) json = r.json() status = str(json['status']) print(' order status: ' + status) if status == 'ready': - self.url_cert = str(json['certificate']) - print(' url for cert: ' + self.url_cert) print('order is ready') return assert(status == 'pending') @@ -289,18 +267,18 @@ class Session: # step 7 - # uses: post_jws_request (also see 'in', 'out' and 'raises' there) + # uses: post_signed_request (also see 'in' and 'out' there) # in: self.csr, self.url_finalize def finalize_order(self): print('finalize order (send CSR)') - csr_data = base64.urlsafe_b64encode( csr.public_bytes(Encoding.DER) ).decode().replace('=', '') - print(' csr adta ' + csr_data) + csr_data = base64url_encode(self.csr.public_bytes(Encoding.DER)) + print(' csr data: ' + csr_data) claims = { 'csr': csr_data } - self.post_jws_request(self.url_finalize, claims, [200]) + self.post_signed_request(self.url_finalize, claims) # step 8 - # uses: post_jws_request (also see 'in', 'out' and 'raises' there) + # uses: get_request # in: self.url_order # out: self.url_cert def wait_order_valid(self): @@ -308,11 +286,13 @@ class Session: print('wait 5 seconds') time.sleep(5) print('check order status') - r = self.post_jws_request(self.url_order, '', [200]) + r = self.get_request(self.url_order) json = r.json() status = str(json['status']) print(' order status: ' + status) if status == 'valid': + self.url_cert = str(json['certificate']) + print(' url for cert: ' + self.url_cert) print('order success') return assert(status == 'processing') @@ -320,20 +300,21 @@ class Session: # step 9 - # uses: post_jws_request (also see 'in', 'out' and 'raises' there) + # uses: get_request # in: self.url_cert def fetch_certificate(self): print('fetch certificate') - r = self.post_jws_request(self.url_cert, '', [200]) - - cert = x509.load_der_x509_certificate(r.text, default_backend()) - pem = issuer_cert.public_bytes(serialization.Encoding.PEM).decode() + r = self.get_request(self.url_cert) + cert = r.text + if len(cert) > 10000000: + raise Exception('certificate too long, max length is 10000000') + print('downloaded certificate:') - print(pem) + print(cert) print('write certificate to file: ' + out_cert_file) with open(out_cert_file, 'w') as f: - f.write(pem) + f.write(cert) # all steps