mirror of
https://github.com/moepman/acertmgr.git
synced 2024-11-16 00:39:12 +01:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
9ca6dae048 | |||
|
274351415d | ||
|
f78cb5c554 | ||
|
c3736c0838 | ||
|
1a98f86aad | ||
|
ef81ea62d1 | ||
|
d1caaf80ef | ||
|
ba644d44f1 | ||
c15b6ec441 | |||
|
2d230e30d9 | ||
|
6f0ccfdc91 | ||
|
460b0119ac | ||
|
e2f7b09b18 | ||
93e28437ff | |||
|
2e1f5cd894 | ||
|
ce157a5c8a | ||
|
9953cb4527 |
37
.github/workflows/release.yml
vendored
37
.github/workflows/release.yml
vendored
@ -5,14 +5,14 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt update -qq -y
|
sudo apt update -qq -y
|
||||||
sudo apt install -qq -y build-essential fakeroot git python-all python3-cryptography python3-pip python3-stdeb python3-wheel twine
|
sudo apt install -qq -y build-essential fakeroot git python-all python3-cryptography python3-pip python3-stdeb python3-wheel twine dh-python
|
||||||
sudo pip3 install --upgrade setuptools wheel
|
sudo pip3 install --upgrade setuptools wheel
|
||||||
|
|
||||||
- name: Prepare build process
|
- name: Prepare build process
|
||||||
@ -22,7 +22,7 @@ jobs:
|
|||||||
git fetch --tags -f
|
git fetch --tags -f
|
||||||
VER="$(python3 setup.py --version)"
|
VER="$(python3 setup.py --version)"
|
||||||
echo "Version found: $VER"
|
echo "Version found: $VER"
|
||||||
echo "::set-output name=version::$VER"
|
echo "version=$VER" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build python package using setuptools (source/wheel)
|
- name: Build python package using setuptools (source/wheel)
|
||||||
run: |
|
run: |
|
||||||
@ -36,12 +36,20 @@ jobs:
|
|||||||
sed "s/=determine_version()/='$(python3 setup.py --version)'/gi" -i setup.py
|
sed "s/=determine_version()/='$(python3 setup.py --version)'/gi" -i setup.py
|
||||||
sed "s@('readme'@('share/doc/python3-acertmgr'@" -i setup.py
|
sed "s@('readme'@('share/doc/python3-acertmgr'@" -i setup.py
|
||||||
# Determine recommended dependencies for deb package
|
# Determine recommended dependencies for deb package
|
||||||
echo "::set-output name=recommends3::$(echo "python3-pkg-resources")"
|
echo "recommends3=$(echo "python3-pkg-resources")" >> $GITHUB_OUTPUT
|
||||||
# Find optional dependencies to suggest in deb package
|
# Find optional dependencies to suggest in deb package
|
||||||
echo "::set-output name=suggests3::$(python3 -c "from setup import extra_requirements; print('\n'.join(['\n'.join(x) for x in extra_requirements.values()]))" | grep -v cryptography | sed 's/PyYAML/yaml/gi' | awk '{ printf("python3-%s, ",$1)};' | awk '{$1=$1; print}')"
|
echo "suggests3=$(python3 -c "from setup import extra_requirements; print('\n'.join(['\n'.join(x) for x in extra_requirements.values()]))" | grep -v cryptography | sed 's/PyYAML/yaml/gi' | awk '{ printf("python3-%s, ",$1)};' | awk '{$1=$1; print}')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build debian package using setuptools and stdeb
|
- name: Build debian package using setuptools and stdeb
|
||||||
run: python3 setup.py --command-packages=stdeb.command sdist_dsc --with-python2=False --with-python3=True --recommends3="${{ steps.stdebprep.outputs.recommends3 }}" --suggests3="${{ steps.stdebprep.outputs.suggests3 }}" bdist_deb
|
run: |
|
||||||
|
# Create debianized source directory
|
||||||
|
python3 setup.py --command-packages=stdeb.command sdist_dsc --with-python2=False --with-python3=True --recommends3="${{ steps.stdebprep.outputs.recommends3 }}" --suggests3="${{ steps.stdebprep.outputs.suggests3 }}"
|
||||||
|
# Enter debianzied source directory (first sub-directory under deb_dist)
|
||||||
|
cd "$(find deb_dist -maxdepth 1 -type d | grep -v 'deb_dist$\|tmp_sdist_dsc' | head -n1)"
|
||||||
|
# Enforce GZIP compressed debian package info for older OS versions
|
||||||
|
echo -e 'override_dh_builddeb:\n\tdh_builddeb -- -Zgzip\n' >> debian/rules
|
||||||
|
# Build deb package
|
||||||
|
DEB_BUILD_OPTIONS=nocheck dpkg-buildpackage -rfakeroot -uc -us
|
||||||
|
|
||||||
- name: Create a changelog from git log since last non-pre/rc tag
|
- name: Create a changelog from git log since last non-pre/rc tag
|
||||||
run: |
|
run: |
|
||||||
@ -85,9 +93,22 @@ jobs:
|
|||||||
artifacts/*.whl
|
artifacts/*.whl
|
||||||
artifacts/*.deb
|
artifacts/*.deb
|
||||||
|
|
||||||
|
- name: Check PyPI secrets
|
||||||
|
id: checksecrets
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "$USER" == "" -o "$PASSWORD" == "" ]; then
|
||||||
|
echo "secretspresent=NO" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "secretspresent=YES" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
USER: ${{ secrets.PYPI_USERNAME }}
|
||||||
|
PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||||
|
|
||||||
- name: Create new PyPI release
|
- name: Create new PyPI release
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && (!(contains(github.ref, 'rc') || contains(github.ref, 'pre')))
|
if: steps.checksecrets.outputs.secretspresent == 'YES' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && (!(contains(github.ref, 'rc') || contains(github.ref, 'pre')))
|
||||||
uses: pypa/gh-action-pypi-publish@master
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
with:
|
||||||
user: ${{ secrets.PYPI_USERNAME }}
|
user: ${{ secrets.PYPI_USERNAME }}
|
||||||
password: ${{ secrets.PYPI_PASSWORD }}
|
password: ${{ secrets.PYPI_PASSWORD }}
|
||||||
|
@ -12,7 +12,7 @@ Requirements
|
|||||||
------------
|
------------
|
||||||
|
|
||||||
* Python (2.7+ and 3.5+ should work)
|
* Python (2.7+ and 3.5+ should work)
|
||||||
* cryptography>=0.6 (usually includes the optional idna module)
|
* cryptography>=0.6
|
||||||
|
|
||||||
Optional requirements (to use specified features)
|
Optional requirements (to use specified features)
|
||||||
------------------------------------------------------
|
------------------------------------------------------
|
||||||
@ -56,7 +56,7 @@ By default the directory (work_dir) containing the working data (csr,certificate
|
|||||||
4 configuration contexts are known (*domainconfig (d) > globalconfig (g) > commandline (c) > built-in defaults*) with the following directives (subject to change, usual usage context written bold):
|
4 configuration contexts are known (*domainconfig (d) > globalconfig (g) > commandline (c) > built-in defaults*) with the following directives (subject to change, usual usage context written bold):
|
||||||
|
|
||||||
| Directive | Context | Description | Built-in Default |
|
| Directive | Context | Description | Built-in Default |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- |----------------------------------------------------------------------------------------------------------------------------------------------| --- |
|
||||||
| -c/--config-file | **c** | global configuration file (optional) | /etc/acertmgr/acertmgr.conf |
|
| -c/--config-file | **c** | global configuration file (optional) | /etc/acertmgr/acertmgr.conf |
|
||||||
| -d/--config-dir | **c** | directory containing domain configuration files (ending with .conf, globalconfig will be excluded automatically if in same directory) | /etc/acertmgr/*.conf |
|
| -d/--config-dir | **c** | directory containing domain configuration files (ending with .conf, globalconfig will be excluded automatically if in same directory) | /etc/acertmgr/*.conf |
|
||||||
| -w/--work-dir | **c** | working directory containing csr/certificates/keys/ca files | /etc/acertmgr |
|
| -w/--work-dir | **c** | working directory containing csr/certificates/keys/ca files | /etc/acertmgr |
|
||||||
@ -65,7 +65,7 @@ By default the directory (work_dir) containing the working data (csr,certificate
|
|||||||
| --revoke-reason | **c** | Provide a reason code for the revocation (see https://tools.ietf.org/html/rfc5280#section-5.3.1 for valid values) | |
|
| --revoke-reason | **c** | Provide a reason code for the revocation (see https://tools.ietf.org/html/rfc5280#section-5.3.1 for valid values) | |
|
||||||
| domain (san-domain...): | **d** | (domainconfig section start) Domains to use in the cert request. This value will be MD5-hashed as cert_id. | |
|
| domain (san-domain...): | **d** | (domainconfig section start) Domains to use in the cert request. This value will be MD5-hashed as cert_id. | |
|
||||||
| api | d,**g** | Determines the API version used | v2 |
|
| api | d,**g** | Determines the API version used | v2 |
|
||||||
| authority | d,**g** | URL to the certificate authorities API | https://acme-v02.api.letsencrypt.org |
|
| authority | d,**g** | URL to the certificate authorities ACME API root (without trailing /directory or similar) | https://acme-v02.api.letsencrypt.org |
|
||||||
| authority_tos_agreement | d,**g**,c | Indicates agreement to the ToS of the certificate authority (--authority-tos-agreement on command line) | |
|
| authority_tos_agreement | d,**g**,c | Indicates agreement to the ToS of the certificate authority (--authority-tos-agreement on command line) | |
|
||||||
| authority_contact_email | d,**g** | (v2 API only) Contact e-mail to be registered with your account key | |
|
| authority_contact_email | d,**g** | (v2 API only) Contact e-mail to be registered with your account key | |
|
||||||
| account_key | d,**g** | Path to the account key | {work_dir}/account.key |
|
| account_key | d,**g** | Path to the account key | {work_dir}/account.key |
|
||||||
@ -74,7 +74,7 @@ By default the directory (work_dir) containing the working data (csr,certificate
|
|||||||
| ttl_days | d,**g** | Renew certificate if it has less than this value validity left | 30 |
|
| ttl_days | d,**g** | Renew certificate if it has less than this value validity left | 30 |
|
||||||
| validate_ocsp | d,**g** | Renew certificate if it's OCSP status is REVOKED. Allowed values for this key are: false, sha1, sha224, sha256, sha384, sha512 | sha1 (as mandated by RFC5019) |
|
| validate_ocsp | d,**g** | Renew certificate if it's OCSP status is REVOKED. Allowed values for this key are: false, sha1, sha224, sha256, sha384, sha512 | sha1 (as mandated by RFC5019) |
|
||||||
| cert_dir | d,**g** | Directory containing all certificate related data (crt,key,csr) | {work_dir} |
|
| cert_dir | d,**g** | Directory containing all certificate related data (crt,key,csr) | {work_dir} |
|
||||||
| key_algorithm | d,**g** | Key-algorithm for newly generated private keys (RSA, EC, ED25519, ED448) | RSA |
|
| key_algorithm | d,**g** | Key-algorithm for newly generated private keys (RSA, ECC, ED25519, ED448) | RSA |
|
||||||
| key_length | d,**g** | Key-length for newly generated RSA private keys (in bits) or EC curve (256=P-256, 384=P-384, 521=P-521) | depends on key_algorithm |
|
| key_length | d,**g** | Key-length for newly generated RSA private keys (in bits) or EC curve (256=P-256, 384=P-384, 521=P-521) | depends on key_algorithm |
|
||||||
| csr_static | **d**,g | Whether to re-use a static CSR or generate a new dynamic CSR | false |
|
| csr_static | **d**,g | Whether to re-use a static CSR or generate a new dynamic CSR | false |
|
||||||
| csr_file | **d**,g | Path to store (and load) the certificate CSR file | {cert_dir}/{cert_id}.csr |
|
| csr_file | **d**,g | Path to store (and load) the certificate CSR file | {cert_dir}/{cert_id}.csr |
|
||||||
|
@ -15,7 +15,7 @@ import sys
|
|||||||
from acertmgr import configuration, tools
|
from acertmgr import configuration, tools
|
||||||
from acertmgr.authority import authority
|
from acertmgr.authority import authority
|
||||||
from acertmgr.modes import challenge_handler
|
from acertmgr.modes import challenge_handler
|
||||||
from acertmgr.tools import log
|
from acertmgr.tools import log, LOG_REPLACEMENTS
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pwd
|
import pwd
|
||||||
@ -139,6 +139,10 @@ def cert_revoke(cert, configs, fallback_authority, reason=None):
|
|||||||
def main():
|
def main():
|
||||||
# load config
|
# load config
|
||||||
runtimeconfig, domainconfigs = configuration.load()
|
runtimeconfig, domainconfigs = configuration.load()
|
||||||
|
# register idna-mapped domains as LOG_REPLACEMENTS for better readability of log output
|
||||||
|
for domainconfig in domainconfigs:
|
||||||
|
LOG_REPLACEMENTS.update({k: "{} [{}]".format(k, v) for k, v in domainconfig['domainlist_idna_mapped'].items()})
|
||||||
|
# Start processing
|
||||||
if runtimeconfig.get('mode') == 'revoke':
|
if runtimeconfig.get('mode') == 'revoke':
|
||||||
# Mode: revoke certificate
|
# Mode: revoke certificate
|
||||||
log("Revoking {}".format(runtimeconfig['revoke']))
|
log("Revoking {}".format(runtimeconfig['revoke']))
|
||||||
|
@ -260,8 +260,8 @@ class ACMEAuthority(AbstractACMEAuthority):
|
|||||||
if code >= 400:
|
if code >= 400:
|
||||||
raise ValueError("Error downloading certificate chain: {0} {1}".format(code, certificate))
|
raise ValueError("Error downloading certificate chain: {0} {1}".format(code, certificate))
|
||||||
|
|
||||||
cert_dict = re.match((r'(?P<cert>-----BEGIN CERTIFICATE-----[^\-]+-----END CERTIFICATE-----)\n\n'
|
cert_dict = re.match((r'(?P<cert>^-----BEGIN CERTIFICATE-----\n[^\-]+\n-----END CERTIFICATE-----)\n*'
|
||||||
r'(?P<ca>-----BEGIN CERTIFICATE-----[^\-]+-----END CERTIFICATE-----)?'),
|
r'(?P<ca>-----BEGIN CERTIFICATE-----\n.+\n-----END CERTIFICATE-----)?$'),
|
||||||
certificate, re.DOTALL).groupdict()
|
certificate, re.DOTALL).groupdict()
|
||||||
cert = tools.convert_pem_str_to_cert(cert_dict['cert'])
|
cert = tools.convert_pem_str_to_cert(cert_dict['cert'])
|
||||||
if cert_dict['ca'] is None:
|
if cert_dict['ca'] is None:
|
||||||
|
@ -90,9 +90,14 @@ def parse_config_entry(entry, globalconfig, runtimeconfig):
|
|||||||
config['id'] = hashlib.md5(domains.encode('utf-8')).hexdigest()
|
config['id'] = hashlib.md5(domains.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
# Convert unicode to IDNA domains
|
# Convert unicode to IDNA domains
|
||||||
config['domaintranslation'] = idna_convert(config['domainlist'])
|
config['domainlist_idna_mapped'] = {}
|
||||||
if len(config['domaintranslation']) > 0:
|
for idx in range(0, len(config['domainlist'])):
|
||||||
config['domainlist'] = [x for x, _ in config['domaintranslation']]
|
if any(ord(c) >= 128 for c in config['domainlist'][idx]):
|
||||||
|
domain_human = config['domainlist'][idx]
|
||||||
|
domain_idna = idna_convert(domain_human)
|
||||||
|
if domain_idna != domain_human:
|
||||||
|
config['domainlist'][idx] = domain_idna # Update domain with idna counterpart
|
||||||
|
config['domainlist_idna_mapped'][domain_idna] = domain_human # Store original domain for reference
|
||||||
|
|
||||||
# Action config defaults
|
# Action config defaults
|
||||||
config['defaults'] = globalconfig.get('defaults', {})
|
config['defaults'] = globalconfig.get('defaults', {})
|
||||||
@ -119,6 +124,17 @@ def parse_config_entry(entry, globalconfig, runtimeconfig):
|
|||||||
# Use a static cert request
|
# Use a static cert request
|
||||||
update_config_value(config, 'csr_static', localconfig, globalconfig, "false")
|
update_config_value(config, 'csr_static', localconfig, globalconfig, "false")
|
||||||
|
|
||||||
|
# SSL key algorithm (if key has to be (re-)generated)
|
||||||
|
update_config_value(config, 'key_algorithm', localconfig, globalconfig, None)
|
||||||
|
# Update config id if we have a key algorithm set to allow for
|
||||||
|
# multiple certs with different algorithms for the same set of domains
|
||||||
|
if config.get('key_algorithm', None):
|
||||||
|
config['id'] += "_" + config['key_algorithm'].lower()
|
||||||
|
|
||||||
|
# SSL key length (if key has to be (re-)generated, converted to int)
|
||||||
|
update_config_value(config, 'key_length', localconfig, globalconfig, None)
|
||||||
|
config['key_length'] = int(config['key_length']) if config['key_length'] else None
|
||||||
|
|
||||||
# SSL cert request location
|
# SSL cert request location
|
||||||
update_config_value(config, 'csr_file', localconfig, globalconfig,
|
update_config_value(config, 'csr_file', localconfig, globalconfig,
|
||||||
os.path.join(config['cert_dir'], "{}.csr".format(config['id'])))
|
os.path.join(config['cert_dir'], "{}.csr".format(config['id'])))
|
||||||
@ -131,13 +147,6 @@ def parse_config_entry(entry, globalconfig, runtimeconfig):
|
|||||||
update_config_value(config, 'key_file', localconfig, globalconfig,
|
update_config_value(config, 'key_file', localconfig, globalconfig,
|
||||||
os.path.join(config['cert_dir'], "{}.key".format(config['id'])))
|
os.path.join(config['cert_dir'], "{}.key".format(config['id'])))
|
||||||
|
|
||||||
# SSL key algorithm (if key has to be (re-)generated)
|
|
||||||
update_config_value(config, 'key_algorithm', localconfig, globalconfig, None)
|
|
||||||
|
|
||||||
# SSL key length (if key has to be (re-)generated, converted to int)
|
|
||||||
update_config_value(config, 'key_length', localconfig, globalconfig, None)
|
|
||||||
config['key_length'] = int(config['key_length']) if config['key_length'] else None
|
|
||||||
|
|
||||||
# SSL CA location / use static
|
# SSL CA location / use static
|
||||||
update_config_value(config, 'ca_file', localconfig, globalconfig,
|
update_config_value(config, 'ca_file', localconfig, globalconfig,
|
||||||
os.path.join(config['cert_dir'], "{}.ca".format(config['id'])))
|
os.path.join(config['cert_dir'], "{}.ca".format(config['id'])))
|
||||||
@ -162,8 +171,8 @@ def parse_config_entry(entry, globalconfig, runtimeconfig):
|
|||||||
cfg.update(genericfgs[0])
|
cfg.update(genericfgs[0])
|
||||||
|
|
||||||
# Update handler config with more specific values (use original names for translated unicode domains)
|
# Update handler config with more specific values (use original names for translated unicode domains)
|
||||||
_domain = _domaintranslation_dict.get(domain, domain)
|
specificcfgs = [x for x in handlerconfigs if
|
||||||
specificcfgs = [x for x in handlerconfigs if 'domain' in x and x['domain'] == _domain]
|
'domain' in x and x['domain'] == config['domainlist_idna_mapped'].get(domain, domain)]
|
||||||
if len(specificcfgs) > 0:
|
if len(specificcfgs) > 0:
|
||||||
cfg.update(specificcfgs[0])
|
cfg.update(specificcfgs[0])
|
||||||
|
|
||||||
@ -222,9 +231,9 @@ def load():
|
|||||||
|
|
||||||
# - force-rewew
|
# - force-rewew
|
||||||
if args.force_renew:
|
if args.force_renew:
|
||||||
domaintranslation = idna_convert(args.force_renew.split(' '))
|
domaintranslation = [idna_convert(d) for d in args.force_renew.split(' ')]
|
||||||
if len(domaintranslation) > 0:
|
if len(domaintranslation) > 0:
|
||||||
runtimeconfig['force_renew'] = [x for x, _ in domaintranslation]
|
runtimeconfig['force_renew'] = domaintranslation
|
||||||
else:
|
else:
|
||||||
runtimeconfig['force_renew'] = args.force_renew.split(' ')
|
runtimeconfig['force_renew'] = args.force_renew.split(' ')
|
||||||
|
|
||||||
@ -255,15 +264,29 @@ def load():
|
|||||||
os.path.abspath(domain_config_file) != os.path.abspath(global_config_file):
|
os.path.abspath(domain_config_file) != os.path.abspath(global_config_file):
|
||||||
with io.open(domain_config_file) as config_fd:
|
with io.open(domain_config_file) as config_fd:
|
||||||
try:
|
try:
|
||||||
for entry in json.load(config_fd).items():
|
data = json.load(config_fd)
|
||||||
domainconfigs.append(parse_config_entry(entry, globalconfig, runtimeconfig))
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
import yaml
|
import yaml
|
||||||
config_fd.seek(0)
|
config_fd.seek(0)
|
||||||
for entry in yaml.safe_load(config_fd).items():
|
data = yaml.safe_load(config_fd)
|
||||||
|
if isinstance(data, list):
|
||||||
|
# Handle newer config in list format (allows for multiple entries with same domains)
|
||||||
|
entries = list()
|
||||||
|
for element in data:
|
||||||
|
entries += element.items()
|
||||||
|
else:
|
||||||
|
# Handle older config format with just one entry per same domain set
|
||||||
|
entries = data.items()
|
||||||
|
for entry in entries:
|
||||||
domainconfigs.append(parse_config_entry(entry, globalconfig, runtimeconfig))
|
domainconfigs.append(parse_config_entry(entry, globalconfig, runtimeconfig))
|
||||||
|
|
||||||
# Define a fallback authority from global configuration / defaults
|
# Define a fallback authority from global configuration / defaults
|
||||||
runtimeconfig['fallback_authority'] = parse_authority([], globalconfig, runtimeconfig)
|
runtimeconfig['fallback_authority'] = parse_authority([], globalconfig, runtimeconfig)
|
||||||
|
|
||||||
return runtimeconfig, domainconfigs
|
return runtimeconfig, domainconfigs
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Simple configuration load test and output
|
||||||
|
from pprint import pprint
|
||||||
|
pprint(load())
|
||||||
|
@ -28,10 +28,20 @@ class HTTPServer6(HTTPServer):
|
|||||||
class ChallengeHandler(HTTPChallengeHandler):
|
class ChallengeHandler(HTTPChallengeHandler):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
HTTPChallengeHandler.__init__(self, config)
|
HTTPChallengeHandler.__init__(self, config)
|
||||||
bind_address = config.get("bind_address", "")
|
self.bind_address = config.get("bind_address", "")
|
||||||
port = int(config.get("port", 80))
|
self.port = int(config.get("port", 80))
|
||||||
|
|
||||||
self.challenges = {} # Initialize the challenge data dict
|
self.challenges = {} # Initialize the challenge data dict
|
||||||
|
self.server_thread = None
|
||||||
|
self.server = None
|
||||||
|
|
||||||
|
def create_challenge(self, domain, thumbprint, token):
|
||||||
|
self.challenges[token] = "{0}.{1}".format(token, thumbprint)
|
||||||
|
|
||||||
|
def destroy_challenge(self, domain, thumbprint, token):
|
||||||
|
del self.challenges[token]
|
||||||
|
|
||||||
|
def start_challenge(self, domain, thumbprint, token):
|
||||||
_self = self
|
_self = self
|
||||||
|
|
||||||
# Custom HTTP request handler
|
# Custom HTTP request handler
|
||||||
@ -54,19 +64,11 @@ class ChallengeHandler(HTTPChallengeHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(value)
|
self.wfile.write(value)
|
||||||
|
|
||||||
self.server_thread = None
|
|
||||||
try:
|
try:
|
||||||
self.server = HTTPServer6((bind_address, port), _HTTPRequestHandler)
|
self.server = HTTPServer6((self.bind_address, self.port), _HTTPRequestHandler)
|
||||||
except socket.gaierror:
|
except socket.gaierror:
|
||||||
self.server = HTTPServer((bind_address, port), _HTTPRequestHandler)
|
self.server = HTTPServer((self.bind_address, self.port), _HTTPRequestHandler)
|
||||||
|
|
||||||
def create_challenge(self, domain, thumbprint, token):
|
|
||||||
self.challenges[token] = "{0}.{1}".format(token, thumbprint)
|
|
||||||
|
|
||||||
def destroy_challenge(self, domain, thumbprint, token):
|
|
||||||
del self.challenges[token]
|
|
||||||
|
|
||||||
def start_challenge(self, domain, thumbprint, token):
|
|
||||||
def _serve():
|
def _serve():
|
||||||
self.server.serve_forever()
|
self.server.serve_forever()
|
||||||
|
|
||||||
@ -78,3 +80,6 @@ class ChallengeHandler(HTTPChallengeHandler):
|
|||||||
if self.server_thread.is_alive():
|
if self.server_thread.is_alive():
|
||||||
self.server.shutdown()
|
self.server.shutdown()
|
||||||
self.server_thread.join()
|
self.server_thread.join()
|
||||||
|
self.server.server_close()
|
||||||
|
self.server = None
|
||||||
|
self.server_thread = None
|
||||||
|
@ -10,6 +10,7 @@ import base64
|
|||||||
import datetime
|
import datetime
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import stat
|
import stat
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
@ -37,6 +38,8 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from urllib2 import urlopen, Request # Python 2
|
from urllib2 import urlopen, Request # Python 2
|
||||||
|
|
||||||
|
LOG_REPLACEMENTS = {}
|
||||||
|
|
||||||
|
|
||||||
class InvalidCertificateError(Exception):
|
class InvalidCertificateError(Exception):
|
||||||
pass
|
pass
|
||||||
@ -58,6 +61,9 @@ def log(msg, exc=None, error=False, warning=False):
|
|||||||
prefix = ""
|
prefix = ""
|
||||||
|
|
||||||
output = prefix + msg
|
output = prefix + msg
|
||||||
|
for k, v in LOG_REPLACEMENTS.items():
|
||||||
|
output = output.replace(k, v)
|
||||||
|
|
||||||
if exc:
|
if exc:
|
||||||
_, exc_value, _ = sys.exc_info()
|
_, exc_value, _ = sys.exc_info()
|
||||||
if not getattr(exc, '__traceback__', None) and exc == exc_value:
|
if not getattr(exc, '__traceback__', None) and exc == exc_value:
|
||||||
@ -138,7 +144,7 @@ def new_ssl_key(path=None, key_algo=None, key_size=None):
|
|||||||
key_size=key_size,
|
key_size=key_size,
|
||||||
backend=default_backend()
|
backend=default_backend()
|
||||||
)
|
)
|
||||||
elif key_algo.lower() == 'ec':
|
elif key_algo.lower() == 'ec' or key_algo.lower() == 'ecc':
|
||||||
if not key_size or key_size == 256:
|
if not key_size or key_size == 256:
|
||||||
key_curve = ec.SECP256R1
|
key_curve = ec.SECP256R1
|
||||||
elif key_size == 384:
|
elif key_size == 384:
|
||||||
@ -242,8 +248,7 @@ def get_cert_domains(cert):
|
|||||||
if san_cert:
|
if san_cert:
|
||||||
for d in san_cert.value:
|
for d in san_cert.value:
|
||||||
domains.add(d.value)
|
domains.add(d.value)
|
||||||
# Convert IDNA domain to correct representation and return the list
|
return domains
|
||||||
return [x for x, _ in idna_convert(domains)]
|
|
||||||
|
|
||||||
|
|
||||||
# @brief determine certificate cn
|
# @brief determine certificate cn
|
||||||
@ -257,15 +262,26 @@ def get_cert_valid_until(cert):
|
|||||||
|
|
||||||
|
|
||||||
# @brief convert certificate to PEM format
|
# @brief convert certificate to PEM format
|
||||||
# @param cert certificate object in pyopenssl format
|
# @param cert certificate object or a list thereof
|
||||||
# @return the certificate in PEM format
|
# @return the certificate in PEM format
|
||||||
def convert_cert_to_pem_str(cert):
|
def convert_cert_to_pem_str(cert):
|
||||||
return cert.public_bytes(serialization.Encoding.PEM).decode('utf8')
|
if not isinstance(cert, list):
|
||||||
|
cert = [cert]
|
||||||
|
result = list()
|
||||||
|
for data in cert:
|
||||||
|
result.append(data.public_bytes(serialization.Encoding.PEM).decode('utf8'))
|
||||||
|
return '\n'.join(result)
|
||||||
|
|
||||||
|
|
||||||
# @brief load a PEM certificate from str
|
# @brief load a PEM certificate from str
|
||||||
|
# @return a certificate object or a list of objects if multiple are in the string
|
||||||
def convert_pem_str_to_cert(certdata):
|
def convert_pem_str_to_cert(certdata):
|
||||||
return x509.load_pem_x509_certificate(certdata.encode('utf8'), default_backend())
|
certs = re.findall(r'(-----BEGIN CERTIFICATE-----\n[^\-]+\n-----END CERTIFICATE-----)',
|
||||||
|
certdata, re.DOTALL)
|
||||||
|
result = list()
|
||||||
|
for data in certs:
|
||||||
|
result.append(x509.load_pem_x509_certificate(data.encode('utf8'), default_backend()))
|
||||||
|
return result[0] if len(result) == 1 else result
|
||||||
|
|
||||||
|
|
||||||
# @brief serialize cert/csr to DER bytes
|
# @brief serialize cert/csr to DER bytes
|
||||||
@ -373,26 +389,19 @@ def target_is_current(target, file):
|
|||||||
return target_date >= crt_date
|
return target_date >= crt_date
|
||||||
|
|
||||||
|
|
||||||
# @brief convert domain list to idna representation (if applicable
|
# @brief convert domain to idna representation (if applicable
|
||||||
def idna_convert(domainlist):
|
def idna_convert(domain):
|
||||||
if any(ord(c) >= 128 for c in ''.join(domainlist)):
|
|
||||||
try:
|
try:
|
||||||
domaintranslation = list()
|
|
||||||
for domain in domainlist:
|
|
||||||
if any(ord(c) >= 128 for c in domain):
|
if any(ord(c) >= 128 for c in domain):
|
||||||
# Translate IDNA domain name from a unicode domain (handle wildcards separately)
|
# Translate IDNA domain name from a unicode domain (handle wildcards separately)
|
||||||
if domain.startswith('*.'):
|
if domain.startswith('*.'):
|
||||||
idna_domain = "*.{}".format(domain[2:].encode('idna').decode('ascii'))
|
idna_domain = "*.{}".format(domain[2:].encode('idna').decode('ascii'))
|
||||||
else:
|
else:
|
||||||
idna_domain = domain.encode('idna').decode('ascii')
|
idna_domain = domain.encode('idna').decode('ascii')
|
||||||
result = idna_domain, domain
|
return idna_domain
|
||||||
else:
|
|
||||||
result = domain, domain
|
|
||||||
domaintranslation.append(result)
|
|
||||||
return domaintranslation
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log("Unicode domain(s) found but IDNA names could not be translated due to error: {}".format(e), error=True)
|
log("Unicode domain(s) found but IDNA names could not be translated due to error: {}".format(e), error=True)
|
||||||
return [(x, x) for x in domainlist]
|
return domain
|
||||||
|
|
||||||
|
|
||||||
# @brief validate the OCSP status for a given certificate by the given issuer
|
# @brief validate the OCSP status for a given certificate by the given issuer
|
||||||
@ -411,6 +420,9 @@ def is_ocsp_valid(cert, issuer, hash_algo):
|
|||||||
log("Invalid hash algorithm '{}' used for OCSP validation. Validation ignored.".format(hash_algo), warning=True)
|
log("Invalid hash algorithm '{}' used for OCSP validation. Validation ignored.".format(hash_algo), warning=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if isinstance(issuer, list):
|
||||||
|
issuer = issuer[0] # First certificate in the CA chain is the immediate issuer
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ocsp_urls = []
|
ocsp_urls = []
|
||||||
aia = cert.extensions.get_extension_for_oid(ExtensionOID.AUTHORITY_INFORMATION_ACCESS)
|
aia = cert.extensions.get_extension_for_oid(ExtensionOID.AUTHORITY_INFORMATION_ACCESS)
|
||||||
@ -420,7 +432,7 @@ def is_ocsp_valid(cert, issuer, hash_algo):
|
|||||||
|
|
||||||
# This is a bit of a hack due to validation problems within cryptography (TODO: Check if this is still true)
|
# This is a bit of a hack due to validation problems within cryptography (TODO: Check if this is still true)
|
||||||
# Correct replacement: ocsprequest = ocsp.OCSPRequestBuilder().add_certificate(cert, issuer, algorithm).build()
|
# Correct replacement: ocsprequest = ocsp.OCSPRequestBuilder().add_certificate(cert, issuer, algorithm).build()
|
||||||
ocsprequest = ocsp.OCSPRequestBuilder((cert, issuer, algorithm)).build()
|
ocsprequest = ocsp.OCSPRequestBuilder((cert, issuer, (algorithm)())).build()
|
||||||
ocsprequestdata = ocsprequest.public_bytes(serialization.Encoding.DER)
|
ocsprequestdata = ocsprequest.public_bytes(serialization.Encoding.DER)
|
||||||
for ocsp_url in ocsp_urls:
|
for ocsp_url in ocsp_urls:
|
||||||
response = get_url(ocsp_url,
|
response = get_url(ocsp_url,
|
||||||
|
@ -68,3 +68,12 @@ mail.example.com smtp.example.com webmail.example.net *.intra.example.com:
|
|||||||
perm: '400'
|
perm: '400'
|
||||||
format: crt,ca
|
format: crt,ca
|
||||||
action: '/etc/init.d/postfix reload'
|
action: '/etc/init.d/postfix reload'
|
||||||
|
|
||||||
|
# this will use a different authority for the following set of domains (buypass.com in this example)
|
||||||
|
buypass-example.com *.buypass-example.com:
|
||||||
|
- authority: 'https://api.buypass.com/acme' # Removed trailing /directory from buypass docs for API endpoint
|
||||||
|
mode: dns.nsupdate
|
||||||
|
nsupdate_keyname: buypass
|
||||||
|
nsupdate_keyvalue: Test1234512359==
|
||||||
|
nsupdate_keyalgorithm: HMAC-MD5.SIG-ALG.REG.INT
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
1.0.3
|
1.0.5
|
||||||
|
Loading…
Reference in New Issue
Block a user