lemur/lemur/certificates/verify.py

182 lines
5.5 KiB
Python
Raw Normal View History

2015-06-22 22:47:27 +02:00
"""
.. module: lemur.certificates.verify
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
2015-06-22 22:47:27 +02:00
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import requests
import subprocess
from flask import current_app
from requests.exceptions import ConnectionError, InvalidSchema
2015-08-27 20:53:37 +02:00
from cryptography import x509
from cryptography.hazmat.backends import default_backend
2015-06-22 22:47:27 +02:00
2015-11-25 23:54:08 +01:00
from lemur.utils import mktempfile
2016-11-29 20:30:44 +01:00
from lemur.common.utils import parse_certificate
2015-08-27 20:53:37 +02:00
crl_cache = {}
2015-06-22 22:47:27 +02:00
2018-09-27 15:28:21 +02:00
def ocsp_verify(cert, cert_path, issuer_chain_path):
2015-06-22 22:47:27 +02:00
"""
Attempts to verify a certificate via OCSP. OCSP is a more modern version
of CRL in that it will query the OCSP URI in order to determine if the
certificate has been revoked
2015-06-22 22:47:27 +02:00
2018-09-27 16:11:13 +02:00
:param cert:
2015-06-22 22:47:27 +02:00
:param cert_path:
:param issuer_chain_path:
:return bool: True if certificate is valid, False otherwise
"""
2019-05-16 16:57:02 +02:00
command = ["openssl", "x509", "-noout", "-ocsp_uri", "-in", cert_path]
2015-06-22 22:47:27 +02:00
p1 = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
url, err = p1.communicate()
if not url:
2019-05-16 16:57:02 +02:00
current_app.logger.debug(
"No OCSP URL in certificate {}".format(cert.serial_number)
)
return None
2019-05-16 16:57:02 +02:00
p2 = subprocess.Popen(
[
"openssl",
"ocsp",
"-issuer",
issuer_chain_path,
"-cert",
cert_path,
"-url",
url.strip(),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
2015-06-22 22:47:27 +02:00
message, err = p2.communicate()
2016-11-29 20:30:44 +01:00
2019-05-16 16:57:02 +02:00
p_message = message.decode("utf-8")
2016-11-29 20:30:44 +01:00
2019-05-16 16:57:02 +02:00
if "error" in p_message or "Error" in p_message:
2015-06-22 22:47:27 +02:00
raise Exception("Got error when parsing OCSP url")
2019-05-16 16:57:02 +02:00
elif "revoked" in p_message:
current_app.logger.debug(
"OCSP reports certificate revoked: {}".format(cert.serial_number)
)
return False
2015-06-22 22:47:27 +02:00
2019-05-16 16:57:02 +02:00
elif "good" not in p_message:
2015-06-22 22:47:27 +02:00
raise Exception("Did not receive a valid response")
return True
def crl_verify(cert, cert_path):
2015-06-22 22:47:27 +02:00
"""
Attempts to verify a certificate using CRL.
2018-09-27 16:11:13 +02:00
:param cert:
2015-06-22 22:47:27 +02:00
:param cert_path:
:return: True if certificate is valid, False otherwise
:raise Exception: If certificate does not have CRL
"""
try:
distribution_points = cert.extensions.get_extension_for_oid(
2018-09-27 15:28:21 +02:00
x509.OID_CRL_DISTRIBUTION_POINTS
).value
except x509.ExtensionNotFound:
2019-05-16 16:57:02 +02:00
current_app.logger.debug(
"No CRLDP extension in certificate {}".format(cert.serial_number)
)
return None
2016-11-29 20:30:44 +01:00
2015-08-27 20:53:37 +02:00
for p in distribution_points:
point = p.full_name[0].value
2016-11-29 20:30:44 +01:00
if point not in crl_cache:
current_app.logger.debug("Retrieving CRL: {}".format(point))
try:
response = requests.get(point)
2016-11-29 20:30:44 +01:00
if response.status_code != 200:
raise Exception("Unable to retrieve CRL: {0}".format(point))
except InvalidSchema:
# Unhandled URI scheme (like ldap://); skip this distribution point.
continue
except ConnectionError:
2016-11-29 20:30:44 +01:00
raise Exception("Unable to retrieve CRL: {0}".format(point))
2019-05-16 16:57:02 +02:00
crl_cache[point] = x509.load_der_x509_crl(
response.content, backend=default_backend()
)
else:
current_app.logger.debug("CRL point is cached {}".format(point))
2016-11-29 20:30:44 +01:00
for r in crl_cache[point]:
if cert.serial_number == r.serial_number:
try:
reason = r.extensions.get_extension_for_class(x509.CRLReason).value
# Handle "removeFromCRL" revoke reason as unrevoked;
# continue with the next distribution point.
# Per RFC 5280 section 6.3.3 (k):
# https://tools.ietf.org/html/rfc5280#section-6.3.3
if reason == x509.ReasonFlags.remove_from_crl:
break
except x509.ExtensionNotFound:
pass
2019-05-16 16:57:02 +02:00
current_app.logger.debug(
"CRL reports certificate " "revoked: {}".format(cert.serial_number)
)
return False
2016-11-29 20:30:44 +01:00
2015-06-22 22:47:27 +02:00
return True
def verify(cert_path, issuer_chain_path):
"""
Verify a certificate using OCSP and CRL
:param cert_path:
:param issuer_chain_path:
:return: True if valid, False otherwise
"""
2019-05-16 16:57:02 +02:00
with open(cert_path, "rt") as c:
try:
cert = parse_certificate(c.read())
except ValueError as e:
current_app.logger.error(e)
return None
2015-06-22 22:47:27 +02:00
# OCSP is our main source of truth, in a lot of cases CRLs
# have been deprecated and are no longer updated
verify_result = ocsp_verify(cert, cert_path, issuer_chain_path)
if verify_result is None:
verify_result = crl_verify(cert, cert_path)
if verify_result is None:
current_app.logger.debug("Failed to verify {}".format(cert.serial_number))
return verify_result
2015-06-22 22:47:27 +02:00
def verify_string(cert_string, issuer_string):
"""
Verify a certificate given only it's string value
:param cert_string:
:param issuer_string:
:return: True if valid, False otherwise
"""
2015-08-27 20:53:37 +02:00
with mktempfile() as cert_tmp:
2019-05-16 16:57:02 +02:00
with open(cert_tmp, "w") as f:
f.write(cert_string)
2015-08-27 20:53:37 +02:00
with mktempfile() as issuer_tmp:
2019-05-16 16:57:02 +02:00
with open(issuer_tmp, "w") as f:
f.write(issuer_string)
status = verify(cert_tmp, issuer_tmp)
2015-06-22 22:47:27 +02:00
return status