From 06ff1079788fedac5e3f1f12ed7bbe69228a7ae0 Mon Sep 17 00:00:00 2001
From: Michael Chang <mchang@suse.com>
Date: Tue, 18 Dec 2012 16:54:03 +0800
Subject: [PATCH] Add secureboot support on efi chainloader

References: fate#314485
Patch-Mainline: no

Expand the chainloader to be able to verify the image by means of shim
lock protocol. The PE/COFF image is loaded and relocated by the
chainloader instead of calling LoadImage and StartImage UEFI boot
Service as they require positive verification result from keys enrolled
in KEK or DB. The shim will use MOK in addition to firmware enrolled
keys to verify the image.

The chainloader module could be used to load other UEFI bootloaders,
such as xen.efi, and could be signed by any of MOK, KEK or DB.

v1:
Use grub_efi_get_secureboot to get secure boot status

Signed-off-by: Michael Chang <mchang@suse.com>
---
 grub-core/loader/efi/chainloader.c |  538 +++++++++++++++++++++++++++++++++--
 1 files changed, 507 insertions(+), 31 deletions(-)

--- a/grub-core/loader/efi/chainloader.c
+++ b/grub-core/loader/efi/chainloader.c
@@ -41,10 +41,24 @@
 #include <grub/i386/macho.h>
 #endif
 
+#ifdef __x86_64__
+#define SUPPORT_SECURE_BOOT
+#endif
+
+#ifdef SUPPORT_SECURE_BOOT
+#include <grub/efi/pe32.h>
+#include <grub/efi/sb.h>
+#endif
+
 GRUB_MOD_LICENSE ("GPLv3+");
 
 static grub_dl_t my_mod;
 
+#ifdef SUPPORT_SECURE_BOOT
+static grub_efi_boolean_t debug_secureboot = 0;
+static grub_efi_status_t (__grub_efi_api *entry_point) (grub_efi_handle_t image_handle,  grub_efi_system_table_t *system_table);
+#endif
+
 static grub_err_t
 grub_chainloader_unload (void *context)
 {
@@ -209,6 +223,421 @@
   return file_path;
 }
 
+#ifdef SUPPORT_SECURE_BOOT
+#define SHIM_LOCK_GUID \
+  { 0x605dab50, 0xe046, 0x4300, {0xab, 0xb6, 0x3d, 0xd8, 0x10, 0xdd, 0x8b, 0x23} }
+
+struct grub_pe32_header_no_msdos_stub
+{
+  char signature[GRUB_PE32_SIGNATURE_SIZE];
+  struct grub_pe32_coff_header coff_header;
+  struct grub_pe64_optional_header optional_header;
+};
+
+struct pe_coff_loader_image_context
+{
+  grub_efi_uint64_t image_address;
+  grub_efi_uint64_t image_size;
+  grub_efi_uint64_t entry_point;
+  grub_efi_uintn_t size_of_headers;
+  grub_efi_uint16_t image_type;
+  grub_efi_uint16_t number_of_sections;
+  struct grub_pe32_section_table *first_section;
+  struct grub_pe32_data_directory *reloc_dir;
+  struct grub_pe32_data_directory *sec_dir;
+  grub_efi_uint64_t number_of_rva_and_sizes;
+  struct grub_pe32_header_no_msdos_stub *pe_hdr;
+};
+
+struct grub_secureboot_chainloader_context
+{
+  grub_efi_physical_address_t address;
+  grub_efi_uintn_t pages;
+  grub_ssize_t fsize;
+  grub_efi_device_path_t *file_path;
+  grub_efi_char16_t *cmdline;
+  grub_ssize_t cmdline_len;
+  grub_efi_handle_t dev_handle;
+};
+
+typedef struct pe_coff_loader_image_context pe_coff_loader_image_context_t;
+
+struct grub_efi_shim_lock
+{
+  grub_efi_status_t (*verify)(void *buffer,
+                              grub_efi_uint32_t size);
+  grub_efi_status_t (*hash)(void *data,
+                            grub_efi_int32_t datasize,
+                            pe_coff_loader_image_context_t *context,
+                            grub_efi_uint8_t *sha256hash,
+                            grub_efi_uint8_t *sha1hash);
+  grub_efi_status_t (*context)(void *data,
+                               grub_efi_uint32_t size,
+                               pe_coff_loader_image_context_t *context);
+};
+
+typedef struct grub_efi_shim_lock grub_efi_shim_lock_t;
+
+static grub_efi_boolean_t
+grub_secure_validate (void *data, grub_efi_uint32_t size)
+{
+  grub_guid_t guid = SHIM_LOCK_GUID;
+  grub_efi_shim_lock_t *shim_lock;
+
+  shim_lock = grub_efi_locate_protocol (&guid, NULL);
+
+  if (!shim_lock)
+    {
+      grub_error (GRUB_ERR_BAD_ARGUMENT, "no shim lock protocol");
+      return 0;
+    }
+
+  if (shim_lock->verify (data, size) == GRUB_EFI_SUCCESS)
+    {
+      grub_dprintf ("chain", "verify success\n");
+      return 1;
+    }
+
+  grub_error (GRUB_ERR_BAD_ARGUMENT, "verify failed");
+  return 0;
+}
+
+static grub_efi_boolean_t
+read_header (void *data, grub_efi_uint32_t size, pe_coff_loader_image_context_t *context)
+{
+  grub_efi_guid_t guid = SHIM_LOCK_GUID;
+  grub_efi_shim_lock_t *shim_lock;
+  grub_efi_status_t status;
+
+  shim_lock = grub_efi_locate_protocol (&guid, NULL);
+
+  if (!shim_lock)
+    {
+      grub_error (GRUB_ERR_BAD_ARGUMENT, "no shim lock protocol");
+      return 0;
+    }
+
+  status = shim_lock->context (data, size, context);
+
+  if (status == GRUB_EFI_SUCCESS)
+    {
+      grub_dprintf ("chain", "context success\n");
+      return 1;
+    }
+
+  switch (status)
+    {
+      case GRUB_EFI_UNSUPPORTED:
+      grub_error (GRUB_ERR_BAD_ARGUMENT, "context error unsupported");
+      break;
+      case GRUB_EFI_INVALID_PARAMETER:
+      grub_error (GRUB_ERR_BAD_ARGUMENT, "context error invalid parameter");
+      break;
+      default:
+      grub_error (GRUB_ERR_BAD_ARGUMENT, "context error code");
+      break;
+    }
+
+  return 0;
+}
+
+static void*
+image_address (void *image, grub_efi_uint64_t sz, grub_efi_uint64_t adr)
+{
+  if (adr > sz)
+    return NULL;
+
+  return ((grub_uint8_t*)image + adr);
+}
+
+static grub_efi_status_t
+relocate_coff (pe_coff_loader_image_context_t *context, void *data)
+{
+  struct grub_pe32_data_directory *reloc_base, *reloc_base_end;
+  grub_efi_uint64_t adjust;
+  grub_efi_uint16_t *reloc, *reloc_end;
+  char *fixup, *fixup_base, *fixup_data = NULL;
+  grub_efi_uint16_t *fixup_16;
+  grub_efi_uint32_t *fixup_32;
+  grub_efi_uint64_t *fixup_64;
+
+  grub_efi_uint64_t size = context->image_size;
+  void *image_end = (char *)data + size;
+
+  context->pe_hdr->optional_header.image_base = (grub_uint64_t)data;
+
+  if (context->number_of_rva_and_sizes <= 5 || context->reloc_dir->size == 0)
+    {
+      grub_dprintf ("chain", "no need to reloc, we are done\n");
+      return GRUB_EFI_SUCCESS;
+    }
+
+  reloc_base = image_address (data, size, context->reloc_dir->rva);
+  reloc_base_end = image_address (data, size, context->reloc_dir->rva + context->reloc_dir->size -1);
+
+  grub_dprintf ("chain", "reloc_base %p reloc_base_end %p\n", reloc_base, reloc_base_end);
+
+  if (!reloc_base || !reloc_base_end)
+    {
+      grub_error (GRUB_ERR_BAD_ARGUMENT, "Reloc table overflows binary");
+      return GRUB_EFI_UNSUPPORTED;
+    }
+
+  adjust = (grub_uint64_t)data - context->image_address;
+
+  while (reloc_base < reloc_base_end)
+    {
+      reloc = (grub_uint16_t *)((char*)reloc_base + sizeof (struct grub_pe32_data_directory));
+      reloc_end = (grub_uint16_t *)((char*)reloc_base + reloc_base->size);
+
+      if ((void *)reloc_end < data || (void *)reloc_end > image_end)
+        {
+          grub_error (GRUB_ERR_BAD_ARGUMENT, "Reloc table overflows binary");
+          return GRUB_EFI_UNSUPPORTED;
+        }
+
+      fixup_base = image_address(data, size, reloc_base->rva);
+
+      if (!fixup_base)
+        {
+          grub_error (GRUB_ERR_BAD_ARGUMENT, "Invalid fixupbase");
+          return GRUB_EFI_UNSUPPORTED;
+        }
+
+      while (reloc < reloc_end)
+        {
+          fixup = fixup_base + (*reloc & 0xFFF);
+          switch ((*reloc) >> 12)
+            {
+              case GRUB_PE32_REL_BASED_ABSOLUTE:
+                break;
+              case GRUB_PE32_REL_BASED_HIGH:
+                fixup_16 = (grub_uint16_t *)fixup;
+                *fixup_16 = (grub_uint16_t) (*fixup_16 + ((grub_uint16_t)((grub_uint32_t)adjust >> 16)));
+                if (fixup_data != NULL)
+                  {
+                    *(grub_uint16_t *) fixup_data = *fixup_16;
+                    fixup_data = fixup_data + sizeof (grub_uint16_t);
+                  }
+                break;
+              case GRUB_PE32_REL_BASED_LOW:
+                fixup_16 = (grub_uint16_t *)fixup;
+                *fixup_16 = (grub_uint16_t) (*fixup_16 + (grub_uint16_t)adjust );
+                if (fixup_data != NULL)
+                  {
+                    *(grub_uint16_t *) fixup_data = *fixup_16;
+                    fixup_data = fixup_data + sizeof (grub_uint16_t);
+                  }
+                break;
+              case GRUB_PE32_REL_BASED_HIGHLOW:
+                fixup_32 = (grub_uint32_t *)fixup;
+                *fixup_32 = *fixup_32 + (grub_uint32_t)adjust;
+                if (fixup_data != NULL)
+                  {
+                    fixup_data = (char *)ALIGN_UP ((grub_addr_t)fixup_data, sizeof (grub_uint32_t));
+                    *(grub_uint32_t *) fixup_data = *fixup_32;
+                    fixup_data += sizeof (grub_uint32_t);
+                  }
+                break;
+              case GRUB_PE32_REL_BASED_DIR64:
+                fixup_64 = (grub_uint64_t *)fixup;
+                *fixup_64 = *fixup_64 + (grub_uint64_t)adjust;
+                if (fixup_data != NULL)
+                  {
+                    fixup_data = (char *)ALIGN_UP ((grub_addr_t)fixup_data, sizeof (grub_uint64_t));
+                    *(grub_uint64_t *) fixup_data = *fixup_64;
+                    fixup_data += sizeof (grub_uint64_t);
+                  }
+                break;
+              default:
+                grub_error (GRUB_ERR_BAD_ARGUMENT, "unknown relocation");
+                return GRUB_EFI_UNSUPPORTED;
+            }
+          reloc += 1;
+        }
+      reloc_base = (struct grub_pe32_data_directory *)reloc_end;
+    }
+
+  return GRUB_EFI_SUCCESS;
+}
+
+static grub_efi_device_path_t *
+grub_efi_get_media_file_path (grub_efi_device_path_t *dp)
+{
+  while (1)
+    {
+      grub_efi_uint8_t type;
+      grub_efi_uint8_t subtype; 
+
+      if (GRUB_EFI_END_ENTIRE_DEVICE_PATH (dp))
+        break;
+
+      type = GRUB_EFI_DEVICE_PATH_TYPE (dp);
+      subtype = GRUB_EFI_DEVICE_PATH_SUBTYPE (dp);
+
+      if (type == GRUB_EFI_MEDIA_DEVICE_PATH_TYPE && subtype == GRUB_EFI_FILE_PATH_DEVICE_PATH_SUBTYPE)
+	return dp;
+
+      dp = GRUB_EFI_NEXT_DEVICE_PATH (dp);
+    }
+
+  return NULL;
+}
+
+static grub_efi_boolean_t
+handle_image (struct grub_secureboot_chainloader_context *load_context)
+{
+  grub_efi_boot_services_t *b;
+  grub_efi_loaded_image_t *li, li_bak;
+  grub_efi_status_t efi_status;
+  void *data = (void *)(unsigned long)load_context->address;
+  grub_efi_uint32_t datasize = load_context->fsize;
+  char *buffer = NULL;
+  char *buffer_aligned = NULL;
+  grub_efi_uint32_t i, size;
+  struct grub_pe32_section_table *section;
+  char *base, *end;
+  pe_coff_loader_image_context_t context;
+  grub_uint32_t section_alignment;
+  grub_uint32_t buffer_size;
+
+  b = grub_efi_system_table->boot_services;
+
+  if (read_header (data, datasize, &context))
+    {
+      grub_dprintf ("chain", "Succeed to read header\n");
+    }
+  else
+    {
+      grub_dprintf ("chain", "Failed to read header\n");
+      goto error_exit;
+    }
+
+  section_alignment = context.pe_hdr->optional_header.section_alignment;
+  buffer_size = context.image_size + section_alignment;
+
+  efi_status = b->allocate_pool (GRUB_EFI_LOADER_DATA,
+			   buffer_size, (void**)&buffer);
+
+  if (efi_status != GRUB_EFI_SUCCESS)
+    {
+      grub_error (GRUB_ERR_OUT_OF_MEMORY, N_("out of memory"));
+      goto error_exit;
+    }
+
+  buffer_aligned = (char *)ALIGN_UP ((grub_addr_t)buffer, section_alignment);
+
+  grub_memcpy (buffer_aligned, data, context.size_of_headers);
+
+  section = context.first_section;
+  for (i = 0; i < context.number_of_sections; i++)
+    {
+      size = section->virtual_size;
+      if (size > section->raw_data_size)
+        size = section->raw_data_size;
+
+      base = image_address (buffer_aligned, context.image_size, section->virtual_address);
+      end = image_address (buffer_aligned, context.image_size, section->virtual_address + size - 1);
+
+      if (!base || !end)
+        {
+          grub_error (GRUB_ERR_BAD_ARGUMENT, "Invalid section size");
+          goto error_exit;
+        }
+
+      if (section->raw_data_size > 0)
+        grub_memcpy (base, (grub_efi_uint8_t*)data + section->raw_data_offset, size);
+
+      if (size < section->virtual_size)
+        grub_memset (base + size, 0, section->virtual_size - size);
+
+      grub_dprintf ("chain", "copied section %s\n", section->name);
+      section += 1;
+    }
+
+  efi_status = relocate_coff (&context, buffer_aligned);
+
+  if (efi_status != GRUB_EFI_SUCCESS)
+    {
+      grub_error (GRUB_ERR_BAD_ARGUMENT, "relocation failed");
+      goto error_exit;
+    }
+
+  entry_point = image_address (buffer_aligned, context.image_size, context.entry_point);
+
+  if (!entry_point)
+    {
+      grub_error (GRUB_ERR_BAD_ARGUMENT, "invalid entry point");
+      goto error_exit;
+    }
+
+  li = grub_efi_get_loaded_image (grub_efi_image_handle);
+  if (!li)
+    {
+      grub_error (GRUB_ERR_BAD_ARGUMENT, "no loaded image available");
+      goto error_exit;
+    }
+
+  grub_memcpy (&li_bak, li, sizeof (grub_efi_loaded_image_t));
+  li->image_base = buffer_aligned;
+  li->image_size = context.image_size;
+  li->load_options = load_context->cmdline;
+  li->load_options_size = load_context->cmdline_len;
+  li->file_path = grub_efi_get_media_file_path (load_context->file_path);
+  li->device_handle = load_context->dev_handle;
+  if (li->file_path)
+    {
+      grub_printf ("file path: ");
+      grub_efi_print_device_path (li->file_path);
+    }
+  else
+    {
+      grub_error (GRUB_ERR_UNKNOWN_DEVICE, "no matching file path found");
+      goto error_exit;
+    }
+
+  efi_status = entry_point (grub_efi_image_handle, grub_efi_system_table);
+
+  grub_memcpy (li, &li_bak, sizeof (grub_efi_loaded_image_t));
+  efi_status = b->free_pool (buffer);
+
+  return 1;
+
+error_exit:
+  if (buffer)
+    b->free_pool (buffer);
+
+  return 0;
+
+}
+
+static grub_err_t
+grub_secureboot_chainloader_unload (void* context)
+{
+  grub_efi_boot_services_t *b;
+  struct grub_secureboot_chainloader_context *sb_context = (struct grub_secureboot_chainloader_context *)context;
+
+  b = grub_efi_system_table->boot_services;
+  b->free_pages (sb_context->address, sb_context->pages);
+  grub_free (sb_context->file_path);
+  grub_free (sb_context->cmdline);
+  grub_free (sb_context);
+
+  grub_dl_unref (my_mod);
+  return GRUB_ERR_NONE;
+}
+
+static grub_err_t
+grub_secureboot_chainloader_boot (void *context)
+{
+  struct grub_secureboot_chainloader_context *sb_context = (struct grub_secureboot_chainloader_context *)context;
+
+  handle_image (sb_context);
+  grub_loader_unset ();
+  return grub_errno;
+}
+#endif
+
 static grub_err_t
 grub_cmd_chainloader (grub_command_t cmd __attribute__ ((unused)),
 		      int argc, char *argv[])
@@ -222,11 +651,12 @@
   grub_efi_loaded_image_t *loaded_image;
   char *filename;
   void *boot_image = 0;
-  grub_efi_handle_t dev_handle = 0;
   grub_efi_physical_address_t address = 0;
   grub_efi_uintn_t pages = 0;
   grub_efi_char16_t *cmdline = NULL;
   grub_efi_handle_t image_handle = NULL;
+  grub_ssize_t cmdline_len = 0;
+  grub_efi_handle_t dev_handle = 0;
 
   if (argc == 0)
     return grub_error (GRUB_ERR_BAD_ARGUMENT, N_("filename expected"));
@@ -236,12 +666,39 @@
 
   b = grub_efi_system_table->boot_services;
 
+  if (argc > 1)
+    {
+      int i;
+      grub_efi_char16_t *p16;
+
+      for (i = 1, cmdline_len = 0; i < argc; i++)
+        cmdline_len += grub_strlen (argv[i]) + 1;
+
+      cmdline_len *= sizeof (grub_efi_char16_t);
+      cmdline = p16 = grub_malloc (cmdline_len);
+      if (! cmdline)
+        goto fail;
+
+      for (i = 1; i < argc; i++)
+        {
+          char *p8;
+
+          p8 = argv[i];
+          while (*p8)
+            *(p16++) = *(p8++);
+
+          *(p16++) = ' ';
+        }
+      *(--p16) = 0;
+    }
+
   file = grub_file_open (filename, GRUB_FILE_TYPE_EFI_CHAINLOADED_IMAGE);
   if (! file)
     goto fail;
 
-  /* Get the root device's device path.  */
-  dev = grub_device_open (0);
+  /* Get the device path from filename. */
+  char *devname = grub_file_get_device_name (filename);
+  dev = grub_device_open (devname);
   if (dev == NULL)
     ;
   else if (dev->disk)
@@ -343,6 +800,28 @@
     }
 #endif
 
+#ifdef SUPPORT_SECURE_BOOT
+  /* FIXME is secure boot possible also with universal binaries? */
+  if (debug_secureboot || (grub_efi_get_secureboot () == GRUB_EFI_SECUREBOOT_MODE_ENABLED && grub_secure_validate ((void *)address, size)))
+    {
+      struct grub_secureboot_chainloader_context *sb_context;
+
+      sb_context = grub_malloc (sizeof (*sb_context));
+      if (!sb_context)
+	goto fail;
+      sb_context->cmdline = cmdline;
+      sb_context->cmdline_len = cmdline_len;
+      sb_context->fsize = size;
+      sb_context->dev_handle = dev_handle;
+      sb_context->address = address;
+      sb_context->pages = pages;
+      sb_context->file_path = file_path;
+      grub_file_close (file);
+      grub_loader_set_ex (grub_secureboot_chainloader_boot, grub_secureboot_chainloader_unload, sb_context, 0);
+      return 0;
+    }
+#endif
+
   status = b->load_image (0, grub_efi_image_handle, file_path,
 			  boot_image, size,
 			  &image_handle);
@@ -368,33 +847,10 @@
   loaded_image->device_handle = dev_handle;
 
   /* Build load options with arguments from chainloader command line. */
-  if (argc > 1)
+  if (cmdline)
     {
-      int i, len;
-      grub_efi_char16_t *p16;
-
-      for (i = 1, len = 0; i < argc; i++)
-        len += grub_strlen (argv[i]) + 1;
-
-      len *= sizeof (grub_efi_char16_t);
-      cmdline = p16 = grub_malloc (len);
-      if (! cmdline)
-        goto fail;
-
-      for (i = 1; i < argc; i++)
-        {
-          char *p8;
-
-          p8 = argv[i];
-          while (*p8)
-            *(p16++) = *(p8++);
-
-          *(p16++) = ' ';
-        }
-      *(--p16) = 0;
-
       loaded_image->load_options = cmdline;
-      loaded_image->load_options_size = len;
+      loaded_image->load_options_size = cmdline_len;
     }
 
   grub_file_close (file);
