Merge branch 'master' into fix-cves

This commit is contained in:
Hossein Shafagh 2020-02-20 13:42:00 -08:00 committed by GitHub
commit 2bcb842d93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 90 additions and 25 deletions

View File

@ -973,6 +973,41 @@ Will be the sender of all notifications, so ensure that it is verified with AWS.
SES if the default notification gateway and will be used unless SMTP settings are configured in the application configuration
settings.
PowerDNS ACME Plugin
~~~~~~~~~~~~~~~~~~~~~~
The following configuration properties are required to use the PowerDNS ACME Plugin for domain validation.
.. data:: ACME_POWERDNS_DOMAIN
:noindex:
This is the FQDN for the PowerDNS API (without path)
.. data:: ACME_POWERDNS_SERVERID
:noindex:
This is the ServerID attribute of the PowerDNS API Server (i.e. "localhost")
.. data:: ACME_POWERDNS_APIKEYNAME
:noindex:
This is the Key name to use for authentication (i.e. "X-API-Key")
.. data:: ACME_POWERDNS_APIKEY
:noindex:
This is the API Key to use for authentication (i.e. "Password")
.. data:: ACME_POWERDNS_RETRIES
:noindex:
This is the number of times DNS Verification should be attempted (i.e. 20)
.. _CommandLineInterface:
Command Line Interface
@ -1071,6 +1106,15 @@ All commands default to `~/.lemur/lemur.conf.py` if a configuration is not speci
lemur notify
.. data:: acme
Handles all ACME related tasks, like ACME plugin testing.
::
lemur acme
Sub-commands
------------
@ -1172,11 +1216,12 @@ Acme
Kevin Glisson <kglisson@netflix.com>,
Curtis Castrapel <ccastrapel@netflix.com>,
Hossein Shafagh <hshafagh@netflix.com>,
Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>,
Chad Sine <csine@netflix.com>
:Type:
Issuer
:Description:
Adds support for the ACME protocol (including LetsEncrypt) with domain validation being handled Route53.
Adds support for the ACME protocol (including LetsEncrypt) with domain validation using several providers.
Atlas

View File

@ -172,7 +172,7 @@ class AcmeHandler(object):
except (AcmeError, TimeoutError):
sentry.captureException(extra={"order_url": str(order.uri)})
metrics.send("request_certificate_error", "counter", 1)
metrics.send("request_certificate_error", "counter", 1, metric_tags={"uri": order.uri})
current_app.logger.error(
f"Unable to resolve Acme order: {order.uri}", exc_info=True
)
@ -183,6 +183,11 @@ class AcmeHandler(object):
else:
raise
metrics.send("request_certificate_success", "counter", 1, metric_tags={"uri": order.uri})
current_app.logger.info(
f"Successfully resolved Acme order: {order.uri}", exc_info=True
)
pem_certificate = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM,
OpenSSL.crypto.load_certificate(
@ -254,8 +259,9 @@ class AcmeHandler(object):
domains = [options["common_name"]]
if options.get("extensions"):
for name in options["extensions"]["sub_alt_names"]["names"]:
domains.append(name)
for dns_name in options["extensions"]["sub_alt_names"]["names"]:
if dns_name.value not in domains:
domains.append(dns_name.value)
current_app.logger.debug("Got these domains: {0}".format(domains))
return domains
@ -640,15 +646,8 @@ class ACMEIssuerPlugin(IssuerPlugin):
domains = self.acme.get_domains(issuer_options)
if not create_immediately:
# Create pending authorizations that we'll need to do the creation
authz_domains = []
for d in domains:
if type(d) == str:
authz_domains.append(d)
else:
authz_domains.append(d.value)
dns_authorization = authorization_service.create(
account_number, authz_domains, provider_type
account_number, domains, provider_type
)
# Return id of the DNS Authorization
return None, None, dns_authorization.id

View File

@ -1,4 +1,6 @@
import unittest
from cryptography.x509 import DNSName
from requests.models import Response
from mock import MagicMock, Mock, patch
@ -74,12 +76,14 @@ class TestAcme(unittest.TestCase):
@patch("acme.client.Client")
@patch("lemur.plugins.lemur_acme.plugin.current_app")
@patch("lemur.plugins.lemur_acme.cloudflare.wait_for_dns_change")
@patch("time.sleep")
def test_complete_dns_challenge_success(
self, mock_wait_for_dns_change, mock_current_app, mock_acme
self, mock_sleep, mock_wait_for_dns_change, mock_current_app, mock_acme
):
mock_dns_provider = Mock()
mock_dns_provider.wait_for_dns_change = Mock(return_value=True)
mock_authz = Mock()
mock_sleep.return_value = False
mock_authz.dns_challenge.response = Mock()
mock_authz.dns_challenge.response.simple_verify = Mock(return_value=True)
mock_authz.authz = []
@ -179,7 +183,7 @@ class TestAcme(unittest.TestCase):
options = {
"common_name": "test.netflix.net",
"extensions": {
"sub_alt_names": {"names": ["test2.netflix.net", "test3.netflix.net"]}
"sub_alt_names": {"names": [DNSName("test2.netflix.net"), DNSName("test3.netflix.net")]}
},
}
result = self.acme.get_domains(options)
@ -187,6 +191,19 @@ class TestAcme(unittest.TestCase):
result, [options["common_name"], "test2.netflix.net", "test3.netflix.net"]
)
@patch("lemur.plugins.lemur_acme.plugin.current_app")
def test_get_domains_san(self, mock_current_app):
options = {
"common_name": "test.netflix.net",
"extensions": {
"sub_alt_names": {"names": [DNSName("test.netflix.net"), DNSName("test2.netflix.net")]}
},
}
result = self.acme.get_domains(options)
self.assertEqual(
result, [options["common_name"], "test2.netflix.net"]
)
@patch(
"lemur.plugins.lemur_acme.plugin.AcmeHandler.start_dns_challenge",
return_value="test",

View File

@ -96,7 +96,7 @@ def build_secret(secret_format, secret_name, body, private_key, cert_chain):
if secret_format == "TLS":
secret["type"] = "kubernetes.io/tls"
secret["data"] = {
"tls.crt": base64encode(cert_chain),
"tls.crt": base64encode(body),
"tls.key": base64encode(private_key),
}
if secret_format == "Certificate":

View File

@ -98,10 +98,14 @@ def process_options(options):
:param options:
:return: dict or valid verisign options
"""
# if there is a config variable with VERISIGN_PRODUCT_<upper(authority.name)> take the value as Cert product-type
# else default to "Server", to be compatoible with former versions
authority = options.get("authority").name.upper()
product_type = current_app.config.get("VERISIGN_PRODUCT_{0}".format(authority), "Server")
data = {
"challenge": get_psuedo_random_string(),
"serverType": "Apache",
"certProductType": "Server",
"certProductType": product_type,
"firstName": current_app.config.get("VERISIGN_FIRST_NAME"),
"lastName": current_app.config.get("VERISIGN_LAST_NAME"),
"signatureAlgorithm": "sha256WithRSAEncryption",
@ -111,11 +115,6 @@ def process_options(options):
data["subject_alt_names"] = ",".join(get_additional_names(options))
if options.get("validity_end") > arrow.utcnow().shift(years=2):
raise Exception(
"Verisign issued certificates cannot exceed two years in validity"
)
if options.get("validity_end"):
# VeriSign (Symantec) only accepts strictly smaller than 2 year end date
if options.get("validity_end") < arrow.utcnow().shift(years=2, days=-1):
@ -210,7 +209,7 @@ class VerisignIssuerPlugin(IssuerPlugin):
response = self.session.post(url, data=data)
try:
cert = handle_response(response.content)["Response"]["Certificate"]
response_dict = handle_response(response.content)
except KeyError:
metrics.send(
"verisign_create_certificate_error",
@ -222,8 +221,13 @@ class VerisignIssuerPlugin(IssuerPlugin):
extra={"common_name": issuer_options.get("common_name", "")}
)
raise Exception(f"Error with Verisign: {response.content}")
# TODO add external id
return cert, current_app.config.get("VERISIGN_INTERMEDIATE"), None
authority = issuer_options.get("authority").name.upper()
cert = response_dict['Response']['Certificate']
external_id = None
if 'Transaction_ID' in response_dict['Response'].keys():
external_id = response_dict['Response']['Transaction_ID']
chain = current_app.config.get("VERISIGN_INTERMEDIATE_{0}".format(authority), current_app.config.get("VERISIGN_INTERMEDIATE"))
return cert, chain, external_id
@staticmethod
def create_authority(options):