[PATCH] tools: add a simple script to generate EFI variables

This script generates EFI variables for U-Boot variable store format.
An example of generating secure boot variables
$ openssl x509 -in foo.crt -outform DER -out foo.der $ efisiglist -a -c foo.der -o foo.esl $ efivar.py -i ubootefi.var add -n db -d foo.esl -t file $ efivar.py -i ubootefi.var add -n kek -d foo.esl -t file $ efivar.py -i ubootefi.var add -n pk -d foo.esl -t file
Signed-off-by: Paulo Alcantara (SUSE) pc@cjr.nz --- tools/efivar.py | 141 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100755 tools/efivar.py
diff --git a/tools/efivar.py b/tools/efivar.py new file mode 100755 index 000000000000..31e5508f08fd --- /dev/null +++ b/tools/efivar.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +## SPDX-License-Identifier: GPL-2.0-only +# +# Generate UEFI variables for U-Boot. +# +# (c) 2020 Paulo Alcantara palcantara@suse.de +# + +import os +import struct +import uuid +import time +import zlib +import argparse +import subprocess as sp + +# U-Boot variable store format (version 1) +UBOOT_EFI_VAR_FILE_MAGIC = 0x0161566966456255 + +# UEFI variable attributes +EFI_VARIABLE_NON_VOLATILE = 0x1 +EFI_VARIABLE_BOOTSERVICE_ACCESS = 0x2 +EFI_VARIABLE_RUNTIME_ACCESS = 0x4 +EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS = 0x20 +NV_BS = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS +NV_BS_RT = NV_BS | EFI_VARIABLE_RUNTIME_ACCESS +NV_BS_RT_AT = NV_BS_RT | EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS + +# UEFI variable GUIDs +EFI_GLOBAL_VARIABLE_GUID = '{8be4df61-93ca-11d2-aa0d-00e098032b8c}' +EFI_IMAGE_SECURITY_DATABASE_GUID = '{d719b2cb-3d3a-4596-a3bc-dad00e67656f}' + +class EfiStruct: + # struct efi_var_file + var_file_fmt = '<QQLL' + var_file_size = struct.calcsize(var_file_fmt) + # struct efi_var_entry + var_entry_fmt = '<LLQ16s' + var_entry_size = struct.calcsize(var_entry_fmt) + +class EfiVariableStore: + def __init__(self, infile): + self.infile = infile + self.efi = EfiStruct() + if os.path.exists(self.infile) and os.stat(self.infile).st_size > self.efi.var_file_size: + with open(self.infile, 'rb') as f: + # skip header since it will be recreated by save() + self.buf = f.read()[self.efi.var_file_size:] + else: + self.buf = bytearray() + + def _set_var(self, guid, name_data, size, attr, tsec): + ent = struct.pack(self.efi.var_entry_fmt, + size, + attr, + tsec, + uuid.UUID(guid).bytes_le) + ent += name_data + self.buf += ent + + def set_var(self, guid, name, data, size, attr): + tsec = int(time.time()) if attr & EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS else 0 + nd = name.encode('utf_16_le') + b"\x00\x00" + data + # U-Boot variable format requires the name + data blob to be 8-byte aligned + pad = ((len(nd) + 7) & ~7) - len(nd) + nd += bytes([0] * pad) + + return self._set_var(guid, nd, size, attr, tsec) + + def save(self): + hdr = struct.pack(self.efi.var_file_fmt, + 0, + UBOOT_EFI_VAR_FILE_MAGIC, + len(self.buf) + self.efi.var_file_size, + zlib.crc32(self.buf) & 0xffffffff) + + with open(self.infile, 'wb') as f: + f.write(hdr) + f.write(self.buf) + +def parse_attrs(attr): + attrs = { + 'nv': EFI_VARIABLE_NON_VOLATILE, + 'bs': EFI_VARIABLE_BOOTSERVICE_ACCESS, + 'rt': EFI_VARIABLE_RUNTIME_ACCESS, + 'at': EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS, + } + v = 0 + for i in attr.split(','): + v |= attrs[i.lower()] + return v + +def parse_data(val, vtype): + fmt = { 'u8': '<B', 'u16': '<H', 'u32': '<L', 'u64': '<Q' } + if vtype.lower() == 'file': + with open(val, 'rb') as f: + data = f.read() + return data, len(data) + if vtype.lower() == 'str': + data = val.encode('utf-8') + b'\x00' + return data, len(data) + i = fmt[vtype.lower()] + return struct.pack(i, int(val)), struct.calcsize(i) + +def cmd_add(args): + env = EfiVariableStore(args.infile) + data, size = parse_data(args.data, args.type) + + if args.name.lower() == 'pk': + env.set_var(guid=EFI_GLOBAL_VARIABLE_GUID, name='PK', data=data, size=size, attr=NV_BS_RT_AT) + elif args.name.lower() == 'kek': + env.set_var(guid=EFI_GLOBAL_VARIABLE_GUID, name='KEK', data=data, size=size, attr=NV_BS_RT_AT) + elif args.name.lower() == 'db': + env.set_var(guid=EFI_IMAGE_SECURITY_DATABASE_GUID, name='db', data=data, size=size, attr=NV_BS_RT_AT) + elif args.name.lower() == 'dbx': + env.set_var(guid=EFI_IMAGE_SECURITY_DATABASE_GUID, name='dbx', data=data, size=size, attr=NV_BS_RT_AT) + else: + guid = args.guid if args.guid else EFI_GLOBAL_VARIABLE_GUID + attr = parse_attrs(args.attr) if args.attr else NV_BS + env.set_var(guid=guid, name=args.name, data=data, size=size, attr=attr) + + env.save() + +def main(): + ap = argparse.ArgumentParser(description='Generate U-Boot variable store') + ap.add_argument('--infile', '-i', required=True, help='file to save the UEFI variables') + subp = ap.add_subparsers(help="sub-command help") + + addp = subp.add_parser('add', help='add UEFI variable') + addp.add_argument('--name', '-n', required=True, help='variable name') + addp.add_argument('--attr', '-a', help='variable attributes (default: nv,bs)') + addp.add_argument('--guid', '-g', help="variable guid (default: %s)"%EFI_GLOBAL_VARIABLE_GUID) + addp.add_argument('--type', '-t', required=True, help='variable type (values: file|u8|u16|u32|u64|str)') + addp.add_argument('--data', '-d', required=True, help='variable data') + addp.set_defaults(func=cmd_add) + + args = ap.parse_args() + args.func(args) + +if __name__ == '__main__': + main()

On 11/30/20 4:16 PM, Paulo Alcantara wrote:
This script generates EFI variables for U-Boot variable store format.
Hello Paulo,
thanks for you valuable contribution.
Wouldn't it make sense to allow overwriting and deleting variables too?
Best regards
Heinrich
An example of generating secure boot variables
$ openssl x509 -in foo.crt -outform DER -out foo.der $ efisiglist -a -c foo.der -o foo.esl $ efivar.py -i ubootefi.var add -n db -d foo.esl -t file $ efivar.py -i ubootefi.var add -n kek -d foo.esl -t file $ efivar.py -i ubootefi.var add -n pk -d foo.esl -t file
Signed-off-by: Paulo Alcantara (SUSE) pc@cjr.nz
tools/efivar.py | 141 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100755 tools/efivar.py
diff --git a/tools/efivar.py b/tools/efivar.py new file mode 100755 index 000000000000..31e5508f08fd --- /dev/null +++ b/tools/efivar.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +## SPDX-License-Identifier: GPL-2.0-only +# +# Generate UEFI variables for U-Boot. +# +# (c) 2020 Paulo Alcantara palcantara@suse.de +#
+import os +import struct +import uuid +import time +import zlib +import argparse +import subprocess as sp
+# U-Boot variable store format (version 1) +UBOOT_EFI_VAR_FILE_MAGIC = 0x0161566966456255
+# UEFI variable attributes +EFI_VARIABLE_NON_VOLATILE = 0x1 +EFI_VARIABLE_BOOTSERVICE_ACCESS = 0x2 +EFI_VARIABLE_RUNTIME_ACCESS = 0x4 +EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS = 0x20 +NV_BS = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS +NV_BS_RT = NV_BS | EFI_VARIABLE_RUNTIME_ACCESS +NV_BS_RT_AT = NV_BS_RT | EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS
+# UEFI variable GUIDs +EFI_GLOBAL_VARIABLE_GUID = '{8be4df61-93ca-11d2-aa0d-00e098032b8c}' +EFI_IMAGE_SECURITY_DATABASE_GUID = '{d719b2cb-3d3a-4596-a3bc-dad00e67656f}'
+class EfiStruct:
# struct efi_var_file
var_file_fmt = '<QQLL'
var_file_size = struct.calcsize(var_file_fmt)
# struct efi_var_entry
var_entry_fmt = '<LLQ16s'
var_entry_size = struct.calcsize(var_entry_fmt)
+class EfiVariableStore:
- def __init__(self, infile):
self.infile = infile
self.efi = EfiStruct()
if os.path.exists(self.infile) and os.stat(self.infile).st_size > self.efi.var_file_size:
with open(self.infile, 'rb') as f:
# skip header since it will be recreated by save()
self.buf = f.read()[self.efi.var_file_size:]
else:
self.buf = bytearray()
- def _set_var(self, guid, name_data, size, attr, tsec):
ent = struct.pack(self.efi.var_entry_fmt,
size,
attr,
tsec,
uuid.UUID(guid).bytes_le)
ent += name_data
self.buf += ent
- def set_var(self, guid, name, data, size, attr):
tsec = int(time.time()) if attr & EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS else 0
nd = name.encode('utf_16_le') + b"\x00\x00" + data
# U-Boot variable format requires the name + data blob to be 8-byte aligned
pad = ((len(nd) + 7) & ~7) - len(nd)
nd += bytes([0] * pad)
return self._set_var(guid, nd, size, attr, tsec)
- def save(self):
hdr = struct.pack(self.efi.var_file_fmt,
0,
UBOOT_EFI_VAR_FILE_MAGIC,
len(self.buf) + self.efi.var_file_size,
zlib.crc32(self.buf) & 0xffffffff)
with open(self.infile, 'wb') as f:
f.write(hdr)
f.write(self.buf)
+def parse_attrs(attr):
- attrs = {
'nv': EFI_VARIABLE_NON_VOLATILE,
'bs': EFI_VARIABLE_BOOTSERVICE_ACCESS,
'rt': EFI_VARIABLE_RUNTIME_ACCESS,
'at': EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS,
- }
- v = 0
- for i in attr.split(','):
v |= attrs[i.lower()]
- return v
+def parse_data(val, vtype):
- fmt = { 'u8': '<B', 'u16': '<H', 'u32': '<L', 'u64': '<Q' }
- if vtype.lower() == 'file':
with open(val, 'rb') as f:
data = f.read()
return data, len(data)
- if vtype.lower() == 'str':
data = val.encode('utf-8') + b'\x00'
return data, len(data)
- i = fmt[vtype.lower()]
- return struct.pack(i, int(val)), struct.calcsize(i)
+def cmd_add(args):
- env = EfiVariableStore(args.infile)
- data, size = parse_data(args.data, args.type)
- if args.name.lower() == 'pk':
env.set_var(guid=EFI_GLOBAL_VARIABLE_GUID, name='PK', data=data, size=size, attr=NV_BS_RT_AT)
- elif args.name.lower() == 'kek':
env.set_var(guid=EFI_GLOBAL_VARIABLE_GUID, name='KEK', data=data, size=size, attr=NV_BS_RT_AT)
- elif args.name.lower() == 'db':
env.set_var(guid=EFI_IMAGE_SECURITY_DATABASE_GUID, name='db', data=data, size=size, attr=NV_BS_RT_AT)
- elif args.name.lower() == 'dbx':
env.set_var(guid=EFI_IMAGE_SECURITY_DATABASE_GUID, name='dbx', data=data, size=size, attr=NV_BS_RT_AT)
- else:
guid = args.guid if args.guid else EFI_GLOBAL_VARIABLE_GUID
attr = parse_attrs(args.attr) if args.attr else NV_BS
env.set_var(guid=guid, name=args.name, data=data, size=size, attr=attr)
- env.save()
+def main():
- ap = argparse.ArgumentParser(description='Generate U-Boot variable store')
- ap.add_argument('--infile', '-i', required=True, help='file to save the UEFI variables')
- subp = ap.add_subparsers(help="sub-command help")
- addp = subp.add_parser('add', help='add UEFI variable')
- addp.add_argument('--name', '-n', required=True, help='variable name')
- addp.add_argument('--attr', '-a', help='variable attributes (default: nv,bs)')
- addp.add_argument('--guid', '-g', help="variable guid (default: %s)"%EFI_GLOBAL_VARIABLE_GUID)
- addp.add_argument('--type', '-t', required=True, help='variable type (values: file|u8|u16|u32|u64|str)')
- addp.add_argument('--data', '-d', required=True, help='variable data')
- addp.set_defaults(func=cmd_add)
- args = ap.parse_args()
- args.func(args)
+if __name__ == '__main__':
- main()

Hi Heinrich,
Heinrich Schuchardt xypron.glpk@gmx.de writes:
On 11/30/20 4:16 PM, Paulo Alcantara wrote:
This script generates EFI variables for U-Boot variable store format.
Wouldn't it make sense to allow overwriting and deleting variables too?
Absolutely. I'll repost it with those features.
Thanks!

This script generates EFI variables for U-Boot variable store format.
A few examples:
- Generating secure boot keys
$ openssl x509 -in foo.crt -outform DER -out foo.der $ efisiglist -a -c foo.der -o foo.esl $ ./efivar.py -i ubootefi.var set -n db -d foo.esl -t file $ ./efivar.py -i ubootefi.var set -n kek -d foo.esl -t file $ ./efivar.py -i ubootefi.var set -n pk -d foo.esl -t file
- Printing out variables
$ ./efivar.py -i ubootefi.var set -n var1 -a nv,bs -d foo -t str $ ./efivar.py -i ubootefi.var set -n var2 -a nv,bs -d bar -t str $ ./efivar.py -i ubootefi.var print var1: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS, DataSize = 0x3 0000000000: 66 6F 6F foo var2: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS, DataSize = 0x3 0000000000: 62 61 72 bar
- Removing variables
$ ./efivar.py -i ubootefi.var set -n var1 -a nv,bs -d foo -t str $ ./efivar.py -i ubootefi.var print -n var1 var1: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS, DataSize = 0x3 0000000000: 66 6F 6F foo $ ./efivar.py -i ubootefi.var set -n var1 $ ./efivar.py -i ubootefi.var print -n var1 err: variable not found
Signed-off-by: Paulo Alcantara (SUSE) pc@cjr.nz --- tools/efivar.py | 276 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100755 tools/efivar.py
diff --git a/tools/efivar.py b/tools/efivar.py new file mode 100755 index 000000000000..ecd12319c43b --- /dev/null +++ b/tools/efivar.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +## SPDX-License-Identifier: GPL-2.0-only +# +# Generate UEFI variables for U-Boot. +# +# (c) 2020 Paulo Alcantara palcantara@suse.de +# + +import os +import struct +import uuid +import time +import zlib +import argparse + +# U-Boot variable store format (version 1) +UBOOT_EFI_VAR_FILE_MAGIC = 0x0161566966456255 + +# UEFI variable attributes +EFI_VARIABLE_NON_VOLATILE = 0x1 +EFI_VARIABLE_BOOTSERVICE_ACCESS = 0x2 +EFI_VARIABLE_RUNTIME_ACCESS = 0x4 +EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS = 0x10 +EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS = 0x20 +EFI_VARIABLE_READ_ONLY = 1 << 31 +NV_BS = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS +NV_BS_RT = NV_BS | EFI_VARIABLE_RUNTIME_ACCESS +NV_BS_RT_AT = NV_BS_RT | EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS + +# UEFI variable GUIDs +EFI_GLOBAL_VARIABLE_GUID = '8be4df61-93ca-11d2-aa0d-00e098032b8c' +EFI_IMAGE_SECURITY_DATABASE_GUID = 'd719b2cb-3d3a-4596-a3bc-dad00e67656f' + +var_attrs = { + 'NV': EFI_VARIABLE_NON_VOLATILE, + 'BS': EFI_VARIABLE_BOOTSERVICE_ACCESS, + 'RT': EFI_VARIABLE_RUNTIME_ACCESS, + 'AT': EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS, + 'RO': EFI_VARIABLE_READ_ONLY, + 'AW': EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS, +} + +var_guids = { + 'EFI_GLOBAL_VARIABLE_GUID': '8be4df61-93ca-11d2-aa0d-00e098032b8c', + 'EFI_IMAGE_SECURITY_DATABASE_GUID': 'd719b2cb-3d3a-4596-a3bc-dad00e67656f', +} + +class EfiStruct: + # struct efi_var_file + var_file_fmt = '<QQLL' + var_file_size = struct.calcsize(var_file_fmt) + # struct efi_var_entry + var_entry_fmt = '<LLQ16s' + var_entry_size = struct.calcsize(var_entry_fmt) + +class EfiVariable: + def __init__(self, size, attrs, time, guid, name, data): + self.size = size + self.attrs = attrs + self.time = time + self.guid = guid + self.name = name + self.data = data + +class EfiVariableStore: + def __init__(self, infile): + self.infile = infile + self.efi = EfiStruct() + if os.path.exists(self.infile) and os.stat(self.infile).st_size > self.efi.var_file_size: + with open(self.infile, 'rb') as f: + buf = f.read() + self._check_header(buf) + self.ents = buf[self.efi.var_file_size:] + else: + self.ents = bytearray() + + def _check_header(self, buf): + hdr = struct.unpack_from(self.efi.var_file_fmt, buf, 0) + magic, crc32 = hdr[1], hdr[3] + + if magic != UBOOT_EFI_VAR_FILE_MAGIC: + print("err: invalid magic number: %s"%hex(magic)) + exit(1) + if crc32 != zlib.crc32(buf[self.efi.var_file_size:]) & 0xffffffff: + print("err: invalid crc32: %s"%hex(crc32)) + exit(1) + + def _get_var_name(self, buf): + name = '' + for i in range(0, len(buf) - 1, 2): + if not buf[i] and not buf[i+1]: + break + name += chr(buf[i]) + return ''.join([chr(x) for x in name.encode('utf_16_le') if x]), i + 2 + + def _next_var(self, offs=0): + size, attrs, time, guid = struct.unpack_from(self.efi.var_entry_fmt, self.ents, offs) + data_fmt = str(size)+"s" + offs += self.efi.var_entry_size + name, namelen = self._get_var_name(self.ents[offs:]) + offs += namelen + data = struct.unpack_from(data_fmt, self.ents, offs)[0] + offs = (offs + len(data) + 7) & ~7 + return EfiVariable(size, attrs, time, uuid.UUID(bytes_le=guid), name, data), offs + + def __iter__(self): + self.offs = 0 + return self + + def __next__(self): + if self.offs < len(self.ents): + var, noffs = self._next_var(self.offs) + self.offs = noffs + return var + else: + raise StopIteration + + def __len__(self): + return len(self.ents) + + def _set_var(self, guid, name_data, size, attrs, tsec): + ent = struct.pack(self.efi.var_entry_fmt, + size, + attrs, + tsec, + uuid.UUID(guid).bytes_le) + ent += name_data + self.ents += ent + + def set_var(self, guid, name, data, size, attrs): + offs = 0 + while offs < len(self.ents): + var, loffs = self._next_var(offs) + if var.name == name and str(var.guid) == guid: + if not data or not attrs: + self.ents = self.ents[:offs] + self.ents[loffs:] + return + if var.attrs != attrs: + print("err: invalid attributes") + exit(1) + # make room for updating var + self.ents = self.ents[:offs] + self.ents[loffs:] + break + offs = loffs + + if not data or not attrs: + print("err: variable not found") + exit(1) + + tsec = int(time.time()) if attrs & EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS else 0 + nd = name.encode('utf_16_le') + b"\x00\x00" + data + # U-Boot variable format requires the name + data blob to be 8-byte aligned + pad = ((len(nd) + 7) & ~7) - len(nd) + nd += bytes([0] * pad) + + return self._set_var(guid, nd, size, attrs, tsec) + + def save(self): + hdr = struct.pack(self.efi.var_file_fmt, + 0, + UBOOT_EFI_VAR_FILE_MAGIC, + len(self.ents) + self.efi.var_file_size, + zlib.crc32(self.ents) & 0xffffffff) + + with open(self.infile, 'wb') as f: + f.write(hdr) + f.write(self.ents) + +def parse_attrs(attrs): + v = 0 + if attrs: + for i in attrs.split(','): + v |= var_attrs[i.upper()] + return v + +def parse_data(val, vtype): + if not val or not vtype: + return None, 0 + fmt = { 'u8': '<B', 'u16': '<H', 'u32': '<L', 'u64': '<Q' } + if vtype.lower() == 'file': + with open(val, 'rb') as f: + data = f.read() + return data, len(data) + if vtype.lower() == 'str': + data = val.encode('utf-8') + return data, len(data) + i = fmt[vtype.lower()] + return struct.pack(i, int(val)), struct.calcsize(i) + +def cmd_set(args): + env = EfiVariableStore(args.infile) + data, size = parse_data(args.data, args.type) + + if args.name.lower() == 'pk': + env.set_var(guid=EFI_GLOBAL_VARIABLE_GUID, name='PK', data=data, size=size, attrs=NV_BS_RT_AT) + elif args.name.lower() == 'kek': + env.set_var(guid=EFI_GLOBAL_VARIABLE_GUID, name='KEK', data=data, size=size, attrs=NV_BS_RT_AT) + elif args.name.lower() == 'db': + env.set_var(guid=EFI_IMAGE_SECURITY_DATABASE_GUID, name='db', data=data, size=size, attrs=NV_BS_RT_AT) + elif args.name.lower() == 'dbx': + env.set_var(guid=EFI_IMAGE_SECURITY_DATABASE_GUID, name='dbx', data=data, size=size, attrs=NV_BS_RT_AT) + else: + guid = args.guid if args.guid else EFI_GLOBAL_VARIABLE_GUID + attrs = parse_attrs(args.attrs) + env.set_var(guid=guid, name=args.name, data=data, size=size, attrs=attrs) + + env.save() + +def print_var(var): + print(var.name+':') + print(" "+str(var.guid)+' '+''.join([x for x in var_guids if str(var.guid) == var_guids[x]])) + print(" "+'|'.join([x for x in var_attrs if var.attrs & var_attrs[x]])+", DataSize = %s"%hex(var.size)) + hexdump(var.data) + +def cmd_print(args): + env = EfiVariableStore(args.infile) + if not args.name and not args.guid and not len(env): + return + + found = False + for var in env: + if not args.name: + if args.guid and args.guid != str(var.guid): + continue + print_var(var) + found = True + else: + if args.name != var.name or (args.guid and args.guid != str(var.guid)): + continue + print_var(var) + found = True + + if not found: + print("err: variable not found") + exit(1) + +def main(): + ap = argparse.ArgumentParser(description='Generate U-Boot variable store') + ap.add_argument('--infile', '-i', required=True, help='file to save the UEFI variables') + subp = ap.add_subparsers(help="sub-command help") + + printp = subp.add_parser('print', help='get/list UEFI variables') + printp.add_argument('--guid', '-g', help='variable guid') + printp.add_argument('--name', '-n', help='variable name') + printp.set_defaults(func=cmd_print) + + setp = subp.add_parser('set', help='set UEFI variable') + setp.add_argument('--name', '-n', required=True, help='variable name') + setp.add_argument('--attrs', '-a', help='variable attributes (values: nv,bs,rt,at,ro,aw)') + setp.add_argument('--guid', '-g', help="variable guid (default: %s)"%EFI_GLOBAL_VARIABLE_GUID) + setp.add_argument('--type', '-t', help='variable type (values: file|u8|u16|u32|u64|str)') + setp.add_argument('--data', '-d', help='variable data') + setp.set_defaults(func=cmd_set) + + args = ap.parse_args() + args.func(args) + +def group(a, *ns): + for n in ns: + a = [a[i:i+n] for i in range(0, len(a), n)] + return a + +def join(a, *cs): + return [cs[0].join(join(t, *cs[1:])) for t in a] if cs else a + +def hexdump(data): + toHex = lambda c: '{:02X}'.format(c) + toChr = lambda c: chr(c) if 32 <= c < 127 else '.' + make = lambda f, *cs: join(group(list(map(f, data)), 8, 2), *cs) + hs = make(toHex, ' ', ' ') + cs = make(toChr, ' ', '') + for i, (h, c) in enumerate(zip(hs, cs)): + print (' {:010X}: {:48} {:16}'.format(i * 16, h, c)) + +if __name__ == '__main__': + main()

On 12/1/20 11:58 PM, Paulo Alcantara wrote:
This script generates EFI variables for U-Boot variable store format.
A few examples:
Generating secure boot keys
$ openssl x509 -in foo.crt -outform DER -out foo.der $ efisiglist -a -c foo.der -o foo.esl $ ./efivar.py -i ubootefi.var set -n db -d foo.esl -t file $ ./efivar.py -i ubootefi.var set -n kek -d foo.esl -t file $ ./efivar.py -i ubootefi.var set -n pk -d foo.esl -t file
Printing out variables
$ ./efivar.py -i ubootefi.var set -n var1 -a nv,bs -d foo -t str $ ./efivar.py -i ubootefi.var set -n var2 -a nv,bs -d bar -t str $ ./efivar.py -i ubootefi.var print var1: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS, DataSize = 0x3 0000000000: 66 6F 6F foo var2: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS, DataSize = 0x3 0000000000: 62 61 72 bar
Removing variables
$ ./efivar.py -i ubootefi.var set -n var1 -a nv,bs -d foo -t str $ ./efivar.py -i ubootefi.var print -n var1 var1: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS, DataSize = 0x3 0000000000: 66 6F 6F foo $ ./efivar.py -i ubootefi.var set -n var1 $ ./efivar.py -i ubootefi.var print -n var1 err: variable not found
Signed-off-by: Paulo Alcantara (SUSE) pc@cjr.nz
tools/efivar.py | 276 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100755 tools/efivar.py
diff --git a/tools/efivar.py b/tools/efivar.py new file mode 100755 index 000000000000..ecd12319c43b --- /dev/null +++ b/tools/efivar.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +## SPDX-License-Identifier: GPL-2.0-only +# +# Generate UEFI variables for U-Boot. +# +# (c) 2020 Paulo Alcantara palcantara@suse.de +#
+import os +import struct +import uuid +import time +import zlib +import argparse
+# U-Boot variable store format (version 1) +UBOOT_EFI_VAR_FILE_MAGIC = 0x0161566966456255
+# UEFI variable attributes +EFI_VARIABLE_NON_VOLATILE = 0x1 +EFI_VARIABLE_BOOTSERVICE_ACCESS = 0x2 +EFI_VARIABLE_RUNTIME_ACCESS = 0x4 +EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS = 0x10 +EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS = 0x20 +EFI_VARIABLE_READ_ONLY = 1 << 31 +NV_BS = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS +NV_BS_RT = NV_BS | EFI_VARIABLE_RUNTIME_ACCESS +NV_BS_RT_AT = NV_BS_RT | EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS
+# UEFI variable GUIDs +EFI_GLOBAL_VARIABLE_GUID = '8be4df61-93ca-11d2-aa0d-00e098032b8c' +EFI_IMAGE_SECURITY_DATABASE_GUID = 'd719b2cb-3d3a-4596-a3bc-dad00e67656f'
+var_attrs = {
'NV': EFI_VARIABLE_NON_VOLATILE,
'BS': EFI_VARIABLE_BOOTSERVICE_ACCESS,
'RT': EFI_VARIABLE_RUNTIME_ACCESS,
'AT': EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS,
'RO': EFI_VARIABLE_READ_ONLY,
'AW': EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS,
+}
+var_guids = {
'EFI_GLOBAL_VARIABLE_GUID': '8be4df61-93ca-11d2-aa0d-00e098032b8c',
'EFI_IMAGE_SECURITY_DATABASE_GUID': 'd719b2cb-3d3a-4596-a3bc-dad00e67656f',
+}
+class EfiStruct:
# struct efi_var_file
var_file_fmt = '<QQLL'
var_file_size = struct.calcsize(var_file_fmt)
# struct efi_var_entry
var_entry_fmt = '<LLQ16s'
var_entry_size = struct.calcsize(var_entry_fmt)
+class EfiVariable:
- def __init__(self, size, attrs, time, guid, name, data):
self.size = size
self.attrs = attrs
self.time = time
self.guid = guid
self.name = name
self.data = data
+class EfiVariableStore:
- def __init__(self, infile):
self.infile = infile
self.efi = EfiStruct()
if os.path.exists(self.infile) and os.stat(self.infile).st_size > self.efi.var_file_size:
with open(self.infile, 'rb') as f:
buf = f.read()
self._check_header(buf)
self.ents = buf[self.efi.var_file_size:]
else:
self.ents = bytearray()
- def _check_header(self, buf):
hdr = struct.unpack_from(self.efi.var_file_fmt, buf, 0)
magic, crc32 = hdr[1], hdr[3]
if magic != UBOOT_EFI_VAR_FILE_MAGIC:
print("err: invalid magic number: %s"%hex(magic))
exit(1)
if crc32 != zlib.crc32(buf[self.efi.var_file_size:]) & 0xffffffff:
print("err: invalid crc32: %s"%hex(crc32))
exit(1)
- def _get_var_name(self, buf):
name = ''
for i in range(0, len(buf) - 1, 2):
if not buf[i] and not buf[i+1]:
break
name += chr(buf[i])
return ''.join([chr(x) for x in name.encode('utf_16_le') if x]), i + 2
- def _next_var(self, offs=0):
size, attrs, time, guid = struct.unpack_from(self.efi.var_entry_fmt, self.ents, offs)
data_fmt = str(size)+"s"
offs += self.efi.var_entry_size
name, namelen = self._get_var_name(self.ents[offs:])
offs += namelen
data = struct.unpack_from(data_fmt, self.ents, offs)[0]
offs = (offs + len(data) + 7) & ~7
return EfiVariable(size, attrs, time, uuid.UUID(bytes_le=guid), name, data), offs
- def __iter__(self):
self.offs = 0
return self
- def __next__(self):
if self.offs < len(self.ents):
var, noffs = self._next_var(self.offs)
self.offs = noffs
return var
else:
raise StopIteration
- def __len__(self):
return len(self.ents)
- def _set_var(self, guid, name_data, size, attrs, tsec):
ent = struct.pack(self.efi.var_entry_fmt,
size,
attrs,
tsec,
uuid.UUID(guid).bytes_le)
ent += name_data
self.ents += ent
- def set_var(self, guid, name, data, size, attrs):
offs = 0
while offs < len(self.ents):
var, loffs = self._next_var(offs)
if var.name == name and str(var.guid) == guid:
if not data or not attrs:
self.ents = self.ents[:offs] + self.ents[loffs:]
return
if var.attrs != attrs:
print("err: invalid attributes")
Thanks for adding deletion and modification of variables.
print("attributes don't match")
would better convey the meaning here.
exit(1)
# make room for updating var
self.ents = self.ents[:offs] + self.ents[loffs:]
break
offs = loffs
if not data or not attrs:
print("err: variable not found")
exit(1)
tsec = int(time.time()) if attrs & EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS else 0
nd = name.encode('utf_16_le') + b"\x00\x00" + data
# U-Boot variable format requires the name + data blob to be 8-byte aligned
pad = ((len(nd) + 7) & ~7) - len(nd)
nd += bytes([0] * pad)
return self._set_var(guid, nd, size, attrs, tsec)
- def save(self):
hdr = struct.pack(self.efi.var_file_fmt,
0,
UBOOT_EFI_VAR_FILE_MAGIC,
len(self.ents) + self.efi.var_file_size,
zlib.crc32(self.ents) & 0xffffffff)
with open(self.infile, 'wb') as f:
f.write(hdr)
f.write(self.ents)
+def parse_attrs(attrs):
- v = 0
- if attrs:
for i in attrs.split(','):
v |= var_attrs[i.upper()]
- return v
+def parse_data(val, vtype):
- if not val or not vtype:
return None, 0
- fmt = { 'u8': '<B', 'u16': '<H', 'u32': '<L', 'u64': '<Q' }
- if vtype.lower() == 'file':
with open(val, 'rb') as f:
data = f.read()
return data, len(data)
- if vtype.lower() == 'str':
data = val.encode('utf-8')
return data, len(data)
- i = fmt[vtype.lower()]
- return struct.pack(i, int(val)), struct.calcsize(i)
+def cmd_set(args):
- env = EfiVariableStore(args.infile)
- data, size = parse_data(args.data, args.type)
- if args.name.lower() == 'pk':
env.set_var(guid=EFI_GLOBAL_VARIABLE_GUID, name='PK', data=data, size=size, attrs=NV_BS_RT_AT)
- elif args.name.lower() == 'kek':
env.set_var(guid=EFI_GLOBAL_VARIABLE_GUID, name='KEK', data=data, size=size, attrs=NV_BS_RT_AT)
- elif args.name.lower() == 'db':
env.set_var(guid=EFI_IMAGE_SECURITY_DATABASE_GUID, name='db', data=data, size=size, attrs=NV_BS_RT_AT)
- elif args.name.lower() == 'dbx':
env.set_var(guid=EFI_IMAGE_SECURITY_DATABASE_GUID, name='dbx', data=data, size=size, attrs=NV_BS_RT_AT)
- else:
guid = args.guid if args.guid else EFI_GLOBAL_VARIABLE_GUID
attrs = parse_attrs(args.attrs)
env.set_var(guid=guid, name=args.name, data=data, size=size, attrs=attrs)
- env.save()
+def print_var(var):
- print(var.name+':')
- print(" "+str(var.guid)+' '+''.join([x for x in var_guids if str(var.guid) == var_guids[x]]))
- print(" "+'|'.join([x for x in var_attrs if var.attrs & var_attrs[x]])+", DataSize = %s"%hex(var.size))
- hexdump(var.data)
+def cmd_print(args):
- env = EfiVariableStore(args.infile)
- if not args.name and not args.guid and not len(env):
return
- found = False
- for var in env:
if not args.name:
if args.guid and args.guid != str(var.guid):
continue
print_var(var)
found = True
else:
if args.name != var.name or (args.guid and args.guid != str(var.guid)):
continue
print_var(var)
found = True
- if not found:
print("err: variable not found")
exit(1)
+def main():
- ap = argparse.ArgumentParser(description='Generate U-Boot variable store')
- ap.add_argument('--infile', '-i', required=True, help='file to save the UEFI variables')
- subp = ap.add_subparsers(help="sub-command help")
- printp = subp.add_parser('print', help='get/list UEFI variables')
- printp.add_argument('--guid', '-g', help='variable guid')
- printp.add_argument('--name', '-n', help='variable name')
- printp.set_defaults(func=cmd_print)
- setp = subp.add_parser('set', help='set UEFI variable')
- setp.add_argument('--name', '-n', required=True, help='variable name')
- setp.add_argument('--attrs', '-a', help='variable attributes (values: nv,bs,rt,at,ro,aw)')
The online help should indicate how deletion works.
I find it problematic that if a user forgets to specify -a, the variable is deleted inadvertently. PK, KEK, db, dbx cannot be deleted at all.
Please, use a separate sub-command for deletion and reintroduce the default for the attributes as nv,bs,rt which seems more useful than nv,bs.
- setp.add_argument('--guid', '-g', help="variable guid (default: %s)"%EFI_GLOBAL_VARIABLE_GUID)
%s/variable guid/vendor GUID/
- setp.add_argument('--type', '-t', help='variable type (values: file|u8|u16|u32|u64|str)')
- setp.add_argument('--data', '-d', help='variable data')
help='data or filename'
Best regards
Heinrich
- setp.set_defaults(func=cmd_set)
- args = ap.parse_args()
- args.func(args)
+def group(a, *ns):
- for n in ns:
a = [a[i:i+n] for i in range(0, len(a), n)]
- return a
+def join(a, *cs):
- return [cs[0].join(join(t, *cs[1:])) for t in a] if cs else a
+def hexdump(data):
- toHex = lambda c: '{:02X}'.format(c)
- toChr = lambda c: chr(c) if 32 <= c < 127 else '.'
- make = lambda f, *cs: join(group(list(map(f, data)), 8, 2), *cs)
- hs = make(toHex, ' ', ' ')
- cs = make(toChr, ' ', '')
- for i, (h, c) in enumerate(zip(hs, cs)):
print (' {:010X}: {:48} {:16}'.format(i * 16, h, c))
+if __name__ == '__main__':
- main()

This script generates EFI variables for U-Boot variable store format.
A few examples:
- Generating secure boot keys
$ openssl x509 -in foo.crt -outform DER -out foo.der $ efisiglist -a -c foo.der -o foo.esl $ ./efivar.py -i ubootefi.var set -n db -d foo.esl -t file $ ./efivar.py -i ubootefi.var set -n kek -d foo.esl -t file $ ./efivar.py -i ubootefi.var set -n pk -d foo.esl -t file
- Printing out variables
$ ./efivar.py -i ubootefi.var set -n var1 -d foo -t str $ ./efivar.py -i ubootefi.var set -n var2 -d bar -t str $ ./efivar.py -i ubootefi.var print var1: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS|RT, DataSize = 0x3 0000000000: 66 6F 6F foo var2: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS|RT, DataSize = 0x3 0000000000: 62 61 72 bar
- Removing variables
$ ./efivar.py -i ubootefi.var set -n var1 -a nv,bs -d foo -t str $ ./efivar.py -i ubootefi.var print -n var1 var1: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS, DataSize = 0x3 0000000000: 66 6F 6F foo $ ./efivar.py -i ubootefi.var del -n var1 err: attributes don't match $ ./efivar.py -i ubootefi.var del -n var1 -a nv,bs $ ./efivar.py -i ubootefi.var print -n var1 err: variable not found
Signed-off-by: Paulo Alcantara (SUSE) pc@cjr.nz --- tools/efivar.py | 306 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100755 tools/efivar.py
diff --git a/tools/efivar.py b/tools/efivar.py new file mode 100755 index 000000000000..75fbce09b555 --- /dev/null +++ b/tools/efivar.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +## SPDX-License-Identifier: GPL-2.0-only +# +# Generate UEFI variables for U-Boot. +# +# (c) 2020 Paulo Alcantara palcantara@suse.de +# + +import os +import struct +import uuid +import time +import zlib +import argparse + +# U-Boot variable store format (version 1) +UBOOT_EFI_VAR_FILE_MAGIC = 0x0161566966456255 + +# UEFI variable attributes +EFI_VARIABLE_NON_VOLATILE = 0x1 +EFI_VARIABLE_BOOTSERVICE_ACCESS = 0x2 +EFI_VARIABLE_RUNTIME_ACCESS = 0x4 +EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS = 0x10 +EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS = 0x20 +EFI_VARIABLE_READ_ONLY = 1 << 31 +NV_BS = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS +NV_BS_RT = NV_BS | EFI_VARIABLE_RUNTIME_ACCESS +NV_BS_RT_AT = NV_BS_RT | EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS +DEFAULT_VAR_ATTRS = NV_BS_RT + +# vendor GUIDs +EFI_GLOBAL_VARIABLE_GUID = '8be4df61-93ca-11d2-aa0d-00e098032b8c' +EFI_IMAGE_SECURITY_DATABASE_GUID = 'd719b2cb-3d3a-4596-a3bc-dad00e67656f' + +var_attrs = { + 'NV': EFI_VARIABLE_NON_VOLATILE, + 'BS': EFI_VARIABLE_BOOTSERVICE_ACCESS, + 'RT': EFI_VARIABLE_RUNTIME_ACCESS, + 'AT': EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS, + 'RO': EFI_VARIABLE_READ_ONLY, + 'AW': EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS, +} + +var_guids = { + 'EFI_GLOBAL_VARIABLE_GUID': EFI_GLOBAL_VARIABLE_GUID, + 'EFI_IMAGE_SECURITY_DATABASE_GUID': EFI_IMAGE_SECURITY_DATABASE_GUID, +} + +class EfiStruct: + # struct efi_var_file + var_file_fmt = '<QQLL' + var_file_size = struct.calcsize(var_file_fmt) + # struct efi_var_entry + var_entry_fmt = '<LLQ16s' + var_entry_size = struct.calcsize(var_entry_fmt) + +class EfiVariable: + def __init__(self, size, attrs, time, guid, name, data): + self.size = size + self.attrs = attrs + self.time = time + self.guid = guid + self.name = name + self.data = data + +def calc_crc32(buf): + return zlib.crc32(buf) & 0xffffffff + +class EfiVariableStore: + def __init__(self, infile): + self.infile = infile + self.efi = EfiStruct() + if os.path.exists(self.infile) and os.stat(self.infile).st_size > self.efi.var_file_size: + with open(self.infile, 'rb') as f: + buf = f.read() + self._check_header(buf) + self.ents = buf[self.efi.var_file_size:] + else: + self.ents = bytearray() + + def _check_header(self, buf): + hdr = struct.unpack_from(self.efi.var_file_fmt, buf, 0) + magic, crc32 = hdr[1], hdr[3] + + if magic != UBOOT_EFI_VAR_FILE_MAGIC: + print("err: invalid magic number: %s"%hex(magic)) + exit(1) + if crc32 != calc_crc32(buf[self.efi.var_file_size:]): + print("err: invalid crc32: %s"%hex(crc32)) + exit(1) + + def _get_var_name(self, buf): + name = '' + for i in range(0, len(buf) - 1, 2): + if not buf[i] and not buf[i+1]: + break + name += chr(buf[i]) + return ''.join([chr(x) for x in name.encode('utf_16_le') if x]), i + 2 + + def _next_var(self, offs=0): + size, attrs, time, guid = struct.unpack_from(self.efi.var_entry_fmt, self.ents, offs) + data_fmt = str(size)+"s" + offs += self.efi.var_entry_size + name, namelen = self._get_var_name(self.ents[offs:]) + offs += namelen + data = struct.unpack_from(data_fmt, self.ents, offs)[0] + # offset to next 8-byte aligned variable entry + offs = (offs + len(data) + 7) & ~7 + return EfiVariable(size, attrs, time, uuid.UUID(bytes_le=guid), name, data), offs + + def __iter__(self): + self.offs = 0 + return self + + def __next__(self): + if self.offs < len(self.ents): + var, noffs = self._next_var(self.offs) + self.offs = noffs + return var + else: + raise StopIteration + + def __len__(self): + return len(self.ents) + + def _set_var(self, guid, name_data, size, attrs, tsec): + ent = struct.pack(self.efi.var_entry_fmt, + size, + attrs, + tsec, + uuid.UUID(guid).bytes_le) + ent += name_data + self.ents += ent + + def del_var(self, guid, name, attrs): + offs = 0 + while offs < len(self.ents): + var, loffs = self._next_var(offs) + if var.name == name and str(var.guid): + if var.attrs != attrs: + print("err: attributes don't match") + exit(1) + self.ents = self.ents[:offs] + self.ents[loffs:] + return + offs = loffs + print("err: variable not found") + exit(1) + + def set_var(self, guid, name, data, size, attrs): + offs = 0 + while offs < len(self.ents): + var, loffs = self._next_var(offs) + if var.name == name and str(var.guid) == guid: + if var.attrs != attrs: + print("err: attributes don't match") + exit(1) + # make room for updating var + self.ents = self.ents[:offs] + self.ents[loffs:] + break + offs = loffs + + tsec = int(time.time()) if attrs & EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS else 0 + nd = name.encode('utf_16_le') + b"\x00\x00" + data + # U-Boot variable format requires the name + data blob to be 8-byte aligned + pad = ((len(nd) + 7) & ~7) - len(nd) + nd += bytes([0] * pad) + + return self._set_var(guid, nd, size, attrs, tsec) + + def save(self): + hdr = struct.pack(self.efi.var_file_fmt, + 0, + UBOOT_EFI_VAR_FILE_MAGIC, + len(self.ents) + self.efi.var_file_size, + calc_crc32(self.ents)) + + with open(self.infile, 'wb') as f: + f.write(hdr) + f.write(self.ents) + +def parse_attrs(attrs): + v = DEFAULT_VAR_ATTRS + if attrs: + v = 0 + for i in attrs.split(','): + v |= var_attrs[i.upper()] + return v + +def parse_data(val, vtype): + if not val or not vtype: + return None, 0 + fmt = { 'u8': '<B', 'u16': '<H', 'u32': '<L', 'u64': '<Q' } + if vtype.lower() == 'file': + with open(val, 'rb') as f: + data = f.read() + return data, len(data) + if vtype.lower() == 'str': + data = val.encode('utf-8') + return data, len(data) + i = fmt[vtype.lower()] + return struct.pack(i, int(val)), struct.calcsize(i) + +def parse_set_args(args): + name = args.name + attrs = parse_attrs(args.attrs) + guid = args.guid if args.guid else EFI_GLOBAL_VARIABLE_GUID + + if name.lower() == 'db' or name.lower() == 'dbx': + name = name.lower() + guid = EFI_IMAGE_SECURITY_DATABASE_GUID + attrs = NV_BS_RT_AT + elif name.lower() == 'pk' or name.lower() == 'kek': + name = name.upper() + guid = EFI_GLOBAL_VARIABLE_GUID + attrs = NV_BS_RT_AT + + data, size = parse_data(args.data, args.type) + return guid, name, attrs, data, size + +def cmd_set(args): + env = EfiVariableStore(args.infile) + guid, name, attrs, data, size = parse_set_args(args) + env.set_var(guid=guid, name=name, data=data, size=size, attrs=attrs) + env.save() + +def print_var(var): + print(var.name+':') + print(" "+str(var.guid)+' '+''.join([x for x in var_guids if str(var.guid) == var_guids[x]])) + print(" "+'|'.join([x for x in var_attrs if var.attrs & var_attrs[x]])+", DataSize = %s"%hex(var.size)) + hexdump(var.data) + +def cmd_print(args): + env = EfiVariableStore(args.infile) + if not args.name and not args.guid and not len(env): + return + + found = False + for var in env: + if not args.name: + if args.guid and args.guid != str(var.guid): + continue + print_var(var) + found = True + else: + if args.name != var.name or (args.guid and args.guid != str(var.guid)): + continue + print_var(var) + found = True + + if not found: + print("err: variable not found") + exit(1) + +def cmd_del(args): + env = EfiVariableStore(args.infile) + attrs = parse_attrs(args.attrs) + guid = args.guid if args.guid else EFI_GLOBAL_VARIABLE_GUID + env.del_var(guid, args.name, attrs) + env.save() + +def main(): + ap = argparse.ArgumentParser(description='Generate U-Boot variable store') + ap.add_argument('--infile', '-i', required=True, help='file to save the UEFI variables') + subp = ap.add_subparsers(help="sub-command help") + + printp = subp.add_parser('print', help='get/list UEFI variables') + printp.add_argument('--name', '-n', help='variable name') + printp.add_argument('--guid', '-g', help='vendor GUID') + printp.set_defaults(func=cmd_print) + + setp = subp.add_parser('set', help='set UEFI variable') + setp.add_argument('--name', '-n', required=True, help='variable name') + setp.add_argument('--attrs', '-a', help='variable attributes (values: nv,bs,rt,at,ro,aw)') + setp.add_argument('--guid', '-g', help="vendor GUID (default: %s)"%EFI_GLOBAL_VARIABLE_GUID) + setp.add_argument('--type', '-t', help='variable type (values: file|u8|u16|u32|u64|str)') + setp.add_argument('--data', '-d', help='data or filename') + setp.set_defaults(func=cmd_set) + + delp = subp.add_parser('del', help='delete UEFI variable') + delp.add_argument('--name', '-n', required=True, help='variable name') + delp.add_argument('--attrs', '-a', help='variable attributes (values: nv,bs,rt,at,ro,aw)') + delp.add_argument('--guid', '-g', help="vendor GUID (default: %s)"%EFI_GLOBAL_VARIABLE_GUID) + delp.set_defaults(func=cmd_del) + + args = ap.parse_args() + args.func(args) + +def group(a, *ns): + for n in ns: + a = [a[i:i+n] for i in range(0, len(a), n)] + return a + +def join(a, *cs): + return [cs[0].join(join(t, *cs[1:])) for t in a] if cs else a + +def hexdump(data): + toHex = lambda c: '{:02X}'.format(c) + toChr = lambda c: chr(c) if 32 <= c < 127 else '.' + make = lambda f, *cs: join(group(list(map(f, data)), 8, 2), *cs) + hs = make(toHex, ' ', ' ') + cs = make(toChr, ' ', '') + for i, (h, c) in enumerate(zip(hs, cs)): + print (' {:010X}: {:48} {:16}'.format(i * 16, h, c)) + +if __name__ == '__main__': + main()

This script generates EFI variables for U-Boot variable store format.
A few examples:
- Generating secure boot keys
$ openssl x509 -in foo.crt -outform DER -out foo.der $ efisiglist -a -c foo.der -o foo.esl $ ./efivar.py -i ubootefi.var set -n db -d foo.esl -t file $ ./efivar.py -i ubootefi.var set -n kek -d foo.esl -t file $ ./efivar.py -i ubootefi.var set -n pk -d foo.esl -t file
- Printing out variables
$ ./efivar.py -i ubootefi.var set -n var1 -d foo -t str $ ./efivar.py -i ubootefi.var set -n var2 -d bar -t str $ ./efivar.py -i ubootefi.var print var1: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS|RT, DataSize = 0x3 0000000000: 66 6F 6F foo var2: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS|RT, DataSize = 0x3 0000000000: 62 61 72 bar
- Removing variables
$ ./efivar.py -i ubootefi.var set -n var1 -a nv,bs -d foo -t str $ ./efivar.py -i ubootefi.var print -n var1 var1: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS, DataSize = 0x3 0000000000: 66 6F 6F foo $ ./efivar.py -i ubootefi.var del -n var1 err: attributes don't match $ ./efivar.py -i ubootefi.var del -n var1 -a nv,bs $ ./efivar.py -i ubootefi.var print -n var1 err: variable not found
Signed-off-by: Paulo Alcantara (SUSE) pc@cjr.nz --- v4: add 'sign' command for authenticated EFI payloads --- tools/efivar.py | 380 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100755 tools/efivar.py
diff --git a/tools/efivar.py b/tools/efivar.py new file mode 100755 index 000000000000..ebfcab2f0a2c --- /dev/null +++ b/tools/efivar.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python3 +## SPDX-License-Identifier: GPL-2.0-only +# +# EFI variable store utilities. +# +# (c) 2020 Paulo Alcantara palcantara@suse.de +# + +import os +import struct +import uuid +import time +import zlib +import argparse +from OpenSSL import crypto + +# U-Boot variable store format (version 1) +UBOOT_EFI_VAR_FILE_MAGIC = 0x0161566966456255 + +# UEFI variable attributes +EFI_VARIABLE_NON_VOLATILE = 0x1 +EFI_VARIABLE_BOOTSERVICE_ACCESS = 0x2 +EFI_VARIABLE_RUNTIME_ACCESS = 0x4 +EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS = 0x10 +EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS = 0x20 +EFI_VARIABLE_READ_ONLY = 1 << 31 +NV_BS = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS +NV_BS_RT = NV_BS | EFI_VARIABLE_RUNTIME_ACCESS +NV_BS_RT_AT = NV_BS_RT | EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS +DEFAULT_VAR_ATTRS = NV_BS_RT + +# vendor GUIDs +EFI_GLOBAL_VARIABLE_GUID = '8be4df61-93ca-11d2-aa0d-00e098032b8c' +EFI_IMAGE_SECURITY_DATABASE_GUID = 'd719b2cb-3d3a-4596-a3bc-dad00e67656f' +EFI_CERT_TYPE_PKCS7_GUID = '4aafd29d-68df-49ee-8aa9-347d375665a7' +WIN_CERT_TYPE_EFI_GUID = 0x0ef1 +WIN_CERT_REVISION = 0x0200 + +var_attrs = { + 'NV': EFI_VARIABLE_NON_VOLATILE, + 'BS': EFI_VARIABLE_BOOTSERVICE_ACCESS, + 'RT': EFI_VARIABLE_RUNTIME_ACCESS, + 'AT': EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS, + 'RO': EFI_VARIABLE_READ_ONLY, + 'AW': EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS, +} + +var_guids = { + 'EFI_GLOBAL_VARIABLE_GUID': EFI_GLOBAL_VARIABLE_GUID, + 'EFI_IMAGE_SECURITY_DATABASE_GUID': EFI_IMAGE_SECURITY_DATABASE_GUID, +} + +class EfiStruct: + # struct efi_var_file + var_file_fmt = '<QQLL' + var_file_size = struct.calcsize(var_file_fmt) + # struct efi_var_entry + var_entry_fmt = '<LLQ16s' + var_entry_size = struct.calcsize(var_entry_fmt) + # struct efi_time + var_time_fmt = '<H6BLh2B' + var_time_size = struct.calcsize(var_time_fmt) + # WIN_CERTIFICATE + var_win_cert_fmt = '<L2H' + var_win_cert_size = struct.calcsize(var_win_cert_fmt) + # WIN_CERTIFICATE_UEFI_GUID + var_win_cert_uefi_guid_fmt = var_win_cert_fmt+'16s' + var_win_cert_uefi_guid_size = struct.calcsize(var_win_cert_uefi_guid_fmt) + +class EfiVariable: + def __init__(self, size, attrs, time, guid, name, data): + self.size = size + self.attrs = attrs + self.time = time + self.guid = guid + self.name = name + self.data = data + +def calc_crc32(buf): + return zlib.crc32(buf) & 0xffffffff + +class EfiVariableStore: + def __init__(self, infile): + self.infile = infile + self.efi = EfiStruct() + if os.path.exists(self.infile) and os.stat(self.infile).st_size > self.efi.var_file_size: + with open(self.infile, 'rb') as f: + buf = f.read() + self._check_header(buf) + self.ents = buf[self.efi.var_file_size:] + else: + self.ents = bytearray() + + def _check_header(self, buf): + hdr = struct.unpack_from(self.efi.var_file_fmt, buf, 0) + magic, crc32 = hdr[1], hdr[3] + + if magic != UBOOT_EFI_VAR_FILE_MAGIC: + print("err: invalid magic number: %s"%hex(magic)) + exit(1) + if crc32 != calc_crc32(buf[self.efi.var_file_size:]): + print("err: invalid crc32: %s"%hex(crc32)) + exit(1) + + def _get_var_name(self, buf): + name = '' + for i in range(0, len(buf) - 1, 2): + if not buf[i] and not buf[i+1]: + break + name += chr(buf[i]) + return ''.join([chr(x) for x in name.encode('utf_16_le') if x]), i + 2 + + def _next_var(self, offs=0): + size, attrs, time, guid = struct.unpack_from(self.efi.var_entry_fmt, self.ents, offs) + data_fmt = str(size)+"s" + offs += self.efi.var_entry_size + name, namelen = self._get_var_name(self.ents[offs:]) + offs += namelen + data = struct.unpack_from(data_fmt, self.ents, offs)[0] + # offset to next 8-byte aligned variable entry + offs = (offs + len(data) + 7) & ~7 + return EfiVariable(size, attrs, time, uuid.UUID(bytes_le=guid), name, data), offs + + def __iter__(self): + self.offs = 0 + return self + + def __next__(self): + if self.offs < len(self.ents): + var, noffs = self._next_var(self.offs) + self.offs = noffs + return var + else: + raise StopIteration + + def __len__(self): + return len(self.ents) + + def _set_var(self, guid, name_data, size, attrs, tsec): + ent = struct.pack(self.efi.var_entry_fmt, + size, + attrs, + tsec, + uuid.UUID(guid).bytes_le) + ent += name_data + self.ents += ent + + def del_var(self, guid, name, attrs): + offs = 0 + while offs < len(self.ents): + var, loffs = self._next_var(offs) + if var.name == name and str(var.guid): + if var.attrs != attrs: + print("err: attributes don't match") + exit(1) + self.ents = self.ents[:offs] + self.ents[loffs:] + return + offs = loffs + print("err: variable not found") + exit(1) + + def set_var(self, guid, name, data, size, attrs): + offs = 0 + while offs < len(self.ents): + var, loffs = self._next_var(offs) + if var.name == name and str(var.guid) == guid: + if var.attrs != attrs: + print("err: attributes don't match") + exit(1) + # make room for updating var + self.ents = self.ents[:offs] + self.ents[loffs:] + break + offs = loffs + + tsec = int(time.time()) if attrs & EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS else 0 + nd = name.encode('utf_16_le') + b"\x00\x00" + data + # U-Boot variable format requires the name + data blob to be 8-byte aligned + pad = ((len(nd) + 7) & ~7) - len(nd) + nd += bytes([0] * pad) + + return self._set_var(guid, nd, size, attrs, tsec) + + def save(self): + hdr = struct.pack(self.efi.var_file_fmt, + 0, + UBOOT_EFI_VAR_FILE_MAGIC, + len(self.ents) + self.efi.var_file_size, + calc_crc32(self.ents)) + + with open(self.infile, 'wb') as f: + f.write(hdr) + f.write(self.ents) + +def parse_attrs(attrs): + v = DEFAULT_VAR_ATTRS + if attrs: + v = 0 + for i in attrs.split(','): + v |= var_attrs[i.upper()] + return v + +def parse_data(val, vtype): + if not val or not vtype: + return None, 0 + fmt = { 'u8': '<B', 'u16': '<H', 'u32': '<L', 'u64': '<Q' } + if vtype.lower() == 'file': + with open(val, 'rb') as f: + data = f.read() + return data, len(data) + if vtype.lower() == 'str': + data = val.encode('utf-8') + return data, len(data) + if vtype.lower() == 'nil': + return None, 0 + i = fmt[vtype.lower()] + return struct.pack(i, int(val)), struct.calcsize(i) + +def parse_args(args): + name = args.name + attrs = parse_attrs(args.attrs) + guid = args.guid if args.guid else EFI_GLOBAL_VARIABLE_GUID + + if name.lower() == 'db' or name.lower() == 'dbx': + name = name.lower() + guid = EFI_IMAGE_SECURITY_DATABASE_GUID + attrs = NV_BS_RT_AT + elif name.lower() == 'pk' or name.lower() == 'kek': + name = name.upper() + guid = EFI_GLOBAL_VARIABLE_GUID + attrs = NV_BS_RT_AT + + data, size = parse_data(args.data, args.type) + return guid, name, attrs, data, size + +def cmd_set(args): + env = EfiVariableStore(args.infile) + guid, name, attrs, data, size = parse_args(args) + env.set_var(guid=guid, name=name, data=data, size=size, attrs=attrs) + env.save() + +def print_var(var): + print(var.name+':') + print(" "+str(var.guid)+' '+''.join([x for x in var_guids if str(var.guid) == var_guids[x]])) + print(" "+'|'.join([x for x in var_attrs if var.attrs & var_attrs[x]])+", DataSize = %s"%hex(var.size)) + hexdump(var.data) + +def cmd_print(args): + env = EfiVariableStore(args.infile) + if not args.name and not args.guid and not len(env): + return + + found = False + for var in env: + if not args.name: + if args.guid and args.guid != str(var.guid): + continue + print_var(var) + found = True + else: + if args.name != var.name or (args.guid and args.guid != str(var.guid)): + continue + print_var(var) + found = True + + if not found: + print("err: variable not found") + exit(1) + +def cmd_del(args): + env = EfiVariableStore(args.infile) + attrs = parse_attrs(args.attrs) + guid = args.guid if args.guid else EFI_GLOBAL_VARIABLE_GUID + env.del_var(guid, args.name, attrs) + env.save() + +def pkcs7_sign(cert, key, buf): + with open(cert, 'r') as f: + crt = crypto.load_certificate(crypto.FILETYPE_PEM, f.read()) + with open(key, 'r') as f: + pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read()) + + PKCS7_BINARY = 0x80 + PKCS7_DETACHED = 0x40 + PKCS7_NOATTR = 0x100 + + bio_in = crypto._new_mem_buf(buf) + p7 = crypto._lib.PKCS7_sign(crt._x509, pkey._pkey, crypto._ffi.NULL, bio_in, + PKCS7_BINARY|PKCS7_DETACHED|PKCS7_NOATTR) + bio_out = crypto._new_mem_buf() + crypto._lib.i2d_PKCS7_bio(bio_out, p7) + return crypto._bio_to_string(bio_out) + +# UEFI 2.8 Errata B "8.2.2 Using the EFI_VARIABLE_AUTHENTICATION_2 descriptor" +def cmd_sign(args): + guid, name, attrs, data, size = parse_args(args) + attrs |= EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS + efi = EfiStruct() + + tm = time.localtime() + etime = struct.pack(efi.var_time_fmt, + tm.tm_year, tm.tm_mon, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + 0, 0, 0, 0, 0) + + buf = name.encode('utf_16_le') + uuid.UUID(guid).bytes_le + attrs.to_bytes(4, byteorder='little') + etime + if data: + buf += data + sig = pkcs7_sign(args.cert, args.key, buf) + + desc = struct.pack(efi.var_win_cert_uefi_guid_fmt, + efi.var_win_cert_uefi_guid_size + len(sig), + WIN_CERT_REVISION, + WIN_CERT_TYPE_EFI_GUID, + uuid.UUID(EFI_CERT_TYPE_PKCS7_GUID).bytes_le) + + with open(args.outfile, 'wb') as f: + if data: + f.write(etime + desc + sig + data) + else: + f.write(etime + desc + sig) + +def main(): + ap = argparse.ArgumentParser(description='EFI variable store utilities') + subp = ap.add_subparsers(help="sub-command help") + + printp = subp.add_parser('print', help='get/list EFI variables') + printp.add_argument('--infile', '-i', required=True, help='file to save the EFI variables') + printp.add_argument('--name', '-n', help='variable name') + printp.add_argument('--guid', '-g', help='vendor GUID') + printp.set_defaults(func=cmd_print) + + setp = subp.add_parser('set', help='set EFI variable') + setp.add_argument('--infile', '-i', required=True, help='file to save the EFI variables') + setp.add_argument('--name', '-n', required=True, help='variable name') + setp.add_argument('--attrs', '-a', help='variable attributes (values: nv,bs,rt,at,ro,aw)') + setp.add_argument('--guid', '-g', help="vendor GUID (default: %s)"%EFI_GLOBAL_VARIABLE_GUID) + setp.add_argument('--type', '-t', help='variable type (values: file|u8|u16|u32|u64|str)') + setp.add_argument('--data', '-d', help='data or filename') + setp.set_defaults(func=cmd_set) + + delp = subp.add_parser('del', help='delete EFI variable') + delp.add_argument('--infile', '-i', required=True, help='file to save the EFI variables') + delp.add_argument('--name', '-n', required=True, help='variable name') + delp.add_argument('--attrs', '-a', help='variable attributes (values: nv,bs,rt,at,ro,aw)') + delp.add_argument('--guid', '-g', help="vendor GUID (default: %s)"%EFI_GLOBAL_VARIABLE_GUID) + delp.set_defaults(func=cmd_del) + + signp = subp.add_parser('sign', help='sign time-based EFI payload') + signp.add_argument('--cert', '-c', required=True, help='x509 certificate filename in PEM format') + signp.add_argument('--key', '-k', required=True, help='signing certificate filename in PEM format') + signp.add_argument('--name', '-n', required=True, help='variable name') + signp.add_argument('--attrs', '-a', help='variable attributes (values: nv,bs,rt,at,ro,aw)') + signp.add_argument('--guid', '-g', help="vendor GUID (default: %s)"%EFI_GLOBAL_VARIABLE_GUID) + signp.add_argument('--type', '-t', required=True, help='variable type (values: file|u8|u16|u32|u64|str|nil)') + signp.add_argument('--data', '-d', help='data or filename') + signp.add_argument('--outfile', '-o', required=True, help='output filename of signed EFI payload') + signp.set_defaults(func=cmd_sign) + + args = ap.parse_args() + args.func(args) + +def group(a, *ns): + for n in ns: + a = [a[i:i+n] for i in range(0, len(a), n)] + return a + +def join(a, *cs): + return [cs[0].join(join(t, *cs[1:])) for t in a] if cs else a + +def hexdump(data): + toHex = lambda c: '{:02X}'.format(c) + toChr = lambda c: chr(c) if 32 <= c < 127 else '.' + make = lambda f, *cs: join(group(list(map(f, data)), 8, 2), *cs) + hs = make(toHex, ' ', ' ') + cs = make(toChr, ' ', '') + for i, (h, c) in enumerate(zip(hs, cs)): + print (' {:010X}: {:48} {:16}'.format(i * 16, h, c)) + +if __name__ == '__main__': + main() -- 2.29.2

On 12/9/20 12:10 AM, Paulo Alcantara wrote:
This script generates EFI variables for U-Boot variable store format.
A few examples:
Generating secure boot keys
$ openssl x509 -in foo.crt -outform DER -out foo.der $ efisiglist -a -c foo.der -o foo.esl $ ./efivar.py -i ubootefi.var set -n db -d foo.esl -t file $ ./efivar.py -i ubootefi.var set -n kek -d foo.esl -t file
efivar.py requires that 'set' is the first parameter. I will fix the commit message.
Could you, please, provide a man-page for the tool as restructured text in doc/usage/. Please, check that it is correct with 'make htmldocs'.
Best regards
Heinrich
$ ./efivar.py -i ubootefi.var set -n pk -d foo.esl -t file
Printing out variables
$ ./efivar.py -i ubootefi.var set -n var1 -d foo -t str $ ./efivar.py -i ubootefi.var set -n var2 -d bar -t str $ ./efivar.py -i ubootefi.var print var1: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS|RT, DataSize = 0x3 0000000000: 66 6F 6F foo var2: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS|RT, DataSize = 0x3 0000000000: 62 61 72 bar
Removing variables
$ ./efivar.py -i ubootefi.var set -n var1 -a nv,bs -d foo -t str $ ./efivar.py -i ubootefi.var print -n var1 var1: 8be4df61-93ca-11d2-aa0d-00e098032b8c EFI_GLOBAL_VARIABLE_GUID NV|BS, DataSize = 0x3 0000000000: 66 6F 6F foo $ ./efivar.py -i ubootefi.var del -n var1 err: attributes don't match $ ./efivar.py -i ubootefi.var del -n var1 -a nv,bs $ ./efivar.py -i ubootefi.var print -n var1 err: variable not found
Signed-off-by: Paulo Alcantara (SUSE) pc@cjr.nz
v4: add 'sign' command for authenticated EFI payloads
tools/efivar.py | 380 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100755 tools/efivar.py
diff --git a/tools/efivar.py b/tools/efivar.py new file mode 100755 index 000000000000..ebfcab2f0a2c --- /dev/null +++ b/tools/efivar.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python3 +## SPDX-License-Identifier: GPL-2.0-only +# +# EFI variable store utilities. +# +# (c) 2020 Paulo Alcantara palcantara@suse.de +#
+import os +import struct +import uuid +import time +import zlib +import argparse +from OpenSSL import crypto
+# U-Boot variable store format (version 1) +UBOOT_EFI_VAR_FILE_MAGIC = 0x0161566966456255
+# UEFI variable attributes +EFI_VARIABLE_NON_VOLATILE = 0x1 +EFI_VARIABLE_BOOTSERVICE_ACCESS = 0x2 +EFI_VARIABLE_RUNTIME_ACCESS = 0x4 +EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS = 0x10 +EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS = 0x20 +EFI_VARIABLE_READ_ONLY = 1 << 31 +NV_BS = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS +NV_BS_RT = NV_BS | EFI_VARIABLE_RUNTIME_ACCESS +NV_BS_RT_AT = NV_BS_RT | EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS +DEFAULT_VAR_ATTRS = NV_BS_RT
+# vendor GUIDs +EFI_GLOBAL_VARIABLE_GUID = '8be4df61-93ca-11d2-aa0d-00e098032b8c' +EFI_IMAGE_SECURITY_DATABASE_GUID = 'd719b2cb-3d3a-4596-a3bc-dad00e67656f' +EFI_CERT_TYPE_PKCS7_GUID = '4aafd29d-68df-49ee-8aa9-347d375665a7' +WIN_CERT_TYPE_EFI_GUID = 0x0ef1 +WIN_CERT_REVISION = 0x0200
+var_attrs = {
'NV': EFI_VARIABLE_NON_VOLATILE,
'BS': EFI_VARIABLE_BOOTSERVICE_ACCESS,
'RT': EFI_VARIABLE_RUNTIME_ACCESS,
'AT': EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS,
'RO': EFI_VARIABLE_READ_ONLY,
'AW': EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS,
+}
+var_guids = {
'EFI_GLOBAL_VARIABLE_GUID': EFI_GLOBAL_VARIABLE_GUID,
'EFI_IMAGE_SECURITY_DATABASE_GUID': EFI_IMAGE_SECURITY_DATABASE_GUID,
+}
+class EfiStruct:
# struct efi_var_file
var_file_fmt = '<QQLL'
var_file_size = struct.calcsize(var_file_fmt)
# struct efi_var_entry
var_entry_fmt = '<LLQ16s'
var_entry_size = struct.calcsize(var_entry_fmt)
# struct efi_time
var_time_fmt = '<H6BLh2B'
var_time_size = struct.calcsize(var_time_fmt)
# WIN_CERTIFICATE
var_win_cert_fmt = '<L2H'
var_win_cert_size = struct.calcsize(var_win_cert_fmt)
# WIN_CERTIFICATE_UEFI_GUID
var_win_cert_uefi_guid_fmt = var_win_cert_fmt+'16s'
var_win_cert_uefi_guid_size = struct.calcsize(var_win_cert_uefi_guid_fmt)
+class EfiVariable:
- def __init__(self, size, attrs, time, guid, name, data):
self.size = size
self.attrs = attrs
self.time = time
self.guid = guid
self.name = name
self.data = data
+def calc_crc32(buf):
- return zlib.crc32(buf) & 0xffffffff
+class EfiVariableStore:
- def __init__(self, infile):
self.infile = infile
self.efi = EfiStruct()
if os.path.exists(self.infile) and os.stat(self.infile).st_size > self.efi.var_file_size:
with open(self.infile, 'rb') as f:
buf = f.read()
self._check_header(buf)
self.ents = buf[self.efi.var_file_size:]
else:
self.ents = bytearray()
- def _check_header(self, buf):
hdr = struct.unpack_from(self.efi.var_file_fmt, buf, 0)
magic, crc32 = hdr[1], hdr[3]
if magic != UBOOT_EFI_VAR_FILE_MAGIC:
print("err: invalid magic number: %s"%hex(magic))
exit(1)
if crc32 != calc_crc32(buf[self.efi.var_file_size:]):
print("err: invalid crc32: %s"%hex(crc32))
exit(1)
- def _get_var_name(self, buf):
name = ''
for i in range(0, len(buf) - 1, 2):
if not buf[i] and not buf[i+1]:
break
name += chr(buf[i])
return ''.join([chr(x) for x in name.encode('utf_16_le') if x]), i + 2
- def _next_var(self, offs=0):
size, attrs, time, guid = struct.unpack_from(self.efi.var_entry_fmt, self.ents, offs)
data_fmt = str(size)+"s"
offs += self.efi.var_entry_size
name, namelen = self._get_var_name(self.ents[offs:])
offs += namelen
data = struct.unpack_from(data_fmt, self.ents, offs)[0]
# offset to next 8-byte aligned variable entry
offs = (offs + len(data) + 7) & ~7
return EfiVariable(size, attrs, time, uuid.UUID(bytes_le=guid), name, data), offs
- def __iter__(self):
self.offs = 0
return self
- def __next__(self):
if self.offs < len(self.ents):
var, noffs = self._next_var(self.offs)
self.offs = noffs
return var
else:
raise StopIteration
- def __len__(self):
return len(self.ents)
- def _set_var(self, guid, name_data, size, attrs, tsec):
ent = struct.pack(self.efi.var_entry_fmt,
size,
attrs,
tsec,
uuid.UUID(guid).bytes_le)
ent += name_data
self.ents += ent
- def del_var(self, guid, name, attrs):
offs = 0
while offs < len(self.ents):
var, loffs = self._next_var(offs)
if var.name == name and str(var.guid):
if var.attrs != attrs:
print("err: attributes don't match")
exit(1)
self.ents = self.ents[:offs] + self.ents[loffs:]
return
offs = loffs
print("err: variable not found")
exit(1)
- def set_var(self, guid, name, data, size, attrs):
offs = 0
while offs < len(self.ents):
var, loffs = self._next_var(offs)
if var.name == name and str(var.guid) == guid:
if var.attrs != attrs:
print("err: attributes don't match")
exit(1)
# make room for updating var
self.ents = self.ents[:offs] + self.ents[loffs:]
break
offs = loffs
tsec = int(time.time()) if attrs & EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS else 0
nd = name.encode('utf_16_le') + b"\x00\x00" + data
# U-Boot variable format requires the name + data blob to be 8-byte aligned
pad = ((len(nd) + 7) & ~7) - len(nd)
nd += bytes([0] * pad)
return self._set_var(guid, nd, size, attrs, tsec)
- def save(self):
hdr = struct.pack(self.efi.var_file_fmt,
0,
UBOOT_EFI_VAR_FILE_MAGIC,
len(self.ents) + self.efi.var_file_size,
calc_crc32(self.ents))
with open(self.infile, 'wb') as f:
f.write(hdr)
f.write(self.ents)
+def parse_attrs(attrs):
- v = DEFAULT_VAR_ATTRS
- if attrs:
v = 0
for i in attrs.split(','):
v |= var_attrs[i.upper()]
- return v
+def parse_data(val, vtype):
- if not val or not vtype:
return None, 0
- fmt = { 'u8': '<B', 'u16': '<H', 'u32': '<L', 'u64': '<Q' }
- if vtype.lower() == 'file':
with open(val, 'rb') as f:
data = f.read()
return data, len(data)
- if vtype.lower() == 'str':
data = val.encode('utf-8')
return data, len(data)
- if vtype.lower() == 'nil':
return None, 0
- i = fmt[vtype.lower()]
- return struct.pack(i, int(val)), struct.calcsize(i)
+def parse_args(args):
- name = args.name
- attrs = parse_attrs(args.attrs)
- guid = args.guid if args.guid else EFI_GLOBAL_VARIABLE_GUID
- if name.lower() == 'db' or name.lower() == 'dbx':
name = name.lower()
guid = EFI_IMAGE_SECURITY_DATABASE_GUID
attrs = NV_BS_RT_AT
- elif name.lower() == 'pk' or name.lower() == 'kek':
name = name.upper()
guid = EFI_GLOBAL_VARIABLE_GUID
attrs = NV_BS_RT_AT
- data, size = parse_data(args.data, args.type)
- return guid, name, attrs, data, size
+def cmd_set(args):
- env = EfiVariableStore(args.infile)
- guid, name, attrs, data, size = parse_args(args)
- env.set_var(guid=guid, name=name, data=data, size=size, attrs=attrs)
- env.save()
+def print_var(var):
- print(var.name+':')
- print(" "+str(var.guid)+' '+''.join([x for x in var_guids if str(var.guid) == var_guids[x]]))
- print(" "+'|'.join([x for x in var_attrs if var.attrs & var_attrs[x]])+", DataSize = %s"%hex(var.size))
- hexdump(var.data)
+def cmd_print(args):
- env = EfiVariableStore(args.infile)
- if not args.name and not args.guid and not len(env):
return
- found = False
- for var in env:
if not args.name:
if args.guid and args.guid != str(var.guid):
continue
print_var(var)
found = True
else:
if args.name != var.name or (args.guid and args.guid != str(var.guid)):
continue
print_var(var)
found = True
- if not found:
print("err: variable not found")
exit(1)
+def cmd_del(args):
- env = EfiVariableStore(args.infile)
- attrs = parse_attrs(args.attrs)
- guid = args.guid if args.guid else EFI_GLOBAL_VARIABLE_GUID
- env.del_var(guid, args.name, attrs)
- env.save()
+def pkcs7_sign(cert, key, buf):
- with open(cert, 'r') as f:
crt = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
- with open(key, 'r') as f:
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read())
- PKCS7_BINARY = 0x80
- PKCS7_DETACHED = 0x40
- PKCS7_NOATTR = 0x100
- bio_in = crypto._new_mem_buf(buf)
- p7 = crypto._lib.PKCS7_sign(crt._x509, pkey._pkey, crypto._ffi.NULL, bio_in,
PKCS7_BINARY|PKCS7_DETACHED|PKCS7_NOATTR)
- bio_out = crypto._new_mem_buf()
- crypto._lib.i2d_PKCS7_bio(bio_out, p7)
- return crypto._bio_to_string(bio_out)
+# UEFI 2.8 Errata B "8.2.2 Using the EFI_VARIABLE_AUTHENTICATION_2 descriptor" +def cmd_sign(args):
- guid, name, attrs, data, size = parse_args(args)
- attrs |= EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS
- efi = EfiStruct()
- tm = time.localtime()
- etime = struct.pack(efi.var_time_fmt,
tm.tm_year, tm.tm_mon, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec,
0, 0, 0, 0, 0)
- buf = name.encode('utf_16_le') + uuid.UUID(guid).bytes_le + attrs.to_bytes(4, byteorder='little') + etime
- if data:
buf += data
- sig = pkcs7_sign(args.cert, args.key, buf)
- desc = struct.pack(efi.var_win_cert_uefi_guid_fmt,
efi.var_win_cert_uefi_guid_size + len(sig),
WIN_CERT_REVISION,
WIN_CERT_TYPE_EFI_GUID,
uuid.UUID(EFI_CERT_TYPE_PKCS7_GUID).bytes_le)
- with open(args.outfile, 'wb') as f:
if data:
f.write(etime + desc + sig + data)
else:
f.write(etime + desc + sig)
+def main():
- ap = argparse.ArgumentParser(description='EFI variable store utilities')
- subp = ap.add_subparsers(help="sub-command help")
- printp = subp.add_parser('print', help='get/list EFI variables')
- printp.add_argument('--infile', '-i', required=True, help='file to save the EFI variables')
- printp.add_argument('--name', '-n', help='variable name')
- printp.add_argument('--guid', '-g', help='vendor GUID')
- printp.set_defaults(func=cmd_print)
- setp = subp.add_parser('set', help='set EFI variable')
- setp.add_argument('--infile', '-i', required=True, help='file to save the EFI variables')
- setp.add_argument('--name', '-n', required=True, help='variable name')
- setp.add_argument('--attrs', '-a', help='variable attributes (values: nv,bs,rt,at,ro,aw)')
- setp.add_argument('--guid', '-g', help="vendor GUID (default: %s)"%EFI_GLOBAL_VARIABLE_GUID)
- setp.add_argument('--type', '-t', help='variable type (values: file|u8|u16|u32|u64|str)')
- setp.add_argument('--data', '-d', help='data or filename')
- setp.set_defaults(func=cmd_set)
- delp = subp.add_parser('del', help='delete EFI variable')
- delp.add_argument('--infile', '-i', required=True, help='file to save the EFI variables')
- delp.add_argument('--name', '-n', required=True, help='variable name')
- delp.add_argument('--attrs', '-a', help='variable attributes (values: nv,bs,rt,at,ro,aw)')
- delp.add_argument('--guid', '-g', help="vendor GUID (default: %s)"%EFI_GLOBAL_VARIABLE_GUID)
- delp.set_defaults(func=cmd_del)
- signp = subp.add_parser('sign', help='sign time-based EFI payload')
- signp.add_argument('--cert', '-c', required=True, help='x509 certificate filename in PEM format')
- signp.add_argument('--key', '-k', required=True, help='signing certificate filename in PEM format')
- signp.add_argument('--name', '-n', required=True, help='variable name')
- signp.add_argument('--attrs', '-a', help='variable attributes (values: nv,bs,rt,at,ro,aw)')
- signp.add_argument('--guid', '-g', help="vendor GUID (default: %s)"%EFI_GLOBAL_VARIABLE_GUID)
- signp.add_argument('--type', '-t', required=True, help='variable type (values: file|u8|u16|u32|u64|str|nil)')
- signp.add_argument('--data', '-d', help='data or filename')
- signp.add_argument('--outfile', '-o', required=True, help='output filename of signed EFI payload')
- signp.set_defaults(func=cmd_sign)
- args = ap.parse_args()
- args.func(args)
+def group(a, *ns):
- for n in ns:
a = [a[i:i+n] for i in range(0, len(a), n)]
- return a
+def join(a, *cs):
- return [cs[0].join(join(t, *cs[1:])) for t in a] if cs else a
+def hexdump(data):
- toHex = lambda c: '{:02X}'.format(c)
- toChr = lambda c: chr(c) if 32 <= c < 127 else '.'
- make = lambda f, *cs: join(group(list(map(f, data)), 8, 2), *cs)
- hs = make(toHex, ' ', ' ')
- cs = make(toChr, ' ', '')
- for i, (h, c) in enumerate(zip(hs, cs)):
print (' {:010X}: {:48} {:16}'.format(i * 16, h, c))
+if __name__ == '__main__':
- main()
-- 2.29.2

Heinrich Schuchardt xypron.glpk@gmx.de writes:
efivar.py requires that 'set' is the first parameter. I will fix the commit message.
Thanks!
Could you, please, provide a man-page for the tool as restructured text in doc/usage/. Please, check that it is correct with 'make htmldocs'.
Yes, will do it.
participants (2)
-
Heinrich Schuchardt
-
Paulo Alcantara