keylime/0004-cloud_verifier-Support...

357 lines
15 KiB
Diff

From 16d3a31145e3d0001e6c6621adb6dbaf831cb03f Mon Sep 17 00:00:00 2001
From: Daiki Ueno <dueno@redhat.com>
Date: Mon, 21 Mar 2022 13:08:29 +0100
Subject: [PATCH 4/5] cloud_verifier: Support /notifications/revocation REST
API
Signed-off-by: Daiki Ueno <dueno@redhat.com>
---
keylime.conf | 22 ++++--
keylime/cloud_verifier_common.py | 16 +----
keylime/cloud_verifier_tornado.py | 108 +++++++++++++++++++++++-------
keylime/revocation_notifier.py | 18 ++++-
4 files changed, 119 insertions(+), 45 deletions(-)
diff --git a/keylime.conf b/keylime.conf
index fbd1119..c393e22 100644
--- a/keylime.conf
+++ b/keylime.conf
@@ -269,9 +269,21 @@ max_retries = 5
# will done as fast as possible. Floating point values accepted here.
quote_interval = 2
-# Whether to turn on the zero mq based revocation notifier system.
-# Currently this only works if you are using keylime-CA.
-revocation_notifier = True
+# Enable listed revocation notification methods.
+#
+# Available methods are:
+#
+# "zeromq": Enable the ZeroMQ based revocation notification method;
+# revocation_notifier_ip and revocation_notifier_port options must be
+# set. Currently this only works if you are using keylime-CA.
+#
+# "webhook": Send notification via webhook. The endpoint URL must be
+# configured with webhook_url option. This can be used to notify other
+# systems that do not have a Keylime agent running.
+#
+# "agent": Deliver notification directly to the agent via the REST
+# protocol.
+revocation_notifiers = zeromq
# The binding address and port of the revocation notifier service.
# If the 'revocation_notifier' option is set to "true", then the verifier
@@ -279,10 +291,6 @@ revocation_notifier = True
revocation_notifier_ip = 127.0.0.1
revocation_notifier_port = 8992
-# Enable revocation notifications via webhook. This can be used to notify other
-# systems that do not have a Keylime agent running.
-revocation_notifier_webhook = False
-
# Webhook url for revocation notifications.
webhook_url = ''
diff --git a/keylime/cloud_verifier_common.py b/keylime/cloud_verifier_common.py
index 52d6908..ab00768 100644
--- a/keylime/cloud_verifier_common.py
+++ b/keylime/cloud_verifier_common.py
@@ -7,7 +7,7 @@ import ast
import base64
import time
-from keylime import config, crypto, json, keylime_logging, revocation_notifier
+from keylime import config, crypto, json, keylime_logging
from keylime.agentstates import AgentAttestStates
from keylime.common import algorithms, validators
from keylime.failure import Component, Failure
@@ -268,14 +268,7 @@ def process_get_status(agent):
# sign a message with revocation key. telling of verification problem
-
-
-def notify_error(agent, msgtype="revocation", event=None):
- send_mq = config.getboolean("cloud_verifier", "revocation_notifier")
- send_webhook = config.getboolean("cloud_verifier", "revocation_notifier_webhook", fallback=False)
- if not (send_mq or send_webhook):
- return
-
+def prepare_error(agent, msgtype="revocation", event=None):
# prepare the revocation message:
revocation = {
"type": msgtype,
@@ -300,10 +293,7 @@ def notify_error(agent, msgtype="revocation", event=None):
else:
tosend["signature"] = "none"
- if send_mq:
- revocation_notifier.notify(tosend)
- if send_webhook:
- revocation_notifier.notify_webhook(tosend)
+ return tosend
def validate_agent_data(agent_data):
diff --git a/keylime/cloud_verifier_tornado.py b/keylime/cloud_verifier_tornado.py
index 60abf37..a8c08d2 100644
--- a/keylime/cloud_verifier_tornado.py
+++ b/keylime/cloud_verifier_tornado.py
@@ -9,6 +9,7 @@ import os
import signal
import sys
import traceback
+from concurrent.futures import ThreadPoolExecutor
from multiprocessing import Process
import tornado.ioloop
@@ -35,6 +36,9 @@ from keylime.failure import MAX_SEVERITY_LABEL, Component, Failure
logger = keylime_logging.init_logging("cloudverifier")
+# mTLS configuration to connect to the agent
+mtls_options = None
+
try:
engine = DBEngineManager().make_engine("cloud_verifier")
@@ -232,11 +236,6 @@ class VersionHandler(BaseHandler):
class AgentsHandler(BaseHandler):
- mtls_options = None # Stores the cert, key and password used by the verifier for mTLS connections
-
- def initialize(self, mtls_options):
- self.mtls_options = mtls_options
-
def head(self):
"""HEAD not supported"""
web_util.echo_json_response(self, 405, "HEAD not supported")
@@ -496,9 +495,7 @@ class AgentsHandler(BaseHandler):
mtls_cert = agent_data["mtls_cert"]
agent_data["ssl_context"] = None
if agent_mtls_cert_enabled and mtls_cert:
- agent_data["ssl_context"] = web_util.generate_agent_mtls_context(
- mtls_cert, self.mtls_options
- )
+ agent_data["ssl_context"] = web_util.generate_agent_mtls_context(mtls_cert, mtls_options)
if agent_data["ssl_context"] is None:
logger.warning("Connecting to agent without mTLS: %s", agent_id)
@@ -566,7 +563,7 @@ class AgentsHandler(BaseHandler):
if not isinstance(agent, dict):
agent = _from_db_obj(agent)
if agent["mtls_cert"]:
- agent["ssl_context"] = web_util.generate_agent_mtls_context(agent["mtls_cert"], self.mtls_options)
+ agent["ssl_context"] = web_util.generate_agent_mtls_context(agent["mtls_cert"], mtls_options)
agent["operational_state"] = states.START
asyncio.ensure_future(process_agent(agent, states.GET_QUOTE))
web_util.echo_json_response(self, 200, "Success")
@@ -860,6 +857,70 @@ async def invoke_provide_v(agent):
asyncio.ensure_future(process_agent(agent, states.GET_QUOTE))
+async def invoke_notify_error(agent, tosend):
+ if agent is None:
+ logger.warning("Agent deleted while being processed")
+ return
+ kwargs = {
+ "data": tosend,
+ }
+ if agent["ssl_context"]:
+ kwargs["context"] = agent["ssl_context"]
+ res = tornado_requests.request(
+ "POST",
+ f"http://{agent['ip']}:{agent['port']}/v{agent['supported_version']}/notifications/revocation",
+ **kwargs,
+ )
+ response = await res
+
+ if response is None:
+ logger.warning(
+ "Empty Notify Revocation response from cloud agent %s",
+ agent["agent_id"],
+ )
+ elif response.status_code != 200:
+ logger.warning(
+ "Unexpected Notify Revocation response error for cloud agent %s, Error: %s",
+ agent["agent_id"],
+ response.status_code,
+ )
+
+
+async def notify_error(agent, msgtype="revocation", event=None):
+ notifiers = revocation_notifier.get_notifiers()
+ if len(notifiers) == 0:
+ return
+
+ tosend = cloud_verifier_common.prepare_error(agent, msgtype, event)
+ if "webhook" in notifiers:
+ revocation_notifier.notify_webhook(tosend)
+ if "zeromq" in notifiers:
+ revocation_notifier.notify(tosend)
+ if "agent" in notifiers:
+ verifier_id = config.get(
+ "cloud_verifier", "cloudverifier_id", fallback=cloud_verifier_common.DEFAULT_VERIFIER_ID
+ )
+ session = get_session()
+ agents = session.query(VerfierMain).filter_by(verifier_id=verifier_id).all()
+ futures = []
+ loop = asyncio.get_event_loop()
+ # Notify all agents asynchronously through a thread pool
+ with ThreadPoolExecutor() as pool:
+ for agent_db_obj in agents:
+ if agent_db_obj.agent_id != agent["agent_id"]:
+ agent = _from_db_obj(agent_db_obj)
+ if agent["mtls_cert"]:
+ agent["ssl_context"] = web_util.generate_agent_mtls_context(agent["mtls_cert"], mtls_options)
+ func = functools.partial(invoke_notify_error, agent, tosend)
+ futures.append(await loop.run_in_executor(pool, func))
+ # Wait for all tasks complete in 60 seconds
+ try:
+ for f in asyncio.as_completed(futures, timeout=60):
+ await f
+ except asyncio.TimeoutError as e:
+ logger.error("Timeout during notifying error to agents: %s", e)
+
+
async def process_agent(agent, new_operational_state, failure=Failure(Component.INTERNAL, ["verifier"])):
# Convert to dict if the agent arg is a db object
if not isinstance(agent, dict):
@@ -900,7 +961,7 @@ async def process_agent(agent, new_operational_state, failure=Failure(Component.
# issue notification for invalid quotes
if new_operational_state == states.INVALID_QUOTE:
- cloud_verifier_common.notify_error(agent, event=failure.highest_severity_event)
+ await notify_error(agent, event=failure.highest_severity_event)
# When the failure is irrecoverable we stop polling the agent
if not failure.recoverable or failure.highest_severity == MAX_SEVERITY_LABEL:
@@ -975,9 +1036,7 @@ async def process_agent(agent, new_operational_state, failure=Failure(Component.
)
failure.add_event("not_reachable", "agent was not reachable from verifier", False)
if agent["first_verified"]: # only notify on previously good agents
- cloud_verifier_common.notify_error(
- agent, msgtype="comm_error", event=failure.highest_severity_event
- )
+ await notify_error(agent, msgtype="comm_error", event=failure.highest_severity_event)
else:
logger.debug("Communication error for new agent. No notification will be sent")
await process_agent(agent, states.FAILED, failure)
@@ -1004,7 +1063,7 @@ async def process_agent(agent, new_operational_state, failure=Failure(Component.
maxr,
)
failure.add_event("not_reachable_v", "agent was not reachable to provide V", False)
- cloud_verifier_common.notify_error(agent, msgtype="comm_error", event=failure.highest_severity_event)
+ await notify_error(agent, msgtype="comm_error", event=failure.highest_severity_event)
await process_agent(agent, states.FAILED, failure)
else:
agent["operational_state"] = states.PROVIDE_V
@@ -1031,7 +1090,7 @@ async def process_agent(agent, new_operational_state, failure=Failure(Component.
await process_agent(agent, states.FAILED, failure)
-async def activate_agents(verifier_id, verifier_ip, verifier_port, mtls_options):
+async def activate_agents(verifier_id, verifier_ip, verifier_port):
session = get_session()
aas = get_AgentAttestStates()
try:
@@ -1100,7 +1159,7 @@ def main():
# print out API versions we support
keylime_api_version.log_api_versions(logger)
- context, mtls_options = web_util.init_mtls(logger=logger)
+ context, server_mtls_options = web_util.init_mtls(logger=logger)
# Check for user defined CA to connect to agent
agent_mtls_cert = config.get("cloud_verifier", "agent_mtls_cert", fallback=None)
@@ -1108,12 +1167,15 @@ def main():
agent_mtls_private_key_pw = config.get("cloud_verifier", "agent_mtls_private_key_pw", fallback=None)
# Only set custom options if the cert should not be the same as used by the verifier
- if agent_mtls_cert != "CV":
+ global mtls_options
+ if agent_mtls_cert == "CV":
+ mtls_options = server_mtls_options
+ else:
mtls_options = (agent_mtls_cert, agent_mtls_private_key, agent_mtls_private_key_pw)
app = tornado.web.Application(
[
- (r"/v?[0-9]+(?:\.[0-9]+)?/agents/.*", AgentsHandler, {"mtls_options": mtls_options}),
+ (r"/v?[0-9]+(?:\.[0-9]+)?/agents/.*", AgentsHandler),
(r"/v?[0-9]+(?:\.[0-9]+)?/allowlists/.*", AllowlistHandler),
(r"/versions?", VersionHandler),
(r".*", MainHandler),
@@ -1149,17 +1211,17 @@ def main():
server.start()
if task_id == 0:
# Reactivate agents
- asyncio.ensure_future(
- activate_agents(cloudverifier_id, cloudverifier_host, cloudverifier_port, mtls_options)
- )
+ asyncio.ensure_future(activate_agents(cloudverifier_id, cloudverifier_host, cloudverifier_port))
tornado.ioloop.IOLoop.current().start()
logger.debug("Server %s stopped.", task_id)
sys.exit(0)
processes = []
+ run_revocation_notifier = "zeromq" in revocation_notifier.get_notifiers()
+
def sig_handler(*_):
- if config.getboolean("cloud_verifier", "revocation_notifier"):
+ if run_revocation_notifier:
revocation_notifier.stop_broker()
for p in processes:
p.join()
@@ -1167,7 +1229,7 @@ def main():
signal.signal(signal.SIGINT, sig_handler)
signal.signal(signal.SIGTERM, sig_handler)
- if config.getboolean("cloud_verifier", "revocation_notifier"):
+ if run_revocation_notifier:
logger.info(
"Starting service for revocation notifications on port %s",
config.getint("cloud_verifier", "revocation_notifier_port"),
diff --git a/keylime/revocation_notifier.py b/keylime/revocation_notifier.py
index 7cfe0e5..0628a64 100644
--- a/keylime/revocation_notifier.py
+++ b/keylime/revocation_notifier.py
@@ -22,8 +22,22 @@ broker_proc: Optional[Process] = None
_SOCKET_PATH = "/var/run/keylime/keylime.verifier.ipc"
+# return the revocation notification methods for cloud verifier
+def get_notifiers():
+ notifiers = set(config.get("cloud_verifier", "revocation_notifiers", fallback="").split(","))
+ if ("zeromq" not in notifiers) and config.getboolean("cloud_verifier", "revocation_notifier", fallback=False):
+ logger.warning("Warning: 'revocation_notifier' option is deprecated; use 'revocation_notifiers'")
+ notifiers.add("zeromq")
+ if ("webhook" not in notifiers) and config.getboolean(
+ "cloud_verifier", "revocation_notifier_webhook", fallback=False
+ ):
+ logger.warning("Warning: 'revocation_notifier_webhook' option is deprecated; use 'revocation_notifiers'")
+ notifiers.add("webhook")
+ return notifiers.intersection({"zeromq", "webhook", "agent"})
+
+
def start_broker():
- assert config.getboolean("cloud_verifier", "revocation_notifier")
+ assert "zeromq" in get_notifiers()
try:
import zmq # pylint: disable=import-outside-toplevel
except ImportError as error:
@@ -78,7 +92,7 @@ def stop_broker():
def notify(tosend):
- assert config.getboolean("cloud_verifier", "revocation_notifier")
+ assert "zeromq" in get_notifiers()
try:
import zmq # pylint: disable=import-outside-toplevel
except ImportError as error:
--
2.35.1