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 binascii
|
||||
import copy
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
||||
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):
|
||||
'''
|
||||
|
@ -83,6 +104,14 @@ def simple_get(module, url):
|
|||
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
|
||||
def write_file(module, dest, content):
|
||||
'''
|
||||
|
@ -141,6 +170,296 @@ def write_file(module, dest, content):
|
|||
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):
|
||||
'''
|
||||
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)
|
||||
|
||||
# Create a key file from content, key (path) and key content are mutually exclusive
|
||||
if self.key_content is not 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(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 self.key is not None or self.key_content is not None:
|
||||
error, self.key_data = self.parse_key(self.key, self.key_content)
|
||||
if error:
|
||||
raise ModuleFailException("error while parsing account key: %s" % error)
|
||||
self.jwk = self.key_data['jwk']
|
||||
|
@ -234,136 +537,37 @@ class ACMEAccount(object):
|
|||
thumbprint = nopad_b64(hashlib.sha256(accountkey_json.encode('utf8')).digest())
|
||||
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
|
||||
(error, key_data).
|
||||
'''
|
||||
account_key_type = None
|
||||
with open(key, "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 = [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'
|
||||
if key_file is None and key_content is None:
|
||||
raise AssertionError('One of key_file and key_content must be specified!')
|
||||
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||
return _parse_key_cryptography(self.module, key_file, key_content)
|
||||
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, {
|
||||
'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,
|
||||
}
|
||||
return _parse_key_openssl(self._openssl_bin, self.module, key_file, key_content)
|
||||
|
||||
def sign_request(self, protected, payload, key_data, key):
|
||||
def sign_request(self, protected, payload, key_data):
|
||||
try:
|
||||
payload64 = nopad_b64(self.module.jsonify(payload).encode('utf8'))
|
||||
protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8'))
|
||||
except Exception as 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]
|
||||
sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
|
||||
dummy, out, dummy = self.module.run_command(openssl_sign_cmd, data=sign_payload, check_rc=True, binary_data=True)
|
||||
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||
return _sign_request_cryptography(self.module, payload64, protected64, key_data)
|
||||
else:
|
||||
return _sign_request_openssl(self._openssl_bin, self.module, payload64, protected64, key_data)
|
||||
|
||||
if key_data['type'] == 'ec':
|
||||
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):
|
||||
def send_signed_request(self, url, payload, key_data=None, jws_header=None):
|
||||
'''
|
||||
Sends a JWS signed HTTP POST request to the ACME server and returns
|
||||
the response as dictionary
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-6.2
|
||||
'''
|
||||
key_data = key_data or self.key_data
|
||||
key = key or self.key
|
||||
jws_header = jws_header or self.jws_header
|
||||
failed_tries = 0
|
||||
while True:
|
||||
|
@ -372,7 +576,7 @@ class ACMEAccount(object):
|
|||
if self.version != 1:
|
||||
protected["url"] = url
|
||||
|
||||
data = self.sign_request(protected, payload, key_data, key)
|
||||
data = self.sign_request(protected, payload, key_data)
|
||||
if self.version == 1:
|
||||
data["header"] = jws_header
|
||||
data = self.module.jsonify(data)
|
||||
|
@ -530,3 +734,67 @@ class ACMEAccount(object):
|
|||
result, dummy = self.send_signed_request(self.uri, upd_reg)
|
||||
changed = True
|
||||
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 (
|
||||
ModuleFailException, ACMEAccount
|
||||
ModuleFailException, ACMEAccount, set_crypto_backend,
|
||||
)
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -141,6 +136,7 @@ def main():
|
|||
contact=dict(required=False, type='list', default=[]),
|
||||
new_account_key_src=dict(type='path'),
|
||||
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=(
|
||||
['account_key_src', 'account_key_content'],
|
||||
|
@ -156,6 +152,7 @@ def main():
|
|||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
set_crypto_backend(module)
|
||||
|
||||
if not module.params.get('validate_certs'):
|
||||
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.')
|
||||
module.exit_json(changed=changed, account_uri=account.uri)
|
||||
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
|
||||
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:
|
||||
raise ModuleFailException("error while parsing account key: %s" % error)
|
||||
# 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,
|
||||
# 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
|
||||
result, info = account.send_signed_request(url, data)
|
||||
if info['status'] != 200:
|
||||
|
|
|
@ -328,7 +328,9 @@ account_uri:
|
|||
'''
|
||||
|
||||
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
|
||||
|
@ -350,6 +352,8 @@ def get_cert_days(module, cert_file):
|
|||
if the file was not found. If cert_file contains more than one
|
||||
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):
|
||||
return -1
|
||||
|
||||
|
@ -422,6 +426,8 @@ class ACMEClient(object):
|
|||
'''
|
||||
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"]
|
||||
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
|
||||
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"]
|
||||
dummy, out, dummy = self.module.run_command(openssl_csr_cmd, check_rc=True)
|
||||
|
||||
csr = pem_to_der(self.csr)
|
||||
new_cert = {
|
||||
"csr": nopad_b64(to_bytes(out)),
|
||||
"csr": nopad_b64(csr),
|
||||
}
|
||||
result, info = self.account.send_signed_request(self.finalize_uri, new_cert)
|
||||
if info['status'] not in [200]:
|
||||
|
@ -650,12 +654,10 @@ class ACMEClient(object):
|
|||
Return the certificate object as dict
|
||||
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"]
|
||||
dummy, out, dummy = self.module.run_command(openssl_csr_cmd, check_rc=True)
|
||||
|
||||
csr = pem_to_der(self.csr)
|
||||
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)
|
||||
|
||||
|
@ -883,6 +885,7 @@ def main():
|
|||
remaining_days=dict(required=False, default=10, type='int'),
|
||||
deactivate_authzs=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=(
|
||||
['account_key_src', 'account_key_content'],
|
||||
|
@ -895,6 +898,7 @@ def main():
|
|||
)
|
||||
if module._name == 'letsencrypt':
|
||||
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.
|
||||
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,
|
||||
or to revoke your certificates without knowing their private keys
|
||||
—, 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:
|
||||
description:
|
||||
- "One of the revocation reasonCodes defined in
|
||||
|
@ -80,16 +84,10 @@ RETURN = '''
|
|||
'''
|
||||
|
||||
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._text import to_native
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -104,6 +102,7 @@ def main():
|
|||
private_key_content=dict(type='str', no_log=True),
|
||||
certificate=dict(required=True, type='path'),
|
||||
revoke_reason=dict(required=False, type='int'),
|
||||
select_crypto_backend=dict(required=False, choices=['auto', 'openssl', 'cryptography'], default='auto', type='str'),
|
||||
),
|
||||
required_one_of=(
|
||||
['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'],
|
||||
|
@ -113,6 +112,7 @@ def main():
|
|||
),
|
||||
supports_check_mode=False,
|
||||
)
|
||||
set_crypto_backend(module)
|
||||
|
||||
if not module.params.get('validate_certs'):
|
||||
module.warn(warning='Disabling certificate validation for communications with ACME endpoint. ' +
|
||||
|
@ -122,22 +122,8 @@ def main():
|
|||
try:
|
||||
account = ACMEAccount(module)
|
||||
# Load certificate
|
||||
certificate_lines = []
|
||||
try:
|
||||
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)))
|
||||
certificate = pem_to_der(module.params.get('certificate'))
|
||||
certificate = nopad_b64(certificate)
|
||||
# Construct payload
|
||||
payload = {
|
||||
'certificate': certificate
|
||||
|
@ -152,24 +138,11 @@ def main():
|
|||
endpoint = account.directory['revokeCert']
|
||||
# Get hold of private key (if available) and make sure it comes from disk
|
||||
private_key = module.params.get('private_key_src')
|
||||
if module.params.get('private_key_content') is not 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('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()
|
||||
private_key_content = module.params.get('private_key_content')
|
||||
# Revoke certificate
|
||||
if private_key:
|
||||
if private_key or private_key_content:
|
||||
# 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:
|
||||
raise ModuleFailException("error while parsing private key: %s" % error)
|
||||
# Step 2: sign revokation request with private key
|
||||
|
@ -177,8 +150,7 @@ def main():
|
|||
"alg": private_key_data['alg'],
|
||||
"jwk": private_key_data['jwk'],
|
||||
}
|
||||
result, info = account.send_signed_request(endpoint, payload, key=private_key,
|
||||
key_data=private_key_data, jws_header=jws_header)
|
||||
result, info = account.send_signed_request(endpoint, payload, key_data=private_key_data, jws_header=jws_header)
|
||||
else:
|
||||
# Step 1: get hold of account URI
|
||||
changed = account.init_account(
|
||||
|
|
|
@ -8,9 +8,18 @@ class ModuleDocFragment(object):
|
|||
|
||||
# Standard files documentation fragment
|
||||
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:
|
||||
- "python >= 2.6"
|
||||
- openssl
|
||||
- "either openssl, ..."
|
||||
- "... or L(cryptography,https://cryptography.io/) >= 1.5"
|
||||
options:
|
||||
account_key_src:
|
||||
description:
|
||||
|
@ -32,6 +41,10 @@ options:
|
|||
important private key — it can be used to change the account key,
|
||||
or to revoke your certificates without knowing their private keys
|
||||
—, 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"
|
||||
acme_version:
|
||||
description:
|
||||
|
@ -64,5 +77,20 @@ options:
|
|||
for example when testing against a local Pebble server."
|
||||
type: bool
|
||||
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:
|
||||
- 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:
|
||||
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
|
||||
- name: Running tests with OpenSSL backend
|
||||
include_tasks: impl.yml
|
||||
vars:
|
||||
select_crypto_backend: openssl
|
||||
|
||||
- import_tasks: ../tests/validate.yml
|
||||
|
||||
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
||||
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:
|
||||
## 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:
|
||||
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
|
||||
- name: Running tests with OpenSSL backend
|
||||
include_tasks: impl.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
|
||||
select_crypto_backend: openssl
|
||||
|
||||
- import_tasks: ../tests/validate.yml
|
||||
|
||||
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
||||
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:
|
||||
## 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
|
||||
- name: Running tests with OpenSSL backend
|
||||
include_tasks: impl.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:
|
||||
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
|
||||
select_crypto_backend: openssl
|
||||
|
||||
- import_tasks: ../tests/validate.yml
|
||||
|
||||
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
||||
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 ################################################################################
|
||||
- name: ({{ certgen_title }}) Obtain cert, step 1
|
||||
acme_certificate:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
|
@ -41,6 +42,7 @@
|
|||
when: account_key_content is not defined
|
||||
- name: ({{ certgen_title }}) Obtain cert, step 1 (using account key data)
|
||||
acme_certificate:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
|
@ -96,6 +98,7 @@
|
|||
## ACME STEP 2 ################################################################################
|
||||
- name: ({{ certgen_title }}) Obtain cert, step 2
|
||||
acme_certificate:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
|
@ -115,6 +118,7 @@
|
|||
when: challenge_data is changed and account_key_content is not defined
|
||||
- name: ({{ certgen_title }}) Obtain cert, step 2 (using account key data)
|
||||
acme_certificate:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
|
|
Loading…
Reference in a new issue