CCF
Loading...
Searching...
No Matches
uvm_endorsements.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
5#include "ccf/crypto/base64.h"
6#include "ccf/ds/json.h"
10#include "node/cose_common.h"
11#include "node/did.h"
12
13#include <algorithm>
14#include <didx509cpp/didx509cpp.h>
15#include <nlohmann/json.hpp>
16#include <qcbor/qcbor.h>
17#include <qcbor/qcbor_spiffy_decode.h>
18#include <span>
19#include <stdexcept>
20#include <string>
21#include <t_cose/t_cose_sign1_verify.h>
22
23namespace ccf
24{
26 {
29 std::string svn;
30
31 bool operator==(const UVMEndorsements&) const = default;
32 };
35
37 {
38 int64_t alg;
39 std::string content_type;
40 std::vector<std::vector<uint8_t>> x5_chain;
41 std::string iss;
42 std::string feed;
43 };
44
45 // Roots of trust for UVM endorsements/measurement in AMD SEV-SNP attestations
46 static std::vector<UVMEndorsements> default_uvm_roots_of_trust = {
47 // Confidential Azure Kubertnetes Service (AKS)
48 {"did:x509:0:sha256:I__iuL25oXEVFdTP_aBLx_eT1RPHbCQ_ECBQfYZpt9s::eku:1.3.6."
49 "1.4.1.311.76.59.1.2",
50 "ContainerPlat-AMD-UVM",
51 "100"},
52 // Confidential Azure Container Instances (ACI)
53 {"did:x509:0:sha256:I__iuL25oXEVFdTP_aBLx_eT1RPHbCQ_ECBQfYZpt9s::eku:1.3.6."
54 "1.4.1.311.76.59.1.5",
55 "ConfAKS-AMD-UVM",
56 "0"}};
57
59 const UVMEndorsements& endorsements,
60 const std::vector<UVMEndorsements>& uvm_roots_of_trust)
61 {
62 return std::any_of(
63 uvm_roots_of_trust.begin(),
64 uvm_roots_of_trust.end(),
65 [&](const auto& uvm_root_of_trust) {
66 size_t root_of_trust_svn = 0;
67 try
68 {
69 static_assert(sizeof(size_t) == sizeof(unsigned long));
70 root_of_trust_svn = std::stoul(uvm_root_of_trust.svn);
71 }
72 catch (const std::logic_error&)
73 {
74 throw std::runtime_error(fmt::format(
75 "Unable to parse svn value {} to unsigned in UVM root of trust",
76 uvm_root_of_trust.svn));
77 }
78
79 size_t endorsement_svn = 0;
80 try
81 {
82 static_assert(sizeof(size_t) == sizeof(unsigned long));
83 endorsement_svn = std::stoul(endorsements.svn);
84 }
85 catch (const std::logic_error&)
86 {
87 throw std::runtime_error(fmt::format(
88 "Unable to parse svn value {} to unsigned in UVM endorsements",
89 endorsements.svn));
90 }
91
92 return uvm_root_of_trust.did == endorsements.did &&
93 uvm_root_of_trust.feed == endorsements.feed &&
94 root_of_trust_svn <= endorsement_svn;
95 });
96 }
97
98 namespace cose
99 {
100 static constexpr auto HEADER_PARAM_ISSUER = "iss";
101 static constexpr auto HEADER_PARAM_FEED = "feed";
102
103 static std::vector<std::vector<uint8_t>> decode_x5chain(
104 QCBORDecodeContext& ctx, const QCBORItem& x5chain)
105 {
106 std::vector<std::vector<uint8_t>> parsed;
107
108 if (x5chain.uDataType == QCBOR_TYPE_ARRAY)
109 {
110 QCBORDecode_EnterArrayFromMapN(&ctx, headers::PARAM_X5CHAIN);
111 while (true)
112 {
113 QCBORItem item;
114 auto result = QCBORDecode_GetNext(&ctx, &item);
115 if (result == QCBOR_ERR_NO_MORE_ITEMS)
116 {
117 break;
118 }
119 if (result != QCBOR_SUCCESS)
120 {
121 throw COSEDecodeError("Item in x5chain is not well-formed");
122 }
123 if (item.uDataType == QCBOR_TYPE_BYTE_STRING)
124 {
125 parsed.push_back(qcbor_buf_to_byte_vector(item.val.string));
126 }
127 else
128 {
129 throw COSEDecodeError(
130 "Next item in x5chain was not of type byte string");
131 }
132 }
133 QCBORDecode_ExitArray(&ctx);
134 if (parsed.empty())
135 {
136 throw COSEDecodeError("x5chain array length was 0 in COSE header");
137 }
138 }
139 else if (x5chain.uDataType == QCBOR_TYPE_BYTE_STRING)
140 {
141 parsed.push_back(qcbor_buf_to_byte_vector(x5chain.val.string));
142 }
143 else
144 {
145 throw COSEDecodeError(fmt::format(
146 "Value type {} of x5chain in COSE header is not array or byte string",
147 x5chain.uDataType));
148 }
149
150 return parsed;
151 }
152
153 static UvmEndorsementsProtectedHeader decode_protected_header(
154 const std::vector<uint8_t>& uvm_endorsements_raw)
155 {
156 UsefulBufC msg{uvm_endorsements_raw.data(), uvm_endorsements_raw.size()};
157
158 QCBORError qcbor_result;
159
160 QCBORDecodeContext ctx;
161 QCBORDecode_Init(&ctx, msg, QCBOR_DECODE_MODE_NORMAL);
162
163 QCBORDecode_EnterArray(&ctx, nullptr);
164 qcbor_result = QCBORDecode_GetError(&ctx);
165 if (qcbor_result != QCBOR_SUCCESS)
166 {
167 throw COSEDecodeError("Failed to parse COSE_Sign1 outer array");
168 }
169
170 uint64_t tag = QCBORDecode_GetNthTagOfLast(&ctx, 0);
171 if (tag != CBOR_TAG_COSE_SIGN1)
172 {
173 throw COSEDecodeError("Failed to parse COSE_Sign1 tag");
174 }
175
176 struct q_useful_buf_c protected_parameters;
177 QCBORDecode_EnterBstrWrapped(
178 &ctx, QCBOR_TAG_REQUIREMENT_NOT_A_TAG, &protected_parameters);
179 QCBORDecode_EnterMap(&ctx, NULL);
180
181 enum
182 {
183 ALG_INDEX,
184 CONTENT_TYPE_INDEX,
185 X5_CHAIN_INDEX,
186 ISS_INDEX,
187 FEED_INDEX,
188 END_INDEX
189 };
190 QCBORItem header_items[END_INDEX + 1];
191
192 header_items[ALG_INDEX].label.int64 = headers::PARAM_ALG;
193 header_items[ALG_INDEX].uLabelType = QCBOR_TYPE_INT64;
194 header_items[ALG_INDEX].uDataType = QCBOR_TYPE_INT64;
195
196 header_items[CONTENT_TYPE_INDEX].label.int64 =
197 headers::PARAM_CONTENT_TYPE;
198 header_items[CONTENT_TYPE_INDEX].uLabelType = QCBOR_TYPE_INT64;
199 header_items[CONTENT_TYPE_INDEX].uDataType = QCBOR_TYPE_TEXT_STRING;
200
201 header_items[X5_CHAIN_INDEX].label.int64 = headers::PARAM_X5CHAIN;
202 header_items[X5_CHAIN_INDEX].uLabelType = QCBOR_TYPE_INT64;
203 header_items[X5_CHAIN_INDEX].uDataType = QCBOR_TYPE_ANY;
204
205 header_items[ISS_INDEX].label.string =
206 UsefulBuf_FromSZ(HEADER_PARAM_ISSUER);
207 header_items[ISS_INDEX].uLabelType = QCBOR_TYPE_TEXT_STRING;
208 header_items[ISS_INDEX].uDataType = QCBOR_TYPE_TEXT_STRING;
209
210 header_items[FEED_INDEX].label.string =
211 UsefulBuf_FromSZ(HEADER_PARAM_FEED);
212 header_items[FEED_INDEX].uLabelType = QCBOR_TYPE_TEXT_STRING;
213 header_items[FEED_INDEX].uDataType = QCBOR_TYPE_TEXT_STRING;
214
215 header_items[END_INDEX].uLabelType = QCBOR_TYPE_NONE;
216
217 QCBORDecode_GetItemsInMap(&ctx, header_items);
218 qcbor_result = QCBORDecode_GetError(&ctx);
219 if (qcbor_result != QCBOR_SUCCESS)
220 {
221 throw COSEDecodeError("Failed to decode protected header");
222 }
223
224 UvmEndorsementsProtectedHeader phdr = {};
225
226 if (header_items[ALG_INDEX].uDataType != QCBOR_TYPE_NONE)
227 {
228 phdr.alg = header_items[ALG_INDEX].val.int64;
229 }
230
231 if (header_items[CONTENT_TYPE_INDEX].uDataType != QCBOR_TYPE_NONE)
232 {
233 phdr.content_type =
234 qcbor_buf_to_string(header_items[CONTENT_TYPE_INDEX].val.string);
235 }
236
237 if (header_items[X5_CHAIN_INDEX].uDataType != QCBOR_TYPE_NONE)
238 {
239 phdr.x5_chain = decode_x5chain(ctx, header_items[X5_CHAIN_INDEX]);
240 }
241
242 if (header_items[ISS_INDEX].uDataType != QCBOR_TYPE_NONE)
243 {
244 phdr.iss = qcbor_buf_to_string(header_items[ISS_INDEX].val.string);
245 }
246
247 if (header_items[FEED_INDEX].uDataType != QCBOR_TYPE_NONE)
248 {
249 phdr.feed = qcbor_buf_to_string(header_items[FEED_INDEX].val.string);
250 }
251
252 QCBORDecode_ExitMap(&ctx);
253 QCBORDecode_ExitBstrWrapped(&ctx);
254
255 qcbor_result = QCBORDecode_GetError(&ctx);
256 if (qcbor_result != QCBOR_SUCCESS)
257 {
258 throw COSEDecodeError(
259 fmt::format("Failed to decode protected header: {}", qcbor_result));
260 }
261
262 return phdr;
263 }
264 }
265
266 static std::span<const uint8_t> verify_uvm_endorsements_signature(
267 const ccf::crypto::Pem& leaf_cert_pub_key,
268 const std::vector<uint8_t>& uvm_endorsements_raw)
269 {
270 auto verifier = ccf::crypto::make_cose_verifier_from_key(leaf_cert_pub_key);
271
272 std::span<uint8_t> payload;
273 if (!verifier->verify(uvm_endorsements_raw, payload))
274 {
275 throw cose::COSESignatureValidationError("Signature verification failed");
276 }
277
278 return payload;
279 }
280
281 static UVMEndorsements verify_uvm_endorsements(
282 const std::vector<uint8_t>& uvm_endorsements_raw,
283 const pal::PlatformAttestationMeasurement& uvm_measurement,
284 const std::vector<UVMEndorsements>& uvm_roots_of_trust =
285 default_uvm_roots_of_trust)
286 {
287 auto phdr = cose::decode_protected_header(uvm_endorsements_raw);
288
289 if (!(cose::is_rsa_alg(phdr.alg) || cose::is_ecdsa_alg(phdr.alg)))
290 {
291 throw std::logic_error(fmt::format(
292 "Signature algorithm {} is not one of expected: RSA, ECDSA", phdr.alg));
293 }
294
295 std::string pem_chain;
296 for (auto const& c : phdr.x5_chain)
297 {
298 pem_chain += ccf::crypto::cert_der_to_pem(c).str();
299 }
300
301 const auto& did = phdr.iss;
302
303 auto did_document_str =
304 didx509::resolve(pem_chain, did, true /* ignore time */);
305 did::DIDDocument did_document = nlohmann::json::parse(did_document_str);
306
307 if (did_document.verification_method.empty())
308 {
309 throw std::logic_error(fmt::format(
310 "Could not find verification method for DID document: {}",
311 did_document_str));
312 }
313
314 ccf::crypto::Pem pubk;
315 for (auto const& vm : did_document.verification_method)
316 {
317 if (vm.controller == did && vm.public_key_jwk.has_value())
318 {
319 auto jwk = vm.public_key_jwk.value().get<ccf::crypto::JsonWebKey>();
320 switch (jwk.kty)
321 {
323 {
324 auto rsa_jwk =
325 vm.public_key_jwk->get<ccf::crypto::JsonWebKeyRSAPublic>();
326 pubk = ccf::crypto::make_rsa_public_key(rsa_jwk)->public_key_pem();
327 break;
328 }
330 {
331 auto ec_jwk =
332 vm.public_key_jwk->get<ccf::crypto::JsonWebKeyECPublic>();
333 pubk = ccf::crypto::make_public_key(ec_jwk)->public_key_pem();
334 break;
335 }
336 default:
337 {
338 throw std::logic_error(fmt::format(
339 "Unsupported public key type ({}) for DID {}", jwk.kty, did));
340 }
341 }
342 }
343 }
344
345 if (pubk.empty())
346 {
347 throw std::logic_error(fmt::format(
348 "Could not find matching public key for DID {} in {}",
349 did,
350 did_document_str));
351 }
352
353 auto raw_payload =
354 verify_uvm_endorsements_signature(pubk, uvm_endorsements_raw);
355
356 if (phdr.content_type != cose::headers::CONTENT_TYPE_APPLICATION_JSON_VALUE)
357 {
358 throw std::logic_error(fmt::format(
359 "Unexpected payload content type {}, expected {}",
360 phdr.content_type,
361 cose::headers::CONTENT_TYPE_APPLICATION_JSON_VALUE));
362 }
363
364 auto payload = nlohmann::json::parse(raw_payload);
365 std::string sevsnpvm_launch_measurement =
366 payload["x-ms-sevsnpvm-launchmeasurement"].get<std::string>();
367 auto sevsnpvm_guest_svn_obj = payload["x-ms-sevsnpvm-guestsvn"];
368 std::string sevsnpvm_guest_svn;
369 if (sevsnpvm_guest_svn_obj.is_string())
370 {
371 sevsnpvm_guest_svn = sevsnpvm_guest_svn_obj.get<std::string>();
372 size_t uintval = 0;
373 try
374 {
375 static_assert(sizeof(size_t) == sizeof(unsigned long));
376 uintval = std::stoul(sevsnpvm_guest_svn);
377 }
378 catch (const std::logic_error&)
379 {
380 throw std::logic_error(fmt::format(
381 "Unable to parse sevsnpvm_guest_svn value {} to unsigned in UVM "
382 "endorsements "
383 "payload",
384 sevsnpvm_guest_svn));
385 }
386 }
387 else if (sevsnpvm_guest_svn_obj.is_number_unsigned())
388 {
389 sevsnpvm_guest_svn = std::to_string(sevsnpvm_guest_svn_obj.get<size_t>());
390 }
391 else
392 {
393 throw std::logic_error(fmt::format(
394 "Unexpected type {} for sevsnpvm_guest_svn in UVM endorsements "
395 "payload, expected string or unsigned integer",
396 sevsnpvm_guest_svn_obj.type_name()));
397 }
398
399 if (sevsnpvm_launch_measurement != uvm_measurement.hex_str())
400 {
401 throw std::logic_error(fmt::format(
402 "Launch measurement in UVM endorsements payload {} is not equal "
403 "to UVM attestation measurement {}",
404 sevsnpvm_launch_measurement,
405 uvm_measurement.hex_str()));
406 }
407
409 "Successfully verified endorsements for attested measurement {} against "
410 "{}, feed {}, svn {}",
411 sevsnpvm_launch_measurement,
412 did,
413 phdr.feed,
414 sevsnpvm_guest_svn);
415
416 UVMEndorsements end{did, phdr.feed, sevsnpvm_guest_svn};
417
418 if (!matches_uvm_roots_of_trust(end, uvm_roots_of_trust))
419 {
420 throw std::logic_error(fmt::format(
421 "UVM endorsements did {}, feed {}, svn {} "
422 "do not match any of the known UVM roots of trust",
423 end.did,
424 end.feed,
425 end.svn));
426 }
427
428 return end;
429 }
430}
Definition pem.h:18
bool empty() const
Definition pem.h:66
const std::string & str() const
Definition pem.h:46
#define DECLARE_JSON_REQUIRED_FIELDS(TYPE,...)
Definition json.h:712
#define DECLARE_JSON_TYPE(TYPE)
Definition json.h:661
#define LOG_INFO_FMT
Definition logger.h:395
ccf::crypto::Pem cert_der_to_pem(const std::vector< uint8_t > &der)
Definition verifier.cpp:33
COSEVerifierUniquePtr make_cose_verifier_from_key(const Pem &public_key)
Definition cose_verifier.cpp:222
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
Definition app_interface.h:15
bool matches_uvm_roots_of_trust(const UVMEndorsements &endorsements, const std::vector< UVMEndorsements > &uvm_roots_of_trust)
Definition uvm_endorsements.h:58
std::string DID
Definition uvm_endorsements.h:20
std::string Feed
Definition uvm_endorsements.h:21
Definition uvm_endorsements.h:26
Feed feed
Definition uvm_endorsements.h:28
std::string svn
Definition uvm_endorsements.h:29
bool operator==(const UVMEndorsements &) const =default
DID did
Definition uvm_endorsements.h:27
Definition uvm_endorsements.h:37
std::string feed
Definition uvm_endorsements.h:42
int64_t alg
Definition uvm_endorsements.h:38
std::string iss
Definition uvm_endorsements.h:41
std::vector< std::vector< uint8_t > > x5_chain
Definition uvm_endorsements.h:40
std::string content_type
Definition uvm_endorsements.h:39
Definition jwk.h:26