letsencrypt: Add support for Elliptic Curve account keys (#34852)

pull/34953/head
Felix Fontein 7 years ago committed by René Moser
parent 8d69eb4488
commit c42c0f0cb3

@ -43,14 +43,16 @@ requirements:
options: options:
account_key_src: account_key_src:
description: description:
- "Path to a file containing the Let's Encrypt account RSA key." - "Path to a file containing the Let's Encrypt account RSA or Elliptic Curve
- "Can be created with C(openssl rsa ...)." key."
- "RSA keys can be created with C(openssl rsa ...). Elliptic curve keys can
be created with C(openssl ecparam -genkey ...)."
- "Mutually exclusive with C(account_key_content)." - "Mutually exclusive with C(account_key_content)."
- "Required if C(account_key_content) is not used." - "Required if C(account_key_content) is not used."
aliases: [ account_key ] aliases: [ account_key ]
account_key_content: account_key_content:
description: description:
- "Content of the Let's Encrypt account RSA key." - "Content of the Let's Encrypt account RSA or Elliptic Curve key."
- "Mutually exclusive with C(account_key_src)." - "Mutually exclusive with C(account_key_src)."
- "Required if C(account_key_src) is not used." - "Required if C(account_key_src) is not used."
version_added: "2.5" version_added: "2.5"
@ -374,14 +376,12 @@ class ACMEAccount(object):
module.fail_json(msg="failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc()) module.fail_json(msg="failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
f.close() f.close()
pub_hex, pub_exp = self._parse_account_key(self.key) error, self.key_data = self._parse_account_key(self.key)
if error:
module.fail_json(msg="error while parsing account key: %s" % error)
self.jws_header = { self.jws_header = {
"alg": "RS256", "alg": self.key_data['alg'],
"jwk": { "jwk": self.key_data['jwk'],
"e": nopad_b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
"kty": "RSA",
"n": nopad_b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
},
} }
self.init_account() self.init_account()
@ -396,20 +396,82 @@ class ACMEAccount(object):
def _parse_account_key(self, key): def _parse_account_key(self, key):
''' '''
Parses an RSA key file in PEM format and returns the modulus Parses an RSA or Elliptic Curve key file in PEM format and returns a pair
and public exponent of the key (error, key_data).
''' '''
openssl_keydump_cmd = [self._openssl_bin, "rsa", "-in", key, "-noout", "-text"] account_key_type = None
_, out, _ = self.module.run_command(openssl_keydump_cmd, check_rc=True) 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 not in ("rsa", "ec"):
return 'unknown key type "%s" % account_key_type', {}
pub_hex, pub_exp = re.search( openssl_keydump_cmd = [self._openssl_bin, account_key_type, "-in", key, "-noout", "-text"]
r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)", _, out, _ = self.module.run_command(openssl_keydump_cmd, check_rc=True)
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 pub_hex, pub_exp 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"))
curve = pub_data.group(3).lower()
if curve == 'p-256':
bits = 256
alg = 'ES256'
hash = 'sha256'
point_size = 32
elif curve == 'p-384':
bits = 384
alg = 'ES384'
hash = 'sha384'
point_size = 48
elif 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
else:
return 'unknown elliptic curve: %s' % curve, {}
bytes = (bits + 7) // 8
if len(pub_hex) != 2 * bytes:
return 'bad elliptic curve point (%s)' % curve, {}
return None, {
'type': 'ec',
'alg': alg,
'jwk': {
"kty": "EC",
"crv": curve.upper(),
"x": nopad_b64(pub_hex[:bytes]),
"y": nopad_b64(pub_hex[bytes:]),
},
'hash': hash,
'point_size': point_size,
}
def send_signed_request(self, url, payload): def send_signed_request(self, url, payload):
''' '''
@ -426,10 +488,26 @@ class ACMEAccount(object):
except Exception as e: except Exception as e:
self.module.fail_json(msg="Failed to encode payload / headers as JSON: {0}".format(e)) self.module.fail_json(msg="Failed to encode payload / headers as JSON: {0}".format(e))
openssl_sign_cmd = [self._openssl_bin, "dgst", "-sha256", "-sign", self.key] openssl_sign_cmd = [self._openssl_bin, "dgst", "-{0}".format(self.key_data['hash']), "-sign", self.key]
sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8') sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
_, out, _ = self.module.run_command(openssl_sign_cmd, data=sign_payload, check_rc=True, binary_data=True) _, out, _ = self.module.run_command(openssl_sign_cmd, data=sign_payload, check_rc=True, binary_data=True)
if self.key_data['type'] == 'ec':
_, der_out, _ = self.module.run_command(
[self._openssl_bin, "asn1parse", "-inform", "DER"],
data=out, binary_data=True)
expected_len = 2 * self.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:
self.module.fail_json(
msg="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])
data = self.module.jsonify({ data = self.module.jsonify({
"header": self.jws_header, "header": self.jws_header,
"protected": protected64, "protected": protected64,

Loading…
Cancel
Save