dissect-image: optionally, validate dm-verity signatures in userspace

Getting certificates for dm-verity roothash signing into the trusted
kernel keychain is a royal PITA (means recompiling or rebooting with
shim), hence let's add a minimal userspace PKCS7 validation as well.

The mechanism is really simple and compatible with the verification the
kernel does. The only difference is that the certificates are searched
in /etc/verity.d/*.crt (and similar dirs in /usr/lib/, …).

We'll first try validation by passing the PKCS#7 data to the kernel, but
if that doesn't work we'll see if one of the certificates found that way
works and then attempt to attach the image without passing the PKCS#7
data to the kernel.

This makes it very easy to have fully validated GPT disk images. For
example, just copy the 'mkosi.secure-boot.crt' file you have in your
mkosi build dir to /etc/verity.d/ and things should just work.
This commit is contained in:
Lennart Poettering 2021-09-09 17:55:36 +02:00
parent d5fcc5b053
commit c2fa92e7e8
3 changed files with 165 additions and 25 deletions

View file

@ -9,13 +9,11 @@
#include <openssl/x509v3.h>
#include "io-util.h"
#include "openssl-util.h"
#include "resolved-dns-stream.h"
#include "resolved-dnstls.h"
#include "resolved-manager.h"
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(SSL*, SSL_free, NULL);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(BIO*, BIO_free, NULL);
static int dnstls_flush_write_buffer(DnsStream *stream) {
ssize_t ss;

View file

@ -11,6 +11,12 @@
#include <sys/wait.h>
#include <sysexits.h>
#if HAVE_OPENSSL
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#endif
#include "sd-device.h"
#include "sd-id128.h"
@ -18,6 +24,7 @@
#include "ask-password-api.h"
#include "blkid-util.h"
#include "blockdev-util.h"
#include "conf-files.h"
#include "copy.h"
#include "cryptsetup-util.h"
#include "def.h"
@ -43,6 +50,7 @@
#include "mountpoint-util.h"
#include "namespace-util.h"
#include "nulstr-util.h"
#include "openssl-util.h"
#include "os-util.h"
#include "path-util.h"
#include "process-util.h"
@ -2259,6 +2267,146 @@ static inline char* dm_deferred_remove_clean(char *name) {
}
DEFINE_TRIVIAL_CLEANUP_FUNC(char *, dm_deferred_remove_clean);
static int validate_signature_userspace(const VeritySettings *verity) {
#if HAVE_OPENSSL
_cleanup_(sk_X509_free_allp) STACK_OF(X509) *sk = NULL;
_cleanup_strv_free_ char **certs = NULL;
_cleanup_(PKCS7_freep) PKCS7 *p7 = NULL;
_cleanup_free_ char *s = NULL;
_cleanup_(BIO_freep) BIO *bio = NULL; /* 'bio' must be freed first, 's' second, hence keep this order
* of declaration in place, please */
const unsigned char *d;
char **i;
int r;
assert(verity);
assert(verity->root_hash);
assert(verity->root_hash_sig);
/* Because installing a signature certificate into the kernel chain is so messy, let's optionally do
* userspace validation. */
r = conf_files_list_nulstr(&certs, ".crt", NULL, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, CONF_PATHS_NULSTR("verity.d"));
if (r < 0)
return log_debug_errno(r, "Failed to enumerate certificates: %m");
if (strv_isempty(certs)) {
log_debug("No userspace dm-verity certificates found.");
return 0;
}
d = verity->root_hash_sig;
p7 = d2i_PKCS7(NULL, &d, (long) verity->root_hash_sig_size);
if (!p7)
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse PKCS7 DER signature data.");
s = hexmem(verity->root_hash, verity->root_hash_size);
if (!s)
return log_oom_debug();
bio = BIO_new_mem_buf(s, strlen(s));
if (!bio)
return log_oom_debug();
sk = sk_X509_new_null();
if (!sk)
return log_oom_debug();
STRV_FOREACH(i, certs) {
_cleanup_(X509_freep) X509 *c = NULL;
_cleanup_fclose_ FILE *f = NULL;
f = fopen(*i, "re");
if (!f) {
log_debug_errno(errno, "Failed to open '%s', ignoring: %m", *i);
continue;
}
c = PEM_read_X509(f, NULL, NULL, NULL);
if (!c) {
log_debug("Failed to load X509 certificate '%s', ignoring.", *i);
continue;
}
if (sk_X509_push(sk, c) == 0)
return log_oom_debug();
TAKE_PTR(c);
}
r = PKCS7_verify(p7, sk, NULL, bio, NULL, PKCS7_NOINTERN|PKCS7_NOVERIFY);
if (r)
log_debug("Userspace PKCS#7 validation succeeded.");
else
log_debug("Userspace PKCS#7 validation failed: %s", ERR_error_string(ERR_get_error(), NULL));
return r;
#else
log_debug("Not doing client-side validation of dm-verity root hash signatures, OpenSSL support disabled.");
return 0;
#endif
}
static int do_crypt_activate_verity(
struct crypt_device *cd,
const char *name,
const VeritySettings *verity) {
bool check_signature;
int r;
assert(cd);
assert(name);
assert(verity);
if (verity->root_hash_sig) {
r = getenv_bool_secure("SYSTEMD_DISSECT_VERITY_SIGNATURE");
if (r < 0 && r != -ENXIO)
log_debug_errno(r, "Failed to parse $SYSTEMD_DISSECT_VERITY_SIGNATURE");
check_signature = r != 0;
} else
check_signature = false;
if (check_signature) {
#if HAVE_CRYPT_ACTIVATE_BY_SIGNED_KEY
/* First, if we have support for signed keys in the kernel, then try that first. */
r = sym_crypt_activate_by_signed_key(
cd,
name,
verity->root_hash,
verity->root_hash_size,
verity->root_hash_sig,
verity->root_hash_sig_size,
CRYPT_ACTIVATE_READONLY);
if (r >= 0)
return r;
log_debug("Validation of dm-verity signature failed via the kernel, trying userspace validation instead.");
#else
log_debug("Activation of verity device with signature requested, but not supported via the kernel by %s due to missing crypt_activate_by_signed_key(), trying userspace validation instead.",
program_invocation_short_name);
#endif
/* So this didn't work via the kernel, then let's try userspace validation instead. If that
* works we'll try to activate without telling the kernel the signature. */
r = validate_signature_userspace(verity);
if (r < 0)
return r;
if (r == 0)
return log_debug_errno(SYNTHETIC_ERRNO(ENOKEY),
"Activation of signed Verity volume worked neither via the kernel nor in userspace, can't activate.");
}
return sym_crypt_activate_by_volume_key(
cd,
name,
verity->root_hash,
verity->root_hash_size,
CRYPT_ACTIVATE_READONLY);
}
static int verity_partition(
PartitionDesignator designator,
DissectedPartition *m,
@ -2330,27 +2478,8 @@ static int verity_partition(
* In case of ENODEV/ENOENT, which can happen if another process is activating at the exact same time,
* retry a few times before giving up. */
for (unsigned i = 0; i < N_DEVICE_NODE_LIST_ATTEMPTS; i++) {
if (verity->root_hash_sig) {
#if HAVE_CRYPT_ACTIVATE_BY_SIGNED_KEY
r = sym_crypt_activate_by_signed_key(
cd,
name,
verity->root_hash,
verity->root_hash_size,
verity->root_hash_sig,
verity->root_hash_sig_size,
CRYPT_ACTIVATE_READONLY);
#else
r = log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"Activation of verity device with signature requested, but not supported by %s due to missing crypt_activate_by_signed_key().", program_invocation_short_name);
#endif
} else
r = sym_crypt_activate_by_volume_key(
cd,
name,
verity->root_hash,
verity->root_hash_size,
CRYPT_ACTIVATE_READONLY);
r = do_crypt_activate_verity(cd, name, verity);
/* libdevmapper can return EINVAL when the device is already in the activation stage.
* There's no way to distinguish this situation from a genuine error due to invalid
* parameters, so immediately fall back to activating the device with a unique name.

View file

@ -4,13 +4,26 @@
#include "macro.h"
#if HAVE_OPENSSL
# include <openssl/bio.h>
# include <openssl/evp.h>
# include <openssl/x509.h>
# include <openssl/pkcs7.h>
# include <openssl/ssl.h>
# include <openssl/x509v3.h>
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(X509*, X509_free, NULL);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(X509_NAME*, X509_NAME_free, NULL);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(EVP_PKEY_CTX*, EVP_PKEY_CTX_free, NULL);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(EVP_CIPHER_CTX*, EVP_CIPHER_CTX_free, NULL);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(PKCS7*, PKCS7_free, NULL);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(SSL*, SSL_free, NULL);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(BIO*, BIO_free, NULL);
static inline void sk_X509_free_allp(STACK_OF(X509) **sk) {
if (!sk || !*sk)
return;
sk_X509_pop_free(*sk, X509_free);
}
int rsa_encrypt_bytes(EVP_PKEY *pkey, const void *decrypted_key, size_t decrypted_key_size, void **ret_encrypt_key, size_t *ret_encrypt_key_size);