[PATCH 3/4] livepatch: Embed public key in Xen

Ross Lagerwall posted 4 patches 7 months, 2 weeks ago
There is a newer version of this series
[PATCH 3/4] livepatch: Embed public key in Xen
Posted by Ross Lagerwall 7 months, 2 weeks ago
From: Kevin Lampis <klampis@cloud.com>

Make it possible to embed a public key in Xen to be used when verifying
live patch payloads. Inclusion of the public key is optional.

To avoid needing to include a DER / X.509 parser in the hypervisor, the
public key is unpacked at build time and included in a form that is
convenient for the hypervisor to consume. This is different approach
from that used by Linux which embeds the entire X.509 certificate and
builds in a parser for it.

A suitable key can be created using openssl:

openssl req -x509 -newkey rsa:2048 -keyout priv.pem -out pub.pem \
    -sha256 -days 3650 -nodes \
    -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"
openssl x509 -inform PEM -in pub.pem -outform PEM -pubkey -nocert -out crypto/signing_key.pem

Signed-off-by: Kevin Lampis <klampis@cloud.com>
Signed-off-by: Ross Lagerwall <ross.lagerwall@citrix.com>
---
 xen/common/Kconfig       | 18 ++++++++++++++++++
 xen/common/Makefile      |  2 +-
 xen/common/livepatch.c   | 41 ++++++++++++++++++++++++++++++++++++++++
 xen/crypto/Makefile      | 14 +++++++++++++-
 xen/tools/extract-key.py | 37 ++++++++++++++++++++++++++++++++++++
 5 files changed, 110 insertions(+), 2 deletions(-)
 create mode 100755 xen/tools/extract-key.py

diff --git a/xen/common/Kconfig b/xen/common/Kconfig
index 4bec78c6f267..e3e4fe2f3477 100644
--- a/xen/common/Kconfig
+++ b/xen/common/Kconfig
@@ -481,6 +481,24 @@ config LIVEPATCH
 
 	  If unsure, say Y.
 
+config PAYLOAD_SIGNING
+	bool "Verify signed LivePatch payloads"
+	depends on LIVEPATCH
+	select CRYPTO
+	help
+	  Verify signed LivePatch payloads using an RSA public key built
+	  into the Xen hypervisor. Selecting this option requires a
+	  public key in PEM format to be available for embedding during
+	  the build.
+
+config PAYLOAD_SIG_KEY
+	string "File name of payload signing public key"
+	default "signing_key.pem"
+	depends on PAYLOAD_SIGNING
+	help
+	  The file name of an RSA public key in PEM format to be used for
+	  verifying signed LivePatch payloads.
+
 config FAST_SYMBOL_LOOKUP
 	bool "Fast symbol lookup (bigger binary)"
 	default y
diff --git a/xen/common/Makefile b/xen/common/Makefile
index ece6548bb072..c75cbfa868a0 100644
--- a/xen/common/Makefile
+++ b/xen/common/Makefile
@@ -28,7 +28,7 @@ obj-$(CONFIG_LIVEPATCH) += livepatch.o livepatch_elf.o
 obj-$(CONFIG_LLC_COLORING) += llc-coloring.o
 obj-$(CONFIG_VM_EVENT) += mem_access.o
 obj-y += memory.o
-obj-y += mpi.o
+obj-$(CONFIG_PAYLOAD_SIGNING) += mpi.o
 obj-y += multicall.o
 obj-y += notifier.o
 obj-$(CONFIG_NUMA) += numa.o
diff --git a/xen/common/livepatch.c b/xen/common/livepatch.c
index be9b7e367553..947d05671b4f 100644
--- a/xen/common/livepatch.c
+++ b/xen/common/livepatch.c
@@ -11,6 +11,8 @@
 #include <xen/lib.h>
 #include <xen/list.h>
 #include <xen/mm.h>
+#include <xen/mpi.h>
+#include <xen/rsa.h>
 #include <xen/sched.h>
 #include <xen/smp.h>
 #include <xen/softirq.h>
@@ -73,6 +75,12 @@ static struct livepatch_work livepatch_work;
 static DEFINE_PER_CPU(bool, work_to_do);
 static DEFINE_PER_CPU(struct tasklet, livepatch_tasklet);
 
+#ifdef CONFIG_PAYLOAD_SIGNING
+/* The public key contained with Xen used to verify payload signatures. */
+extern const uint8_t xen_livepatch_key_data[];
+static struct rsa_public_key builtin_payload_key;
+#endif
+
 static int get_name(const struct xen_livepatch_name *name, char *n)
 {
     if ( !name->size || name->size > XEN_LIVEPATCH_NAME_SIZE )
@@ -2287,6 +2295,34 @@ static void cf_check livepatch_printall(unsigned char key)
     spin_unlock(&payload_lock);
 }
 
+#ifdef CONFIG_PAYLOAD_SIGNING
+static int __init load_builtin_payload_key(void)
+{
+    const uint8_t *ptr;
+    uint32_t len;
+
+    rsa_public_key_init(&builtin_payload_key);
+
+    ptr = xen_livepatch_key_data;
+
+    memcpy(&len, ptr, sizeof(len));
+    ptr += sizeof(len);
+    builtin_payload_key.n = mpi_read_raw_data(ptr, len);
+    ptr += len;
+
+    memcpy(&len, ptr, sizeof(len));
+    ptr += sizeof(len);
+    builtin_payload_key.e = mpi_read_raw_data(ptr, len);
+
+    return rsa_public_key_prepare(&builtin_payload_key);
+}
+#else
+static int __init load_builtin_payload_key(void)
+{
+    return 0;
+}
+#endif
+
 static int cf_check cpu_callback(
     struct notifier_block *nfb, unsigned long action, void *hcpu)
 {
@@ -2305,6 +2341,11 @@ static struct notifier_block cpu_nfb = {
 static int __init cf_check livepatch_init(void)
 {
     unsigned int cpu;
+    int err;
+
+    err = load_builtin_payload_key();
+    if (err)
+        return err;
 
     for_each_online_cpu ( cpu )
     {
diff --git a/xen/crypto/Makefile b/xen/crypto/Makefile
index d88374ddf221..e81302d7cd54 100644
--- a/xen/crypto/Makefile
+++ b/xen/crypto/Makefile
@@ -1,3 +1,15 @@
 obj-y += rijndael.o
-obj-y += rsa.o
+obj-$(CONFIG_PAYLOAD_SIGNING) += rsa.o
 obj-y += vmac.o
+
+obj-$(CONFIG_PAYLOAD_SIGNING) += builtin_payload_key.o
+
+ifeq ($(CONFIG_PAYLOAD_SIGNING),y)
+key_path := $(srctree)/crypto/$(patsubst "%",%,$(CONFIG_PAYLOAD_SIG_KEY))
+$(obj)/builtin_payload_key.bin: $(key_path) $(srctree)/tools/extract-key.py
+	$(srctree)/tools/extract-key.py < $< > $@.new
+	$(call move-if-changed,$@.new,$@)
+
+$(obj)/builtin_payload_key.S: $(srctree)/tools/binfile $(obj)/builtin_payload_key.bin FORCE
+	$(call if_changed,binfile,$(obj)/builtin_payload_key.bin xen_livepatch_key_data)
+endif
diff --git a/xen/tools/extract-key.py b/xen/tools/extract-key.py
new file mode 100755
index 000000000000..2980264b757d
--- /dev/null
+++ b/xen/tools/extract-key.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+
+# SPDX-License-Identifier: GPL-2.0
+
+import binascii
+import struct
+import sys
+import subprocess
+import re
+
+# Decode a certificate into a format suitable for embedding in Xen.
+
+out = subprocess.check_output(['openssl', 'rsa', '-pubin', '-inform', 'PEM',
+                               '-noout', '-text'], stdin=sys.stdin,
+                               universal_newlines=True)
+combined = ''
+for line in out.split('\n'):
+    line = line.rstrip()
+    if line.startswith('    '):
+        combined += line.strip().replace(':', '')
+    match = re.match(r'Exponent: .* \(0x(.*)\)', line)
+    if match:
+        e = match.group(1)
+
+n = combined.lstrip('0')
+if len(n) % 2 == 1:
+    n = '0' + n
+n = binascii.unhexlify(n)
+e = e.lstrip('0')
+if len(e) % 2 == 1:
+    e = '0' + e
+e = binascii.unhexlify(e)
+
+sys.stdout.buffer.write(struct.pack('I', len(n)))
+sys.stdout.buffer.write(n)
+sys.stdout.buffer.write(struct.pack('I', len(e)))
+sys.stdout.buffer.write(e)
-- 
2.49.0
Re: [PATCH 3/4] livepatch: Embed public key in Xen
Posted by Jan Beulich 7 months, 1 week ago
On 06.05.2025 16:32, Ross Lagerwall wrote:
> From: Kevin Lampis <klampis@cloud.com>
> 
> Make it possible to embed a public key in Xen to be used when verifying
> live patch payloads. Inclusion of the public key is optional.
> 
> To avoid needing to include a DER / X.509 parser in the hypervisor, the
> public key is unpacked at build time and included in a form that is
> convenient for the hypervisor to consume. This is different approach
> from that used by Linux which embeds the entire X.509 certificate and
> builds in a parser for it.
> 
> A suitable key can be created using openssl:
> 
> openssl req -x509 -newkey rsa:2048 -keyout priv.pem -out pub.pem \
>     -sha256 -days 3650 -nodes \
>     -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"
> openssl x509 -inform PEM -in pub.pem -outform PEM -pubkey -nocert -out crypto/signing_key.pem

According to this the .pem file isn't really a source one; how does ...

> --- a/xen/common/Kconfig
> +++ b/xen/common/Kconfig
> @@ -481,6 +481,24 @@ config LIVEPATCH
>  
>  	  If unsure, say Y.
>  
> +config PAYLOAD_SIGNING
> +	bool "Verify signed LivePatch payloads"
> +	depends on LIVEPATCH
> +	select CRYPTO
> +	help
> +	  Verify signed LivePatch payloads using an RSA public key built
> +	  into the Xen hypervisor. Selecting this option requires a
> +	  public key in PEM format to be available for embedding during
> +	  the build.
> +
> +config PAYLOAD_SIG_KEY
> +	string "File name of payload signing public key"
> +	default "signing_key.pem"

... this work in an out-of-tree build?

> +	depends on PAYLOAD_SIGNING

As to the name of this: According to its description, it's signature
verification, not signing. I think this wants reflecting in the name.

> --- a/xen/common/Makefile
> +++ b/xen/common/Makefile
> @@ -28,7 +28,7 @@ obj-$(CONFIG_LIVEPATCH) += livepatch.o livepatch_elf.o
>  obj-$(CONFIG_LLC_COLORING) += llc-coloring.o
>  obj-$(CONFIG_VM_EVENT) += mem_access.o
>  obj-y += memory.o
> -obj-y += mpi.o
> +obj-$(CONFIG_PAYLOAD_SIGNING) += mpi.o

Looks like the Kconfig symbol then wants introducing in the earlier
patch, or as a prereq thereto.

> --- a/xen/common/livepatch.c
> +++ b/xen/common/livepatch.c
> @@ -11,6 +11,8 @@
>  #include <xen/lib.h>
>  #include <xen/list.h>
>  #include <xen/mm.h>
> +#include <xen/mpi.h>
> +#include <xen/rsa.h>
>  #include <xen/sched.h>
>  #include <xen/smp.h>
>  #include <xen/softirq.h>
> @@ -73,6 +75,12 @@ static struct livepatch_work livepatch_work;
>  static DEFINE_PER_CPU(bool, work_to_do);
>  static DEFINE_PER_CPU(struct tasklet, livepatch_tasklet);
>  
> +#ifdef CONFIG_PAYLOAD_SIGNING
> +/* The public key contained with Xen used to verify payload signatures. */
> +extern const uint8_t xen_livepatch_key_data[];

Iirc Misra demands that declarations appear in headers.

> +static struct rsa_public_key builtin_payload_key;
> +#endif
> +
>  static int get_name(const struct xen_livepatch_name *name, char *n)
>  {
>      if ( !name->size || name->size > XEN_LIVEPATCH_NAME_SIZE )
> @@ -2287,6 +2295,34 @@ static void cf_check livepatch_printall(unsigned char key)
>      spin_unlock(&payload_lock);
>  }
>  
> +#ifdef CONFIG_PAYLOAD_SIGNING

Nit: The #ifdef would better appear inside the function body, to
reduce redundancy.

> +static int __init load_builtin_payload_key(void)
> +{
> +    const uint8_t *ptr;
> +    uint32_t len;
> +
> +    rsa_public_key_init(&builtin_payload_key);
> +
> +    ptr = xen_livepatch_key_data;

This being the sole place where the array is used, ...

> @@ -2305,6 +2341,11 @@ static struct notifier_block cpu_nfb = {
>  static int __init cf_check livepatch_init(void)
>  {
>      unsigned int cpu;
> +    int err;
> +
> +    err = load_builtin_payload_key();
> +    if (err)

(Nit: style)

> --- a/xen/crypto/Makefile
> +++ b/xen/crypto/Makefile
> @@ -1,3 +1,15 @@
>  obj-y += rijndael.o
> -obj-y += rsa.o
> +obj-$(CONFIG_PAYLOAD_SIGNING) += rsa.o
>  obj-y += vmac.o
> +
> +obj-$(CONFIG_PAYLOAD_SIGNING) += builtin_payload_key.o
> +
> +ifeq ($(CONFIG_PAYLOAD_SIGNING),y)
> +key_path := $(srctree)/crypto/$(patsubst "%",%,$(CONFIG_PAYLOAD_SIG_KEY))
> +$(obj)/builtin_payload_key.bin: $(key_path) $(srctree)/tools/extract-key.py
> +	$(srctree)/tools/extract-key.py < $< > $@.new
> +	$(call move-if-changed,$@.new,$@)
> +
> +$(obj)/builtin_payload_key.S: $(srctree)/tools/binfile $(obj)/builtin_payload_key.bin FORCE
> +	$(call if_changed,binfile,$(obj)/builtin_payload_key.bin xen_livepatch_key_data)

... arrangements want making for the array to live ideally in .init.rodata,
but at least somewhere in .init.*. E.g. by passing -i to tools/binfile.

Jan