summaryrefslogtreecommitdiff
path: root/0011-Automatically-update-agent-API-version.patch
blob: c87b309a3973624d0cef871f0d3f8dad78ef9b40 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
From b0cf69c9db20eb319ea2e90c22f500e09b704224 Mon Sep 17 00:00:00 2001
From: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Date: Wed, 23 Aug 2023 16:24:15 +0200
Subject: [PATCH] Implement automatic agent API version bump

Automatically update the agent supported API version in the database if
the agent is updated and its API version is bumped.

Previously, if an agent was added to a verifier while it used an old API
version, and then it is updated with an API version bump, the
attestation would fail as the verifier would try to reach the agent
using the old API version.

Fixes #1457

Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
---
 keylime/cloud_verifier_tornado.py | 185 +++++++++++++++++++++++++++---
 1 file changed, 167 insertions(+), 18 deletions(-)

diff --git a/keylime/cloud_verifier_tornado.py b/keylime/cloud_verifier_tornado.py
index 261022ac6..31e6f7159 100644
--- a/keylime/cloud_verifier_tornado.py
+++ b/keylime/cloud_verifier_tornado.py
@@ -32,6 +32,7 @@
 )
 from keylime.agentstates import AgentAttestState, AgentAttestStates
 from keylime.common import retry, states, validators
+from keylime.common.version import str_to_version
 from keylime.da import record
 from keylime.db.keylime_db import DBEngineManager, SessionManager
 from keylime.db.verifier_db import VerfierMain, VerifierAllowlist
@@ -998,6 +999,80 @@ def data_received(self, chunk: Any) -> None:
         raise NotImplementedError()
 
 
+async def update_agent_api_version(agent: Dict[str, Any], timeout: float = 60.0) -> Union[Dict[str, Any], None]:
+    agent_id = agent["agent_id"]
+
+    logger.info("Agent %s API version bump detected, trying to update stored API version", agent_id)
+    kwargs = {}
+    if agent["ssl_context"]:
+        kwargs["context"] = agent["ssl_context"]
+
+    res = tornado_requests.request(
+        "GET",
+        f"http://{agent['ip']}:{agent['port']}/version",
+        **kwargs,
+        timeout=timeout,
+    )
+    response = await res
+
+    if response.status_code != 200:
+        logger.warning(
+            "Could not get agent %s supported API version, Error: %s",
+            agent["agent_id"],
+            response.status_code,
+        )
+        return None
+
+    try:
+        json_response = json.loads(response.body)
+        new_version = json_response["results"]["supported_version"]
+        old_version = agent["supported_version"]
+
+        # Only update the API version to use if it is supported by the verifier
+        if new_version in keylime_api_version.all_versions():
+            new_version_tuple = str_to_version(new_version)
+            old_version_tuple = str_to_version(old_version)
+
+            assert new_version_tuple, f"Agent {agent_id} version {new_version} is invalid"
+            assert old_version_tuple, f"Agent {agent_id} version {old_version} is invalid"
+
+            # Check that the new version is greater than current version
+            if new_version_tuple <= old_version_tuple:
+                logger.warning(
+                    "Agent %s API version %s is lower or equal to previous version %s",
+                    agent_id,
+                    new_version,
+                    old_version,
+                )
+                return None
+
+            logger.info("Agent %s new API version %s is supported", agent_id, new_version)
+            session = get_session()
+            agent["supported_version"] = new_version
+
+            # Remove keys that should not go to the DB
+            agent_db = dict(agent)
+            for key in exclude_db:
+                if key in agent_db:
+                    del agent_db[key]
+
+            session.query(VerfierMain).filter_by(agent_id=agent_id).update(agent_db)  # pyright: ignore
+            session.commit()
+        else:
+            logger.warning("Agent %s new API version %s is not supported", agent_id, new_version)
+            return None
+
+    except SQLAlchemyError as e:
+        logger.error("SQLAlchemy Error updating API version for agent %s: %s", agent_id, e)
+        return None
+    except Exception as e:
+        logger.exception(e)
+        return None
+
+    logger.info("Agent %s API version updated to %s", agent["agent_id"], agent["supported_version"])
+    return agent
+
+
 async def invoke_get_quote(
     agent: Dict[str, Any], runtime_policy: str, need_pubkey: bool, timeout: float = 60.0
 ) -> None:
@@ -1028,15 +1103,43 @@ async def invoke_get_quote(
         # this is a connection error, retry get quote
         if response.status_code in [408, 500, 599]:
             asyncio.ensure_future(process_agent(agent, states.GET_QUOTE_RETRY))
-        else:
-            # catastrophic error, do not continue
-            logger.critical(
-                "Unexpected Get Quote response error for cloud agent %s, Error: %s",
-                agent["agent_id"],
-                response.status_code,
-            )
-            failure.add_event("no_quote", "Unexpected Get Quote reponse from agent", False)
-            asyncio.ensure_future(process_agent(agent, states.FAILED, failure))
+            return
+
+        if response.status_code == 400:
+            try:
+                json_response = json.loads(response.body)
+                if "API version not supported" in json_response["status"]:
+                    update = update_agent_api_version(agent)
+                    updated = await update
+
+                    if updated:
+                        asyncio.ensure_future(process_agent(updated, states.GET_QUOTE_RETRY))
+                    else:
+                        logger.warning("Could not update stored agent %s API version", agent["agent_id"])
+                        failure.add_event(
+                            "version_not_supported",
+                            {"context": "Agent API version not supported", "data": json_response},
+                            False,
+                        )
+                        asyncio.ensure_future(process_agent(agent, states.FAILED, failure))
+                    return
+
+            except Exception as e:
+                logger.exception(e)
+                failure.add_event(
+                    "exception", {"context": "Agent caused the verifier to throw an exception", "data": str(e)}, False
+                )
+                asyncio.ensure_future(process_agent(agent, states.FAILED, failure))
+                return
+
+        # catastrophic error, do not continue
+        logger.critical(
+            "Unexpected Get Quote response error for cloud agent %s, Error: %s",
+            agent["agent_id"],
+            response.status_code,
+        )
+        failure.add_event("no_quote", "Unexpected Get Quote reponse from agent", False)
+        asyncio.ensure_future(process_agent(agent, states.FAILED, failure))
     else:
         try:
             json_response = json.loads(response.body)
@@ -1100,15 +1203,43 @@ async def invoke_provide_v(agent: Dict[str, Any], timeout: float = 60.0) -> None
     if response.status_code != 200:
         if response.status_code in [408, 500, 599]:
             asyncio.ensure_future(process_agent(agent, states.PROVIDE_V_RETRY))
-        else:
-            # catastrophic error, do not continue
-            logger.critical(
-                "Unexpected Provide V response error for cloud agent %s, Error: %s",
-                agent["agent_id"],
-                response.status_code,
-            )
-            failure.add_event("no_v", {"message": "Unexpected provide V response", "data": response.status_code}, False)
-            asyncio.ensure_future(process_agent(agent, states.FAILED, failure))
+            return
+
+        if response.status_code == 400:
+            try:
+                json_response = json.loads(response.body)
+                if "API version not supported" in json_response["status"]:
+                    update = update_agent_api_version(agent)
+                    updated = await update
+
+                    if updated:
+                        asyncio.ensure_future(process_agent(updated, states.PROVIDE_V_RETRY))
+                    else:
+                        logger.warning("Could not update stored agent %s API version", agent["agent_id"])
+                        failure.add_event(
+                            "version_not_supported",
+                            {"context": "Agent API version not supported", "data": json_response},
+                            False,
+                        )
+                        asyncio.ensure_future(process_agent(agent, states.FAILED, failure))
+                    return
+
+            except Exception as e:
+                logger.exception(e)
+                failure.add_event(
+                    "exception", {"context": "Agent caused the verifier to throw an exception", "data": str(e)}, False
+                )
+                asyncio.ensure_future(process_agent(agent, states.FAILED, failure))
+                return
+
+        # catastrophic error, do not continue
+        logger.critical(
+            "Unexpected Provide V response error for cloud agent %s, Error: %s",
+            agent["agent_id"],
+            response.status_code,
+        )
+        failure.add_event("no_v", {"message": "Unexpected provide V response", "data": response.status_code}, False)
+        asyncio.ensure_future(process_agent(agent, states.FAILED, failure))
     else:
         asyncio.ensure_future(process_agent(agent, states.GET_QUOTE))
 
@@ -1134,6 +1265,24 @@ async def invoke_notify_error(agent: Dict[str, Any], tosend: Dict[str, Any], tim
             agent["agent_id"],
         )
     elif response.status_code != 200:
+        if response.status_code == 400:
+            try:
+                json_response = json.loads(response.body)
+                if "API version not supported" in json_response["status"]:
+                    update = update_agent_api_version(agent)
+                    updated = await update
+
+                    if updated:
+                        asyncio.ensure_future(invoke_notify_error(updated, tosend))
+                    else:
+                        logger.warning("Could not update stored agent %s API version", agent["agent_id"])
+
+                    return
+
+            except Exception as e:
+                logger.exception(e)
+                return
+
         logger.warning(
             "Unexpected Notify Revocation response error for cloud agent %s, Error: %s",
             agent["agent_id"],