/* * AWS Nitro Secure Module (NSM) device * * Copyright (c) 2024 Dorjoy Chowdhury * * This work is licensed under the terms of the GNU GPL, version 2 or * (at your option) any later version. See the COPYING file in the * top-level directory. */ #include "qemu/osdep.h" #include "qemu/iov.h" #include "qemu/guest-random.h" #include "qapi/error.h" #include "crypto/hash.h" #include "hw/virtio/virtio.h" #include "hw/virtio/virtio-nsm.h" #include "hw/virtio/cbor-helpers.h" #include "standard-headers/linux/virtio_ids.h" #define NSM_REQUEST_MAX_SIZE 0x1000 #define NSM_RESPONSE_BUF_SIZE 0x3000 #define NSM_RND_BUF_SIZE 256 enum NSMResponseTypes { NSM_SUCCESS = 0, NSM_INVALID_ARGUMENT = 1, NSM_INVALID_INDEX = 2, NSM_READONLY_INDEX = 3, NSM_INVALID_OPERATION = 4, NSM_BUFFER_TOO_SMALL = 5, NSM_INPUT_TOO_LARGE = 6, NSM_INTERNAL_ERROR = 7, }; static const char *error_string(enum NSMResponseTypes type) { const char *str; switch (type) { case NSM_INVALID_ARGUMENT: str = "InvalidArgument"; break; case NSM_INVALID_INDEX: str = "InvalidIndex"; break; case NSM_READONLY_INDEX: str = "ReadOnlyIndex"; break; case NSM_INVALID_OPERATION: str = "InvalidOperation"; break; case NSM_BUFFER_TOO_SMALL: str = "BufferTooSmall"; break; case NSM_INPUT_TOO_LARGE: str = "InputTooLarge"; break; default: str = "InternalError"; break; } return str; } /* * Error response structure: * * { * Map(1) { * key = String("Error"), * value = String(error_name) * } * } * * where error_name can be one of the following: * InvalidArgument * InvalidIndex * InvalidResponse * ReadOnlyIndex * InvalidOperation * BufferTooSmall * InputTooLarge * InternalError */ static bool error_response(struct iovec *response, enum NSMResponseTypes error, Error **errp) { cbor_item_t *root; size_t len; bool r = false; root = cbor_new_definite_map(1); if (!root) { goto err; } if (!qemu_cbor_add_string_to_map(root, "Error", error_string(error))) { goto err; } len = cbor_serialize(root, response->iov_base, response->iov_len); if (len == 0) { error_setg(errp, "Response buffer is small for %s response", error_string(error)); goto out; } response->iov_len = len; r = true; out: if (root) { cbor_decref(&root); } return r; err: error_setg(errp, "Failed to initialize %s response", error_string(error)); goto out; } /* * GetRandom response structure: * * { * Map(1) { * key = String("GetRandom"), * value = Map(1) { * key = String("random"), * value = Byte_String() * } * } * } */ static bool handle_get_random(VirtIONSM *vnsm, struct iovec *request, struct iovec *response, Error **errp) { cbor_item_t *root, *nested_map; size_t len; uint8_t rnd[NSM_RND_BUF_SIZE]; bool r = false; root = cbor_new_definite_map(1); if (!root) { goto err; } if (!qemu_cbor_add_map_to_map(root, "GetRandom", 1, &nested_map)) { goto err; } qemu_guest_getrandom_nofail(rnd, NSM_RND_BUF_SIZE); if (!qemu_cbor_add_bytestring_to_map(nested_map, "random", rnd, NSM_RND_BUF_SIZE)) { goto err; } len = cbor_serialize(root, response->iov_base, response->iov_len); if (len == 0) { if (error_response(response, NSM_INPUT_TOO_LARGE, errp)) { r = true; } goto out; } response->iov_len = len; r = true; out: if (root) { cbor_decref(&root); } return r; err: error_setg(errp, "Failed to initialize GetRandom response"); goto out; } /* * DescribeNSM response structure: * * { * Map(1) { * key = String("DescribeNSM"), * value = Map(7) { * key = String("digest"), * value = String("SHA384"), * key = String("max_pcrs"), * value = Uint8(32), * key = String("module_id"), * value = String("i-1234-enc5678"), * key = String("locked_pcrs"), * value = Array(), * key = String("version_major"), * value = Uint8(1), * key = String("version_minor"), * value = Uint8(0), * key = String("version_patch"), * value = Uint8(0) * } * } * } */ static bool handle_describe_nsm(VirtIONSM *vnsm, struct iovec *request, struct iovec *response, Error **errp) { cbor_item_t *root, *nested_map; uint16_t locked_pcrs_cnt = 0; uint8_t locked_pcrs_ind[NSM_MAX_PCRS]; size_t len; bool r = false; root = cbor_new_definite_map(1); if (!root) { goto err; } if (!qemu_cbor_add_map_to_map(root, "DescribeNSM", 7, &nested_map)) { goto err; } if (!qemu_cbor_add_string_to_map(nested_map, "digest", vnsm->digest)) { goto err; } if (!qemu_cbor_add_uint8_to_map(nested_map, "max_pcrs", vnsm->max_pcrs)) { goto err; } if (!qemu_cbor_add_string_to_map(nested_map, "module_id", vnsm->module_id)) { goto err; } for (uint8_t i = 0; i < NSM_MAX_PCRS; ++i) { if (vnsm->pcrs[i].locked) { locked_pcrs_ind[locked_pcrs_cnt++] = i; } } if (!qemu_cbor_add_uint8_array_to_map(nested_map, "locked_pcrs", locked_pcrs_ind, locked_pcrs_cnt)) { goto err; } if (!qemu_cbor_add_uint8_to_map(nested_map, "version_major", vnsm->version_major)) { goto err; } if (!qemu_cbor_add_uint8_to_map(nested_map, "version_minor", vnsm->version_minor)) { goto err; } if (!qemu_cbor_add_uint8_to_map(nested_map, "version_patch", vnsm->version_patch)) { goto err; } len = cbor_serialize(root, response->iov_base, response->iov_len); if (len == 0) { if (error_response(response, NSM_INPUT_TOO_LARGE, errp)) { r = true; } goto out; } response->iov_len = len; r = true; out: if (root) { cbor_decref(&root); } return r; err: error_setg(errp, "Failed to initialize DescribeNSM response"); goto out; } /* * DescribePCR request structure: * * { * Map(1) { * key = String("DescribePCR"), * value = Map(1) { * key = String("index"), * value = Uint8(pcr) * } * } * } */ typedef struct NSMDescribePCRReq { uint8_t index; } NSMDescribePCRReq; static enum NSMResponseTypes get_nsm_describe_pcr_req( uint8_t *req, size_t len, NSMDescribePCRReq *nsm_req) { size_t size; uint8_t *str; struct cbor_pair *pair; cbor_item_t *item = NULL; struct cbor_load_result result; enum NSMResponseTypes r = NSM_INVALID_OPERATION; item = cbor_load(req, len, &result); if (!item || result.error.code != CBOR_ERR_NONE) { goto cleanup; } pair = cbor_map_handle(item); if (!cbor_isa_map(pair->value)) { goto cleanup; } size = cbor_map_size(pair->value); if (size < 1) { goto cleanup; } pair = cbor_map_handle(pair->value); for (int i = 0; i < size; ++i) { if (!cbor_isa_string(pair[i].key)) { continue; } str = cbor_string_handle(pair[i].key); if (str && cbor_string_length(pair[i].key) == 5 && memcmp(str, "index", 5) == 0) { if (!cbor_isa_uint(pair[i].value) || cbor_int_get_width(pair[i].value) != CBOR_INT_8) { break; } nsm_req->index = cbor_get_uint8(pair[i].value); r = NSM_SUCCESS; break; } } cleanup: if (item) { cbor_decref(&item); } return r; } /* * DescribePCR response structure: * * { * Map(1) { * key = String("DescribePCR"), * value = Map(2) { * key = String("data"), * value = Byte_String(), * key = String("lock"), * value = Bool() * } * } * } */ static bool handle_describe_pcr(VirtIONSM *vnsm, struct iovec *request, struct iovec *response, Error **errp) { cbor_item_t *root = NULL; cbor_item_t *nested_map; size_t len; NSMDescribePCRReq nsm_req; enum NSMResponseTypes type; struct PCRInfo *pcr; bool r = false; type = get_nsm_describe_pcr_req(request->iov_base, request->iov_len, &nsm_req); if (type != NSM_SUCCESS) { if (error_response(response, type, errp)) { r = true; } goto out; } if (nsm_req.index >= vnsm->max_pcrs) { if (error_response(response, NSM_INVALID_INDEX, errp)) { r = true; } goto out; } pcr = &(vnsm->pcrs[nsm_req.index]); root = cbor_new_definite_map(1); if (!root) { goto err; } if (!qemu_cbor_add_map_to_map(root, "DescribePCR", 2, &nested_map)) { goto err; } if (!qemu_cbor_add_bytestring_to_map(nested_map, "data", pcr->data, QCRYPTO_HASH_DIGEST_LEN_SHA384)) { goto err; } if (!qemu_cbor_add_bool_to_map(nested_map, "lock", pcr->locked)) { goto err; } len = cbor_serialize(root, response->iov_base, response->iov_len); if (len == 0) { if (error_response(response, NSM_INPUT_TOO_LARGE, errp)) { r = true; } goto out; } response->iov_len = len; r = true; out: if (root) { cbor_decref(&root); } return r; err: error_setg(errp, "Failed to initialize DescribePCR response"); goto out; } /* * ExtendPCR request structure: * * { * Map(1) { * key = String("ExtendPCR"), * value = Map(2) { * key = String("index"), * value = Uint8(pcr), * key = String("data"), * value = Byte_String(data) || String(data), * } * } * } */ typedef struct NSMExtendPCRReq { uint8_t index; uint16_t data_len; uint8_t data[NSM_REQUEST_MAX_SIZE]; } NSMExtendPCRReq; static enum NSMResponseTypes get_nsm_extend_pcr_req(uint8_t *req, size_t len, NSMExtendPCRReq *nsm_req) { cbor_item_t *item = NULL; size_t size ; uint8_t *str; bool index_found = false; bool data_found = false; struct cbor_pair *pair; struct cbor_load_result result; enum NSMResponseTypes r = NSM_INVALID_OPERATION; item = cbor_load(req, len, &result); if (!item || result.error.code != CBOR_ERR_NONE) { goto cleanup; } pair = cbor_map_handle(item); if (!cbor_isa_map(pair->value)) { goto cleanup; } size = cbor_map_size(pair->value); if (size < 2) { goto cleanup; } pair = cbor_map_handle(pair->value); for (int i = 0; i < size; ++i) { if (!cbor_isa_string(pair[i].key)) { continue; } str = cbor_string_handle(pair[i].key); if (!str) { continue; } if (cbor_string_length(pair[i].key) == 5 && memcmp(str, "index", 5) == 0) { if (!cbor_isa_uint(pair[i].value) || cbor_int_get_width(pair[i].value) != CBOR_INT_8) { goto cleanup; } nsm_req->index = cbor_get_uint8(pair[i].value); index_found = true; continue; } if (cbor_string_length(pair[i].key) == 4 && memcmp(str, "data", 4) == 0) { if (cbor_isa_bytestring(pair[i].value)) { str = cbor_bytestring_handle(pair[i].value); if (!str) { goto cleanup; } nsm_req->data_len = cbor_bytestring_length(pair[i].value); } else if (cbor_isa_string(pair[i].value)) { str = cbor_string_handle(pair[i].value); if (!str) { goto cleanup; } nsm_req->data_len = cbor_string_length(pair[i].value); } else { goto cleanup; } /* * nsm_req->data_len will be smaller than NSM_REQUEST_MAX_SIZE as * we already check for the max request size before processing * any request. So it's safe to copy. */ memcpy(nsm_req->data, str, nsm_req->data_len); data_found = true; continue; } } if (index_found && data_found) { r = NSM_SUCCESS; } cleanup: if (item) { cbor_decref(&item); } return r; } /* * ExtendPCR response structure: * * { * Map(1) { * key = String("ExtendPCR"), * value = Map(1) { * key = String("data"), * value = Byte_String() * } * } * } */ static bool handle_extend_pcr(VirtIONSM *vnsm, struct iovec *request, struct iovec *response, Error **errp) { cbor_item_t *root = NULL; cbor_item_t *nested_map; size_t len; struct PCRInfo *pcr; enum NSMResponseTypes type; bool r = false; g_autofree NSMExtendPCRReq *nsm_req = g_malloc(sizeof(NSMExtendPCRReq)); type = get_nsm_extend_pcr_req(request->iov_base, request->iov_len, nsm_req); if (type != NSM_SUCCESS) { if (error_response(response, type, errp)) { r = true; } goto out; } if (nsm_req->index >= vnsm->max_pcrs) { if (error_response(response, NSM_INVALID_INDEX, errp)) { r = true; } goto out; } pcr = &(vnsm->pcrs[nsm_req->index]); if (pcr->locked) { if (error_response(response, NSM_READONLY_INDEX, errp)) { r = true; } goto out; } if (!vnsm->extend_pcr(vnsm, nsm_req->index, nsm_req->data, nsm_req->data_len)) { if (error_response(response, NSM_INTERNAL_ERROR, errp)) { r = true; } goto out; } root = cbor_new_definite_map(1); if (!root) { goto err; } if (!qemu_cbor_add_map_to_map(root, "ExtendPCR", 1, &nested_map)) { goto err; } if (!qemu_cbor_add_bytestring_to_map(nested_map, "data", pcr->data, QCRYPTO_HASH_DIGEST_LEN_SHA384)) { goto err; } len = cbor_serialize(root, response->iov_base, response->iov_len); if (len == 0) { if (error_response(response, NSM_BUFFER_TOO_SMALL, errp)) { r = true; } goto out; } response->iov_len = len; r = true; out: if (root) { cbor_decref(&root); } return r; err: error_setg(errp, "Failed to initialize DescribePCR response"); goto out; } /* * LockPCR request structure: * * { * Map(1) { * key = String("LockPCR"), * value = Map(1) { * key = String("index"), * value = Uint8(pcr) * } * } * } */ typedef struct NSMLockPCRReq { uint8_t index; } NSMLockPCRReq; static enum NSMResponseTypes get_nsm_lock_pcr_req(uint8_t *req, size_t len, NSMLockPCRReq *nsm_req) { cbor_item_t *item = NULL; size_t size; uint8_t *str; struct cbor_pair *pair; struct cbor_load_result result; enum NSMResponseTypes r = NSM_INVALID_OPERATION; item = cbor_load(req, len, &result); if (!item || result.error.code != CBOR_ERR_NONE) { goto cleanup; } pair = cbor_map_handle(item); if (!cbor_isa_map(pair->value)) { goto cleanup; } size = cbor_map_size(pair->value); if (size < 1) { goto cleanup; } pair = cbor_map_handle(pair->value); for (int i = 0; i < size; ++i) { if (!cbor_isa_string(pair[i].key)) { continue; } str = cbor_string_handle(pair[i].key); if (str && cbor_string_length(pair[i].key) == 5 && memcmp(str, "index", 5) == 0) { if (!cbor_isa_uint(pair[i].value) || cbor_int_get_width(pair[i].value) != CBOR_INT_8) { break; } nsm_req->index = cbor_get_uint8(pair[i].value); r = NSM_SUCCESS; break; } } cleanup: if (item) { cbor_decref(&item); } return r; } /* * LockPCR success response structure: * { * String("LockPCR") * } */ static bool handle_lock_pcr(VirtIONSM *vnsm, struct iovec *request, struct iovec *response, Error **errp) { cbor_item_t *root = NULL; size_t len; NSMLockPCRReq nsm_req; enum NSMResponseTypes type; struct PCRInfo *pcr; bool r = false; type = get_nsm_lock_pcr_req(request->iov_base, request->iov_len, &nsm_req); if (type != NSM_SUCCESS) { if (error_response(response, type, errp)) { r = true; } goto cleanup; } if (nsm_req.index >= vnsm->max_pcrs) { if (error_response(response, NSM_INVALID_INDEX, errp)) { r = true; } goto cleanup; } pcr = &(vnsm->pcrs[nsm_req.index]); if (pcr->locked) { if (error_response(response, NSM_READONLY_INDEX, errp)) { r = true; } goto cleanup; } pcr->locked = true; root = cbor_build_string("LockPCR"); if (!root) { goto err; } len = cbor_serialize(root, response->iov_base, response->iov_len); if (len == 0) { if (error_response(response, NSM_BUFFER_TOO_SMALL, errp)) { r = true; } goto cleanup; } response->iov_len = len; r = true; goto cleanup; err: error_setg(errp, "Failed to initialize LockPCR response"); cleanup: if (root) { cbor_decref(&root); } return r; } /* * LockPCRs request structure: * * { * Map(1) { * key = String("LockPCRs"), * value = Map(1) { * key = String("range"), * value = Uint8(pcr) * } * } * } */ typedef struct NSMLockPCRsReq { uint16_t range; } NSMLockPCRsReq; static enum NSMResponseTypes get_nsm_lock_pcrs_req(uint8_t *req, size_t len, NSMLockPCRsReq *nsm_req) { cbor_item_t *item = NULL; size_t size; uint8_t *str; struct cbor_pair *pair; struct cbor_load_result result; enum NSMResponseTypes r = NSM_INVALID_OPERATION; item = cbor_load(req, len, &result); if (!item || result.error.code != CBOR_ERR_NONE) { goto cleanup; } pair = cbor_map_handle(item); if (!cbor_isa_map(pair->value)) { goto cleanup; } size = cbor_map_size(pair->value); if (size < 1) { goto cleanup; } pair = cbor_map_handle(pair->value); for (int i = 0; i < size; ++i) { if (!cbor_isa_string(pair[i].key)) { continue; } str = cbor_string_handle(pair[i].key); if (str && cbor_string_length(pair[i].key) == 5 && memcmp(str, "range", 5) == 0) { if (!cbor_isa_uint(pair[i].value) || cbor_int_get_width(pair[i].value) != CBOR_INT_8) { break; } nsm_req->range = cbor_get_uint8(pair[i].value); r = NSM_SUCCESS; break; } } cleanup: if (item) { cbor_decref(&item); } return r; } /* * LockPCRs success response structure: * { * String("LockPCRs") * } */ static bool handle_lock_pcrs(VirtIONSM *vnsm, struct iovec *request, struct iovec *response, Error **errp) { cbor_item_t *root = NULL; size_t len; NSMLockPCRsReq nsm_req; enum NSMResponseTypes type; bool r = false; type = get_nsm_lock_pcrs_req(request->iov_base, request->iov_len, &nsm_req); if (type != NSM_SUCCESS) { if (error_response(response, type, errp)) { r = true; } goto cleanup; } if (nsm_req.range > vnsm->max_pcrs) { if (error_response(response, NSM_INVALID_INDEX, errp)) { r = true; } goto cleanup; } for (int i = 0; i < nsm_req.range; ++i) { vnsm->pcrs[i].locked = true; } root = cbor_build_string("LockPCRs"); if (!root) { goto err; } len = cbor_serialize(root, response->iov_base, response->iov_len); if (len == 0) { if (error_response(response, NSM_BUFFER_TOO_SMALL, errp)) { r = true; } goto cleanup; } response->iov_len = len; r = true; goto cleanup; err: error_setg(errp, "Failed to initialize response"); cleanup: if (root) { cbor_decref(&root); } return r; } /* * Attestation request structure: * * Map(1) { * key = String("Attestation"), * value = Map(3) { * key = String("user_data"), * value = Byte_String() || null, // Optional * key = String("nonce"), * value = Byte_String() || null, // Optional * key = String("public_key"), * value = Byte_String() || null, // Optional * } * } * } */ struct AttestationProperty { bool is_null; /* True if property is not present in map or is null */ uint16_t len; uint8_t buf[NSM_REQUEST_MAX_SIZE]; }; typedef struct NSMAttestationReq { struct AttestationProperty public_key; struct AttestationProperty user_data; struct AttestationProperty nonce; } NSMAttestationReq; static bool fill_attestation_property(struct AttestationProperty *prop, cbor_item_t *value) { uint8_t *str; bool ret = false; if (cbor_is_null(value)) { prop->is_null = true; ret = true; goto out; } else if (cbor_isa_bytestring(value)) { str = cbor_bytestring_handle(value); if (!str) { goto out; } prop->len = cbor_bytestring_length(value); } else if (cbor_isa_string(value)) { str = cbor_string_handle(value); if (!str) { goto out; } prop->len = cbor_string_length(value); } else { goto out; } /* * prop->len will be smaller than NSM_REQUEST_MAX_SIZE as we * already check for the max request size before processing * any request. So it's safe to copy. */ memcpy(prop->buf, str, prop->len); prop->is_null = false; ret = true; out: return ret; } static enum NSMResponseTypes get_nsm_attestation_req(uint8_t *req, size_t len, NSMAttestationReq *nsm_req) { cbor_item_t *item = NULL; size_t size; uint8_t *str; struct cbor_pair *pair; struct cbor_load_result result; enum NSMResponseTypes r = NSM_INVALID_OPERATION; nsm_req->public_key.is_null = true; nsm_req->user_data.is_null = true; nsm_req->nonce.is_null = true; item = cbor_load(req, len, &result); if (!item || result.error.code != CBOR_ERR_NONE) { goto cleanup; } pair = cbor_map_handle(item); if (!cbor_isa_map(pair->value)) { goto cleanup; } size = cbor_map_size(pair->value); if (size == 0) { r = NSM_SUCCESS; goto cleanup; } pair = cbor_map_handle(pair->value); for (int i = 0; i < size; ++i) { if (!cbor_isa_string(pair[i].key)) { continue; } str = cbor_string_handle(pair[i].key); if (!str) { continue; } if (cbor_string_length(pair[i].key) == 10 && memcmp(str, "public_key", 10) == 0) { if (!fill_attestation_property(&(nsm_req->public_key), pair[i].value)) { goto cleanup; } continue; } if (cbor_string_length(pair[i].key) == 9 && memcmp(str, "user_data", 9) == 0) { if (!fill_attestation_property(&(nsm_req->user_data), pair[i].value)) { goto cleanup; } continue; } if (cbor_string_length(pair[i].key) == 5 && memcmp(str, "nonce", 5) == 0) { if (!fill_attestation_property(&(nsm_req->nonce), pair[i].value)) { goto cleanup; } continue; } } r = NSM_SUCCESS; cleanup: if (item) { cbor_decref(&item); } return r; } static bool add_protected_header_to_cose(cbor_item_t *cose) { cbor_item_t *map = NULL; cbor_item_t *key = NULL; cbor_item_t *value = NULL; cbor_item_t *bs = NULL; size_t len; bool r = false; size_t buf_len = 4096; g_autofree uint8_t *buf = g_malloc(buf_len); map = cbor_new_definite_map(1); if (!map) { goto cleanup; } key = cbor_build_uint8(1); if (!key) { goto cleanup; } value = cbor_new_int8(); if (!value) { goto cleanup; } cbor_mark_negint(value); /* we don't actually sign the data, so we use -1 as the 'alg' value */ cbor_set_uint8(value, 0); if (!qemu_cbor_map_add(map, key, value)) { goto cleanup; } len = cbor_serialize(map, buf, buf_len); if (len == 0) { goto cleanup_map; } bs = cbor_build_bytestring(buf, len); if (!bs) { goto cleanup_map; } if (!qemu_cbor_array_push(cose, bs)) { cbor_decref(&bs); goto cleanup_map; } r = true; goto cleanup_map; cleanup: if (key) { cbor_decref(&key); } if (value) { cbor_decref(&value); } cleanup_map: if (map) { cbor_decref(&map); } return r; } static bool add_unprotected_header_to_cose(cbor_item_t *cose) { cbor_item_t *map = cbor_new_definite_map(0); if (!map) { goto cleanup; } if (!qemu_cbor_array_push(cose, map)) { goto cleanup; } return true; cleanup: if (map) { cbor_decref(&map); } return false; } static bool add_ca_bundle_to_payload(cbor_item_t *map) { cbor_item_t *key_cbor = NULL; cbor_item_t *value_cbor = NULL; cbor_item_t *bs = NULL; uint8_t zero[64] = {0}; key_cbor = cbor_build_string("cabundle"); if (!key_cbor) { goto cleanup; } value_cbor = cbor_new_definite_array(1); if (!value_cbor) { goto cleanup; } bs = cbor_build_bytestring(zero, 64); if (!bs) { goto cleanup; } if (!qemu_cbor_array_push(value_cbor, bs)) { cbor_decref(&bs); goto cleanup; } if (!qemu_cbor_map_add(map, key_cbor, value_cbor)) { goto cleanup; } return true; cleanup: if (key_cbor) { cbor_decref(&key_cbor); } if (value_cbor) { cbor_decref(&value_cbor); } return false; } static bool add_payload_to_cose(cbor_item_t *cose, VirtIONSM *vnsm, NSMAttestationReq *req) { cbor_item_t *root = NULL; cbor_item_t *nested_map; cbor_item_t *bs = NULL; size_t locked_cnt; uint8_t ind[NSM_MAX_PCRS]; size_t payload_map_size = 9; size_t len; struct PCRInfo *pcr; uint8_t zero[64] = {0}; bool r = false; size_t buf_len = 16384; g_autofree uint8_t *buf = g_malloc(buf_len); root = cbor_new_definite_map(payload_map_size); if (!root) { goto cleanup; } if (!qemu_cbor_add_string_to_map(root, "module_id", vnsm->module_id)) { goto cleanup; } if (!qemu_cbor_add_string_to_map(root, "digest", vnsm->digest)) { goto cleanup; } if (!qemu_cbor_add_uint64_to_map(root, "timestamp", (uint64_t) time(NULL) * 1000)) { goto cleanup; } locked_cnt = 0; for (uint8_t i = 0; i < NSM_MAX_PCRS; ++i) { if (vnsm->pcrs[i].locked) { ind[locked_cnt++] = i; } } if (!qemu_cbor_add_map_to_map(root, "pcrs", locked_cnt, &nested_map)) { goto cleanup; } for (uint8_t i = 0; i < locked_cnt; ++i) { pcr = &(vnsm->pcrs[ind[i]]); if (!qemu_cbor_add_uint8_key_bytestring_to_map( nested_map, ind[i], pcr->data, QCRYPTO_HASH_DIGEST_LEN_SHA384)) { goto cleanup; } } if (!qemu_cbor_add_bytestring_to_map(root, "certificate", zero, 64)) { goto cleanup; } if (!add_ca_bundle_to_payload(root)) { goto cleanup; } if (req->public_key.is_null) { if (!qemu_cbor_add_null_to_map(root, "public_key")) { goto cleanup; } } else if (!qemu_cbor_add_bytestring_to_map(root, "public_key", req->public_key.buf, req->public_key.len)) { goto cleanup; } if (req->user_data.is_null) { if (!qemu_cbor_add_null_to_map(root, "user_data")) { goto cleanup; } } else if (!qemu_cbor_add_bytestring_to_map(root, "user_data", req->user_data.buf, req->user_data.len)) { goto cleanup; } if (req->nonce.is_null) { if (!qemu_cbor_add_null_to_map(root, "nonce")) { goto cleanup; } } else if (!qemu_cbor_add_bytestring_to_map(root, "nonce", req->nonce.buf, req->nonce.len)) { goto cleanup; } len = cbor_serialize(root, buf, buf_len); if (len == 0) { goto cleanup; } bs = cbor_build_bytestring(buf, len); if (!bs) { goto cleanup; } if (!qemu_cbor_array_push(cose, bs)) { cbor_decref(&bs); goto cleanup; } r = true; cleanup: if (root) { cbor_decref(&root); } return r; } static bool add_signature_to_cose(cbor_item_t *cose) { cbor_item_t *bs = NULL; uint8_t zero[64] = {0}; /* we don't actually sign the data, so we just put 64 zero bytes */ bs = cbor_build_bytestring(zero, 64); if (!bs) { goto cleanup; } if (!qemu_cbor_array_push(cose, bs)) { goto cleanup; } return true; cleanup: if (bs) { cbor_decref(&bs); } return false; } /* * Attestation response structure: * * { * Map(1) { * key = String("Attestation"), * value = Map(1) { * key = String("document"), * value = Byte_String() * } * } * } * * The document is a serialized COSE sign1 blob of the structure: * { * Array(4) { * [0] { ByteString() }, // serialized protected header * [1] { Map(0) }, // 0 length map * [2] { ByteString() }, // serialized payload * [3] { ByteString() }, // signature * } * } * * where [0] protected header is a serialized CBOR blob of the structure: * { * Map(1) { * key = Uint8(1) // alg * value = NegativeInt8() // Signing algorithm * } * } * * [2] payload is serialized CBOR blob of the structure: * { * Map(9) { * [0] { key = String("module_id"), value = String(module_id) }, * [1] { key = String("digest"), value = String("SHA384") }, * [2] { * key = String("timestamp"), * value = Uint64(unix epoch of when document was created) * }, * [3] { * key = String("pcrs"), * value = Map(locked_pcr_cnt) { * key = Uint8(pcr_index), * value = ByteString(pcr_data) * }, * }, * [4] { * key = String("certificate"), * value = ByteString(Signing certificate) * }, * [5] { key = String("cabundle"), value = Array(N) { ByteString()... } }, * [6] { key = String("public_key"), value = ByteString() || null }, * [7] { key = String("user_data"), value = ByteString() || null}, * [8] { key = String("nonce"), value = ByteString() || null}, * } * } */ static bool handle_attestation(VirtIONSM *vnsm, struct iovec *request, struct iovec *response, Error **errp) { cbor_item_t *root = NULL; cbor_item_t *cose = NULL; cbor_item_t *nested_map; size_t len; enum NSMResponseTypes type; bool r = false; size_t buf_len = 16384; g_autofree uint8_t *buf = g_malloc(buf_len); g_autofree NSMAttestationReq *nsm_req = g_malloc(sizeof(NSMAttestationReq)); nsm_req->public_key.is_null = true; nsm_req->user_data.is_null = true; nsm_req->nonce.is_null = true; type = get_nsm_attestation_req(request->iov_base, request->iov_len, nsm_req); if (type != NSM_SUCCESS) { if (error_response(response, type, errp)) { r = true; } goto out; } cose = cbor_new_definite_array(4); if (!cose) { goto err; } if (!add_protected_header_to_cose(cose)) { goto err; } if (!add_unprotected_header_to_cose(cose)) { goto err; } if (!add_payload_to_cose(cose, vnsm, nsm_req)) { goto err; } if (!add_signature_to_cose(cose)) { goto err; } len = cbor_serialize(cose, buf, buf_len); if (len == 0) { goto err; } root = cbor_new_definite_map(1); if (!root) { goto err; } if (!qemu_cbor_add_map_to_map(root, "Attestation", 1, &nested_map)) { goto err; } if (!qemu_cbor_add_bytestring_to_map(nested_map, "document", buf, len)) { goto err; } len = cbor_serialize(root, response->iov_base, response->iov_len); if (len == 0) { if (error_response(response, NSM_INPUT_TOO_LARGE, errp)) { r = true; } goto out; } response->iov_len = len; r = true; out: if (root) { cbor_decref(&root); } if (cose) { cbor_decref(&cose); } return r; err: error_setg(errp, "Failed to initialize Attestation response"); goto out; } enum CBOR_ROOT_TYPE { CBOR_ROOT_TYPE_STRING = 0, CBOR_ROOT_TYPE_MAP = 1, }; struct nsm_cmd { char name[16]; /* * There are 2 types of request * 1) String(); "GetRandom", "DescribeNSM" * 2) Map(1) { key: String(), value: ... } */ enum CBOR_ROOT_TYPE root_type; bool (*response_fn)(VirtIONSM *vnsm, struct iovec *request, struct iovec *response, Error **errp); }; const struct nsm_cmd nsm_cmds[] = { { "GetRandom", CBOR_ROOT_TYPE_STRING, handle_get_random }, { "DescribeNSM", CBOR_ROOT_TYPE_STRING, handle_describe_nsm }, { "DescribePCR", CBOR_ROOT_TYPE_MAP, handle_describe_pcr }, { "ExtendPCR", CBOR_ROOT_TYPE_MAP, handle_extend_pcr }, { "LockPCR", CBOR_ROOT_TYPE_MAP, handle_lock_pcr }, { "LockPCRs", CBOR_ROOT_TYPE_MAP, handle_lock_pcrs }, { "Attestation", CBOR_ROOT_TYPE_MAP, handle_attestation }, }; static const struct nsm_cmd *get_nsm_request_cmd(uint8_t *buf, size_t len) { size_t size; uint8_t *req; enum CBOR_ROOT_TYPE root_type; struct cbor_load_result result; cbor_item_t *item = cbor_load(buf, len, &result); if (!item || result.error.code != CBOR_ERR_NONE) { goto cleanup; } if (cbor_isa_string(item)) { size = cbor_string_length(item); req = cbor_string_handle(item); root_type = CBOR_ROOT_TYPE_STRING; } else if (cbor_isa_map(item) && cbor_map_size(item) == 1) { struct cbor_pair *handle = cbor_map_handle(item); if (cbor_isa_string(handle->key)) { size = cbor_string_length(handle->key); req = cbor_string_handle(handle->key); root_type = CBOR_ROOT_TYPE_MAP; } else { goto cleanup; } } else { goto cleanup; } if (size == 0 || req == NULL) { goto cleanup; } for (int i = 0; i < ARRAY_SIZE(nsm_cmds); ++i) { if (nsm_cmds[i].root_type == root_type && strlen(nsm_cmds[i].name) == size && memcmp(nsm_cmds[i].name, req, size) == 0) { cbor_decref(&item); return &nsm_cmds[i]; } } cleanup: if (item) { cbor_decref(&item); } return NULL; } static bool get_nsm_request_response(VirtIONSM *vnsm, struct iovec *req, struct iovec *resp, Error **errp) { const struct nsm_cmd *cmd; if (req->iov_len > NSM_REQUEST_MAX_SIZE) { if (error_response(resp, NSM_INPUT_TOO_LARGE, errp)) { return true; } error_setg(errp, "Failed to initialize InputTooLarge response"); return false; } cmd = get_nsm_request_cmd(req->iov_base, req->iov_len); if (cmd == NULL) { if (error_response(resp, NSM_INVALID_OPERATION, errp)) { return true; } error_setg(errp, "Failed to initialize InvalidOperation response"); return false; } return cmd->response_fn(vnsm, req, resp, errp); } static void handle_input(VirtIODevice *vdev, VirtQueue *vq) { g_autofree VirtQueueElement *out_elem = NULL; g_autofree VirtQueueElement *in_elem = NULL; VirtIONSM *vnsm = VIRTIO_NSM(vdev); Error *err = NULL; size_t sz; struct iovec req = {.iov_base = NULL, .iov_len = 0}; struct iovec res = {.iov_base = NULL, .iov_len = 0}; out_elem = virtqueue_pop(vq, sizeof(VirtQueueElement)); if (!out_elem) { /* nothing in virtqueue */ return; } sz = iov_size(out_elem->out_sg, out_elem->out_num); if (sz == 0) { virtio_error(vdev, "Expected non-zero sized request buffer in " "virtqueue"); goto cleanup; } in_elem = virtqueue_pop(vq, sizeof(VirtQueueElement)); if (!in_elem) { virtio_error(vdev, "Expected response buffer after request buffer " "in virtqueue"); goto cleanup; } if (iov_size(in_elem->in_sg, in_elem->in_num) != NSM_RESPONSE_BUF_SIZE) { virtio_error(vdev, "Expected response buffer of length 0x3000"); goto cleanup; } req.iov_base = g_malloc(sz); req.iov_len = iov_to_buf(out_elem->out_sg, out_elem->out_num, 0, req.iov_base, sz); if (req.iov_len != sz) { virtio_error(vdev, "Failed to copy request buffer"); goto cleanup; } res.iov_base = g_malloc(NSM_RESPONSE_BUF_SIZE); res.iov_len = NSM_RESPONSE_BUF_SIZE; if (!get_nsm_request_response(vnsm, &req, &res, &err)) { error_report_err(err); virtio_error(vdev, "Failed to get NSM request response"); goto cleanup; } sz = iov_from_buf(in_elem->in_sg, in_elem->in_num, 0, res.iov_base, res.iov_len); if (sz != res.iov_len) { virtio_error(vdev, "Failed to copy response buffer"); goto cleanup; } g_free(req.iov_base); g_free(res.iov_base); virtqueue_push(vq, out_elem, 0); virtqueue_push(vq, in_elem, in_elem->in_sg->iov_len); virtio_notify(vdev, vq); return; cleanup: g_free(req.iov_base); g_free(res.iov_base); if (out_elem) { virtqueue_detach_element(vq, out_elem, 0); } if (in_elem) { virtqueue_detach_element(vq, in_elem, 0); } return; } static uint64_t get_features(VirtIODevice *vdev, uint64_t f, Error **errp) { return f; } static bool extend_pcr(VirtIONSM *vnsm, int ind, uint8_t *data, uint16_t len) { Error *err = NULL; struct PCRInfo *pcr = &(vnsm->pcrs[ind]); size_t digest_len = QCRYPTO_HASH_DIGEST_LEN_SHA384; uint8_t result[QCRYPTO_HASH_DIGEST_LEN_SHA384]; uint8_t *ptr = result; struct iovec iov[2] = { { .iov_base = pcr->data, .iov_len = QCRYPTO_HASH_DIGEST_LEN_SHA384 }, { .iov_base = data, .iov_len = len }, }; if (qcrypto_hash_bytesv(QCRYPTO_HASH_ALGO_SHA384, iov, 2, &ptr, &digest_len, &err) < 0) { return false; } memcpy(pcr->data, result, QCRYPTO_HASH_DIGEST_LEN_SHA384); return true; } static void lock_pcr(VirtIONSM *vnsm, int ind) { vnsm->pcrs[ind].locked = true; } static void virtio_nsm_device_realize(DeviceState *dev, Error **errp) { VirtIODevice *vdev = VIRTIO_DEVICE(dev); VirtIONSM *vnsm = VIRTIO_NSM(dev); vnsm->max_pcrs = NSM_MAX_PCRS; vnsm->digest = (char *) "SHA384"; if (vnsm->module_id == NULL) { vnsm->module_id = (char *) "i-234-enc5678"; } vnsm->version_major = 1; vnsm->version_minor = 0; vnsm->version_patch = 0; vnsm->extend_pcr = extend_pcr; vnsm->lock_pcr = lock_pcr; virtio_init(vdev, VIRTIO_ID_NITRO_SEC_MOD, 0); vnsm->vq = virtio_add_queue(vdev, 2, handle_input); } static void virtio_nsm_device_unrealize(DeviceState *dev) { VirtIODevice *vdev = VIRTIO_DEVICE(dev); virtio_del_queue(vdev, 0); virtio_cleanup(vdev); } static const VMStateDescription vmstate_pcr_info_entry = { .name = "pcr_info_entry", .minimum_version_id = 1, .version_id = 1, .fields = (const VMStateField[]) { VMSTATE_BOOL(locked, struct PCRInfo), VMSTATE_UINT8_ARRAY(data, struct PCRInfo, QCRYPTO_HASH_DIGEST_LEN_SHA384), VMSTATE_END_OF_LIST() }, }; static const VMStateDescription vmstate_virtio_nsm_device = { .name = "virtio-nsm-device", .minimum_version_id = 1, .version_id = 1, .fields = (const VMStateField[]) { VMSTATE_STRUCT_ARRAY(pcrs, VirtIONSM, NSM_MAX_PCRS, 1, vmstate_pcr_info_entry, struct PCRInfo), VMSTATE_END_OF_LIST() }, }; static const VMStateDescription vmstate_virtio_nsm = { .name = "virtio-nsm", .minimum_version_id = 1, .version_id = 1, .fields = (const VMStateField[]) { VMSTATE_VIRTIO_DEVICE, VMSTATE_END_OF_LIST() }, }; static const Property virtio_nsm_properties[] = { DEFINE_PROP_STRING("module-id", VirtIONSM, module_id), DEFINE_PROP_END_OF_LIST(), }; static void virtio_nsm_class_init(ObjectClass *klass, void *data) { DeviceClass *dc = DEVICE_CLASS(klass); VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass); device_class_set_props(dc, virtio_nsm_properties); dc->vmsd = &vmstate_virtio_nsm; set_bit(DEVICE_CATEGORY_MISC, dc->categories); vdc->realize = virtio_nsm_device_realize; vdc->unrealize = virtio_nsm_device_unrealize; vdc->get_features = get_features; vdc->vmsd = &vmstate_virtio_nsm_device; } static const TypeInfo virtio_nsm_info = { .name = TYPE_VIRTIO_NSM, .parent = TYPE_VIRTIO_DEVICE, .instance_size = sizeof(VirtIONSM), .class_init = virtio_nsm_class_init, }; static void virtio_register_types(void) { type_register_static(&virtio_nsm_info); } type_init(virtio_register_types)