mirror of
https://github.com/certbot/certbot.git
synced 2026-08-02 03:22:14 +02:00
add ip address support to certbot/acme lib (2021 ver) (#8904)
* add ip address support to acme saving * remove client-site check for ip address * using right prefix for san parsing * remove type hint for backward compatibility * remove bare ip blocking check from main_test * upppercase * lint tix * add additional tests for new IP support * support for ipv6 bare address * make apache and nginx plugin raise error for certs with ip address * linting * add pem file's last newline char * gen_ss_cert ip support and comment fixup * fix test coverage * indent fix and assetTrue to assetIN * indent mistake, made a note where class end * acme lib now receive IPs as separate list * fix typos * type 2 * fix tests * Deny IP address on certbot/certbot side as LE don't support it * remove excess empty line to rerun tox * comment indent and typo fix Apply suggestions from code review Co-authored-by: Brad Warren <bmw@users.noreply.github.com> * trim unused functions * trim unused import * make raw san list extraction as separate function * Apply suggestions from code review mostly comment suggestions here Co-authored-by: Brad Warren <bmw@users.noreply.github.com> * apply patches suggested on review. * remove excessive empty lines * update CHANGELOG.md * added acme lib update about ipaddress support in CHANGELOG.md Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
This commit is contained in:
co-authored by
Brad Warren
parent
694c03bd6a
commit
52e207a404
+5
-1
@@ -655,11 +655,15 @@ class ClientV2(ClientBase):
|
|||||||
csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)
|
csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr)
|
dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr)
|
||||||
|
ipNames = crypto_util._pyopenssl_cert_or_req_san_ip(csr)
|
||||||
|
# ipNames is now []string
|
||||||
identifiers = []
|
identifiers = []
|
||||||
for name in dnsNames:
|
for name in dnsNames:
|
||||||
identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_FQDN,
|
identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_FQDN,
|
||||||
value=name))
|
value=name))
|
||||||
|
for ips in ipNames:
|
||||||
|
identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_IP,
|
||||||
|
value=ips))
|
||||||
order = messages.NewOrder(identifiers=identifiers)
|
order = messages.NewOrder(identifiers=identifiers)
|
||||||
response = self._post(self.directory['newOrder'], order)
|
response = self._post(self.directory['newOrder'], order)
|
||||||
body = messages.Order.from_json(response.json())
|
body = messages.Order.from_json(response.json())
|
||||||
|
|||||||
+92
-24
@@ -187,23 +187,42 @@ def probe_sni(name, host, port=443, timeout=300, # pylint: disable=too-many-argu
|
|||||||
return client_ssl.get_peer_certificate()
|
return client_ssl.get_peer_certificate()
|
||||||
|
|
||||||
|
|
||||||
def make_csr(private_key_pem, domains, must_staple=False):
|
def make_csr(private_key_pem, domains=None, must_staple=False, ipaddrs=None):
|
||||||
"""Generate a CSR containing a list of domains as subjectAltNames.
|
"""Generate a CSR containing domains or IPs as subjectAltNames.
|
||||||
|
|
||||||
:param buffer private_key_pem: Private key, in PEM PKCS#8 format.
|
:param buffer private_key_pem: Private key, in PEM PKCS#8 format.
|
||||||
:param list domains: List of DNS names to include in subjectAltNames of CSR.
|
:param list domains: List of DNS names to include in subjectAltNames of CSR.
|
||||||
:param bool must_staple: Whether to include the TLS Feature extension (aka
|
:param bool must_staple: Whether to include the TLS Feature extension (aka
|
||||||
OCSP Must Staple: https://tools.ietf.org/html/rfc7633).
|
OCSP Must Staple: https://tools.ietf.org/html/rfc7633).
|
||||||
|
:param list ipaddrs: List of IPaddress(type ipaddress.IPv4Address or ipaddress.IPv6Address)
|
||||||
|
names to include in subbjectAltNames of CSR.
|
||||||
|
params ordered this way for backward competablity when called by positional argument.
|
||||||
:returns: buffer PEM-encoded Certificate Signing Request.
|
:returns: buffer PEM-encoded Certificate Signing Request.
|
||||||
"""
|
"""
|
||||||
private_key = crypto.load_privatekey(
|
private_key = crypto.load_privatekey(
|
||||||
crypto.FILETYPE_PEM, private_key_pem)
|
crypto.FILETYPE_PEM, private_key_pem)
|
||||||
csr = crypto.X509Req()
|
csr = crypto.X509Req()
|
||||||
|
sanlist = []
|
||||||
|
# if domain or ip list not supplied make it empty list so it's easier to iterate
|
||||||
|
if domains is None:
|
||||||
|
domains = []
|
||||||
|
if ipaddrs is None:
|
||||||
|
ipaddrs = []
|
||||||
|
if len(domains)+len(ipaddrs) == 0:
|
||||||
|
raise ValueError("At least one of domains or ipaddrs parameter need to be not empty")
|
||||||
|
for address in domains:
|
||||||
|
sanlist.append('DNS:' + address)
|
||||||
|
for ips in ipaddrs:
|
||||||
|
sanlist.append('IP:' + ips.exploded)
|
||||||
|
# make sure its ascii encoded
|
||||||
|
san_string = ', '.join(sanlist).encode('ascii')
|
||||||
|
# for IP san it's actually need to be octet-string,
|
||||||
|
# but somewhere downsteam thankfully handle it for us
|
||||||
extensions = [
|
extensions = [
|
||||||
crypto.X509Extension(
|
crypto.X509Extension(
|
||||||
b'subjectAltName',
|
b'subjectAltName',
|
||||||
critical=False,
|
critical=False,
|
||||||
value=', '.join('DNS:' + d for d in domains).encode('ascii')
|
value=san_string
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
if must_staple:
|
if must_staple:
|
||||||
@@ -220,6 +239,7 @@ def make_csr(private_key_pem, domains, must_staple=False):
|
|||||||
|
|
||||||
|
|
||||||
def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req):
|
def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req):
|
||||||
|
# unlike its name this only outputs DNS names, other type of idents will ignored
|
||||||
common_name = loaded_cert_or_req.get_subject().CN
|
common_name = loaded_cert_or_req.get_subject().CN
|
||||||
sans = _pyopenssl_cert_or_req_san(loaded_cert_or_req)
|
sans = _pyopenssl_cert_or_req_san(loaded_cert_or_req)
|
||||||
|
|
||||||
@@ -239,20 +259,56 @@ def _pyopenssl_cert_or_req_san(cert_or_req):
|
|||||||
:param cert_or_req: Certificate or CSR.
|
:param cert_or_req: Certificate or CSR.
|
||||||
:type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
|
:type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
|
||||||
|
|
||||||
:returns: A list of Subject Alternative Names.
|
:returns: A list of Subject Alternative Names that is DNS.
|
||||||
|
:rtype: `list` of `unicode`
|
||||||
|
|
||||||
|
"""
|
||||||
|
# This function finds SANs with dns name
|
||||||
|
|
||||||
|
# constants based on PyOpenSSL certificate/CSR text dump
|
||||||
|
part_separator = ":"
|
||||||
|
prefix = "DNS" + part_separator
|
||||||
|
|
||||||
|
sans_parts = _pyopenssl_extract_san_list_raw(cert_or_req)
|
||||||
|
|
||||||
|
return [part.split(part_separator)[1]
|
||||||
|
for part in sans_parts if part.startswith(prefix)]
|
||||||
|
|
||||||
|
|
||||||
|
def _pyopenssl_cert_or_req_san_ip(cert_or_req):
|
||||||
|
"""Get Subject Alternative Names IPs from certificate or CSR using pyOpenSSL.
|
||||||
|
|
||||||
|
:param cert_or_req: Certificate or CSR.
|
||||||
|
:type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
|
||||||
|
|
||||||
|
:returns: A list of Subject Alternative Names that are IP Addresses.
|
||||||
|
:rtype: `list` of `unicode`. note that this returns as string, not IPaddress object
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# constants based on PyOpenSSL certificate/CSR text dump
|
||||||
|
part_separator = ":"
|
||||||
|
prefix = "IP Address" + part_separator
|
||||||
|
|
||||||
|
sans_parts = _pyopenssl_extract_san_list_raw(cert_or_req)
|
||||||
|
|
||||||
|
return [part[len(prefix):] for part in sans_parts if part.startswith(prefix)]
|
||||||
|
|
||||||
|
|
||||||
|
def _pyopenssl_extract_san_list_raw(cert_or_req):
|
||||||
|
"""Get raw SAN string from cert or csr, parse it as UTF-8 and return.
|
||||||
|
|
||||||
|
:param cert_or_req: Certificate or CSR.
|
||||||
|
:type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
|
||||||
|
|
||||||
|
:returns: raw san strings, parsed byte as utf-8
|
||||||
:rtype: `list` of `unicode`
|
:rtype: `list` of `unicode`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# This function finds SANs by dumping the certificate/CSR to text and
|
# This function finds SANs by dumping the certificate/CSR to text and
|
||||||
# searching for "X509v3 Subject Alternative Name" in the text. This method
|
# searching for "X509v3 Subject Alternative Name" in the text. This method
|
||||||
# is used to support PyOpenSSL version 0.13 where the
|
# is used to because in PyOpenSSL version <0.17 `_subjectAltNameString` methods are
|
||||||
# `_subjectAltNameString` and `get_extensions` methods are not available
|
# not able to Parse IP Addresses in subjectAltName string.
|
||||||
# for CSRs.
|
|
||||||
|
|
||||||
# constants based on PyOpenSSL certificate/CSR text dump
|
|
||||||
part_separator = ":"
|
|
||||||
parts_separator = ", "
|
|
||||||
prefix = "DNS" + part_separator
|
|
||||||
|
|
||||||
if isinstance(cert_or_req, crypto.X509):
|
if isinstance(cert_or_req, crypto.X509):
|
||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
@@ -262,17 +318,17 @@ def _pyopenssl_cert_or_req_san(cert_or_req):
|
|||||||
text = func(crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8")
|
text = func(crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8")
|
||||||
# WARNING: this function does not support multiple SANs extensions.
|
# WARNING: this function does not support multiple SANs extensions.
|
||||||
# Multiple X509v3 extensions of the same type is disallowed by RFC 5280.
|
# Multiple X509v3 extensions of the same type is disallowed by RFC 5280.
|
||||||
match = re.search(r"X509v3 Subject Alternative Name:(?: critical)?\s*(.*)", text)
|
raw_san = re.search(r"X509v3 Subject Alternative Name:(?: critical)?\s*(.*)", text)
|
||||||
|
|
||||||
|
parts_separator = ", "
|
||||||
# WARNING: this function assumes that no SAN can include
|
# WARNING: this function assumes that no SAN can include
|
||||||
# parts_separator, hence the split!
|
# parts_separator, hence the split!
|
||||||
sans_parts = [] if match is None else match.group(1).split(parts_separator)
|
sans_parts = [] if raw_san is None else raw_san.group(1).split(parts_separator)
|
||||||
|
return sans_parts
|
||||||
return [part.split(part_separator)[1]
|
|
||||||
for part in sans_parts if part.startswith(prefix)]
|
|
||||||
|
|
||||||
|
|
||||||
def gen_ss_cert(key, domains, not_before=None,
|
def gen_ss_cert(key, domains=None, not_before=None,
|
||||||
validity=(7 * 24 * 60 * 60), force_san=True, extensions=None):
|
validity=(7 * 24 * 60 * 60), force_san=True, extensions=None, ips=None):
|
||||||
"""Generate new self-signed certificate.
|
"""Generate new self-signed certificate.
|
||||||
|
|
||||||
:type domains: `list` of `unicode`
|
:type domains: `list` of `unicode`
|
||||||
@@ -280,6 +336,7 @@ def gen_ss_cert(key, domains, not_before=None,
|
|||||||
:param bool force_san:
|
:param bool force_san:
|
||||||
:param extensions: List of additional extensions to include in the cert.
|
:param extensions: List of additional extensions to include in the cert.
|
||||||
:type extensions: `list` of `OpenSSL.crypto.X509Extension`
|
:type extensions: `list` of `OpenSSL.crypto.X509Extension`
|
||||||
|
:type ips: `list` of (`ipaddress.IPv4Address` or `ipaddress.IPv6Address`)
|
||||||
|
|
||||||
If more than one domain is provided, all of the domains are put into
|
If more than one domain is provided, all of the domains are put into
|
||||||
``subjectAltName`` X.509 extension and first domain is set as the
|
``subjectAltName`` X.509 extension and first domain is set as the
|
||||||
@@ -287,28 +344,39 @@ def gen_ss_cert(key, domains, not_before=None,
|
|||||||
extension is used, unless `force_san` is ``True``.
|
extension is used, unless `force_san` is ``True``.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
assert domains, "Must provide one or more hostnames for the cert."
|
assert domains or ips, "Must provide one or more hostnames or IPs for the cert."
|
||||||
|
|
||||||
cert = crypto.X509()
|
cert = crypto.X509()
|
||||||
cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16))
|
cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16))
|
||||||
cert.set_version(2)
|
cert.set_version(2)
|
||||||
|
|
||||||
if extensions is None:
|
if extensions is None:
|
||||||
extensions = []
|
extensions = []
|
||||||
|
if domains is None:
|
||||||
|
domains = []
|
||||||
|
if ips is None:
|
||||||
|
ips = []
|
||||||
extensions.append(
|
extensions.append(
|
||||||
crypto.X509Extension(
|
crypto.X509Extension(
|
||||||
b"basicConstraints", True, b"CA:TRUE, pathlen:0"),
|
b"basicConstraints", True, b"CA:TRUE, pathlen:0"),
|
||||||
)
|
)
|
||||||
|
|
||||||
cert.get_subject().CN = domains[0]
|
if len(domains) > 0:
|
||||||
|
cert.get_subject().CN = domains[0]
|
||||||
# TODO: what to put into cert.get_subject()?
|
# TODO: what to put into cert.get_subject()?
|
||||||
cert.set_issuer(cert.get_subject())
|
cert.set_issuer(cert.get_subject())
|
||||||
|
|
||||||
if force_san or len(domains) > 1:
|
sanlist = []
|
||||||
|
for address in domains:
|
||||||
|
sanlist.append('DNS:' + address)
|
||||||
|
for ip in ips:
|
||||||
|
sanlist.append('IP:' + ip.exploded)
|
||||||
|
san_string = ', '.join(sanlist).encode('ascii')
|
||||||
|
if force_san or len(domains) > 1 or len(ips) > 0:
|
||||||
extensions.append(crypto.X509Extension(
|
extensions.append(crypto.X509Extension(
|
||||||
b"subjectAltName",
|
b"subjectAltName",
|
||||||
critical=False,
|
critical=False,
|
||||||
value=b", ".join(b"DNS:" + d.encode() for d in domains)
|
value=san_string
|
||||||
))
|
))
|
||||||
|
|
||||||
cert.add_extensions(extensions)
|
cert.add_extensions(extensions)
|
||||||
|
|||||||
@@ -172,7 +172,9 @@ STATUS_DEACTIVATED = Status('deactivated')
|
|||||||
class IdentifierType(_Constant):
|
class IdentifierType(_Constant):
|
||||||
"""ACME identifier type."""
|
"""ACME identifier type."""
|
||||||
POSSIBLE_NAMES: Dict[str, 'IdentifierType'] = {}
|
POSSIBLE_NAMES: Dict[str, 'IdentifierType'] = {}
|
||||||
|
# class def ends here
|
||||||
IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder
|
IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder
|
||||||
|
IDENTIFIER_IP = IdentifierType('ip') # IdentifierIP in pebble - not in Boulder yet
|
||||||
|
|
||||||
|
|
||||||
class Identifier(jose.JSONObjectWithFields):
|
class Identifier(jose.JSONObjectWithFields):
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""ACME utilities."""
|
"""ACME utilities."""
|
||||||
|
|
||||||
|
|
||||||
def map_keys(dikt, func):
|
def map_keys(dikt, func):
|
||||||
"""Map dictionary keys."""
|
"""Map dictionary keys."""
|
||||||
return {func(key): value for key, value in dikt.items()}
|
return {func(key): value for key, value in dikt.items()}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import http.client as http_client
|
import http.client as http_client
|
||||||
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
@@ -23,6 +24,7 @@ import test_util
|
|||||||
CERT_DER = test_util.load_vector('cert.der')
|
CERT_DER = test_util.load_vector('cert.der')
|
||||||
CERT_SAN_PEM = test_util.load_vector('cert-san.pem')
|
CERT_SAN_PEM = test_util.load_vector('cert-san.pem')
|
||||||
CSR_SAN_PEM = test_util.load_vector('csr-san.pem')
|
CSR_SAN_PEM = test_util.load_vector('csr-san.pem')
|
||||||
|
CSR_MIXED_PEM = test_util.load_vector('csr-mixed.pem')
|
||||||
KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
|
KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
|
||||||
KEY2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
|
KEY2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
|
||||||
|
|
||||||
@@ -740,7 +742,7 @@ class ClientV2Test(ClientTestBase):
|
|||||||
self.orderr = messages.OrderResource(
|
self.orderr = messages.OrderResource(
|
||||||
body=self.order,
|
body=self.order,
|
||||||
uri='https://www.letsencrypt-demo.org/acme/acct/1/order/1',
|
uri='https://www.letsencrypt-demo.org/acme/acct/1/order/1',
|
||||||
authorizations=[self.authzr, self.authzr2], csr_pem=CSR_SAN_PEM)
|
authorizations=[self.authzr, self.authzr2], csr_pem=CSR_MIXED_PEM)
|
||||||
|
|
||||||
def test_new_account(self):
|
def test_new_account(self):
|
||||||
self.response.status_code = http_client.CREATED
|
self.response.status_code = http_client.CREATED
|
||||||
@@ -770,7 +772,7 @@ class ClientV2Test(ClientTestBase):
|
|||||||
|
|
||||||
with mock.patch('acme.client.ClientV2._post_as_get') as mock_post_as_get:
|
with mock.patch('acme.client.ClientV2._post_as_get') as mock_post_as_get:
|
||||||
mock_post_as_get.side_effect = (authz_response, authz_response2)
|
mock_post_as_get.side_effect = (authz_response, authz_response2)
|
||||||
self.assertEqual(self.client.new_order(CSR_SAN_PEM), self.orderr)
|
self.assertEqual(self.client.new_order(CSR_MIXED_PEM), self.orderr)
|
||||||
|
|
||||||
@mock.patch('acme.client.datetime')
|
@mock.patch('acme.client.datetime')
|
||||||
def test_poll_and_finalize(self, mock_datetime):
|
def test_poll_and_finalize(self, mock_datetime):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Tests for acme.crypto_util."""
|
"""Tests for acme.crypto_util."""
|
||||||
import itertools
|
import itertools
|
||||||
|
import ipaddress
|
||||||
import socket
|
import socket
|
||||||
import socketserver
|
import socketserver
|
||||||
import threading
|
import threading
|
||||||
@@ -108,7 +109,6 @@ class PyOpenSSLCertOrReqAllNamesTest(unittest.TestCase):
|
|||||||
class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
|
class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
|
||||||
"""Test for acme.crypto_util._pyopenssl_cert_or_req_san."""
|
"""Test for acme.crypto_util._pyopenssl_cert_or_req_san."""
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _call(cls, loader, name):
|
def _call(cls, loader, name):
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
@@ -174,9 +174,50 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
|
|||||||
['chicago-cubs.venafi.example', 'cubs.venafi.example'])
|
['chicago-cubs.venafi.example', 'cubs.venafi.example'])
|
||||||
|
|
||||||
|
|
||||||
|
class PyOpenSSLCertOrReqSANIPTest(unittest.TestCase):
|
||||||
|
"""Test for acme.crypto_util._pyopenssl_cert_or_req_san_ip."""
|
||||||
|
|
||||||
class RandomSnTest(unittest.TestCase):
|
@classmethod
|
||||||
"""Test for random certificate serial numbers."""
|
def _call(cls, loader, name):
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
from acme.crypto_util import _pyopenssl_cert_or_req_san_ip
|
||||||
|
return _pyopenssl_cert_or_req_san_ip(loader(name))
|
||||||
|
|
||||||
|
def _call_cert(self, name):
|
||||||
|
return self._call(test_util.load_cert, name)
|
||||||
|
|
||||||
|
def _call_csr(self, name):
|
||||||
|
return self._call(test_util.load_csr, name)
|
||||||
|
|
||||||
|
def test_cert_no_sans(self):
|
||||||
|
self.assertEqual(self._call_cert('cert.pem'), [])
|
||||||
|
|
||||||
|
def test_csr_no_sans(self):
|
||||||
|
self.assertEqual(self._call_csr('csr-nosans.pem'), [])
|
||||||
|
|
||||||
|
def test_cert_domain_sans(self):
|
||||||
|
self.assertEqual(self._call_cert('cert-san.pem'), [])
|
||||||
|
|
||||||
|
def test_csr_domain_sans(self):
|
||||||
|
self.assertEqual(self._call_csr('csr-san.pem'), [])
|
||||||
|
|
||||||
|
def test_cert_ip_two_sans(self):
|
||||||
|
self.assertEqual(self._call_cert('cert-ipsans.pem'), ['192.0.2.145', '203.0.113.1'])
|
||||||
|
|
||||||
|
def test_csr_ip_two_sans(self):
|
||||||
|
self.assertEqual(self._call_csr('csr-ipsans.pem'), ['192.0.2.145', '203.0.113.1'])
|
||||||
|
|
||||||
|
def test_csr_ipv6_sans(self):
|
||||||
|
self.assertEqual(self._call_csr('csr-ipv6sans.pem'),
|
||||||
|
['0:0:0:0:0:0:0:1', 'A3BE:32F3:206E:C75D:956:CEE:9858:5EC5'])
|
||||||
|
|
||||||
|
def test_cert_ipv6_sans(self):
|
||||||
|
self.assertEqual(self._call_cert('cert-ipv6sans.pem'),
|
||||||
|
['0:0:0:0:0:0:0:1', 'A3BE:32F3:206E:C75D:956:CEE:9858:5EC5'])
|
||||||
|
|
||||||
|
|
||||||
|
class GenSsCertTest(unittest.TestCase):
|
||||||
|
"""Test for gen_ss_cert (generation of self-signed cert)."""
|
||||||
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -187,11 +228,19 @@ class RandomSnTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_sn_collisions(self):
|
def test_sn_collisions(self):
|
||||||
from acme.crypto_util import gen_ss_cert
|
from acme.crypto_util import gen_ss_cert
|
||||||
|
|
||||||
for _ in range(self.cert_count):
|
for _ in range(self.cert_count):
|
||||||
cert = gen_ss_cert(self.key, ['dummy'], force_san=True)
|
cert = gen_ss_cert(self.key, ['dummy'], force_san=True,
|
||||||
|
ips=[ipaddress.ip_address("10.10.10.10")])
|
||||||
self.serial_num.append(cert.get_serial_number())
|
self.serial_num.append(cert.get_serial_number())
|
||||||
self.assertGreater(len(set(self.serial_num)), 1)
|
self.assertGreaterEqual(len(set(self.serial_num)), self.cert_count)
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_name(self):
|
||||||
|
from acme.crypto_util import gen_ss_cert
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
gen_ss_cert(self.key, ips=[ipaddress.ip_address("1.1.1.1")])
|
||||||
|
gen_ss_cert(self.key)
|
||||||
|
|
||||||
|
|
||||||
class MakeCSRTest(unittest.TestCase):
|
class MakeCSRTest(unittest.TestCase):
|
||||||
"""Test for standalone functions."""
|
"""Test for standalone functions."""
|
||||||
@@ -223,6 +272,27 @@ class MakeCSRTest(unittest.TestCase):
|
|||||||
).get_data(),
|
).get_data(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_make_csr_ip(self):
|
||||||
|
csr_pem = self._call_with_key(["a.example"], False, [ipaddress.ip_address('127.0.0.1'), ipaddress.ip_address('::1')])
|
||||||
|
self.assertIn(b'--BEGIN CERTIFICATE REQUEST--' , csr_pem)
|
||||||
|
self.assertIn(b'--END CERTIFICATE REQUEST--' , csr_pem)
|
||||||
|
csr = OpenSSL.crypto.load_certificate_request(
|
||||||
|
OpenSSL.crypto.FILETYPE_PEM, csr_pem)
|
||||||
|
# In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't
|
||||||
|
# have a get_extensions() method, so we skip this test if the method
|
||||||
|
# isn't available.
|
||||||
|
if hasattr(csr, 'get_extensions'):
|
||||||
|
self.assertEqual(len(csr.get_extensions()), 1)
|
||||||
|
self.assertEqual(csr.get_extensions()[0].get_data(),
|
||||||
|
OpenSSL.crypto.X509Extension(
|
||||||
|
b'subjectAltName',
|
||||||
|
critical=False,
|
||||||
|
value=b'DNS:a.example, IP:127.0.0.1, IP:::1',
|
||||||
|
).get_data(),
|
||||||
|
)
|
||||||
|
# for IP san it's actually need to be octet-string,
|
||||||
|
# but somewhere downstream thankfully handle it for us
|
||||||
|
|
||||||
def test_make_csr_must_staple(self):
|
def test_make_csr_must_staple(self):
|
||||||
csr_pem = self._call_with_key(["a.example"], must_staple=True)
|
csr_pem = self._call_with_key(["a.example"], must_staple=True)
|
||||||
csr = OpenSSL.crypto.load_certificate_request(
|
csr = OpenSSL.crypto.load_certificate_request(
|
||||||
@@ -241,6 +311,9 @@ class MakeCSRTest(unittest.TestCase):
|
|||||||
self.assertEqual(len(must_staple_exts), 1,
|
self.assertEqual(len(must_staple_exts), 1,
|
||||||
"Expected exactly one Must Staple extension")
|
"Expected exactly one Must Staple extension")
|
||||||
|
|
||||||
|
def test_make_csr_without_hostname(self):
|
||||||
|
self.assertRaises(ValueError, self._call_with_key)
|
||||||
|
|
||||||
|
|
||||||
class DumpPyopensslChainTest(unittest.TestCase):
|
class DumpPyopensslChainTest(unittest.TestCase):
|
||||||
"""Test for dump_pyopenssl_chain."""
|
"""Test for dump_pyopenssl_chain."""
|
||||||
|
|||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDizCCAnOgAwIBAgIIPNBLQXwhoUkwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
|
||||||
|
AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxNzNiMjYwHhcNMjAwNTI5MTkxODA5
|
||||||
|
WhcNMjUwNTI5MTkxODA5WjAWMRQwEgYDVQQDEwsxOTIuMC4yLjE0NTCCASIwDQYJ
|
||||||
|
KoZIhvcNAQEBBQADggEPADCCAQoCggEBALyChb+NDA26GF1AfC0nzEdfOTchKw0h
|
||||||
|
q41xEjonvg5UXgZf/aH/ntvugIkYP0MaFifNAjebOVVsemEVEtyWcUKTfBHKZGbZ
|
||||||
|
ukTDwFIjfTccCfo6U/B2H7ZLzJIywl8DcUw9DypadeQBm8PS0VVR2ncy73dvaqym
|
||||||
|
crhAwlASyXU0mhLqRDMMxfg5Bn/FWpcsIcDpLmPn8Q/FvdRc2t5ryBNw/aWOlwqT
|
||||||
|
Oy16nbfLj2T0zG1A3aPuD+eT/JFUe/o3K7R+FAx7wt+RziQO46wLVVF1SueZUrIU
|
||||||
|
zqN04Gl8Kt1WM2SniZ0gq/rORUNcPtT0NAEsEslTQfA+Trq6j2peqyMCAwEAAaOB
|
||||||
|
yjCBxzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
|
||||||
|
BwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFHj1mwZzP//nMIH2i58NRUl/arHn
|
||||||
|
MB8GA1UdIwQYMBaAFF5DVAKabvIUvKFHGouscA2Qdpe6MDEGCCsGAQUFBwEBBCUw
|
||||||
|
IzAhBggrBgEFBQcwAYYVaHR0cDovLzEyNy4wLjAuMTo0MDAyMBUGA1UdEQQOMAyH
|
||||||
|
BMAAApGHBMsAcQEwDQYJKoZIhvcNAQELBQADggEBAHjSgDg76/UCIMSYddyhj18r
|
||||||
|
LdNKjA7p8ovnErSkebFT4lIZ9f3Sma9moNr0w64M33NamuFyHe/KTdk90mvoW8Uu
|
||||||
|
26aDekiRIeeMakzbAtDKn67tt2tbedKIYRATcSYVwsV46uZKbM621dZKIjjxOWpo
|
||||||
|
IY6rZYrku8LYhoXJXOqRduV3cTRVuTm5bBa9FfVNtt6N1T5JOtKKDEhuSaF4RSug
|
||||||
|
PDy3hQIiHrVvhPfVrXU3j6owz/8UCS5549inES9ONTFrvM9o0H1R/MsmGNXR5hF5
|
||||||
|
iJqHKC7n8LZujhVnoFIpHu2Dsiefbfr+yRYJS4I+ezy6Nq/Ok8rc8zp0eoX+uyY=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDmzCCAoOgAwIBAgIIFdxeZP+v2rgwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
|
||||||
|
AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA0M2M5NTcwHhcNMjAwNTMwMDQwNzMw
|
||||||
|
WhcNMjUwNTMwMDQwNzMwWjAOMQwwCgYDVQQDEwM6OjEwggEiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4IBDwAwggEKAoIBAQC7VidVduJvqKtrSH0fw6PjE0cqL4Kfzo7klexWUkHG
|
||||||
|
KVAa0fRVZFZ462jxKOt417V2U4WJQ6WHHO9PJ+3gW62d/MhCw8FRtUQS4nYFjqB6
|
||||||
|
32+RFU21VRN7cWoQEqSwnEPbh/v/zv/KS5JhQ+swWUo79AOLm1kjnZWCKtcqh1Lc
|
||||||
|
Ug5Tkpot6luoxTKp52MkchvXDpj0q2B/XpLJ8/pw5cqjv7mH12EDOK2HXllA+WwX
|
||||||
|
ZpstcEhaA4FqtaHOW/OHnwTX5MUbINXE5YYHVEDR6moVM31/W/3pe9NDUMTDE7Si
|
||||||
|
lVQnZbXM9NYbzZqlh+WhemDWwnIfGI6rtsfNEiirVEOlAgMBAAGjgeIwgd8wDgYD
|
||||||
|
VR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV
|
||||||
|
HRMBAf8EAjAAMB0GA1UdDgQWBBS8DL+MZfDIy6AKky69Tgry2Vxq5DAfBgNVHSME
|
||||||
|
GDAWgBRAsFqVenRRKgB1YPzWKzb9bzZ/ozAxBggrBgEFBQcBAQQlMCMwIQYIKwYB
|
||||||
|
BQUHMAGGFWh0dHA6Ly8xMjcuMC4wLjE6NDAwMjAtBgNVHREEJjAkhxAAAAAAAAAA
|
||||||
|
AAAAAAAAAAABhxCjvjLzIG7HXQlWDO6YWF7FMA0GCSqGSIb3DQEBCwUAA4IBAQBY
|
||||||
|
M9UTZ3uaKMQ+He9kWR3p9jh6hTSD0FNi79ZdfkG0lgSzhhduhN7OhzQH2ihUUfa6
|
||||||
|
rtKTw74fGbszhizCd9UB8YPKlm3si1Xbg6ZUQlA1RtoQo7RUGEa6ZbR68PKGm9Go
|
||||||
|
hTTFIl/JS8jzxBR8jywZdyqtprUx+nnNUDiNk0hJtFLhw7OJH0AHlAUNqHsfD08m
|
||||||
|
HXRdaV6q14HXU5g31slBat9H4D6tCU/2uqBURwW0wVdnqh4QeRfAeqiatJS9EmSF
|
||||||
|
ctbc7n894Idy2Xce7NFoIy5cht3m6Rd42o/LmBsJopBmQcDPZT70/XzRtc2qE0cS
|
||||||
|
CzBIGQHUJ6BfmBjrCQnp
|
||||||
|
-----END CERTIFICATE-----
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN CERTIFICATE REQUEST-----
|
||||||
|
MIICbTCCAVUCAQIwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKT/
|
||||||
|
CE7Y5EYBvI4p7Frt763upIKHDHO/R5/TWMjG8Jm9qTMui8sbMgyh2Yh+lR/j/5Xd
|
||||||
|
tQrhgC6wx10MrW2+3JtYS88HP1p6si8zU1dbK34n3NyyklR2RivW0R7dXgnYNy7t
|
||||||
|
5YcDYLCrbRMIPINV/uHrmzIHWYUDNcZVdAfIM2AHfKYuV6Mepcn///5GR+l4GcAh
|
||||||
|
Nkf9CW8OdAIuKdbyLCxVr0mUW/vJz1b12uxPsgUdax9sjXgZdT4pfMXADsFd1NeF
|
||||||
|
atpsXU073inqtHru+2F9ijHTQ75TC+u/rr6eYl3BnBntac0gp/ADtDBii7/Q1JOO
|
||||||
|
Bhq7xJNqqxIEdiyM7zcCAwEAAaAoMCYGCSqGSIb3DQEJDjEZMBcwFQYDVR0RBA4w
|
||||||
|
DIcEwAACkYcEywBxATANBgkqhkiG9w0BAQsFAAOCAQEADG5g3zdbSCaXpZhWHkzE
|
||||||
|
Mek3f442TUE1pB+ITRpthmM4N3zZWETYmbLCIAO624uMrRnbCCMvAoLs/L/9ETg/
|
||||||
|
XMMFtonQC8u9i9tV8B1ceBh8lpIfa+8b9TMWH3bqnrbWQ+YIl+Yd0gXiCZWJ9vK4
|
||||||
|
eM1Gddu/2bR6s/k4h/XAWRgEexqk57EHr1z0N+T9OoX939n3mVcNI+u9kfd5VJ0z
|
||||||
|
VyA3R8WR6T6KlEl5P5pcWe5Kuyhi7xMmLVImXqBtvKq4O1AMfM+gQr/yn9aE8IRq
|
||||||
|
khP7JrMBLUIub1c/qu2TfvnynNPSM/ZcOX+6PHdHmRkR3nI0Ndpv7Ntv31FTplAm
|
||||||
|
Dw==
|
||||||
|
-----END CERTIFICATE REQUEST-----
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN CERTIFICATE REQUEST-----
|
||||||
|
MIIChTCCAW0CAQIwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOIc
|
||||||
|
UAppcqJfTkSqqOFqGt1v7lIJZPOcF4bcKI3d5cHAGbOuVxbC7uMaDuObwYLzoiED
|
||||||
|
qnvs1NaEq2phO6KsgGESB7IE2LUjJivO7OnSZjNRpL5si/9egvBiNCn/50lULaWG
|
||||||
|
gLEuyMfk3awZy2mVAymy7Grhbx069A4TH8TqsHuq2RpKyuDL27e/jUt6yYecb3pu
|
||||||
|
hWMiWy3segif4tI46pkOW0/I6DpxyYD2OqOvzxm/voS9RMqE2+7YJA327H7bEi3N
|
||||||
|
lJZEZ1zy7clZ9ga5fBQaetzbg2RyxTrZ7F919NQXSFoXgxb10Eg64wIpz0L3ooCm
|
||||||
|
GEHehsZZexa3J5ccIvMCAwEAAaBAMD4GCSqGSIb3DQEJDjExMC8wLQYDVR0RBCYw
|
||||||
|
JIcQAAAAAAAAAAAAAAAAAAAAAYcQo74y8yBux10JVgzumFhexTANBgkqhkiG9w0B
|
||||||
|
AQsFAAOCAQEALvwVn0A/JPTCiNzcozHFnp5M23C9PXCplWc5u4k34d4XXzpSeFDz
|
||||||
|
fL4gy7NpYIueme2K2ppw2j3PNQUdR6vQ5a75sriegWYrosL+7Q6Joh51ZyEUZQoD
|
||||||
|
mNl4M4S4oX85EaChR6NFGBywTfjFarYi32XBTbFE7rK8N8KM+DQkNdwL1MXqaHWz
|
||||||
|
F1obQKpNXlLedbCBOteV5Eg4zG3565zu/Gw/NhwzzV3mQmgxUcd1sMJxAfHQz4Vl
|
||||||
|
ImLL+xMcR03nDsH2bgtDbK2tJm7WszSxA9tC+Xp2lRewxrnQloRWPYDz177WGQ5Q
|
||||||
|
SoGDzTTtA6uWZxG8h7CkNLOGvA8LtU2rNA==
|
||||||
|
-----END CERTIFICATE REQUEST-----
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN CERTIFICATE REQUEST-----
|
||||||
|
MIICdjCCAV4CAQIwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMXq
|
||||||
|
v1y8EIcCbaUIzCtOcLkLS0MJ35oS+6DmV5WB1A0cIk6YrjsHIsY2lwMm13BWIvmw
|
||||||
|
tY+Y6n0rr7eViNx5ZRGHpHEI/TL3Neb+VefTydL5CgvK3dd4ex2kSbTaed3fmpOx
|
||||||
|
qMajEduwNcZPCcmoEXPkfrCP8w2vKQUkQ+JRPcdX1nTuzticeRP5B7YCmJsmxkEh
|
||||||
|
Y0tzzZ+NIRDARoYNofefY86h3e5q66gtJxccNchmIM3YQahhg5n3Xoo8hGfM/TIc
|
||||||
|
R7ncCBCLO6vtqo0QFva/NQODrgOmOsmgvqPkUWQFdZfWM8yIaU826dktx0CPB78t
|
||||||
|
TudnJ1rBRvGsjHMsZikCAwEAAaAxMC8GCSqGSIb3DQEJDjEiMCAwHgYDVR0RBBcw
|
||||||
|
FYINYS5leGVtcGxlLmNvbYcEwAACbzANBgkqhkiG9w0BAQsFAAOCAQEAdGMcRCxq
|
||||||
|
1X09gn1TNdMt64XUv+wdJCKDaJ+AgyIJj7QvVw8H5k7dOnxS4I+a/yo4jE+LDl2/
|
||||||
|
AuHcBLFEI4ddewdJSMrTNZjuRYuOdr3KP7fL7MffICSBi45vw5EOXg0tnjJCEiKu
|
||||||
|
6gcJgbLSP5JMMd7Haf33Q/VWsmHofR3VwOMdrnakwAU3Ff5WTuXTNVhL1kT/uLFX
|
||||||
|
yW1ru6BF4unwNqSR2UeulljpNfRBsiN4zJK11W6n9KT0NkBr9zY5WCM4sW7i8k9V
|
||||||
|
TeypWGo3jBKzYAGeuxZsB97U77jZ2lrGdBLZKfbcjnTeRVqCvCRrui4El7UGYFmj
|
||||||
|
7s6OJyWx5DSV8w==
|
||||||
|
-----END CERTIFICATE REQUEST-----
|
||||||
@@ -9,6 +9,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
|
|||||||
* The certbot-dns-rfc2136 plugin always assumed the use of an IP address as the
|
* The certbot-dns-rfc2136 plugin always assumed the use of an IP address as the
|
||||||
target server, but this was never checked. Until now. The plugin raises an error
|
target server, but this was never checked. Until now. The plugin raises an error
|
||||||
if the configured target server is not a valid IPv4 or IPv6 address.
|
if the configured target server is not a valid IPv4 or IPv6 address.
|
||||||
|
* Our acme library now supports requesting certificates for IP addresses.
|
||||||
|
This feature is still unsupported by Certbot and Let's Encrypt.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
+25
-7
@@ -483,6 +483,7 @@ def enforce_le_validity(domain):
|
|||||||
Encrypt currently will not issue certificates
|
Encrypt currently will not issue certificates
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
domain = enforce_domain_sanity(domain)
|
domain = enforce_domain_sanity(domain)
|
||||||
if not re.match("^[A-Za-z0-9.-]*$", domain):
|
if not re.match("^[A-Za-z0-9.-]*$", domain):
|
||||||
raise errors.ConfigurationError(
|
raise errors.ConfigurationError(
|
||||||
@@ -541,17 +542,11 @@ def enforce_domain_sanity(domain):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Explain separately that IP addresses aren't allowed (apart from not
|
if is_ipaddress(domain):
|
||||||
# being FQDNs) because hope springs eternal concerning this point
|
|
||||||
try:
|
|
||||||
socket.inet_aton(domain)
|
|
||||||
raise errors.ConfigurationError(
|
raise errors.ConfigurationError(
|
||||||
"Requested name {0} is an IP address. The Let's Encrypt "
|
"Requested name {0} is an IP address. The Let's Encrypt "
|
||||||
"certificate authority will not issue certificates for a "
|
"certificate authority will not issue certificates for a "
|
||||||
"bare IP address.".format(domain))
|
"bare IP address.".format(domain))
|
||||||
except socket.error:
|
|
||||||
# It wasn't an IP address, so that's good
|
|
||||||
pass
|
|
||||||
|
|
||||||
# FQDN checks according to RFC 2181: domain name should be less than 255
|
# FQDN checks according to RFC 2181: domain name should be less than 255
|
||||||
# octets (inclusive). And each label is 1 - 63 octets (inclusive).
|
# octets (inclusive). And each label is 1 - 63 octets (inclusive).
|
||||||
@@ -569,6 +564,29 @@ def enforce_domain_sanity(domain):
|
|||||||
return domain
|
return domain
|
||||||
|
|
||||||
|
|
||||||
|
def is_ipaddress(address):
|
||||||
|
"""Is given address string form of IP(v4 or v6) address?
|
||||||
|
|
||||||
|
:param address: address to check
|
||||||
|
:type address: `str` or `unicode`
|
||||||
|
|
||||||
|
:returns: True if address is valid IP address, otherwise return False.
|
||||||
|
:rtype: bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
socket.inet_pton(socket.AF_INET, address)
|
||||||
|
# If this line runs it was ip address (ipv4)
|
||||||
|
return True
|
||||||
|
except socket.error:
|
||||||
|
# It wasn't an IPv4 address, so try ipv6
|
||||||
|
try:
|
||||||
|
socket.inet_pton(socket.AF_INET6, address)
|
||||||
|
return True
|
||||||
|
except socket.error:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_wildcard_domain(domain):
|
def is_wildcard_domain(domain):
|
||||||
""""Is domain a wildcard domain?
|
""""Is domain a wildcard domain?
|
||||||
|
|
||||||
|
|||||||
@@ -1000,6 +1000,10 @@ class MainTest(test_util.ConfigTestCase):
|
|||||||
self.assertRaises(errors.ConfigurationError,
|
self.assertRaises(errors.ConfigurationError,
|
||||||
self._call,
|
self._call,
|
||||||
['-d', '204.11.231.35'])
|
['-d', '204.11.231.35'])
|
||||||
|
# Bare IPv6 address
|
||||||
|
self.assertRaises(errors.ConfigurationError,
|
||||||
|
self._call,
|
||||||
|
['-d', '2001:db8:ac69:3ff:b1cb:c8c6:5a84:a31b'])
|
||||||
|
|
||||||
def test_csr_with_besteffort(self):
|
def test_csr_with_besteffort(self):
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
|
|||||||
Reference in New Issue
Block a user