ACME: use Cryptography (if a new enough version is available) instead of OpenSSL (#42170)
* Collecting PEM -> DER conversions. * Using cryptography instead of OpenSSL binary in some situations. * Moving key-to-disk writing for key content to parse_account_key. * Rename parse_account_key -> parse_key. * Move OpenSSL specific code for key parsing and request signing into global functions. * Also using cryptography for key parsing and request signing. * Remove assert statements. * Fixing handling of key contents for cryptography code path. * Allow to disable the use of cryptography. * Updating documentation. * 1.5 seems to work as well (earlier versions don't have EC sign function). Making Python 2.x adjustments. * Changing option to select_crypto_backend. * Python 2.6 compatibility. * Trying to test both backends separately for acme_account. * Also testing both backends separately for acme_certificate and acme_certificate_revoke. * Adding changelog entry which informs about select_crypto_backend option in case autodetect fails. * Fixing YAML.
This commit is contained in:
parent
7f41f0168a
commit
aef16ee195
13 changed files with 1031 additions and 671 deletions
8
changelogs/fragments/42170-acme-backend-selection.yaml
Normal file
8
changelogs/fragments/42170-acme-backend-selection.yaml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
minor_changes:
|
||||||
|
- "The acme_account and acme_certificate modules now support two backends:
|
||||||
|
the Python cryptograpy module or the OpenSSL binary. By default, the
|
||||||
|
modules detect if a new enough cryptography module is available and
|
||||||
|
use it, with the OpenSSL binary being a fallback. If the detection
|
||||||
|
fails for some reason, the OpenSSL binary backend can be explicitly
|
||||||
|
selected by setting select_crypto_backend to openssl."
|
|
@ -17,17 +17,38 @@ __metaclass__ = type
|
||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import copy
|
import copy
|
||||||
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
from ansible.module_utils._text import to_native, to_text, to_bytes
|
||||||
from ansible.module_utils.urls import fetch_url as _fetch_url
|
from ansible.module_utils.urls import fetch_url as _fetch_url
|
||||||
|
|
||||||
|
try:
|
||||||
|
import cryptography
|
||||||
|
import cryptography.hazmat.backends
|
||||||
|
import cryptography.hazmat.primitives.serialization
|
||||||
|
import cryptography.hazmat.primitives.asymmetric.rsa
|
||||||
|
import cryptography.hazmat.primitives.asymmetric.ec
|
||||||
|
import cryptography.hazmat.primitives.asymmetric.padding
|
||||||
|
import cryptography.hazmat.primitives.hashes
|
||||||
|
import cryptography.hazmat.primitives.asymmetric.utils
|
||||||
|
import cryptography.x509
|
||||||
|
import cryptography.x509.oid
|
||||||
|
from distutils.version import LooseVersion
|
||||||
|
CRYPTOGRAPHY_VERSION = cryptography.__version__
|
||||||
|
HAS_CURRENT_CRYPTOGRAPHY = (LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion('1.5'))
|
||||||
|
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||||
|
_cryptography_backend = cryptography.hazmat.backends.default_backend()
|
||||||
|
except Exception as _:
|
||||||
|
HAS_CURRENT_CRYPTOGRAPHY = False
|
||||||
|
|
||||||
|
|
||||||
class ModuleFailException(Exception):
|
class ModuleFailException(Exception):
|
||||||
'''
|
'''
|
||||||
|
@ -83,6 +104,14 @@ def simple_get(module, url):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def read_file(fn, mode='b'):
|
||||||
|
try:
|
||||||
|
with open(fn, 'r' + mode) as f:
|
||||||
|
return f.read()
|
||||||
|
except Exception as e:
|
||||||
|
raise ModuleFailException('Error while reading file "{0}": {1}'.format(fn, e))
|
||||||
|
|
||||||
|
|
||||||
# function source: network/basics/uri.py
|
# function source: network/basics/uri.py
|
||||||
def write_file(module, dest, content):
|
def write_file(module, dest, content):
|
||||||
'''
|
'''
|
||||||
|
@ -141,6 +170,296 @@ def write_file(module, dest, content):
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def pem_to_der(pem_filename):
|
||||||
|
'''
|
||||||
|
Load PEM file, and convert to DER.
|
||||||
|
|
||||||
|
If PEM contains multiple entities, the first entity will be used.
|
||||||
|
'''
|
||||||
|
certificate_lines = []
|
||||||
|
try:
|
||||||
|
with open(pem_filename, "rt") as f:
|
||||||
|
header_line_count = 0
|
||||||
|
for line in f:
|
||||||
|
if line.startswith('-----'):
|
||||||
|
header_line_count += 1
|
||||||
|
if header_line_count == 2:
|
||||||
|
# If certificate file contains other certs appended
|
||||||
|
# (like intermediate certificates), ignore these.
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
certificate_lines.append(line.strip())
|
||||||
|
except Exception as err:
|
||||||
|
raise ModuleFailException("cannot load PEM file {0}: {1}".format(pem_filename, to_native(err)), exception=traceback.format_exc())
|
||||||
|
return base64.b64decode(''.join(certificate_lines))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_key_openssl(openssl_binary, module, key_file=None, key_content=None):
|
||||||
|
'''
|
||||||
|
Parses an RSA or Elliptic Curve key file in PEM format and returns a pair
|
||||||
|
(error, key_data).
|
||||||
|
'''
|
||||||
|
# If key_file isn't given, but key_content, write that to a temporary file
|
||||||
|
if key_file is None:
|
||||||
|
fd, tmpsrc = tempfile.mkstemp()
|
||||||
|
module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
|
||||||
|
f = os.fdopen(fd, 'wb')
|
||||||
|
try:
|
||||||
|
f.write(key_content.encode('utf-8'))
|
||||||
|
key_file = tmpsrc
|
||||||
|
except Exception as err:
|
||||||
|
try:
|
||||||
|
f.close()
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
|
||||||
|
f.close()
|
||||||
|
# Parse key
|
||||||
|
account_key_type = None
|
||||||
|
with open(key_file, "rt") as f:
|
||||||
|
for line in f:
|
||||||
|
m = re.match(r"^\s*-{5,}BEGIN\s+(EC|RSA)\s+PRIVATE\s+KEY-{5,}\s*$", line)
|
||||||
|
if m is not None:
|
||||||
|
account_key_type = m.group(1).lower()
|
||||||
|
break
|
||||||
|
if account_key_type is None:
|
||||||
|
# This happens for example if openssl_privatekey created this key
|
||||||
|
# (as opposed to the OpenSSL binary). For now, we assume this is
|
||||||
|
# an RSA key.
|
||||||
|
# FIXME: add some kind of auto-detection
|
||||||
|
account_key_type = "rsa"
|
||||||
|
if account_key_type not in ("rsa", "ec"):
|
||||||
|
return 'unknown key type "%s"' % account_key_type, {}
|
||||||
|
|
||||||
|
openssl_keydump_cmd = [openssl_binary, account_key_type, "-in", key_file, "-noout", "-text"]
|
||||||
|
dummy, out, dummy = module.run_command(openssl_keydump_cmd, check_rc=True)
|
||||||
|
|
||||||
|
if account_key_type == 'rsa':
|
||||||
|
pub_hex, pub_exp = re.search(
|
||||||
|
r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)",
|
||||||
|
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL).groups()
|
||||||
|
pub_exp = "{0:x}".format(int(pub_exp))
|
||||||
|
if len(pub_exp) % 2:
|
||||||
|
pub_exp = "0{0}".format(pub_exp)
|
||||||
|
|
||||||
|
return None, {
|
||||||
|
'key_file': key_file,
|
||||||
|
'type': 'rsa',
|
||||||
|
'alg': 'RS256',
|
||||||
|
'jwk': {
|
||||||
|
"kty": "RSA",
|
||||||
|
"e": nopad_b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
|
||||||
|
"n": nopad_b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
|
||||||
|
},
|
||||||
|
'hash': 'sha256',
|
||||||
|
}
|
||||||
|
elif account_key_type == 'ec':
|
||||||
|
pub_data = re.search(
|
||||||
|
r"pub:\s*\n\s+04:([a-f0-9\:\s]+?)\nASN1 OID: (\S+)(?:\nNIST CURVE: (\S+))?",
|
||||||
|
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL)
|
||||||
|
if pub_data is None:
|
||||||
|
return 'cannot parse elliptic curve key', {}
|
||||||
|
pub_hex = binascii.unhexlify(re.sub(r"(\s|:)", "", pub_data.group(1)).encode("utf-8"))
|
||||||
|
asn1_oid_curve = pub_data.group(2).lower()
|
||||||
|
nist_curve = pub_data.group(3).lower() if pub_data.group(3) else None
|
||||||
|
if asn1_oid_curve == 'prime256v1' or nist_curve == 'p-256':
|
||||||
|
bits = 256
|
||||||
|
alg = 'ES256'
|
||||||
|
hash = 'sha256'
|
||||||
|
point_size = 32
|
||||||
|
curve = 'P-256'
|
||||||
|
elif asn1_oid_curve == 'secp384r1' or nist_curve == 'p-384':
|
||||||
|
bits = 384
|
||||||
|
alg = 'ES384'
|
||||||
|
hash = 'sha384'
|
||||||
|
point_size = 48
|
||||||
|
curve = 'P-384'
|
||||||
|
elif asn1_oid_curve == 'secp521r1' or nist_curve == 'p-521':
|
||||||
|
# Not yet supported on Let's Encrypt side, see
|
||||||
|
# https://github.com/letsencrypt/boulder/issues/2217
|
||||||
|
bits = 521
|
||||||
|
alg = 'ES512'
|
||||||
|
hash = 'sha512'
|
||||||
|
point_size = 66
|
||||||
|
curve = 'P-521'
|
||||||
|
else:
|
||||||
|
return 'unknown elliptic curve: %s / %s' % (asn1_oid_curve, nist_curve), {}
|
||||||
|
bytes = (bits + 7) // 8
|
||||||
|
if len(pub_hex) != 2 * bytes:
|
||||||
|
return 'bad elliptic curve point (%s / %s)' % (asn1_oid_curve, nist_curve), {}
|
||||||
|
return None, {
|
||||||
|
'key_file': key_file,
|
||||||
|
'type': 'ec',
|
||||||
|
'alg': alg,
|
||||||
|
'jwk': {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": curve,
|
||||||
|
"x": nopad_b64(pub_hex[:bytes]),
|
||||||
|
"y": nopad_b64(pub_hex[bytes:]),
|
||||||
|
},
|
||||||
|
'hash': hash,
|
||||||
|
'point_size': point_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _sign_request_openssl(openssl_binary, module, payload64, protected64, key_data):
|
||||||
|
openssl_sign_cmd = [openssl_binary, "dgst", "-{0}".format(key_data['hash']), "-sign", key_data['key_file']]
|
||||||
|
sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
|
||||||
|
dummy, out, dummy = module.run_command(openssl_sign_cmd, data=sign_payload, check_rc=True, binary_data=True)
|
||||||
|
|
||||||
|
if key_data['type'] == 'ec':
|
||||||
|
dummy, der_out, dummy = module.run_command(
|
||||||
|
[openssl_binary, "asn1parse", "-inform", "DER"],
|
||||||
|
data=out, binary_data=True)
|
||||||
|
expected_len = 2 * key_data['point_size']
|
||||||
|
sig = re.findall(
|
||||||
|
r"prim:\s+INTEGER\s+:([0-9A-F]{1,%s})\n" % expected_len,
|
||||||
|
to_text(der_out, errors='surrogate_or_strict'))
|
||||||
|
if len(sig) != 2:
|
||||||
|
raise ModuleFailException(
|
||||||
|
"failed to generate Elliptic Curve signature; cannot parse DER output: {0}".format(
|
||||||
|
to_text(der_out, errors='surrogate_or_strict')))
|
||||||
|
sig[0] = (expected_len - len(sig[0])) * '0' + sig[0]
|
||||||
|
sig[1] = (expected_len - len(sig[1])) * '0' + sig[1]
|
||||||
|
out = binascii.unhexlify(sig[0]) + binascii.unhexlify(sig[1])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"protected": protected64,
|
||||||
|
"payload": payload64,
|
||||||
|
"signature": nopad_b64(to_bytes(out)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
# Python 3 (and newer)
|
||||||
|
def _count_bytes(n):
|
||||||
|
return (n.bit_length() + 7) // 8 if n > 0 else 0
|
||||||
|
|
||||||
|
def _convert_int_to_bytes(count, no):
|
||||||
|
return no.to_bytes(count, byteorder='big')
|
||||||
|
|
||||||
|
def _pad_hex(n, digits):
|
||||||
|
res = hex(n)[2:]
|
||||||
|
if len(res) < digits:
|
||||||
|
res = '0' * (digits - len(res)) + res
|
||||||
|
return res
|
||||||
|
else:
|
||||||
|
# Python 2
|
||||||
|
def _count_bytes(n):
|
||||||
|
if n <= 0:
|
||||||
|
return 0
|
||||||
|
h = '%x' % n
|
||||||
|
return (len(h) + 1) // 2
|
||||||
|
|
||||||
|
def _convert_int_to_bytes(count, n):
|
||||||
|
h = '%x' % n
|
||||||
|
if len(h) > 2 * count:
|
||||||
|
raise Exception('Number {1} needs more than {0} bytes!'.format(count, n))
|
||||||
|
return ('0' * (2 * count - len(h)) + h).decode('hex')
|
||||||
|
|
||||||
|
def _pad_hex(n, digits):
|
||||||
|
h = '%x' % n
|
||||||
|
if len(h) < digits:
|
||||||
|
h = '0' * (digits - len(h)) + h
|
||||||
|
return h
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_key_cryptography(module, key_file=None, key_content=None):
|
||||||
|
'''
|
||||||
|
Parses an RSA or Elliptic Curve key file in PEM format and returns a pair
|
||||||
|
(error, key_data).
|
||||||
|
'''
|
||||||
|
# If key_content isn't given, read key_file
|
||||||
|
if key_content is None:
|
||||||
|
key_content = read_file(key_file)
|
||||||
|
else:
|
||||||
|
key_content = to_bytes(key_content)
|
||||||
|
# Parse key
|
||||||
|
try:
|
||||||
|
key = cryptography.hazmat.primitives.serialization.load_pem_private_key(key_content, password=None, backend=_cryptography_backend)
|
||||||
|
except Exception as e:
|
||||||
|
return 'error while loading key: {0}'.format(e), None
|
||||||
|
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||||
|
pk = key.public_key().public_numbers()
|
||||||
|
return None, {
|
||||||
|
'key_obj': key,
|
||||||
|
'type': 'rsa',
|
||||||
|
'alg': 'RS256',
|
||||||
|
'jwk': {
|
||||||
|
"kty": "RSA",
|
||||||
|
"e": nopad_b64(_convert_int_to_bytes(_count_bytes(pk.e), pk.e)),
|
||||||
|
"n": nopad_b64(_convert_int_to_bytes(_count_bytes(pk.n), pk.n)),
|
||||||
|
},
|
||||||
|
'hash': 'sha256',
|
||||||
|
}
|
||||||
|
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||||
|
pk = key.public_key().public_numbers()
|
||||||
|
if pk.curve.name == 'secp256r1':
|
||||||
|
bits = 256
|
||||||
|
alg = 'ES256'
|
||||||
|
hash = 'sha256'
|
||||||
|
point_size = 32
|
||||||
|
curve = 'P-256'
|
||||||
|
elif pk.curve.name == 'secp384r1':
|
||||||
|
bits = 384
|
||||||
|
alg = 'ES384'
|
||||||
|
hash = 'sha384'
|
||||||
|
point_size = 48
|
||||||
|
curve = 'P-384'
|
||||||
|
elif pk.curve.name == 'secp521r1':
|
||||||
|
# Not yet supported on Let's Encrypt side, see
|
||||||
|
# https://github.com/letsencrypt/boulder/issues/2217
|
||||||
|
bits = 521
|
||||||
|
alg = 'ES512'
|
||||||
|
hash = 'sha512'
|
||||||
|
point_size = 66
|
||||||
|
curve = 'P-521'
|
||||||
|
else:
|
||||||
|
return 'unknown elliptic curve: {0}'.format(pk.curve.name), {}
|
||||||
|
bytes = (bits + 7) // 8
|
||||||
|
return None, {
|
||||||
|
'key_obj': key,
|
||||||
|
'type': 'ec',
|
||||||
|
'alg': alg,
|
||||||
|
'jwk': {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": curve,
|
||||||
|
"x": nopad_b64(_convert_int_to_bytes(bytes, pk.x)),
|
||||||
|
"y": nopad_b64(_convert_int_to_bytes(bytes, pk.y)),
|
||||||
|
},
|
||||||
|
'hash': hash,
|
||||||
|
'point_size': point_size,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return 'unknown key type "{0}"'.format(type(key)), {}
|
||||||
|
|
||||||
|
|
||||||
|
def _sign_request_cryptography(module, payload64, protected64, key_data):
|
||||||
|
sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
|
||||||
|
if isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||||
|
padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15()
|
||||||
|
hash = cryptography.hazmat.primitives.hashes.SHA256()
|
||||||
|
signature = key_data['key_obj'].sign(sign_payload, padding, hash)
|
||||||
|
elif isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||||
|
if key_data['hash'] == 'sha256':
|
||||||
|
hash = cryptography.hazmat.primitives.hashes.SHA256
|
||||||
|
elif key_data['hash'] == 'sha384':
|
||||||
|
hash = cryptography.hazmat.primitives.hashes.SHA384
|
||||||
|
elif key_data['hash'] == 'sha512':
|
||||||
|
hash = cryptography.hazmat.primitives.hashes.SHA512
|
||||||
|
ecdsa = cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hash())
|
||||||
|
r, s = cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature(key_data['key_obj'].sign(sign_payload, ecdsa))
|
||||||
|
rr = _pad_hex(r, 2 * key_data['point_size'])
|
||||||
|
ss = _pad_hex(s, 2 * key_data['point_size'])
|
||||||
|
signature = binascii.unhexlify(rr) + binascii.unhexlify(ss)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"protected": protected64,
|
||||||
|
"payload": payload64,
|
||||||
|
"signature": nopad_b64(signature),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ACMEDirectory(object):
|
class ACMEDirectory(object):
|
||||||
'''
|
'''
|
||||||
The ACME server directory. Gives access to the available resources,
|
The ACME server directory. Gives access to the available resources,
|
||||||
|
@ -199,24 +518,8 @@ class ACMEAccount(object):
|
||||||
|
|
||||||
self._openssl_bin = module.get_bin_path('openssl', True)
|
self._openssl_bin = module.get_bin_path('openssl', True)
|
||||||
|
|
||||||
# Create a key file from content, key (path) and key content are mutually exclusive
|
if self.key is not None or self.key_content is not None:
|
||||||
if self.key_content is not None:
|
error, self.key_data = self.parse_key(self.key, self.key_content)
|
||||||
fd, tmpsrc = tempfile.mkstemp()
|
|
||||||
module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
|
|
||||||
f = os.fdopen(fd, 'wb')
|
|
||||||
try:
|
|
||||||
f.write(self.key_content.encode('utf-8'))
|
|
||||||
self.key = tmpsrc
|
|
||||||
except Exception as err:
|
|
||||||
try:
|
|
||||||
f.close()
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
if self.key is not None:
|
|
||||||
error, self.key_data = self.parse_account_key(self.key)
|
|
||||||
if error:
|
if error:
|
||||||
raise ModuleFailException("error while parsing account key: %s" % error)
|
raise ModuleFailException("error while parsing account key: %s" % error)
|
||||||
self.jwk = self.key_data['jwk']
|
self.jwk = self.key_data['jwk']
|
||||||
|
@ -234,136 +537,37 @@ class ACMEAccount(object):
|
||||||
thumbprint = nopad_b64(hashlib.sha256(accountkey_json.encode('utf8')).digest())
|
thumbprint = nopad_b64(hashlib.sha256(accountkey_json.encode('utf8')).digest())
|
||||||
return "{0}.{1}".format(token, thumbprint)
|
return "{0}.{1}".format(token, thumbprint)
|
||||||
|
|
||||||
def parse_account_key(self, key):
|
def parse_key(self, key_file=None, key_content=None):
|
||||||
'''
|
'''
|
||||||
Parses an RSA or Elliptic Curve key file in PEM format and returns a pair
|
Parses an RSA or Elliptic Curve key file in PEM format and returns a pair
|
||||||
(error, key_data).
|
(error, key_data).
|
||||||
'''
|
'''
|
||||||
account_key_type = None
|
if key_file is None and key_content is None:
|
||||||
with open(key, "rt") as f:
|
raise AssertionError('One of key_file and key_content must be specified!')
|
||||||
for line in f:
|
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||||
m = re.match(r"^\s*-{5,}BEGIN\s+(EC|RSA)\s+PRIVATE\s+KEY-{5,}\s*$", line)
|
return _parse_key_cryptography(self.module, key_file, key_content)
|
||||||
if m is not None:
|
|
||||||
account_key_type = m.group(1).lower()
|
|
||||||
break
|
|
||||||
if account_key_type is None:
|
|
||||||
# This happens for example if openssl_privatekey created this key
|
|
||||||
# (as opposed to the OpenSSL binary). For now, we assume this is
|
|
||||||
# an RSA key.
|
|
||||||
# FIXME: add some kind of auto-detection
|
|
||||||
account_key_type = "rsa"
|
|
||||||
if account_key_type not in ("rsa", "ec"):
|
|
||||||
return 'unknown key type "%s"' % account_key_type, {}
|
|
||||||
|
|
||||||
openssl_keydump_cmd = [self._openssl_bin, account_key_type, "-in", key, "-noout", "-text"]
|
|
||||||
dummy, out, dummy = self.module.run_command(openssl_keydump_cmd, check_rc=True)
|
|
||||||
|
|
||||||
if account_key_type == 'rsa':
|
|
||||||
pub_hex, pub_exp = re.search(
|
|
||||||
r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)",
|
|
||||||
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL).groups()
|
|
||||||
pub_exp = "{0:x}".format(int(pub_exp))
|
|
||||||
if len(pub_exp) % 2:
|
|
||||||
pub_exp = "0{0}".format(pub_exp)
|
|
||||||
|
|
||||||
return None, {
|
|
||||||
'type': 'rsa',
|
|
||||||
'alg': 'RS256',
|
|
||||||
'jwk': {
|
|
||||||
"kty": "RSA",
|
|
||||||
"e": nopad_b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
|
|
||||||
"n": nopad_b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
|
|
||||||
},
|
|
||||||
'hash': 'sha256',
|
|
||||||
}
|
|
||||||
elif account_key_type == 'ec':
|
|
||||||
pub_data = re.search(
|
|
||||||
r"pub:\s*\n\s+04:([a-f0-9\:\s]+?)\nASN1 OID: (\S+)(?:\nNIST CURVE: (\S+))?",
|
|
||||||
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL)
|
|
||||||
if pub_data is None:
|
|
||||||
return 'cannot parse elliptic curve key', {}
|
|
||||||
pub_hex = binascii.unhexlify(re.sub(r"(\s|:)", "", pub_data.group(1)).encode("utf-8"))
|
|
||||||
asn1_oid_curve = pub_data.group(2).lower()
|
|
||||||
nist_curve = pub_data.group(3).lower() if pub_data.group(3) else None
|
|
||||||
if asn1_oid_curve == 'prime256v1' or nist_curve == 'p-256':
|
|
||||||
bits = 256
|
|
||||||
alg = 'ES256'
|
|
||||||
hash = 'sha256'
|
|
||||||
point_size = 32
|
|
||||||
curve = 'P-256'
|
|
||||||
elif asn1_oid_curve == 'secp384r1' or nist_curve == 'p-384':
|
|
||||||
bits = 384
|
|
||||||
alg = 'ES384'
|
|
||||||
hash = 'sha384'
|
|
||||||
point_size = 48
|
|
||||||
curve = 'P-384'
|
|
||||||
elif asn1_oid_curve == 'secp521r1' or nist_curve == 'p-521':
|
|
||||||
# Not yet supported on Let's Encrypt side, see
|
|
||||||
# https://github.com/letsencrypt/boulder/issues/2217
|
|
||||||
bits = 521
|
|
||||||
alg = 'ES512'
|
|
||||||
hash = 'sha512'
|
|
||||||
point_size = 66
|
|
||||||
curve = 'P-521'
|
|
||||||
else:
|
else:
|
||||||
return 'unknown elliptic curve: %s / %s' % (asn1_oid_curve, nist_curve), {}
|
return _parse_key_openssl(self._openssl_bin, self.module, key_file, key_content)
|
||||||
bytes = (bits + 7) // 8
|
|
||||||
if len(pub_hex) != 2 * bytes:
|
|
||||||
return 'bad elliptic curve point (%s / %s)' % (asn1_oid_curve, nist_curve), {}
|
|
||||||
return None, {
|
|
||||||
'type': 'ec',
|
|
||||||
'alg': alg,
|
|
||||||
'jwk': {
|
|
||||||
"kty": "EC",
|
|
||||||
"crv": curve,
|
|
||||||
"x": nopad_b64(pub_hex[:bytes]),
|
|
||||||
"y": nopad_b64(pub_hex[bytes:]),
|
|
||||||
},
|
|
||||||
'hash': hash,
|
|
||||||
'point_size': point_size,
|
|
||||||
}
|
|
||||||
|
|
||||||
def sign_request(self, protected, payload, key_data, key):
|
def sign_request(self, protected, payload, key_data):
|
||||||
try:
|
try:
|
||||||
payload64 = nopad_b64(self.module.jsonify(payload).encode('utf8'))
|
payload64 = nopad_b64(self.module.jsonify(payload).encode('utf8'))
|
||||||
protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8'))
|
protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ModuleFailException("Failed to encode payload / headers as JSON: {0}".format(e))
|
raise ModuleFailException("Failed to encode payload / headers as JSON: {0}".format(e))
|
||||||
|
|
||||||
openssl_sign_cmd = [self._openssl_bin, "dgst", "-{0}".format(key_data['hash']), "-sign", key]
|
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||||
sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
|
return _sign_request_cryptography(self.module, payload64, protected64, key_data)
|
||||||
dummy, out, dummy = self.module.run_command(openssl_sign_cmd, data=sign_payload, check_rc=True, binary_data=True)
|
else:
|
||||||
|
return _sign_request_openssl(self._openssl_bin, self.module, payload64, protected64, key_data)
|
||||||
|
|
||||||
if key_data['type'] == 'ec':
|
def send_signed_request(self, url, payload, key_data=None, jws_header=None):
|
||||||
dummy, der_out, dummy = self.module.run_command(
|
|
||||||
[self._openssl_bin, "asn1parse", "-inform", "DER"],
|
|
||||||
data=out, binary_data=True)
|
|
||||||
expected_len = 2 * key_data['point_size']
|
|
||||||
sig = re.findall(
|
|
||||||
r"prim:\s+INTEGER\s+:([0-9A-F]{1,%s})\n" % expected_len,
|
|
||||||
to_text(der_out, errors='surrogate_or_strict'))
|
|
||||||
if len(sig) != 2:
|
|
||||||
raise ModuleFailException(
|
|
||||||
"failed to generate Elliptic Curve signature; cannot parse DER output: {0}".format(
|
|
||||||
to_text(der_out, errors='surrogate_or_strict')))
|
|
||||||
sig[0] = (expected_len - len(sig[0])) * '0' + sig[0]
|
|
||||||
sig[1] = (expected_len - len(sig[1])) * '0' + sig[1]
|
|
||||||
out = binascii.unhexlify(sig[0]) + binascii.unhexlify(sig[1])
|
|
||||||
|
|
||||||
return {
|
|
||||||
"protected": protected64,
|
|
||||||
"payload": payload64,
|
|
||||||
"signature": nopad_b64(to_bytes(out)),
|
|
||||||
}
|
|
||||||
|
|
||||||
def send_signed_request(self, url, payload, key_data=None, key=None, jws_header=None):
|
|
||||||
'''
|
'''
|
||||||
Sends a JWS signed HTTP POST request to the ACME server and returns
|
Sends a JWS signed HTTP POST request to the ACME server and returns
|
||||||
the response as dictionary
|
the response as dictionary
|
||||||
https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-6.2
|
https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-6.2
|
||||||
'''
|
'''
|
||||||
key_data = key_data or self.key_data
|
key_data = key_data or self.key_data
|
||||||
key = key or self.key
|
|
||||||
jws_header = jws_header or self.jws_header
|
jws_header = jws_header or self.jws_header
|
||||||
failed_tries = 0
|
failed_tries = 0
|
||||||
while True:
|
while True:
|
||||||
|
@ -372,7 +576,7 @@ class ACMEAccount(object):
|
||||||
if self.version != 1:
|
if self.version != 1:
|
||||||
protected["url"] = url
|
protected["url"] = url
|
||||||
|
|
||||||
data = self.sign_request(protected, payload, key_data, key)
|
data = self.sign_request(protected, payload, key_data)
|
||||||
if self.version == 1:
|
if self.version == 1:
|
||||||
data["header"] = jws_header
|
data["header"] = jws_header
|
||||||
data = self.module.jsonify(data)
|
data = self.module.jsonify(data)
|
||||||
|
@ -530,3 +734,67 @@ class ACMEAccount(object):
|
||||||
result, dummy = self.send_signed_request(self.uri, upd_reg)
|
result, dummy = self.send_signed_request(self.uri, upd_reg)
|
||||||
changed = True
|
changed = True
|
||||||
return new_account or changed
|
return new_account or changed
|
||||||
|
|
||||||
|
|
||||||
|
def cryptography_get_csr_domains(module, csr_filename):
|
||||||
|
'''
|
||||||
|
Return a set of requested domains (CN and SANs) for the CSR.
|
||||||
|
'''
|
||||||
|
domains = set([])
|
||||||
|
csr = cryptography.x509.load_pem_x509_csr(read_file(csr_filename), _cryptography_backend)
|
||||||
|
for sub in csr.subject:
|
||||||
|
if sub.oid == cryptography.x509.oid.NameOID.COMMON_NAME:
|
||||||
|
domains.add(sub.value)
|
||||||
|
for extension in csr.extensions:
|
||||||
|
if extension.oid == cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
|
||||||
|
for name in extension.value:
|
||||||
|
if isinstance(name, cryptography.x509.DNSName):
|
||||||
|
domains.add(name.value)
|
||||||
|
return domains
|
||||||
|
|
||||||
|
|
||||||
|
def cryptography_get_cert_days(module, cert_file):
|
||||||
|
'''
|
||||||
|
Return the days the certificate in cert_file remains valid and -1
|
||||||
|
if the file was not found. If cert_file contains more than one
|
||||||
|
certificate, only the first one will be considered.
|
||||||
|
'''
|
||||||
|
if not os.path.exists(cert_file):
|
||||||
|
return -1
|
||||||
|
|
||||||
|
try:
|
||||||
|
cert = cryptography.x509.load_pem_x509_certificate(read_file(cert_file), _cryptography_backend)
|
||||||
|
except Exception as e:
|
||||||
|
raise ModuleFailException('Cannot parse certificate {0}: {1}'.format(cert_file, e))
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
return (cert.not_valid_after - now).days
|
||||||
|
|
||||||
|
|
||||||
|
def set_crypto_backend(module):
|
||||||
|
'''
|
||||||
|
Sets which crypto backend to use (default: auto detection).
|
||||||
|
|
||||||
|
Does not care whether a new enough cryptoraphy is available or not. Must
|
||||||
|
be called before any real stuff is done which might evaluate
|
||||||
|
``HAS_CURRENT_CRYPTOGRAPHY``.
|
||||||
|
'''
|
||||||
|
global HAS_CURRENT_CRYPTOGRAPHY
|
||||||
|
# Choose backend
|
||||||
|
backend = module.params['select_crypto_backend']
|
||||||
|
if backend == 'auto':
|
||||||
|
pass
|
||||||
|
elif backend == 'openssl':
|
||||||
|
HAS_CURRENT_CRYPTOGRAPHY = False
|
||||||
|
elif backend == 'cryptography':
|
||||||
|
try:
|
||||||
|
cryptography.__version__
|
||||||
|
except Exception as _:
|
||||||
|
module.fail_json(msg='Cannot find cryptography module!')
|
||||||
|
HAS_CURRENT_CRYPTOGRAPHY = True
|
||||||
|
else:
|
||||||
|
module.fail_json(msg='Unknown crypto backend "{0}"!'.format(backend))
|
||||||
|
# Inform about choices
|
||||||
|
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||||
|
module.debug('Using cryptography backend (library version {0})'.format(CRYPTOGRAPHY_VERSION))
|
||||||
|
else:
|
||||||
|
module.debug('Using OpenSSL binary backend')
|
||||||
|
|
|
@ -116,15 +116,10 @@ account_uri:
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from ansible.module_utils.acme import (
|
from ansible.module_utils.acme import (
|
||||||
ModuleFailException, ACMEAccount
|
ModuleFailException, ACMEAccount, set_crypto_backend,
|
||||||
)
|
)
|
||||||
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
from ansible.module_utils._text import to_native
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -141,6 +136,7 @@ def main():
|
||||||
contact=dict(required=False, type='list', default=[]),
|
contact=dict(required=False, type='list', default=[]),
|
||||||
new_account_key_src=dict(type='path'),
|
new_account_key_src=dict(type='path'),
|
||||||
new_account_key_content=dict(type='str', no_log=True),
|
new_account_key_content=dict(type='str', no_log=True),
|
||||||
|
select_crypto_backend=dict(required=False, choices=['auto', 'openssl', 'cryptography'], default='auto', type='str'),
|
||||||
),
|
),
|
||||||
required_one_of=(
|
required_one_of=(
|
||||||
['account_key_src', 'account_key_content'],
|
['account_key_src', 'account_key_content'],
|
||||||
|
@ -156,6 +152,7 @@ def main():
|
||||||
),
|
),
|
||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
)
|
)
|
||||||
|
set_crypto_backend(module)
|
||||||
|
|
||||||
if not module.params.get('validate_certs'):
|
if not module.params.get('validate_certs'):
|
||||||
module.warn(warning='Disabling certificate validation for communications with ACME endpoint. ' +
|
module.warn(warning='Disabling certificate validation for communications with ACME endpoint. ' +
|
||||||
|
@ -203,24 +200,11 @@ def main():
|
||||||
raise ModuleFailException(msg='Account does not exist or is deactivated.')
|
raise ModuleFailException(msg='Account does not exist or is deactivated.')
|
||||||
module.exit_json(changed=changed, account_uri=account.uri)
|
module.exit_json(changed=changed, account_uri=account.uri)
|
||||||
elif state == 'changed_key':
|
elif state == 'changed_key':
|
||||||
# Get hold of new account key
|
|
||||||
new_key = module.params.get('new_account_key_src')
|
|
||||||
if new_key is None:
|
|
||||||
fd, tmpsrc = tempfile.mkstemp()
|
|
||||||
module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
|
|
||||||
f = os.fdopen(fd, 'wb')
|
|
||||||
try:
|
|
||||||
f.write(module.params.get('new_account_key_content').encode('utf-8'))
|
|
||||||
new_key = tmpsrc
|
|
||||||
except Exception as err:
|
|
||||||
try:
|
|
||||||
f.close()
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
|
|
||||||
f.close()
|
|
||||||
# Parse new account key
|
# Parse new account key
|
||||||
error, new_key_data = account.parse_account_key(new_key)
|
error, new_key_data = account.parse_key(
|
||||||
|
module.params.get('new_account_key_src'),
|
||||||
|
module.params.get('new_account_key_content')
|
||||||
|
)
|
||||||
if error:
|
if error:
|
||||||
raise ModuleFailException("error while parsing account key: %s" % error)
|
raise ModuleFailException("error while parsing account key: %s" % error)
|
||||||
# Verify that the account exists and has not been deactivated
|
# Verify that the account exists and has not been deactivated
|
||||||
|
@ -249,7 +233,7 @@ def main():
|
||||||
"oldKey": account.jwk, # discussed in https://github.com/ietf-wg-acme/acme/pull/425,
|
"oldKey": account.jwk, # discussed in https://github.com/ietf-wg-acme/acme/pull/425,
|
||||||
# might be required in draft 13
|
# might be required in draft 13
|
||||||
}
|
}
|
||||||
data = account.sign_request(protected, payload, new_key_data, new_key)
|
data = account.sign_request(protected, payload, new_key_data)
|
||||||
# Send request and verify result
|
# Send request and verify result
|
||||||
result, info = account.send_signed_request(url, data)
|
result, info = account.send_signed_request(url, data)
|
||||||
if info['status'] != 200:
|
if info['status'] != 200:
|
||||||
|
|
|
@ -328,7 +328,9 @@ account_uri:
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from ansible.module_utils.acme import (
|
from ansible.module_utils.acme import (
|
||||||
ModuleFailException, fetch_url, write_file, nopad_b64, simple_get, ACMEAccount
|
ModuleFailException, fetch_url, write_file, nopad_b64, simple_get, pem_to_der, ACMEAccount,
|
||||||
|
HAS_CURRENT_CRYPTOGRAPHY, cryptography_get_csr_domains, cryptography_get_cert_days,
|
||||||
|
set_crypto_backend,
|
||||||
)
|
)
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
@ -350,6 +352,8 @@ def get_cert_days(module, cert_file):
|
||||||
if the file was not found. If cert_file contains more than one
|
if the file was not found. If cert_file contains more than one
|
||||||
certificate, only the first one will be considered.
|
certificate, only the first one will be considered.
|
||||||
'''
|
'''
|
||||||
|
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||||
|
return cryptography_get_cert_days(module, cert_file)
|
||||||
if not os.path.exists(cert_file):
|
if not os.path.exists(cert_file):
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
@ -422,6 +426,8 @@ class ACMEClient(object):
|
||||||
'''
|
'''
|
||||||
Parse the CSR and return the list of requested domains
|
Parse the CSR and return the list of requested domains
|
||||||
'''
|
'''
|
||||||
|
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||||
|
return cryptography_get_csr_domains(self.module, self.csr)
|
||||||
openssl_csr_cmd = [self._openssl_bin, "req", "-in", self.csr, "-noout", "-text"]
|
openssl_csr_cmd = [self._openssl_bin, "req", "-in", self.csr, "-noout", "-text"]
|
||||||
dummy, out, dummy = self.module.run_command(openssl_csr_cmd, check_rc=True)
|
dummy, out, dummy = self.module.run_command(openssl_csr_cmd, check_rc=True)
|
||||||
|
|
||||||
|
@ -569,11 +575,9 @@ class ACMEClient(object):
|
||||||
Return the certificate object as dict
|
Return the certificate object as dict
|
||||||
https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4
|
https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4
|
||||||
'''
|
'''
|
||||||
openssl_csr_cmd = [self._openssl_bin, "req", "-in", self.csr, "-outform", "DER"]
|
csr = pem_to_der(self.csr)
|
||||||
dummy, out, dummy = self.module.run_command(openssl_csr_cmd, check_rc=True)
|
|
||||||
|
|
||||||
new_cert = {
|
new_cert = {
|
||||||
"csr": nopad_b64(to_bytes(out)),
|
"csr": nopad_b64(csr),
|
||||||
}
|
}
|
||||||
result, info = self.account.send_signed_request(self.finalize_uri, new_cert)
|
result, info = self.account.send_signed_request(self.finalize_uri, new_cert)
|
||||||
if info['status'] not in [200]:
|
if info['status'] not in [200]:
|
||||||
|
@ -650,12 +654,10 @@ class ACMEClient(object):
|
||||||
Return the certificate object as dict
|
Return the certificate object as dict
|
||||||
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5
|
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5
|
||||||
'''
|
'''
|
||||||
openssl_csr_cmd = [self._openssl_bin, "req", "-in", self.csr, "-outform", "DER"]
|
csr = pem_to_der(self.csr)
|
||||||
dummy, out, dummy = self.module.run_command(openssl_csr_cmd, check_rc=True)
|
|
||||||
|
|
||||||
new_cert = {
|
new_cert = {
|
||||||
"resource": "new-cert",
|
"resource": "new-cert",
|
||||||
"csr": nopad_b64(to_bytes(out)),
|
"csr": nopad_b64(csr),
|
||||||
}
|
}
|
||||||
result, info = self.account.send_signed_request(self.directory['new-cert'], new_cert)
|
result, info = self.account.send_signed_request(self.directory['new-cert'], new_cert)
|
||||||
|
|
||||||
|
@ -883,6 +885,7 @@ def main():
|
||||||
remaining_days=dict(required=False, default=10, type='int'),
|
remaining_days=dict(required=False, default=10, type='int'),
|
||||||
deactivate_authzs=dict(required=False, default=False, type='bool'),
|
deactivate_authzs=dict(required=False, default=False, type='bool'),
|
||||||
force=dict(required=False, default=False, type='bool'),
|
force=dict(required=False, default=False, type='bool'),
|
||||||
|
select_crypto_backend=dict(required=False, choices=['auto', 'openssl', 'cryptography'], default='auto', type='str'),
|
||||||
),
|
),
|
||||||
required_one_of=(
|
required_one_of=(
|
||||||
['account_key_src', 'account_key_content'],
|
['account_key_src', 'account_key_content'],
|
||||||
|
@ -895,6 +898,7 @@ def main():
|
||||||
)
|
)
|
||||||
if module._name == 'letsencrypt':
|
if module._name == 'letsencrypt':
|
||||||
module.deprecate("The 'letsencrypt' module is being renamed 'acme_certificate'", version='2.10')
|
module.deprecate("The 'letsencrypt' module is being renamed 'acme_certificate'", version='2.10')
|
||||||
|
set_crypto_backend(module)
|
||||||
|
|
||||||
# AnsibleModule() changes the locale, so change it back to C because we rely on time.strptime() when parsing certificate dates.
|
# AnsibleModule() changes the locale, so change it back to C because we rely on time.strptime() when parsing certificate dates.
|
||||||
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
|
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
|
||||||
|
|
|
@ -53,6 +53,10 @@ options:
|
||||||
important private key — it can be used to change the account key,
|
important private key — it can be used to change the account key,
|
||||||
or to revoke your certificates without knowing their private keys
|
or to revoke your certificates without knowing their private keys
|
||||||
—, this might not be acceptable."
|
—, this might not be acceptable."
|
||||||
|
- "In case C(cryptography) is used, the content is not written into a
|
||||||
|
temporary file. It can still happen that it is written to disk by
|
||||||
|
Ansible in the process of moving the module with its argument to
|
||||||
|
the node where it is executed."
|
||||||
revoke_reason:
|
revoke_reason:
|
||||||
description:
|
description:
|
||||||
- "One of the revocation reasonCodes defined in
|
- "One of the revocation reasonCodes defined in
|
||||||
|
@ -80,16 +84,10 @@ RETURN = '''
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from ansible.module_utils.acme import (
|
from ansible.module_utils.acme import (
|
||||||
ModuleFailException, ACMEAccount, nopad_b64
|
ModuleFailException, ACMEAccount, nopad_b64, pem_to_der, set_crypto_backend,
|
||||||
)
|
)
|
||||||
|
|
||||||
import base64
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
from ansible.module_utils._text import to_native
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -104,6 +102,7 @@ def main():
|
||||||
private_key_content=dict(type='str', no_log=True),
|
private_key_content=dict(type='str', no_log=True),
|
||||||
certificate=dict(required=True, type='path'),
|
certificate=dict(required=True, type='path'),
|
||||||
revoke_reason=dict(required=False, type='int'),
|
revoke_reason=dict(required=False, type='int'),
|
||||||
|
select_crypto_backend=dict(required=False, choices=['auto', 'openssl', 'cryptography'], default='auto', type='str'),
|
||||||
),
|
),
|
||||||
required_one_of=(
|
required_one_of=(
|
||||||
['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'],
|
['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'],
|
||||||
|
@ -113,6 +112,7 @@ def main():
|
||||||
),
|
),
|
||||||
supports_check_mode=False,
|
supports_check_mode=False,
|
||||||
)
|
)
|
||||||
|
set_crypto_backend(module)
|
||||||
|
|
||||||
if not module.params.get('validate_certs'):
|
if not module.params.get('validate_certs'):
|
||||||
module.warn(warning='Disabling certificate validation for communications with ACME endpoint. ' +
|
module.warn(warning='Disabling certificate validation for communications with ACME endpoint. ' +
|
||||||
|
@ -122,22 +122,8 @@ def main():
|
||||||
try:
|
try:
|
||||||
account = ACMEAccount(module)
|
account = ACMEAccount(module)
|
||||||
# Load certificate
|
# Load certificate
|
||||||
certificate_lines = []
|
certificate = pem_to_der(module.params.get('certificate'))
|
||||||
try:
|
certificate = nopad_b64(certificate)
|
||||||
with open(module.params.get('certificate'), "rt") as f:
|
|
||||||
header_line_count = 0
|
|
||||||
for line in f:
|
|
||||||
if line.startswith('-----'):
|
|
||||||
header_line_count += 1
|
|
||||||
if header_line_count == 2:
|
|
||||||
# If certificate file contains other certs appended
|
|
||||||
# (like intermediate certificates), ignore these.
|
|
||||||
break
|
|
||||||
continue
|
|
||||||
certificate_lines.append(line.strip())
|
|
||||||
except Exception as err:
|
|
||||||
raise ModuleFailException("cannot load certificate file: %s" % to_native(err), exception=traceback.format_exc())
|
|
||||||
certificate = nopad_b64(base64.b64decode(''.join(certificate_lines)))
|
|
||||||
# Construct payload
|
# Construct payload
|
||||||
payload = {
|
payload = {
|
||||||
'certificate': certificate
|
'certificate': certificate
|
||||||
|
@ -152,24 +138,11 @@ def main():
|
||||||
endpoint = account.directory['revokeCert']
|
endpoint = account.directory['revokeCert']
|
||||||
# Get hold of private key (if available) and make sure it comes from disk
|
# Get hold of private key (if available) and make sure it comes from disk
|
||||||
private_key = module.params.get('private_key_src')
|
private_key = module.params.get('private_key_src')
|
||||||
if module.params.get('private_key_content') is not None:
|
private_key_content = module.params.get('private_key_content')
|
||||||
fd, tmpsrc = tempfile.mkstemp()
|
|
||||||
module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
|
|
||||||
f = os.fdopen(fd, 'wb')
|
|
||||||
try:
|
|
||||||
f.write(module.params.get('private_key_content').encode('utf-8'))
|
|
||||||
private_key = tmpsrc
|
|
||||||
except Exception as err:
|
|
||||||
try:
|
|
||||||
f.close()
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
|
|
||||||
f.close()
|
|
||||||
# Revoke certificate
|
# Revoke certificate
|
||||||
if private_key:
|
if private_key or private_key_content:
|
||||||
# Step 1: load and parse private key
|
# Step 1: load and parse private key
|
||||||
error, private_key_data = account.parse_account_key(private_key)
|
error, private_key_data = account.parse_key(private_key, private_key_content)
|
||||||
if error:
|
if error:
|
||||||
raise ModuleFailException("error while parsing private key: %s" % error)
|
raise ModuleFailException("error while parsing private key: %s" % error)
|
||||||
# Step 2: sign revokation request with private key
|
# Step 2: sign revokation request with private key
|
||||||
|
@ -177,8 +150,7 @@ def main():
|
||||||
"alg": private_key_data['alg'],
|
"alg": private_key_data['alg'],
|
||||||
"jwk": private_key_data['jwk'],
|
"jwk": private_key_data['jwk'],
|
||||||
}
|
}
|
||||||
result, info = account.send_signed_request(endpoint, payload, key=private_key,
|
result, info = account.send_signed_request(endpoint, payload, key_data=private_key_data, jws_header=jws_header)
|
||||||
key_data=private_key_data, jws_header=jws_header)
|
|
||||||
else:
|
else:
|
||||||
# Step 1: get hold of account URI
|
# Step 1: get hold of account URI
|
||||||
changed = account.init_account(
|
changed = account.init_account(
|
||||||
|
|
|
@ -8,9 +8,18 @@ class ModuleDocFragment(object):
|
||||||
|
|
||||||
# Standard files documentation fragment
|
# Standard files documentation fragment
|
||||||
DOCUMENTATION = """
|
DOCUMENTATION = """
|
||||||
|
description:
|
||||||
|
- "Note that if a new enough version of the C(cryptography) library
|
||||||
|
is available (see Requirements for details), it will be used
|
||||||
|
instead of the C(openssl) binary. This can be explicitly disabled
|
||||||
|
or enabled with the C(select_crypto_backend) option. Note that using
|
||||||
|
the C(openssl) binary will be slower and less secure, as private key
|
||||||
|
contents always have to be stored on disk (see
|
||||||
|
C(account_key_content))."
|
||||||
requirements:
|
requirements:
|
||||||
- "python >= 2.6"
|
- "python >= 2.6"
|
||||||
- openssl
|
- "either openssl, ..."
|
||||||
|
- "... or L(cryptography,https://cryptography.io/) >= 1.5"
|
||||||
options:
|
options:
|
||||||
account_key_src:
|
account_key_src:
|
||||||
description:
|
description:
|
||||||
|
@ -32,6 +41,10 @@ options:
|
||||||
important private key — it can be used to change the account key,
|
important private key — it can be used to change the account key,
|
||||||
or to revoke your certificates without knowing their private keys
|
or to revoke your certificates without knowing their private keys
|
||||||
—, this might not be acceptable."
|
—, this might not be acceptable."
|
||||||
|
- "In case C(cryptography) is used, the content is not written into a
|
||||||
|
temporary file. It can still happen that it is written to disk by
|
||||||
|
Ansible in the process of moving the module with its argument to
|
||||||
|
the node where it is executed."
|
||||||
version_added: "2.5"
|
version_added: "2.5"
|
||||||
acme_version:
|
acme_version:
|
||||||
description:
|
description:
|
||||||
|
@ -64,5 +77,20 @@ options:
|
||||||
for example when testing against a local Pebble server."
|
for example when testing against a local Pebble server."
|
||||||
type: bool
|
type: bool
|
||||||
default: 'yes'
|
default: 'yes'
|
||||||
version_added: 2.5
|
version_added: "2.5"
|
||||||
|
select_crypto_backend:
|
||||||
|
description:
|
||||||
|
- "Determines which crypto backend to use. The default choice is C(auto),
|
||||||
|
which tries to use C(cryptography) if available, and falls back to
|
||||||
|
C(openssl)."
|
||||||
|
- "If set to C(openssl), will try to use the C(openssl) binary."
|
||||||
|
- "If set to C(cryptography), will try to use the
|
||||||
|
L(cryptography,https://cryptography.io/) library."
|
||||||
|
type: str
|
||||||
|
default: 'auto'
|
||||||
|
choices:
|
||||||
|
- auto
|
||||||
|
- cryptography
|
||||||
|
- openssl
|
||||||
|
version_added: "2.7"
|
||||||
"""
|
"""
|
||||||
|
|
157
test/integration/targets/acme_account/tasks/impl.yml
Normal file
157
test/integration/targets/acme_account/tasks/impl.yml
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
- name: Generate account key
|
||||||
|
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem
|
||||||
|
|
||||||
|
- name: Parse account key (to ease debugging some test failures)
|
||||||
|
command: openssl ec -in {{ output_dir }}/accountkey.pem -noout -text
|
||||||
|
|
||||||
|
- name: Do not try to create account
|
||||||
|
acme_account:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||||
|
acme_version: 2
|
||||||
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
validate_certs: no
|
||||||
|
state: present
|
||||||
|
allow_creation: no
|
||||||
|
ignore_errors: yes
|
||||||
|
register: account_not_created
|
||||||
|
|
||||||
|
- name: Create it now
|
||||||
|
acme_account:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||||
|
acme_version: 2
|
||||||
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
validate_certs: no
|
||||||
|
state: present
|
||||||
|
allow_creation: yes
|
||||||
|
terms_agreed: yes
|
||||||
|
contact:
|
||||||
|
- mailto:example@example.org
|
||||||
|
register: account_created
|
||||||
|
|
||||||
|
- name: Change email address
|
||||||
|
acme_account:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
account_key_content: "{{ lookup('file', output_dir ~ '/accountkey.pem') }}"
|
||||||
|
acme_version: 2
|
||||||
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
validate_certs: no
|
||||||
|
state: present
|
||||||
|
# allow_creation: no
|
||||||
|
contact:
|
||||||
|
- mailto:example@example.com
|
||||||
|
register: account_modified
|
||||||
|
|
||||||
|
- name: Change email address (idempotent)
|
||||||
|
acme_account:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||||
|
acme_version: 2
|
||||||
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
validate_certs: no
|
||||||
|
state: present
|
||||||
|
# allow_creation: no
|
||||||
|
contact:
|
||||||
|
- mailto:example@example.com
|
||||||
|
register: account_modified_idempotent
|
||||||
|
|
||||||
|
- name: Generate new account key
|
||||||
|
command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/accountkey2.pem
|
||||||
|
|
||||||
|
- name: Parse account key (to ease debugging some test failures)
|
||||||
|
command: openssl ec -in {{ output_dir }}/accountkey2.pem -noout -text
|
||||||
|
|
||||||
|
# Note that pebble has no change key endpoint implemented yet!
|
||||||
|
# When it has (and the container was updated), uncomment the
|
||||||
|
# uncomment the following tests, and delete the ones below the
|
||||||
|
# out-commented ones.
|
||||||
|
|
||||||
|
# - name: Change account key
|
||||||
|
# acme_account:
|
||||||
|
# select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
# account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||||
|
# acme_version: 2
|
||||||
|
# acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
# validate_certs: no
|
||||||
|
# new_account_key_src: "{{ output_dir }}/accountkey2.pem"
|
||||||
|
# state: changed_key
|
||||||
|
# contact:
|
||||||
|
# - mailto:example@example.com
|
||||||
|
# register: account_change_key
|
||||||
|
|
||||||
|
# - name: Deactivate account
|
||||||
|
# acme_account:
|
||||||
|
# select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
# account_key_src: "{{ output_dir }}/accountkey2.pem"
|
||||||
|
# acme_version: 2
|
||||||
|
# acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
# validate_certs: no
|
||||||
|
# state: absent
|
||||||
|
# register: account_deactivate
|
||||||
|
|
||||||
|
# - name: Deactivate account (idempotent)
|
||||||
|
# acme_account:
|
||||||
|
# select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
# account_key_src: "{{ output_dir }}/accountkey2.pem"
|
||||||
|
# acme_version: 2
|
||||||
|
# acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
# validate_certs: no
|
||||||
|
# state: absent
|
||||||
|
# register: account_deactivate_idempotent
|
||||||
|
|
||||||
|
# - name: Do not try to create account II
|
||||||
|
# acme_account:
|
||||||
|
# select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
# account_key_src: "{{ output_dir }}/accountkey2.pem"
|
||||||
|
# acme_version: 2
|
||||||
|
# acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
# validate_certs: no
|
||||||
|
# state: present
|
||||||
|
# allow_creation: no
|
||||||
|
# ignore_errors: yes
|
||||||
|
# register: account_not_created_2
|
||||||
|
|
||||||
|
# - name: Do not try to create account III
|
||||||
|
# acme_account:
|
||||||
|
# select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
# account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||||
|
# acme_version: 2
|
||||||
|
# acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
# validate_certs: no
|
||||||
|
# state: present
|
||||||
|
# allow_creation: no
|
||||||
|
# ignore_errors: yes
|
||||||
|
# register: account_not_created_3
|
||||||
|
|
||||||
|
- name: Deactivate account
|
||||||
|
acme_account:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||||
|
acme_version: 2
|
||||||
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
validate_certs: no
|
||||||
|
state: absent
|
||||||
|
register: account_deactivate
|
||||||
|
|
||||||
|
- name: Deactivate account (idempotent)
|
||||||
|
acme_account:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||||
|
acme_version: 2
|
||||||
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
validate_certs: no
|
||||||
|
state: absent
|
||||||
|
register: account_deactivate_idempotent
|
||||||
|
|
||||||
|
- name: Do not try to create account II
|
||||||
|
acme_account:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||||
|
acme_version: 2
|
||||||
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
validate_certs: no
|
||||||
|
state: present
|
||||||
|
allow_creation: no
|
||||||
|
ignore_errors: yes
|
||||||
|
register: account_not_created_2
|
|
@ -1,152 +1,31 @@
|
||||||
---
|
---
|
||||||
- block:
|
- block:
|
||||||
- name: Generate account key
|
- name: Running tests with OpenSSL backend
|
||||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem
|
include_tasks: impl.yml
|
||||||
|
vars:
|
||||||
- name: Parse account key (to ease debugging some test failures)
|
select_crypto_backend: openssl
|
||||||
command: openssl ec -in {{ output_dir }}/accountkey.pem -noout -text
|
|
||||||
|
|
||||||
- name: Do not try to create account
|
|
||||||
acme_account:
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
allow_creation: no
|
|
||||||
ignore_errors: yes
|
|
||||||
register: account_not_created
|
|
||||||
|
|
||||||
- name: Create it now
|
|
||||||
acme_account:
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
allow_creation: yes
|
|
||||||
terms_agreed: yes
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.org
|
|
||||||
register: account_created
|
|
||||||
|
|
||||||
- name: Change email address
|
|
||||||
acme_account:
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/accountkey.pem') }}"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
# allow_creation: no
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.com
|
|
||||||
register: account_modified
|
|
||||||
|
|
||||||
- name: Change email address (idempotent)
|
|
||||||
acme_account:
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
# allow_creation: no
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.com
|
|
||||||
register: account_modified_idempotent
|
|
||||||
|
|
||||||
- name: Generate new account key
|
|
||||||
command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/accountkey2.pem
|
|
||||||
|
|
||||||
- name: Parse account key (to ease debugging some test failures)
|
|
||||||
command: openssl ec -in {{ output_dir }}/accountkey2.pem -noout -text
|
|
||||||
|
|
||||||
# Note that pebble has no change key endpoint implemented yet!
|
|
||||||
# When it has (and the container was updated), uncomment the
|
|
||||||
# uncomment the following tests, and delete the ones below the
|
|
||||||
# out-commented ones.
|
|
||||||
|
|
||||||
# - name: Change account key
|
|
||||||
# acme_account:
|
|
||||||
# account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
# acme_version: 2
|
|
||||||
# acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
# validate_certs: no
|
|
||||||
# new_account_key_src: "{{ output_dir }}/accountkey2.pem"
|
|
||||||
# state: changed_key
|
|
||||||
# contact:
|
|
||||||
# - mailto:example@example.com
|
|
||||||
# register: account_change_key
|
|
||||||
|
|
||||||
# - name: Deactivate account
|
|
||||||
# acme_account:
|
|
||||||
# account_key_src: "{{ output_dir }}/accountkey2.pem"
|
|
||||||
# acme_version: 2
|
|
||||||
# acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
# validate_certs: no
|
|
||||||
# state: absent
|
|
||||||
# register: account_deactivate
|
|
||||||
|
|
||||||
# - name: Deactivate account (idempotent)
|
|
||||||
# acme_account:
|
|
||||||
# account_key_src: "{{ output_dir }}/accountkey2.pem"
|
|
||||||
# acme_version: 2
|
|
||||||
# acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
# validate_certs: no
|
|
||||||
# state: absent
|
|
||||||
# register: account_deactivate_idempotent
|
|
||||||
|
|
||||||
# - name: Do not try to create account II
|
|
||||||
# acme_account:
|
|
||||||
# account_key_src: "{{ output_dir }}/accountkey2.pem"
|
|
||||||
# acme_version: 2
|
|
||||||
# acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
# validate_certs: no
|
|
||||||
# state: present
|
|
||||||
# allow_creation: no
|
|
||||||
# ignore_errors: yes
|
|
||||||
# register: account_not_created_2
|
|
||||||
|
|
||||||
# - name: Do not try to create account III
|
|
||||||
# acme_account:
|
|
||||||
# account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
# acme_version: 2
|
|
||||||
# acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
# validate_certs: no
|
|
||||||
# state: present
|
|
||||||
# allow_creation: no
|
|
||||||
# ignore_errors: yes
|
|
||||||
# register: account_not_created_3
|
|
||||||
|
|
||||||
- name: Deactivate account
|
|
||||||
acme_account:
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: absent
|
|
||||||
register: account_deactivate
|
|
||||||
|
|
||||||
- name: Deactivate account (idempotent)
|
|
||||||
acme_account:
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: absent
|
|
||||||
register: account_deactivate_idempotent
|
|
||||||
|
|
||||||
- name: Do not try to create account II
|
|
||||||
acme_account:
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
allow_creation: no
|
|
||||||
ignore_errors: yes
|
|
||||||
register: account_not_created_2
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
- import_tasks: ../tests/validate.yml
|
||||||
|
|
||||||
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
||||||
when: openssl_version.stdout is version('1.0.0', '>=')
|
when: openssl_version.stdout is version('1.0.0', '>=')
|
||||||
|
|
||||||
|
- name: Remove output directory
|
||||||
|
file:
|
||||||
|
path: "{{ output_dir }}"
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: Re-create output directory
|
||||||
|
file:
|
||||||
|
path: "{{ output_dir }}"
|
||||||
|
state: directory
|
||||||
|
|
||||||
|
- block:
|
||||||
|
- name: Running tests with cryptography backend
|
||||||
|
include_tasks: impl.yml
|
||||||
|
vars:
|
||||||
|
select_crypto_backend: cryptography
|
||||||
|
|
||||||
|
- import_tasks: ../tests/validate.yml
|
||||||
|
|
||||||
|
when: cryptography_version.stdout is version('1.5', '>=')
|
||||||
|
|
240
test/integration/targets/acme_certificate/tasks/impl.yml
Normal file
240
test/integration/targets/acme_certificate/tasks/impl.yml
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
---
|
||||||
|
## SET UP ACCOUNT KEYS ########################################################################
|
||||||
|
- name: Create ECC256 account key
|
||||||
|
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem
|
||||||
|
- name: Create ECC384 account key
|
||||||
|
command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/account-ec384.pem
|
||||||
|
- name: Create RSA-2048 account key
|
||||||
|
command: openssl genrsa -out {{ output_dir }}/account-rsa2048.pem 2048
|
||||||
|
## SET UP ACCOUNTS ############################################################################
|
||||||
|
- name: Make sure ECC256 account hasn't been created yet
|
||||||
|
acme_account:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
acme_version: 2
|
||||||
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
validate_certs: no
|
||||||
|
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
||||||
|
state: absent
|
||||||
|
- name: Create ECC384 account
|
||||||
|
acme_account:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
acme_version: 2
|
||||||
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
validate_certs: no
|
||||||
|
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec384.pem') }}"
|
||||||
|
state: present
|
||||||
|
allow_creation: yes
|
||||||
|
terms_agreed: yes
|
||||||
|
contact:
|
||||||
|
- mailto:example@example.org
|
||||||
|
- mailto:example@example.com
|
||||||
|
- name: Create RSA-2048 account
|
||||||
|
acme_account:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
acme_version: 2
|
||||||
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
validate_certs: no
|
||||||
|
account_key_src: "{{ output_dir }}/account-rsa2048.pem"
|
||||||
|
state: present
|
||||||
|
allow_creation: yes
|
||||||
|
terms_agreed: yes
|
||||||
|
contact: []
|
||||||
|
## OBTAIN CERTIFICATES ########################################################################
|
||||||
|
- name: Obtain cert 1
|
||||||
|
include_tasks: obtain-cert.yml
|
||||||
|
vars:
|
||||||
|
certgen_title: Certificate 1
|
||||||
|
certificate_name: cert-1
|
||||||
|
key_type: rsa
|
||||||
|
rsa_bits: 2048
|
||||||
|
subject_alt_name: "DNS:example.com"
|
||||||
|
subject_alt_name_critical: no
|
||||||
|
account_key: account-ec256
|
||||||
|
challenge: http-01
|
||||||
|
modify_account: yes
|
||||||
|
deactivate_authzs: no
|
||||||
|
force: no
|
||||||
|
remaining_days: 10
|
||||||
|
terms_agreed: yes
|
||||||
|
account_email: "example@example.org"
|
||||||
|
- name: Obtain cert 2
|
||||||
|
include_tasks: obtain-cert.yml
|
||||||
|
vars:
|
||||||
|
certgen_title: Certificate 2
|
||||||
|
certificate_name: cert-2
|
||||||
|
key_type: ec256
|
||||||
|
subject_alt_name: "DNS:*.example.com,DNS:example.com"
|
||||||
|
subject_alt_name_critical: yes
|
||||||
|
account_key: account-ec384
|
||||||
|
challenge: dns-01
|
||||||
|
modify_account: no
|
||||||
|
deactivate_authzs: yes
|
||||||
|
force: no
|
||||||
|
remaining_days: 10
|
||||||
|
terms_agreed: no
|
||||||
|
account_email: ""
|
||||||
|
- name: Obtain cert 3
|
||||||
|
include_tasks: obtain-cert.yml
|
||||||
|
vars:
|
||||||
|
certgen_title: Certificate 3
|
||||||
|
certificate_name: cert-3
|
||||||
|
key_type: ec384
|
||||||
|
subject_alt_name: "DNS:*.example.com,DNS:example.org,DNS:t1.example.com"
|
||||||
|
subject_alt_name_critical: no
|
||||||
|
account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa2048.pem') }}"
|
||||||
|
challenge: dns-01
|
||||||
|
modify_account: no
|
||||||
|
deactivate_authzs: no
|
||||||
|
force: no
|
||||||
|
remaining_days: 10
|
||||||
|
terms_agreed: no
|
||||||
|
account_email: ""
|
||||||
|
- name: Obtain cert 4
|
||||||
|
include_tasks: obtain-cert.yml
|
||||||
|
vars:
|
||||||
|
certgen_title: Certificate 4
|
||||||
|
certificate_name: cert-4
|
||||||
|
key_type: rsa
|
||||||
|
rsa_bits: 2048
|
||||||
|
subject_alt_name: "DNS:example.com,DNS:t1.example.com,DNS:test.t2.example.com,DNS:example.org,DNS:test.example.org"
|
||||||
|
subject_alt_name_critical: no
|
||||||
|
account_key: account-rsa2048
|
||||||
|
challenge: http-01
|
||||||
|
modify_account: no
|
||||||
|
deactivate_authzs: yes
|
||||||
|
force: yes
|
||||||
|
remaining_days: 10
|
||||||
|
terms_agreed: no
|
||||||
|
account_email: ""
|
||||||
|
- name: Obtain cert 5
|
||||||
|
include_tasks: obtain-cert.yml
|
||||||
|
vars:
|
||||||
|
certgen_title: Certificate 5, Iteration 1/4
|
||||||
|
certificate_name: cert-5
|
||||||
|
key_type: ec521
|
||||||
|
subject_alt_name: "DNS:t2.example.com"
|
||||||
|
subject_alt_name_critical: no
|
||||||
|
account_key: account-ec384
|
||||||
|
challenge: http-01
|
||||||
|
modify_account: no
|
||||||
|
deactivate_authzs: yes
|
||||||
|
force: yes
|
||||||
|
remaining_days: 10
|
||||||
|
terms_agreed: no
|
||||||
|
account_email: ""
|
||||||
|
- name: Obtain cert 5 (should not, since already there and valid for more than 10 days)
|
||||||
|
include_tasks: obtain-cert.yml
|
||||||
|
vars:
|
||||||
|
certgen_title: Certificate 5, Iteration 2/4
|
||||||
|
certificate_name: cert-5
|
||||||
|
key_type: ec521
|
||||||
|
subject_alt_name: "DNS:t2.example.com"
|
||||||
|
subject_alt_name_critical: no
|
||||||
|
account_key: account-ec384
|
||||||
|
challenge: http-01
|
||||||
|
modify_account: no
|
||||||
|
deactivate_authzs: yes
|
||||||
|
force: no
|
||||||
|
remaining_days: 10
|
||||||
|
terms_agreed: no
|
||||||
|
account_email: ""
|
||||||
|
- set_fact:
|
||||||
|
cert_5_recreate_1: "{{ challenge_data is changed }}"
|
||||||
|
- name: Obtain cert 5 (should again by less days)
|
||||||
|
include_tasks: obtain-cert.yml
|
||||||
|
vars:
|
||||||
|
certgen_title: Certificate 5, Iteration 3/4
|
||||||
|
certificate_name: cert-5
|
||||||
|
key_type: ec521
|
||||||
|
subject_alt_name: "DNS:t2.example.com"
|
||||||
|
subject_alt_name_critical: no
|
||||||
|
account_key: account-ec384
|
||||||
|
challenge: http-01
|
||||||
|
modify_account: no
|
||||||
|
deactivate_authzs: yes
|
||||||
|
force: yes
|
||||||
|
remaining_days: 1000
|
||||||
|
terms_agreed: no
|
||||||
|
account_email: ""
|
||||||
|
- set_fact:
|
||||||
|
cert_5_recreate_2: "{{ challenge_data is changed }}"
|
||||||
|
- name: Obtain cert 5 (should again by force)
|
||||||
|
include_tasks: obtain-cert.yml
|
||||||
|
vars:
|
||||||
|
certgen_title: Certificate 5, Iteration 4/4
|
||||||
|
certificate_name: cert-5
|
||||||
|
key_type: ec521
|
||||||
|
subject_alt_name: "DNS:t2.example.com"
|
||||||
|
subject_alt_name_critical: no
|
||||||
|
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec384.pem') }}"
|
||||||
|
challenge: http-01
|
||||||
|
modify_account: no
|
||||||
|
deactivate_authzs: yes
|
||||||
|
force: yes
|
||||||
|
remaining_days: 10
|
||||||
|
terms_agreed: no
|
||||||
|
account_email: ""
|
||||||
|
- set_fact:
|
||||||
|
cert_5_recreate_3: "{{ challenge_data is changed }}"
|
||||||
|
- name: Obtain cert 6
|
||||||
|
include_tasks: obtain-cert.yml
|
||||||
|
vars:
|
||||||
|
certgen_title: Certificate 6
|
||||||
|
certificate_name: cert-6
|
||||||
|
key_type: rsa
|
||||||
|
rsa_bits: 2048
|
||||||
|
subject_alt_name: "DNS:example.org"
|
||||||
|
subject_alt_name_critical: no
|
||||||
|
account_key: account-ec256
|
||||||
|
challenge: tls-alpn-01
|
||||||
|
modify_account: yes
|
||||||
|
deactivate_authzs: no
|
||||||
|
force: no
|
||||||
|
remaining_days: 10
|
||||||
|
terms_agreed: yes
|
||||||
|
account_email: "example@example.org"
|
||||||
|
## DISSECT CERTIFICATES #######################################################################
|
||||||
|
# Make sure certificates are valid. Root certificate for Pebble equals the chain certificate.
|
||||||
|
- name: Verifying cert 1
|
||||||
|
command: openssl verify -CAfile "{{ output_dir }}/cert-1-chain.pem" "{{ output_dir }}/cert-1.pem"
|
||||||
|
ignore_errors: yes
|
||||||
|
register: cert_1_valid
|
||||||
|
- name: Verifying cert 2
|
||||||
|
command: openssl verify -CAfile "{{ output_dir }}/cert-2-chain.pem" "{{ output_dir }}/cert-2.pem"
|
||||||
|
ignore_errors: yes
|
||||||
|
register: cert_2_valid
|
||||||
|
- name: Verifying cert 3
|
||||||
|
command: openssl verify -CAfile "{{ output_dir }}/cert-3-chain.pem" "{{ output_dir }}/cert-3.pem"
|
||||||
|
ignore_errors: yes
|
||||||
|
register: cert_3_valid
|
||||||
|
- name: Verifying cert 4
|
||||||
|
command: openssl verify -CAfile "{{ output_dir }}/cert-4-chain.pem" "{{ output_dir }}/cert-4.pem"
|
||||||
|
ignore_errors: yes
|
||||||
|
register: cert_4_valid
|
||||||
|
- name: Verifying cert 5
|
||||||
|
command: openssl verify -CAfile "{{ output_dir }}/cert-5-chain.pem" "{{ output_dir }}/cert-5.pem"
|
||||||
|
ignore_errors: yes
|
||||||
|
register: cert_5_valid
|
||||||
|
- name: Verifying cert 6
|
||||||
|
command: openssl verify -CAfile "{{ output_dir }}/cert-6-chain.pem" "{{ output_dir }}/cert-6.pem"
|
||||||
|
ignore_errors: yes
|
||||||
|
register: cert_6_valid
|
||||||
|
# Dump certificate info
|
||||||
|
- name: Dumping cert 1
|
||||||
|
command: openssl x509 -in "{{ output_dir }}/cert-1.pem" -noout -text
|
||||||
|
register: cert_1_text
|
||||||
|
- name: Dumping cert 2
|
||||||
|
command: openssl x509 -in "{{ output_dir }}/cert-2.pem" -noout -text
|
||||||
|
register: cert_2_text
|
||||||
|
- name: Dumping cert 3
|
||||||
|
command: openssl x509 -in "{{ output_dir }}/cert-3.pem" -noout -text
|
||||||
|
register: cert_3_text
|
||||||
|
- name: Dumping cert 4
|
||||||
|
command: openssl x509 -in "{{ output_dir }}/cert-4.pem" -noout -text
|
||||||
|
register: cert_4_text
|
||||||
|
- name: Dumping cert 5
|
||||||
|
command: openssl x509 -in "{{ output_dir }}/cert-5.pem" -noout -text
|
||||||
|
register: cert_5_text
|
||||||
|
- name: Dumping cert 6
|
||||||
|
command: openssl x509 -in "{{ output_dir }}/cert-6.pem" -noout -text
|
||||||
|
register: cert_6_text
|
|
@ -1,243 +1,31 @@
|
||||||
---
|
---
|
||||||
- block:
|
- block:
|
||||||
## SET UP ACCOUNT KEYS ########################################################################
|
- name: Running tests with OpenSSL backend
|
||||||
- name: Create ECC256 account key
|
include_tasks: impl.yml
|
||||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem
|
|
||||||
- name: Create ECC384 account key
|
|
||||||
command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/account-ec384.pem
|
|
||||||
- name: Create RSA-2048 account key
|
|
||||||
command: openssl genrsa -out {{ output_dir }}/account-rsa2048.pem 2048
|
|
||||||
## SET UP ACCOUNTS ############################################################################
|
|
||||||
- name: Make sure ECC256 account hasn't been created yet
|
|
||||||
acme_account:
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
|
||||||
state: absent
|
|
||||||
- name: Create ECC384 account
|
|
||||||
acme_account:
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec384.pem') }}"
|
|
||||||
state: present
|
|
||||||
allow_creation: yes
|
|
||||||
terms_agreed: yes
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.org
|
|
||||||
- mailto:example@example.com
|
|
||||||
- name: Create RSA-2048 account
|
|
||||||
acme_account:
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
account_key_src: "{{ output_dir }}/account-rsa2048.pem"
|
|
||||||
state: present
|
|
||||||
allow_creation: yes
|
|
||||||
terms_agreed: yes
|
|
||||||
contact: []
|
|
||||||
## OBTAIN CERTIFICATES ########################################################################
|
|
||||||
- name: Obtain cert 1
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
vars:
|
||||||
certgen_title: Certificate 1
|
select_crypto_backend: openssl
|
||||||
certificate_name: cert-1
|
|
||||||
key_type: rsa
|
|
||||||
rsa_bits: 2048
|
|
||||||
subject_alt_name: "DNS:example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-ec256
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: yes
|
|
||||||
deactivate_authzs: no
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: yes
|
|
||||||
account_email: "example@example.org"
|
|
||||||
- name: Obtain cert 2
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 2
|
|
||||||
certificate_name: cert-2
|
|
||||||
key_type: ec256
|
|
||||||
subject_alt_name: "DNS:*.example.com,DNS:example.com"
|
|
||||||
subject_alt_name_critical: yes
|
|
||||||
account_key: account-ec384
|
|
||||||
challenge: dns-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
- name: Obtain cert 3
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 3
|
|
||||||
certificate_name: cert-3
|
|
||||||
key_type: ec384
|
|
||||||
subject_alt_name: "DNS:*.example.com,DNS:example.org,DNS:t1.example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa2048.pem') }}"
|
|
||||||
challenge: dns-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: no
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
- name: Obtain cert 4
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 4
|
|
||||||
certificate_name: cert-4
|
|
||||||
key_type: rsa
|
|
||||||
rsa_bits: 2048
|
|
||||||
subject_alt_name: "DNS:example.com,DNS:t1.example.com,DNS:test.t2.example.com,DNS:example.org,DNS:test.example.org"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-rsa2048
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: yes
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
- name: Obtain cert 5
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 5, Iteration 1/4
|
|
||||||
certificate_name: cert-5
|
|
||||||
key_type: ec521
|
|
||||||
subject_alt_name: "DNS:t2.example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-ec384
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: yes
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
- name: Obtain cert 5 (should not, since already there and valid for more than 10 days)
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 5, Iteration 2/4
|
|
||||||
certificate_name: cert-5
|
|
||||||
key_type: ec521
|
|
||||||
subject_alt_name: "DNS:t2.example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-ec384
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
- set_fact:
|
|
||||||
cert_5_recreate_1: "{{ challenge_data is changed }}"
|
|
||||||
- name: Obtain cert 5 (should again by less days)
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 5, Iteration 3/4
|
|
||||||
certificate_name: cert-5
|
|
||||||
key_type: ec521
|
|
||||||
subject_alt_name: "DNS:t2.example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-ec384
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: yes
|
|
||||||
remaining_days: 1000
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
- set_fact:
|
|
||||||
cert_5_recreate_2: "{{ challenge_data is changed }}"
|
|
||||||
- name: Obtain cert 5 (should again by force)
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 5, Iteration 4/4
|
|
||||||
certificate_name: cert-5
|
|
||||||
key_type: ec521
|
|
||||||
subject_alt_name: "DNS:t2.example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec384.pem') }}"
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: yes
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
- set_fact:
|
|
||||||
cert_5_recreate_3: "{{ challenge_data is changed }}"
|
|
||||||
- name: Obtain cert 6
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 6
|
|
||||||
certificate_name: cert-6
|
|
||||||
key_type: rsa
|
|
||||||
rsa_bits: 2048
|
|
||||||
subject_alt_name: "DNS:example.org"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-ec256
|
|
||||||
challenge: tls-alpn-01
|
|
||||||
modify_account: yes
|
|
||||||
deactivate_authzs: no
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: yes
|
|
||||||
account_email: "example@example.org"
|
|
||||||
## DISSECT CERTIFICATES #######################################################################
|
|
||||||
# Make sure certificates are valid. Root certificate for Pebble equals the chain certificate.
|
|
||||||
- name: Verifying cert 1
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-1-chain.pem" "{{ output_dir }}/cert-1.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_1_valid
|
|
||||||
- name: Verifying cert 2
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-2-chain.pem" "{{ output_dir }}/cert-2.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_2_valid
|
|
||||||
- name: Verifying cert 3
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-3-chain.pem" "{{ output_dir }}/cert-3.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_3_valid
|
|
||||||
- name: Verifying cert 4
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-4-chain.pem" "{{ output_dir }}/cert-4.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_4_valid
|
|
||||||
- name: Verifying cert 5
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-5-chain.pem" "{{ output_dir }}/cert-5.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_5_valid
|
|
||||||
- name: Verifying cert 6
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-6-chain.pem" "{{ output_dir }}/cert-6.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_6_valid
|
|
||||||
# Dump certificate info
|
|
||||||
- name: Dumping cert 1
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-1.pem" -noout -text
|
|
||||||
register: cert_1_text
|
|
||||||
- name: Dumping cert 2
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-2.pem" -noout -text
|
|
||||||
register: cert_2_text
|
|
||||||
- name: Dumping cert 3
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-3.pem" -noout -text
|
|
||||||
register: cert_3_text
|
|
||||||
- name: Dumping cert 4
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-4.pem" -noout -text
|
|
||||||
register: cert_4_text
|
|
||||||
- name: Dumping cert 5
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-5.pem" -noout -text
|
|
||||||
register: cert_5_text
|
|
||||||
- name: Dumping cert 6
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-6.pem" -noout -text
|
|
||||||
register: cert_6_text
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
- import_tasks: ../tests/validate.yml
|
||||||
|
|
||||||
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
||||||
when: openssl_version.stdout is version('1.0.0', '>=')
|
when: openssl_version.stdout is version('1.0.0', '>=')
|
||||||
|
|
||||||
|
- name: Remove output directory
|
||||||
|
file:
|
||||||
|
path: "{{ output_dir }}"
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: Re-create output directory
|
||||||
|
file:
|
||||||
|
path: "{{ output_dir }}"
|
||||||
|
state: directory
|
||||||
|
|
||||||
|
- block:
|
||||||
|
- name: Running tests with cryptography backend
|
||||||
|
include_tasks: impl.yml
|
||||||
|
vars:
|
||||||
|
select_crypto_backend: cryptography
|
||||||
|
|
||||||
|
- import_tasks: ../tests/validate.yml
|
||||||
|
|
||||||
|
when: cryptography_version.stdout is version('1.5', '>=')
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
---
|
||||||
|
## SET UP ACCOUNT KEYS ########################################################################
|
||||||
|
- name: Create ECC256 account key
|
||||||
|
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem
|
||||||
|
- name: Create ECC384 account key
|
||||||
|
command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/account-ec384.pem
|
||||||
|
- name: Create RSA-2048 account key
|
||||||
|
command: openssl genrsa -out {{ output_dir }}/account-rsa2048.pem 2048
|
||||||
|
## CREATE ACCOUNTS AND OBTAIN CERTIFICATES ####################################################
|
||||||
|
- name: Obtain cert 1
|
||||||
|
include_tasks: obtain-cert.yml
|
||||||
|
vars:
|
||||||
|
certgen_title: Certificate 1 for revocation
|
||||||
|
certificate_name: cert-1
|
||||||
|
key_type: rsa
|
||||||
|
rsa_bits: 2048
|
||||||
|
subject_alt_name: "DNS:example.com"
|
||||||
|
subject_alt_name_critical: no
|
||||||
|
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec256.pem') }}"
|
||||||
|
challenge: http-01
|
||||||
|
modify_account: yes
|
||||||
|
deactivate_authzs: no
|
||||||
|
force: no
|
||||||
|
remaining_days: 10
|
||||||
|
terms_agreed: yes
|
||||||
|
account_email: "example@example.org"
|
||||||
|
- name: Obtain cert 2
|
||||||
|
include_tasks: obtain-cert.yml
|
||||||
|
vars:
|
||||||
|
certgen_title: Certificate 2 for revocation
|
||||||
|
certificate_name: cert-2
|
||||||
|
key_type: ec256
|
||||||
|
subject_alt_name: "DNS:*.example.com"
|
||||||
|
subject_alt_name_critical: yes
|
||||||
|
account_key: account-ec384
|
||||||
|
challenge: dns-01
|
||||||
|
modify_account: yes
|
||||||
|
deactivate_authzs: yes
|
||||||
|
force: no
|
||||||
|
remaining_days: 10
|
||||||
|
terms_agreed: yes
|
||||||
|
account_email: "example@example.org"
|
||||||
|
- name: Obtain cert 3
|
||||||
|
include_tasks: obtain-cert.yml
|
||||||
|
vars:
|
||||||
|
certgen_title: Certificate 3 for revocation
|
||||||
|
certificate_name: cert-3
|
||||||
|
key_type: ec384
|
||||||
|
subject_alt_name: "DNS:t1.example.com"
|
||||||
|
subject_alt_name_critical: no
|
||||||
|
account_key: account-rsa2048
|
||||||
|
challenge: dns-01
|
||||||
|
modify_account: yes
|
||||||
|
deactivate_authzs: no
|
||||||
|
force: no
|
||||||
|
remaining_days: 10
|
||||||
|
terms_agreed: yes
|
||||||
|
account_email: "example@example.org"
|
||||||
|
## REVOKE CERTIFICATES ########################################################################
|
||||||
|
- name: Revoke certificate 1 via account key
|
||||||
|
acme_certificate_revoke:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
||||||
|
certificate: "{{ output_dir }}/cert-1.pem"
|
||||||
|
acme_version: 2
|
||||||
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
validate_certs: no
|
||||||
|
ignore_errors: yes
|
||||||
|
register: cert_1_revoke
|
||||||
|
- name: Revoke certificate 2 via certificate private key
|
||||||
|
acme_certificate_revoke:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
private_key_src: "{{ output_dir }}/cert-2.key"
|
||||||
|
certificate: "{{ output_dir }}/cert-2.pem"
|
||||||
|
acme_version: 2
|
||||||
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
validate_certs: no
|
||||||
|
ignore_errors: yes
|
||||||
|
register: cert_2_revoke
|
||||||
|
- name: Revoke certificate 3 via account key (fullchain)
|
||||||
|
acme_certificate_revoke:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
|
account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa2048.pem') }}"
|
||||||
|
certificate: "{{ output_dir }}/cert-3-fullchain.pem"
|
||||||
|
acme_version: 2
|
||||||
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
|
validate_certs: no
|
||||||
|
ignore_errors: yes
|
||||||
|
register: cert_3_revoke
|
|
@ -1,92 +1,31 @@
|
||||||
---
|
---
|
||||||
- block:
|
- block:
|
||||||
## SET UP ACCOUNT KEYS ########################################################################
|
- name: Running tests with OpenSSL backend
|
||||||
- name: Create ECC256 account key
|
include_tasks: impl.yml
|
||||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem
|
|
||||||
- name: Create ECC384 account key
|
|
||||||
command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/account-ec384.pem
|
|
||||||
- name: Create RSA-2048 account key
|
|
||||||
command: openssl genrsa -out {{ output_dir }}/account-rsa2048.pem 2048
|
|
||||||
## CREATE ACCOUNTS AND OBTAIN CERTIFICATES ####################################################
|
|
||||||
- name: Obtain cert 1
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
vars:
|
||||||
certgen_title: Certificate 1 for revocation
|
select_crypto_backend: openssl
|
||||||
certificate_name: cert-1
|
|
||||||
key_type: rsa
|
|
||||||
rsa_bits: 2048
|
|
||||||
subject_alt_name: "DNS:example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec256.pem') }}"
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: yes
|
|
||||||
deactivate_authzs: no
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: yes
|
|
||||||
account_email: "example@example.org"
|
|
||||||
- name: Obtain cert 2
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 2 for revocation
|
|
||||||
certificate_name: cert-2
|
|
||||||
key_type: ec256
|
|
||||||
subject_alt_name: "DNS:*.example.com"
|
|
||||||
subject_alt_name_critical: yes
|
|
||||||
account_key: account-ec384
|
|
||||||
challenge: dns-01
|
|
||||||
modify_account: yes
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: yes
|
|
||||||
account_email: "example@example.org"
|
|
||||||
- name: Obtain cert 3
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 3 for revocation
|
|
||||||
certificate_name: cert-3
|
|
||||||
key_type: ec384
|
|
||||||
subject_alt_name: "DNS:t1.example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-rsa2048
|
|
||||||
challenge: dns-01
|
|
||||||
modify_account: yes
|
|
||||||
deactivate_authzs: no
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: yes
|
|
||||||
account_email: "example@example.org"
|
|
||||||
## REVOKE CERTIFICATES ########################################################################
|
|
||||||
- name: Revoke certificate 1 via account key
|
|
||||||
acme_certificate_revoke:
|
|
||||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
|
||||||
certificate: "{{ output_dir }}/cert-1.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_1_revoke
|
|
||||||
- name: Revoke certificate 2 via certificate private key
|
|
||||||
acme_certificate_revoke:
|
|
||||||
private_key_src: "{{ output_dir }}/cert-2.key"
|
|
||||||
certificate: "{{ output_dir }}/cert-2.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_2_revoke
|
|
||||||
- name: Revoke certificate 3 via account key (fullchain)
|
|
||||||
acme_certificate_revoke:
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa2048.pem') }}"
|
|
||||||
certificate: "{{ output_dir }}/cert-3-fullchain.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_3_revoke
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
- import_tasks: ../tests/validate.yml
|
||||||
|
|
||||||
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
||||||
when: openssl_version.stdout is version('1.0.0', '>=')
|
when: openssl_version.stdout is version('1.0.0', '>=')
|
||||||
|
|
||||||
|
- name: Remove output directory
|
||||||
|
file:
|
||||||
|
path: "{{ output_dir }}"
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: Re-create output directory
|
||||||
|
file:
|
||||||
|
path: "{{ output_dir }}"
|
||||||
|
state: directory
|
||||||
|
|
||||||
|
- block:
|
||||||
|
- name: Running tests with cryptography backend
|
||||||
|
include_tasks: impl.yml
|
||||||
|
vars:
|
||||||
|
select_crypto_backend: cryptography
|
||||||
|
|
||||||
|
- import_tasks: ../tests/validate.yml
|
||||||
|
|
||||||
|
when: cryptography_version.stdout is version('1.5', '>=')
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
## ACME STEP 1 ################################################################################
|
## ACME STEP 1 ################################################################################
|
||||||
- name: ({{ certgen_title }}) Obtain cert, step 1
|
- name: ({{ certgen_title }}) Obtain cert, step 1
|
||||||
acme_certificate:
|
acme_certificate:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
when: account_key_content is not defined
|
when: account_key_content is not defined
|
||||||
- name: ({{ certgen_title }}) Obtain cert, step 1 (using account key data)
|
- name: ({{ certgen_title }}) Obtain cert, step 1 (using account key data)
|
||||||
acme_certificate:
|
acme_certificate:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
|
@ -96,6 +98,7 @@
|
||||||
## ACME STEP 2 ################################################################################
|
## ACME STEP 2 ################################################################################
|
||||||
- name: ({{ certgen_title }}) Obtain cert, step 2
|
- name: ({{ certgen_title }}) Obtain cert, step 2
|
||||||
acme_certificate:
|
acme_certificate:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
|
@ -115,6 +118,7 @@
|
||||||
when: challenge_data is changed and account_key_content is not defined
|
when: challenge_data is changed and account_key_content is not defined
|
||||||
- name: ({{ certgen_title }}) Obtain cert, step 2 (using account key data)
|
- name: ({{ certgen_title }}) Obtain cert, step 2 (using account key data)
|
||||||
acme_certificate:
|
acme_certificate:
|
||||||
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
|
|
Loading…
Reference in a new issue