CCF
Loading...
Searching...
No Matches
jwt_management.h
Go to the documentation of this file.
1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the Apache 2.0 License.
3#pragma once
4
7#include "ccf/ds/hex.h"
10#include "ccf/tx.h"
11#include "http/http_jwt.h"
12
13#include <set>
14#include <sstream>
15
16namespace
17{
18 std::vector<uint8_t> try_parse_raw_rsa(const ccf::crypto::JsonWebKeyData& jwk)
19 {
20 if (!jwk.e || jwk.e->empty() || !jwk.n || jwk.n->empty())
21 {
22 return {};
23 }
24
25 std::vector<uint8_t> der;
28 data.kid = jwk.kid.value();
29 data.n = jwk.n.value();
30 data.e = jwk.e.value();
31 try
32 {
33 const auto pubkey = ccf::crypto::make_rsa_public_key(data);
34 return pubkey->public_key_der();
35 }
36 catch (const std::invalid_argument& exc)
37 {
38 throw std::logic_error(
39 fmt::format("Failed to construct RSA public key: {}", exc.what()));
40 }
41 }
42
43 std::vector<uint8_t> try_parse_raw_ec(const ccf::crypto::JsonWebKeyData& jwk)
44 {
45 if (!jwk.x || jwk.x->empty() || !jwk.y || jwk.y->empty() || !jwk.crv)
46 {
47 return {};
48 }
49
52 data.kid = jwk.kid.value();
53 data.crv = jwk.crv.value();
54 data.x = jwk.x.value();
55 data.y = jwk.y.value();
56 try
57 {
58 const auto pubkey = ccf::crypto::make_public_key(data);
59 return pubkey->public_key_der();
60 }
61 catch (const std::invalid_argument& exc)
62 {
63 throw std::logic_error(
64 fmt::format("Failed to construct EC public key: {}", exc.what()));
65 }
66 }
67
68 std::vector<uint8_t> try_parse_x5c(const ccf::crypto::JsonWebKeyData& jwk)
69 {
70 if (!jwk.x5c || jwk.x5c->empty())
71 {
72 return {};
73 }
74
75 const auto& kid = jwk.kid.value();
76 auto& der_base64 = jwk.x5c.value()[0];
77 ccf::Cert der;
78 try
79 {
80 der = ccf::crypto::raw_from_b64(der_base64);
81 }
82 catch (const std::invalid_argument& e)
83 {
84 throw std::logic_error(
85 fmt::format("Could not parse x5c of key id {}: {}", kid, e.what()));
86 }
87 try
88 {
89 auto verifier = ccf::crypto::make_unique_verifier(der);
90 return verifier->public_key_der();
91 }
92 catch (std::invalid_argument& exc)
93 {
94 throw std::logic_error(fmt::format(
95 "JWKS kid {} has an invalid X.509 certificate: {}", kid, exc.what()));
96 }
97 }
98
99 std::vector<uint8_t> try_parse_jwk(const ccf::crypto::JsonWebKeyData& jwk)
100 {
101 const auto& kid = jwk.kid.value();
102 auto key = try_parse_raw_rsa(jwk);
103 if (!key.empty())
104 {
105 return key;
106 }
107 key = try_parse_raw_ec(jwk);
108 if (!key.empty())
109 {
110 return key;
111 }
112 key = try_parse_x5c(jwk);
113 if (!key.empty())
114 {
115 return key;
116 }
117
118 throw std::logic_error(
119 fmt::format("JWKS kid {} has neither RSA/EC public key or x5c", kid));
120 }
121}
122
123namespace ccf
124{
125 static void legacy_remove_jwt_public_signing_keys(
126 ccf::kv::Tx& tx, std::string issuer)
127 {
129 Tables::Legacy::JWT_PUBLIC_SIGNING_KEYS);
130 auto key_issuer = tx.rw<Tables::Legacy::JwtPublicSigningKeyIssuer>(
131 Tables::Legacy::JWT_PUBLIC_SIGNING_KEY_ISSUER);
132
133 key_issuer->foreach(
134 [&issuer, &keys, &key_issuer](const auto& k, const auto& v) {
135 if (v == issuer)
136 {
137 keys->remove(k);
138 key_issuer->remove(k);
139 }
140 return true;
141 });
142
143 auto metadata = tx.rw<JwtPublicSigningKeysMetadataLegacy>(
144 Tables::Legacy::JWT_PUBLIC_SIGNING_KEYS_METADATA);
145 metadata->foreach([&issuer, &metadata](const auto& k, const auto& v) {
146 std::vector<OpenIDJWKMetadataLegacy> updated;
147 for (const auto& key : v)
148 {
149 if (key.issuer != issuer)
150 {
151 updated.push_back(key);
152 }
153 }
154
155 if (updated.empty())
156 {
157 metadata->remove(k);
158 }
159 else if (updated.size() < v.size())
160 {
161 metadata->put(k, updated);
162 }
163
164 return true;
165 });
166 }
167
168 static bool check_issuer_constraint(
169 const std::string& issuer, const std::string& constraint)
170 {
171 // Only accept key constraints for the same (sub)domain. This is to avoid
172 // setting keys from issuer A which will be used to validate iss claims
173 // for issuer B, so this doesn't make sense (at least for now).
174
175 const auto issuer_domain = ::http::parse_url_full(issuer).host;
176 const auto constraint_domain = ::http::parse_url_full(constraint).host;
177
178 if (constraint_domain.empty())
179 {
180 return false;
181 }
182
183 // Either constraint's domain == issuer's domain or it is a subdomain,
184 // e.g.: limited.facebook.com
185 // .facebook.com
186 //
187 // It may make sense to support vice-versa too, but we haven't found any
188 // instances of that so far, so leaving it only-way only for
189 // facebook-like cases.
190 if (issuer_domain != constraint_domain)
191 {
192 const auto pattern = "." + constraint_domain;
193 return issuer_domain.ends_with(pattern);
194 }
195
196 return true;
197 }
198
199 static void remove_jwt_public_signing_keys(
200 ccf::kv::Tx& tx, std::string issuer)
201 {
202 // Unlike resetting JWT keys for a particular issuer, removing keys can be
203 // safely done on both table revisions, as soon as the application
204 // shouldn't use them anyway after being ask about that explicitly.
205 legacy_remove_jwt_public_signing_keys(tx, issuer);
206
207 auto keys = tx.rw<JwtPublicSigningKeysMetadata>(
208 Tables::JWT_PUBLIC_SIGNING_KEYS_METADATA);
209
210 keys->foreach([&issuer, &keys](const auto& k, const auto& v) {
211 auto it = find_if(v.begin(), v.end(), [&](const auto& metadata) {
212 return metadata.issuer == issuer;
213 });
214
215 if (it != v.end())
216 {
217 std::vector<OpenIDJWKMetadata> updated(v.begin(), it);
218 updated.insert(updated.end(), ++it, v.end());
219
220 if (!updated.empty())
221 {
222 keys->put(k, updated);
223 }
224 else
225 {
226 keys->remove(k);
227 }
228 }
229 return true;
230 });
231 }
232
233 static bool set_jwt_public_signing_keys(
234 ccf::kv::Tx& tx,
235 const std::string& log_prefix,
236 std::string issuer,
237 const JwtIssuerMetadata& issuer_metadata,
238 const JsonWebKeySet& jwks)
239 {
240 auto keys = tx.rw<JwtPublicSigningKeysMetadata>(
241 Tables::JWT_PUBLIC_SIGNING_KEYS_METADATA);
242 // add keys
243 if (jwks.keys.empty())
244 {
245 LOG_FAIL_FMT("{}: JWKS has no keys", log_prefix);
246 return false;
247 }
248 std::map<std::string, PublicKey> new_keys;
249 std::map<std::string, JwtIssuer> issuer_constraints;
250
251 try
252 {
253 for (auto& jwk : jwks.keys)
254 {
255 if (!jwk.kid.has_value())
256 {
257 throw std::logic_error("Missing kid for JWT signing key");
258 }
259
260 const auto& kid = jwk.kid.value();
261 auto key_der = try_parse_jwk(jwk);
262
263 if (jwk.issuer)
264 {
265 if (!check_issuer_constraint(issuer, *jwk.issuer))
266 {
267 throw std::logic_error(fmt::format(
268 "JWKS kid {} with issuer constraint {} fails validation "
269 "against "
270 "issuer {}",
271 kid,
272 *jwk.issuer,
273 issuer));
274 }
275
276 issuer_constraints.emplace(kid, *jwk.issuer);
277 }
278
279 new_keys.emplace(kid, key_der);
280 }
281 }
282 catch (const std::exception& exc)
283 {
284 LOG_FAIL_FMT("{}: {}", log_prefix, exc.what());
285 return false;
286 }
287
288 if (new_keys.empty())
289 {
290 LOG_FAIL_FMT("{}: no keys left after applying filter", log_prefix);
291 return false;
292 }
293
294 std::set<std::string> existing_kids;
295 keys->foreach([&existing_kids, &issuer_constraints, &issuer](
296 const auto& k, const auto& v) {
297 if (find_if(v.begin(), v.end(), [&](const auto& metadata) {
298 return metadata.issuer == issuer;
299 }) != v.end())
300 {
301 existing_kids.insert(k);
302 }
303
304 return true;
305 });
306
307 for (auto& [kid, der] : new_keys)
308 {
309 OpenIDJWKMetadata value{
310 .public_key = der, .issuer = issuer, .constraint = std::nullopt};
311 value.public_key = der;
312
313 const auto it = issuer_constraints.find(kid);
314 if (it != issuer_constraints.end())
315 {
316 value.constraint = it->second;
317 }
318
319 if (existing_kids.count(kid))
320 {
321 const auto& keys_for_kid = keys->get(kid);
322 if (
323 find_if(
324 keys_for_kid->begin(),
325 keys_for_kid->end(),
326 [&value](const auto& metadata) {
327 return metadata.public_key == value.public_key &&
328 metadata.issuer == value.issuer &&
329 metadata.constraint == value.constraint;
330 }) != keys_for_kid->end())
331 {
332 // Avoid redundant writes. Thus, preserve the behaviour from #5027.
333 continue;
334 }
335 }
336
338 "Save JWT key kid={} issuer={}, constraint={}",
339 kid,
340 value.issuer,
341 value.constraint);
342
343 auto existing_keys = keys->get(kid);
344 if (existing_keys)
345 {
346 const auto prev = find_if(
347 existing_keys->begin(),
348 existing_keys->end(),
349 [&](const auto& issuer_with_constraint) {
350 return issuer_with_constraint.issuer == issuer;
351 });
352
353 if (prev != existing_keys->end())
354 {
355 *prev = value;
356 }
357 else
358 {
359 existing_keys->push_back(std::move(value));
360 }
361 keys->put(kid, *existing_keys);
362 }
363 else
364 {
365 keys->put(kid, std::vector<OpenIDJWKMetadata>{value});
366 }
367 }
368
369 for (auto& kid : existing_kids)
370 {
371 if (!new_keys.contains(kid))
372 {
373 auto updated = keys->get(kid);
374 updated->erase(
375 std::remove_if(
376 updated->begin(),
377 updated->end(),
378 [&](const auto& metadata) { return metadata.issuer == issuer; }),
379 updated->end());
380
381 if (updated->empty())
382 {
383 keys->remove(kid);
384 }
385 else
386 {
387 keys->put(kid, *updated);
388 }
389 }
390 }
391
392 return true;
393 }
394}
Definition tx.h:201
M::Handle * rw(M &m)
Definition tx.h:212
#define LOG_DEBUG_FMT
Definition logger.h:357
#define LOG_FAIL_FMT
Definition logger.h:363
ccf::kv::RawCopySerialisedMap< JwtKeyId, JwtIssuer > JwtPublicSigningKeyIssuer
Definition jwt.h:87
ccf::kv::RawCopySerialisedMap< JwtKeyId, Cert > JwtPublicSigningKeys
Definition jwt.h:85
VerifierUniquePtr make_unique_verifier(const std::vector< uint8_t > &cert)
Definition verifier.cpp:13
std::vector< uint8_t > raw_from_b64(const std::string_view &b64_string)
Definition base64.cpp:12
PublicKeyPtr make_public_key(const Pem &pem)
Definition key_pair.cpp:20
RSAPublicKeyPtr make_rsa_public_key(const Pem &pem)
Definition rsa_key_pair.cpp:13
uint8_t * key
Definition kv_helpers.h:78
Definition app_interface.h:14
std::vector< uint8_t > Cert
Definition jwt.h:37
ServiceMap< JwtKeyId, std::vector< OpenIDJWKMetadata > > JwtPublicSigningKeysMetadata
Definition jwt.h:51
ServiceMap< JwtKeyId, std::vector< OpenIDJWKMetadataLegacy > > JwtPublicSigningKeysMetadataLegacy
Definition jwt.h:64
URL parse_url_full(const std::string &url)
Definition http_parser.h:145
Definition jwk.h:50
std::optional< std::string > n
Definition jwk.h:54
std::optional< std::string > x
Definition jwk.h:56
std::optional< std::vector< std::string > > x5c
Definition jwk.h:53
std::optional< JsonWebKeyECCurve > crv
Definition jwk.h:58
std::optional< std::string > kid
Definition jwk.h:52
std::optional< std::string > issuer
Definition jwk.h:59
std::optional< std::string > e
Definition jwk.h:55
std::optional< std::string > y
Definition jwk.h:57
std::string x
Definition jwk.h:120
JsonWebKeyECCurve crv
Definition jwk.h:119
std::string y
Definition jwk.h:121
std::string e
Definition jwk.h:140
std::string n
Definition jwk.h:139
std::optional< std::string > kid
Definition jwk.h:28
JsonWebKeyType kty
Definition jwk.h:27
std::string host
Definition http_parser.h:138