diff --git a/grub-core/Makefile.core.def b/grub-core/Makefile.core.def index 8e3929710..6824a0ee4 100644 --- a/grub-core/Makefile.core.def +++ b/grub-core/Makefile.core.def @@ -997,6 +997,21 @@ module = { cppflags = '$(CPPFLAGS_GCRY)'; }; +module = { + name = appendedsig; + common = commands/appendedsig/appendedsig.c; + common = commands/appendedsig/x509.c; + common = commands/appendedsig/pkcs7.c; + common = commands/appendedsig/asn1util.c; + common = commands/appendedsig/gnutls_asn1_tab.c; + common = commands/appendedsig/pkix_asn1_tab.c; + enable = emu; + enable = powerpc_ieee1275; + cflags = '$(CFLAGS_GCRY) -Wno-redundant-decls'; + cppflags = '$(CPPFLAGS_GCRY) -I$(srcdir)/lib/libtasn1-grub'; + depends = crypto, gcry_rsa, gcry_sha256, gcry_sha512, mpi, asn1; +}; + module = { name = hdparm; common = commands/hdparm.c; diff --git a/grub-core/commands/appendedsig/appendedsig.c b/grub-core/commands/appendedsig/appendedsig.c new file mode 100644 index 000000000..e53efd2da --- /dev/null +++ b/grub-core/commands/appendedsig/appendedsig.c @@ -0,0 +1,581 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2020, 2021, 2022 Free Software Foundation, Inc. + * Copyright (C) 2020, 2021, 2022, 2025 IBM Corporation + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "appendedsig.h" + +GRUB_MOD_LICENSE ("GPLv3+"); + +/* Public key type. */ +#define PKEY_ID_PKCS7 2 + +/* Appended signature magic string and size. */ +#define SIG_MAGIC "~Module signature appended~\n" +#define SIG_MAGIC_SIZE ((sizeof(SIG_MAGIC) - 1)) + +/* + * This structure is extracted from scripts/sign-file.c in the linux kernel + * source. It was licensed as LGPLv2.1+, which is GPLv3+ compatible. + */ +struct module_signature +{ + grub_uint8_t algo; /* Public-key crypto algorithm [0]. */ + grub_uint8_t hash; /* Digest algorithm [0]. */ + grub_uint8_t id_type; /* Key identifier type [PKEY_ID_PKCS7]. */ + grub_uint8_t signer_len; /* Length of signer's name [0]. */ + grub_uint8_t key_id_len; /* Length of key identifier [0]. */ + grub_uint8_t __pad[3]; + grub_uint32_t sig_len; /* Length of signature data. */ +} GRUB_PACKED; + +#define SIG_METADATA_SIZE (sizeof (struct module_signature)) +#define APPENDED_SIG_SIZE(pkcs7_data_size) \ + (pkcs7_data_size + SIG_MAGIC_SIZE + SIG_METADATA_SIZE) + +/* This represents an entire, parsed, appended signature. */ +struct appended_signature +{ + struct module_signature sig_metadata; /* Module signature metadata. */ + grub_pkcs7_data_t pkcs7; /* Parsed PKCS#7 data. */ + grub_size_t signature_len; /* Length of PKCS#7 data + metadata + magic. */ +}; +typedef struct appended_signature sb_appendedsig_t; + +/* This represents a trusted certificates. */ +struct sb_database +{ + grub_x509_cert_t *certs; /* Certificates. */ + grub_uint32_t cert_entries; /* Number of certificates. */ +}; +typedef struct sb_database sb_database_t; + +/* The db list is used to validate appended signatures. */ +static sb_database_t db = {.certs = NULL, .cert_entries = 0}; + +/* + * Signature verification flag (check_sigs). + * check_sigs: false + * - No signature verification. This is the default. + * check_sigs: true + * - Enforce signature verification, and if signature verification fails, post + * the errors and stop the boot. + */ +static bool check_sigs = false; + +static grub_ssize_t +pseudo_read (struct grub_file *file, char *buf, grub_size_t len) +{ + grub_memcpy (buf, (grub_uint8_t *) file->data + file->offset, len); + return len; +} + +/* Filesystem descriptor. */ +static struct grub_fs pseudo_fs = { + .name = "pseudo", + .fs_read = pseudo_read +}; + +static bool +is_cert_match (const grub_x509_cert_t *cert1, const grub_x509_cert_t *cert2) +{ + if (grub_memcmp (cert1->subject, cert2->subject, cert2->subject_len) == 0 + && grub_memcmp (cert1->issuer, cert2->issuer, cert2->issuer_len) == 0 + && grub_memcmp (cert1->serial, cert2->serial, cert2->serial_len) == 0 + && grub_memcmp (cert1->mpis[GRUB_RSA_PK_MODULUS], cert2->mpis[GRUB_RSA_PK_MODULUS], + sizeof (cert2->mpis[GRUB_RSA_PK_MODULUS])) == 0 + && grub_memcmp (cert1->mpis[GRUB_RSA_PK_EXPONENT], cert2->mpis[GRUB_RSA_PK_EXPONENT], + sizeof (cert2->mpis[GRUB_RSA_PK_EXPONENT])) == 0 + && grub_memcmp (cert1->fingerprint[GRUB_FINGERPRINT_SHA256], + cert2->fingerprint[GRUB_FINGERPRINT_SHA256], + grub_strlen ((char *) cert2->fingerprint[GRUB_FINGERPRINT_SHA256])) == 0) + return true; + + return false; +} + +/* Check the certificate presence in the db list. */ +static bool +check_cert_presence (const grub_x509_cert_t *cert_in, const sb_database_t *sb_database) +{ + grub_x509_cert_t *cert; + + for (cert = sb_database->certs; cert != NULL; cert = cert->next) + if (is_cert_match (cert, cert_in) == true) + return true; + + return false; +} + +/* Add the certificate into the db list */ +static grub_err_t +add_certificate (const grub_uint8_t *data, const grub_size_t data_size, + sb_database_t *sb_database) +{ + grub_err_t rc; + grub_x509_cert_t *cert; + + if (data == NULL || data_size == 0) + return grub_error (GRUB_ERR_OUT_OF_RANGE, "certificate data or size is not available"); + + cert = grub_zalloc (sizeof (grub_x509_cert_t)); + if (cert == NULL) + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "out of memory"); + + rc = grub_x509_cert_parse (data, data_size, cert); + if (rc != GRUB_ERR_NONE) + { + grub_dprintf ("appendedsig", "cannot add a certificate CN='%s' to the db list\n", + cert->subject); + grub_free (cert); + return rc; + } + + if (check_cert_presence (cert, sb_database) == true) + { + grub_dprintf ("appendedsig", + "cannot add a certificate CN='%s', as it is present in the db list", + cert->subject); + grub_x509_cert_release (cert); + grub_free (cert); + + return GRUB_ERR_EXISTS; + } + + grub_dprintf ("appendedsig", "added a certificate CN='%s' to the db list\n", + cert->subject); + + cert->next = sb_database->certs; + sb_database->certs = cert; + sb_database->cert_entries++; + + return rc; +} + +static grub_err_t +file_read_whole (grub_file_t file, grub_uint8_t **buf, grub_size_t *len) +{ + grub_off_t full_file_size; + grub_size_t file_size, total_read_size = 0; + grub_ssize_t read_size; + + full_file_size = grub_file_size (file); + if (full_file_size == GRUB_FILE_SIZE_UNKNOWN) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "cannot read a file of unknown size into a buffer"); + + if (full_file_size > GRUB_SIZE_MAX) + return grub_error (GRUB_ERR_OUT_OF_RANGE, + "file is too large to read: %" PRIuGRUB_OFFSET " bytes", + full_file_size); + + file_size = (grub_size_t) full_file_size; + *buf = grub_malloc (file_size); + if (*buf == NULL) + return grub_error (GRUB_ERR_OUT_OF_MEMORY, + "could not allocate file data buffer size %" PRIuGRUB_SIZE, + file_size); + + while (total_read_size < file_size) + { + read_size = grub_file_read (file, *buf + total_read_size, file_size - total_read_size); + if (read_size < 0) + { + grub_free (*buf); + return grub_errno; + } + else if (read_size == 0) + { + grub_free (*buf); + return grub_error (GRUB_ERR_IO, + "could not read full file size " + "(%" PRIuGRUB_SIZE "), only %" PRIuGRUB_SIZE " bytes read", + file_size, total_read_size); + } + + total_read_size += read_size; + } + + *len = file_size; + + return GRUB_ERR_NONE; +} + +static grub_err_t +extract_appended_signature (const grub_uint8_t *buf, grub_size_t bufsize, + sb_appendedsig_t *sig) +{ + grub_size_t appendedsig_pkcs7_size; + grub_size_t signed_data_size = bufsize; + const grub_uint8_t *signed_data = buf; + + if (signed_data_size < SIG_MAGIC_SIZE) + return grub_error (GRUB_ERR_BAD_SIGNATURE, "file too short for signature magic"); + + /* Fast-forwarding pointer and get signature magic string. */ + signed_data += signed_data_size - SIG_MAGIC_SIZE; + if (grub_strncmp ((const char *) signed_data, SIG_MAGIC, SIG_MAGIC_SIZE)) + return grub_error (GRUB_ERR_BAD_SIGNATURE, "missing or invalid signature magic"); + + signed_data_size -= SIG_MAGIC_SIZE; + if (signed_data_size < SIG_METADATA_SIZE) + return grub_error (GRUB_ERR_BAD_SIGNATURE, "file too short for signature metadata"); + + /* Rewind pointer and extract signature metadata. */ + signed_data -= SIG_METADATA_SIZE; + grub_memcpy (&(sig->sig_metadata), signed_data, SIG_METADATA_SIZE); + + if (sig->sig_metadata.id_type != PKEY_ID_PKCS7) + return grub_error (GRUB_ERR_BAD_SIGNATURE, "wrong signature type"); + + appendedsig_pkcs7_size = grub_be_to_cpu32 (sig->sig_metadata.sig_len); + + signed_data_size -= SIG_METADATA_SIZE; + if (appendedsig_pkcs7_size > signed_data_size) + return grub_error (GRUB_ERR_BAD_SIGNATURE, "file too short for PKCS#7 message"); + + grub_dprintf ("appendedsig", "sig len %" PRIuGRUB_SIZE "\n", appendedsig_pkcs7_size); + + /* Appended signature size. */ + sig->signature_len = APPENDED_SIG_SIZE (appendedsig_pkcs7_size); + /* Rewind pointer and parse appended pkcs7 data. */ + signed_data -= appendedsig_pkcs7_size; + + return grub_pkcs7_data_parse (signed_data, appendedsig_pkcs7_size, &sig->pkcs7); +} + +/* + * Given a hash value 'hval', of hash specification 'hash', prepare the + * S-expressions (sexp) and perform the signature verification. + */ +static grub_err_t +verify_signature (const gcry_mpi_t *pkmpi, const gcry_mpi_t hmpi, + const gcry_md_spec_t *hash, const grub_uint8_t *hval) +{ + gcry_sexp_t hsexp, pubkey, sig; + grub_size_t errof; + + if (_gcry_sexp_build (&hsexp, &errof, "(data (flags %s) (hash %s %b))", "pkcs1", + hash->name, hash->mdlen, hval) != GPG_ERR_NO_ERROR) + return GRUB_ERR_BAD_SIGNATURE; + + if (_gcry_sexp_build (&pubkey, &errof, "(public-key (dsa (n %M) (e %M)))", + pkmpi[0], pkmpi[1]) != GPG_ERR_NO_ERROR) + return GRUB_ERR_BAD_SIGNATURE; + + if (_gcry_sexp_build (&sig, &errof, "(sig-val (rsa (s %M)))", hmpi) != GPG_ERR_NO_ERROR) + return GRUB_ERR_BAD_SIGNATURE; + + _gcry_sexp_dump (sig); + _gcry_sexp_dump (hsexp); + _gcry_sexp_dump (pubkey); + + if (grub_crypto_pk_rsa->verify (sig, hsexp, pubkey) != GPG_ERR_NO_ERROR) + return GRUB_ERR_BAD_SIGNATURE; + + return GRUB_ERR_NONE; +} + +static grub_err_t +grub_verify_appended_signature (const grub_uint8_t *buf, grub_size_t bufsize) +{ + grub_err_t err; + grub_size_t datasize; + void *context; + grub_uint8_t *hash; + grub_x509_cert_t *pk; + sb_appendedsig_t sig; + grub_pkcs7_signer_t *si; + grub_int32_t i; + + if (!db.cert_entries) + return grub_error (GRUB_ERR_BAD_SIGNATURE, "no trusted keys to verify against"); + + err = extract_appended_signature (buf, bufsize, &sig); + if (err != GRUB_ERR_NONE) + return err; + + datasize = bufsize - sig.signature_len; + + /* Verify signature using trusted keys from db list. */ + for (i = 0; i < sig.pkcs7.signer_count; i++) + { + si = &sig.pkcs7.signers[i]; + context = grub_zalloc (si->hash->contextsize); + if (context == NULL) + return grub_errno; + + si->hash->init (context, 0); + si->hash->write (context, buf, datasize); + si->hash->final (context); + hash = si->hash->read (context); + + grub_dprintf ("appendedsig", "data size %" PRIuGRUB_SIZE ", signer %d hash %02x%02x%02x%02x...\n", + datasize, i, hash[0], hash[1], hash[2], hash[3]); + + for (pk = db.certs; pk != NULL; pk = pk->next) + { + err = verify_signature (pk->mpis, si->sig_mpi, si->hash, hash); + if (err == GRUB_ERR_NONE) + { + grub_dprintf ("appendedsig", "verify signer %d with key '%s' succeeded\n", + i, pk->subject); + break; + } + + grub_dprintf ("appendedsig", "verify signer %d with key '%s' failed\n", + i, pk->subject); + } + + grub_free (context); + if (err == GRUB_ERR_NONE) + break; + } + + grub_pkcs7_data_release (&sig.pkcs7); + + if (err != GRUB_ERR_NONE) + return grub_error (err, "failed to verify signature against a trusted key"); + + return err; +} + +/* + * Extract the X.509 certificates from the ELF Note header, parse it, and add + * it to the db list. + */ +static void +load_elf2db (void) +{ + grub_err_t err; + struct grub_module_header *header; + struct grub_file pseudo_file; + grub_uint8_t *cert_data = NULL; + grub_size_t cert_data_size = 0; + + FOR_MODULES (header) + { + /* Not an X.509 certificate, skip. */ + if (header->type != OBJ_TYPE_X509_PUBKEY) + continue; + + grub_memset (&pseudo_file, 0, sizeof (pseudo_file)); + pseudo_file.fs = &pseudo_fs; + pseudo_file.size = header->size - sizeof (struct grub_module_header); + pseudo_file.data = (char *) header + sizeof (struct grub_module_header); + + grub_dprintf ("appendedsig", "found an X.509 certificate, size=%" PRIuGRUB_UINT64_T "\n", + pseudo_file.size); + + err = file_read_whole (&pseudo_file, &cert_data, &cert_data_size); + if (err == GRUB_ERR_OUT_OF_MEMORY) + return; + else if (err != GRUB_ERR_NONE) + continue; + + err = add_certificate (cert_data, cert_data_size, &db); + grub_free (cert_data); + if (err == GRUB_ERR_OUT_OF_MEMORY) + return; + } +} + +/* Free db list memory */ +static void +free_db_list (void) +{ + grub_x509_cert_t *cert; + + while (db.certs != NULL) + { + cert = db.certs; + db.certs = db.certs->next; + grub_x509_cert_release (cert); + grub_free (cert); + } + + grub_memset (&db, 0, sizeof (sb_database_t)); +} + +static const char * +grub_env_read_sec (struct grub_env_var *var __attribute__ ((unused)), + const char *val __attribute__ ((unused))) +{ + if (check_sigs == true) + return "yes"; + + return "no"; +} + +static char * +grub_env_write_sec (struct grub_env_var *var __attribute__ ((unused)), const char *val) +{ + char *ret; + + /* + * Do not allow the value to be changed if signature verification is enabled + * (check_sigs is set to true) and GRUB is locked down. + */ + if (check_sigs == true && grub_is_lockdown () == GRUB_LOCKDOWN_ENABLED) + { + ret = grub_strdup ("yes"); + if (ret == NULL) + grub_error (GRUB_ERR_OUT_OF_MEMORY, "could not duplicate a string enforce"); + + return ret; + } + + if (grub_strcmp (val, "yes") == 0) + check_sigs = true; + else if (grub_strcmp (val, "no") == 0) + check_sigs = false; + + ret = grub_strdup (grub_env_read_sec (NULL, NULL)); + if (ret == NULL) + grub_error (GRUB_ERR_OUT_OF_MEMORY, "could not duplicate a string %s", + grub_env_read_sec (NULL, NULL)); + + return ret; +} + +static grub_err_t +appendedsig_init (grub_file_t io __attribute__ ((unused)), enum grub_file_type type, + void **context __attribute__ ((unused)), enum grub_verify_flags *flags) +{ + if (check_sigs == false) + { + *flags = GRUB_VERIFY_FLAGS_SKIP_VERIFICATION; + return GRUB_ERR_NONE; + } + + switch (type & GRUB_FILE_TYPE_MASK) + { + case GRUB_FILE_TYPE_CERTIFICATE_TRUST: + /* + * This is a certificate to add to trusted keychain. + * + * This needs to be verified or blocked. Ideally we'd write an x509 + * verifier, but we lack the hubris required to take this on. Instead, + * require that it have an appended signature. + */ + case GRUB_FILE_TYPE_LINUX_KERNEL: + case GRUB_FILE_TYPE_GRUB_MODULE: + /* + * Appended signatures are only defined for ELF binaries. Out of an + * abundance of caution, we only verify Linux kernels and GRUB modules + * at this point. + */ + *flags = GRUB_VERIFY_FLAGS_SINGLE_CHUNK; + return GRUB_ERR_NONE; + + case GRUB_FILE_TYPE_ACPI_TABLE: + case GRUB_FILE_TYPE_DEVICE_TREE_IMAGE: + /* + * It is possible to use appended signature verification without + * lockdown - like the PGP verifier. When combined with an embedded + * config file in a signed GRUB binary, this could still be a meaningful + * secure-boot chain - so long as it isn't subverted by something like a + * rouge ACPI table or DT image. Defer them explicitly. + */ + *flags = GRUB_VERIFY_FLAGS_DEFER_AUTH; + return GRUB_ERR_NONE; + + default: + *flags = GRUB_VERIFY_FLAGS_SKIP_VERIFICATION; + return GRUB_ERR_NONE; + } +} + +static grub_err_t +appendedsig_write (void *ctxt __attribute__ ((unused)), void *buf, grub_size_t size) +{ + return grub_verify_appended_signature (buf, size); +} + +struct grub_file_verifier grub_appendedsig_verifier = { + .name = "appendedsig", + .init = appendedsig_init, + .write = appendedsig_write, +}; + +GRUB_MOD_INIT (appendedsig) +{ + grub_int32_t rc; + + /* + * If secure boot is enabled with enforce mode and GRUB is locked down, enable + * signature verification. + */ + if (grub_is_lockdown () == GRUB_LOCKDOWN_ENABLED) + check_sigs = true; + + /* + * This is appended signature verification environment variable. It is + * automatically set to either "no" or "yes" based on the ’ibm,secure-boot’ + * device tree property. + * + * "no": No signature verification. This is the default. + * + * "yes": Enforce signature verification. When GRUB is locked down, user cannot + * change the value by setting the check_appended_signatures variable + * back to ‘no’ + */ + grub_register_variable_hook ("check_appended_signatures", grub_env_read_sec, grub_env_write_sec); + grub_env_export ("check_appended_signatures"); + + rc = grub_asn1_init (); + if (rc != ASN1_SUCCESS) + grub_fatal ("error initing ASN.1 data structures: %d: %s\n", rc, asn1_strerror (rc)); + + /* Extract trusted keys from ELF Note and store them in the db. */ + load_elf2db (); + grub_dprintf ("appendedsig", "the db list now has %u static keys\n", + db.cert_entries); + + grub_verifier_register (&grub_appendedsig_verifier); + grub_dl_set_persistent (mod); +} + +GRUB_MOD_FINI (appendedsig) +{ + /* + * grub_dl_set_persistent should prevent this from actually running, but it + * does still run under emu. + */ + + free_db_list (); + grub_register_variable_hook ("check_appended_signatures", NULL, NULL); + grub_env_unset ("check_appended_signatures"); + grub_verifier_unregister (&grub_appendedsig_verifier); +} diff --git a/include/grub/err.h b/include/grub/err.h index 202fa8a7a..6ab905c93 100644 --- a/include/grub/err.h +++ b/include/grub/err.h @@ -75,7 +75,8 @@ typedef enum GRUB_ERR_BAD_SIGNATURE, GRUB_ERR_BAD_FIRMWARE, GRUB_ERR_STILL_REFERENCED, - GRUB_ERR_RECURSION_DEPTH + GRUB_ERR_RECURSION_DEPTH, + GRUB_ERR_EXISTS } grub_err_t; diff --git a/include/grub/file.h b/include/grub/file.h index a5bf3a792..d678de063 100644 --- a/include/grub/file.h +++ b/include/grub/file.h @@ -80,6 +80,8 @@ enum grub_file_type GRUB_FILE_TYPE_PUBLIC_KEY, /* File holding public key to add to trused keys. */ GRUB_FILE_TYPE_PUBLIC_KEY_TRUST, + /* File holding x509 certificiate to add to trusted keys. */ + GRUB_FILE_TYPE_CERTIFICATE_TRUST, /* File of which we intend to print a blocklist to the user. */ GRUB_FILE_TYPE_PRINT_BLOCKLIST, /* File we intend to use for test loading or testing speed. */