[dm-devel] userspace hashing utility for dm-verity

Mikulas Patocka mpatocka at redhat.com
Sun Mar 4 19:35:27 UTC 2012


This is an userspace utility that creates or verifies hashes for the 
verity target.

The original utility was created by Google and it is located at 
http://git.chromium.org/chromiumos/platform/dm-verity.git

The original utility has some problems:
* the code is really overengineered, they took the kernel code and built 
  an emulation layer in userspace that emulates some of the kernel 
  functions
* it dosn't use library implementations of hash functions, rather it 
  provides its own md5, sha1, sha256 and sha512 implementation
* it is not portable (produces bad result on big-endian machines)

This is much smaller implementation that is portable and uses the crypto 
library.

This code creates compatible format with the original Google code under 
these conditions:
- data block size and hash block size are 4096
- salt has exactly 32 bytes (64 hex digits)

Example use:
Create filesystem on /dev/sdc2 and fill it with some data. Block size must 
be 4096
Unmount the filesystem

Run: ./verity -c /dev/sdc2 /dev/sdc3 sha256 --salt
1234000000000000000000000000000000000000000000000000000000000000
- This creates hash tree on /dev/sdc3 and prints the root block hash

Run: dmsetup -r create verity --table "0 `blockdev --getsize /dev/sdc2` 
verity 0 /dev/sdc2 /dev/sdc3 0 4096 sha256 
f4c97f1f2e6b6757d033b2062268e0b6b42cbe4ff5a5fcc0017305b145cae4de 
1234000000000000000000000000000000000000000000000000000000000000 "
(note: use the real hash reported by "verity" tool instead of f4c9...)

mount -o ro -t ext2 /dev/mapper/verity /mnt/test

Now, the device is mounted and dm-verity target is verifying the hashes.
All data integrity depends only on the root hash 
(f4c97f1f2e6b6757d033b2062268e0b6b42cbe4ff5a5fcc0017305b145cae4de) and 
salt (1234000000000000000000000000000000000000000000000000000000000000). 
If either the data or hash partitions become silently corrupted and 
you read invalid data, dm-verity will return -EIO.

Mikulas

---

/* link with -lpopt -lcrypto */

#define _FILE_OFFSET_BITS	64

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <popt.h>
#include <openssl/evp.h>

#define DEFAULT_BLOCK_SIZE	4096
#define DM_VERITY_MAX_LEVELS	63

#define MODE_VERIFY	0
#define MODE_CREATE	1

static int mode = -1;

static const char *data_device;
static const char *hash_device;
static const char *hash_algorithm;
static const char *root_hash;

static int data_block_size = 0;
static int hash_block_size = 0;
static long long hash_start = 0;
static long long data_blocks = 0;
static const char *salt_string = NULL;

static FILE *data_file;
static FILE *hash_file;

static off_t data_file_blocks;
static off_t hash_file_blocks;
static off_t used_hash_blocks;

static const EVP_MD *evp;

static unsigned char *root_hash_bytes;
static unsigned char *calculated_digest;

static unsigned char *salt_bytes;
static unsigned salt_size;

static unsigned digest_size;
static unsigned char levels;
static unsigned char hash_per_block_bits;

static off_t hash_level_block[DM_VERITY_MAX_LEVELS];
static off_t hash_level_size[DM_VERITY_MAX_LEVELS];

static int retval = 0;

static void help(poptContext popt_context,
		enum poptCallbackReason reason,
		struct poptOption *key,
		const char *arg,
		void *data)
{
	poptPrintHelp(popt_context, stdout, 0);
	exit(0);
}

static struct poptOption popt_help_options[] = {
	{ NULL,			0,	POPT_ARG_CALLBACK, help, 0, NULL, NULL },
	{ "help",		'?',	POPT_ARG_NONE, NULL, 0, "Show help", NULL },
	POPT_TABLEEND
};

static struct poptOption popt_options[] = {
	{ NULL,			'\0', POPT_ARG_INCLUDE_TABLE, popt_help_options, 0, NULL, NULL },
	{ "create",		'c',	POPT_ARG_VAL, &mode, MODE_CREATE, "Create hash", NULL },
	{ "verify",		'v',	POPT_ARG_VAL, &mode, MODE_VERIFY, "Verify integrity", NULL },
	{ "data-block-size",	0, 	POPT_ARG_INT, &data_block_size, 0, "Block size on the data device", "bytes" },
	{ "hash-block-size",	0, 	POPT_ARG_INT, &hash_block_size, 0, "Block size on the hash device", "bytes" },
	{ "hash-start",		0,	POPT_ARG_LONGLONG, &hash_start, 0, "Starting sector on the hash device", "sectors" },
	{ "data-blocks",	0,	POPT_ARG_LONGLONG, &data_blocks, 0, "The number of blocks in the data file", "blocks" },
	{ "salt",		0,	POPT_ARG_STRING, &salt_string, 0, "Salt", "hex string" },
	POPT_TABLEEND
};

static void exit_err(const char *msg, ...)
{
	va_list args;
	va_start(args, msg);
	vfprintf(stderr, msg, args);
	va_end(args);
	fputc('\n', stderr);
	exit(2);
}

static void stream_err(FILE *f, const char *msg)
{
	if (ferror(f)) {
		perror(msg);
		exit(2);
	} else if (feof(f)) {
		exit_err("eof on %s", msg);
	} else {
		exit_err("unknown error on %s", msg);
	}
}

static void *xmalloc(size_t s)
{
	void *ptr = malloc(s);
	if (!ptr) exit_err("out of memory");
	return ptr;
}

static off_t get_size(FILE *f, const char *name)
{
	struct stat st;
	int h = fileno(f);
	if (h < 0) {
		perror("fileno");
		exit(2);
	}
	if (fstat(h, &st)) {
		perror("fstat");
		exit(2);
	}
	if (S_ISREG(st.st_mode)) {
		return st.st_size;
	} else if (S_ISBLK(st.st_mode)) {
		unsigned long long size64;
		unsigned long sizeul;
		if (!ioctl(h, BLKGETSIZE64, &size64)) {
			return_size64:
			if ((off_t)size64 < 0 || (off_t)size64 != size64) {
				size_overflow:
				exit_err("%s: device size overflow", name);
			}
			return size64;
		}
		if (!ioctl(h, BLKGETSIZE, &sizeul)) {
			size64 = (unsigned long long)sizeul * 512;
			if (size64 / 512 != sizeul) goto size_overflow;
			goto return_size64;
		}
		perror("BLKGETSIZE");
		exit(2);
	} else {
		exit_err("%s is not a file or a block device", name);
	}
	return -1;	/* never reached, shut up warning */
}

static void block_fseek(FILE *f, off_t block, int block_size)
{
	unsigned long long pos = (unsigned long long)block * block_size;
	if (pos / block_size != block ||
	    (off_t)pos < 0 ||
	    (off_t)pos != pos)
		exit_err("seek position overflow");
	if (fseeko(f, pos, SEEK_SET)) {
		perror("fseek");
		exit(2);
	}
}

static off_t verity_position_at_level(off_t block, int level)
{
	return block >> (level * hash_per_block_bits);
}

static void calculate_positions(void)
{
	unsigned long long hash_position;
	int i;

	hash_per_block_bits = 0;
	while (((hash_block_size / digest_size) >> hash_per_block_bits) > 1)
		hash_per_block_bits++;
	if (!hash_per_block_bits)
		exit_err("at least two hashes must fit in a hash file block");
	levels = 0;

	if (data_file_blocks) {
		while (hash_per_block_bits * levels < 64 &&
		       (unsigned long long)(data_file_blocks - 1) >>
		       (hash_per_block_bits * levels))
			levels++;
	}

	if (levels > DM_VERITY_MAX_LEVELS) exit_err("too many tree levels");

	hash_position = hash_start * 512 / hash_block_size;
	for (i = levels - 1; i >= 0; i--) {
		off_t s;
		hash_level_block[i] = hash_position;
		s = verity_position_at_level(data_file_blocks, i);
		s = (s >> hash_per_block_bits) +
		    !!(s & ((1 << hash_per_block_bits) - 1));
		hash_level_size[i] = s;
		if (hash_position + s < hash_position ||
		    (off_t)(hash_position + s) < 0 ||
		    (off_t)(hash_position + s) != hash_position + s)
			exit_err("hash device offset overflow");
		hash_position += s;
	}
	used_hash_blocks = hash_position;
}

static void create_or_verify_stream(FILE *rd, FILE *wr, int block_size, off_t blocks)
{
	unsigned char *left_block = xmalloc(hash_block_size);
	unsigned char *data_buffer = xmalloc(block_size);
	unsigned char *read_digest = mode == MODE_VERIFY ? xmalloc(digest_size) : NULL;
	off_t blocks_to_write = (blocks >> hash_per_block_bits) +
				!!(blocks & ((1 << hash_per_block_bits) - 1));
	EVP_MD_CTX ctx;
	EVP_MD_CTX_init(&ctx);
	memset(left_block, 0, hash_block_size);
	while (blocks_to_write--) {
		unsigned x;
		unsigned left_bytes;
		for (x = 0; x < 1 << hash_per_block_bits; x++) {
			if (!blocks)
				break;
			blocks--;
			if (fread(data_buffer, block_size, 1, rd) != 1)
				stream_err(rd, "read");
			if (EVP_DigestInit_ex(&ctx, evp, NULL) != 1)
				exit_err("EVP_DigestInit_ex failed");
			if (EVP_DigestUpdate(&ctx, data_buffer, block_size) != 1)
				exit_err("EVP_DigestUpdate failed");
			if (EVP_DigestUpdate(&ctx, salt_bytes, salt_size) != 1)
				exit_err("EVP_DigestUpdate failed");
			if (EVP_DigestFinal_ex(&ctx, calculated_digest, NULL) != 1)
				exit_err("EVP_DigestFinal_ex failed");
			if (!wr)
				break;
			if (mode == MODE_VERIFY) {
				if (fread(read_digest, digest_size, 1, wr) != 1)
					stream_err(wr, "read");
				if (memcmp(read_digest, calculated_digest, digest_size)) {
					retval = 1;
					fprintf(stderr, "verification failed at position %lld in %s file\n", (long long)ftello(rd) - block_size, rd == data_file ? "data" : "metadata");
				}
			} else {
				if (fwrite(calculated_digest, digest_size, 1, wr) != 1)
					stream_err(wr, "write");
			}
		}
		left_bytes = hash_block_size - x * digest_size;
		if (left_bytes && wr) {
			if (mode == MODE_VERIFY) {
				if (fread(left_block, left_bytes, 1, wr) != 1)
					stream_err(wr, "read");
				for (x = 0; x < left_bytes; x++) if (left_block[x]) {
					retval = 1;
					fprintf(stderr, "spare area is not zeroed at position %lld\n", (long long)ftello(wr) - left_bytes);
				}
			} else {
				if (fwrite(left_block, left_bytes, 1, wr) != 1)
					stream_err(wr, "write");
			}
		}
	}
	if (mode != MODE_VERIFY && wr) {
		if (fflush(wr)) {
			perror("fflush");
			exit(1);
		}
		if (ferror(wr)) {
			stream_err(wr, "write");
		}
	}
	if (EVP_MD_CTX_cleanup(&ctx) != 1)
		exit_err("EVP_MD_CTX_cleanup failed");
	free(left_block);
	free(data_buffer);
	if (mode == MODE_VERIFY) free(read_digest);
}

static void create_or_verify(void)
{
	int i;
	for (i = 0; i < levels; i++) {
		block_fseek(hash_file, hash_level_block[i], hash_block_size);
		if (!i) {
			block_fseek(data_file, 0, data_block_size);
			create_or_verify_stream(data_file, hash_file, data_block_size, data_file_blocks);
		} else {
			FILE *hash_file_2 = fopen(hash_device, "r");
			if (!hash_file_2) {
				perror(hash_device);
				exit(2);
			}
			block_fseek(hash_file_2, hash_level_block[i - 1], hash_block_size);
			create_or_verify_stream(hash_file_2, hash_file, hash_block_size, hash_level_size[i - 1]);
			fclose(hash_file_2);
		}
	}

	if (levels) {
		block_fseek(hash_file, hash_level_block[levels - 1], hash_block_size);
		create_or_verify_stream(hash_file, NULL, hash_block_size, 1);
	} else {
		block_fseek(data_file, 0, data_block_size);
		create_or_verify_stream(data_file, NULL, data_block_size, data_file_blocks);
	}

	if (mode == MODE_VERIFY) {
		if (memcmp(calculated_digest, root_hash_bytes, digest_size)) {
			fprintf(stderr, "verification failed in the root block\n");
			retval = 1;
		}
		if (!retval)
			fprintf(stderr, "hash successfully verified\n");
	} else {
		if (fsync(fileno(hash_file))) {
			perror("fsync");
			exit(1);
		}
		printf("hash device size: %llu\n", (unsigned long long)used_hash_blocks * hash_block_size);
		printf("data block size %u, hash block size %u, %u tree levels\n", data_block_size, hash_block_size, levels);
		printf("root hash: ");
		for (i = 0; i < digest_size; i++) printf("%02x", calculated_digest[i]);
		printf("\n");
		printf("device mapper target line: 0 %llu verity 0 %s %s %llu %u %s ",
			(unsigned long long)data_file_blocks * data_block_size / 512,
			data_device,
			hash_device,
			hash_start,
			data_block_size,
			hash_algorithm
			);
		for (i = 0; i < digest_size; i++) printf("%02x", calculated_digest[i]);
		printf(" ");
		if (!salt_size) printf("-");
		else for (i = 0; i < salt_size; i++) printf("%02x", salt_bytes[i]);
		if (hash_block_size != data_block_size) printf(" %u", hash_block_size);
		printf("\n");
		if (data_block_size == 4096 && hash_block_size == 4096 && salt_size == 32)
			printf("compatible with the original Google code\n");
		else {
			printf("incompatible with the original Google code:\n");
			if (!(data_block_size == 4096 && hash_block_size == 4096)) printf("\tdata and hash block size must be 4096\n");
			if (!(salt_size == 32)) printf("\tsalt must have exactly 32 bytes (64 hex digits)\n");
		}
	}
}

static void get_hex(const char *string, unsigned char **result, size_t len, const char *description)
{
	size_t rl = strlen(string);
	unsigned u;
	if (strspn(string, "0123456789ABCDEFabcdef") != rl)
		exit_err("invalid %s", description);
	if (rl != len * 2)
		exit_err("invalid length of %s", description);
	*result = xmalloc(len);
	memset(*result, 0, len);
	for (u = 0; u < rl; u++) {
		unsigned char c = (string[u] & 15) + (string[u] > '9' ? 9 : 0);
		(*result)[u / 2] |= c << (((u & 1) ^ 1) << 2);
	}
}

int main(int argc, const char **argv)
{
	poptContext popt_context;
	int r;
	const char *s;

	popt_context = poptGetContext("verity", argc, argv, popt_options, 0);

	poptSetOtherOptionHelp(popt_context, "[-c | -v] <data device> <hash device> <algorithm> [<root hash> if verifying] [OPTION...]");

	if (argc <= 1) {
		poptPrintHelp(popt_context, stdout, 0);
		exit(1);
	}

	r = poptGetNextOpt(popt_context);
	if (r < -1) exit_err("bad option %s", poptBadOption(popt_context, 0));

	if (mode < 0) exit_err("verify or create mode not specified");

	if (!data_block_size) data_block_size = DEFAULT_BLOCK_SIZE;
	if (!hash_block_size) hash_block_size = data_block_size;

	if (data_block_size <= 0 || (data_block_size & (data_block_size - 1)))
		exit_err("invalid data block size");

	if (hash_block_size <= 0 || (hash_block_size & (hash_block_size - 1)))
		exit_err("invalid hash block size");

	if (hash_start < 0 ||
	    (unsigned long long)hash_start * 512 / 512 != hash_start) exit_err("invalid hash start");
	if (data_blocks < 0 || (off_t)data_blocks < 0 || (off_t)data_blocks != data_blocks) exit_err("invalid number of data blocks");

	data_device = poptGetArg(popt_context);
	if (!data_device) exit_err("data device is missing");

	hash_device = poptGetArg(popt_context);
	if (!hash_device) exit_err("metadata device is missing");

	hash_algorithm = poptGetArg(popt_context);
	if (!hash_algorithm) exit_err("hash algorithm not specified");

	if (mode == MODE_VERIFY) {
		root_hash = poptGetArg(popt_context);
		if (!root_hash) exit_err("root hash not specified");
	}

	s = poptGetArg(popt_context);
	if (s) exit_err("extra argument %s", s);

	data_file = fopen(data_device, "r");
	if (!data_file) {
		perror(data_device);
		exit(2);
	}

	hash_file = fopen(hash_device, mode == MODE_VERIFY ? "r" : "r+");
	if (!hash_file && errno == ENOENT && mode != MODE_VERIFY)
		hash_file = fopen(hash_device, "w+");
	if (!hash_file) {
		perror(hash_device);
		exit(2);
	}

	data_file_blocks = get_size(data_file, data_device) / data_block_size;
	hash_file_blocks = get_size(hash_file, hash_device) / hash_block_size;

	if ((unsigned long long)hash_start * 512 % hash_block_size) exit_err("hash start not aligned on block size");
	if (data_file_blocks < data_blocks) exit_err("data file is too small");
	if (data_blocks) data_file_blocks = data_blocks;

	OpenSSL_add_all_digests();
	evp = EVP_get_digestbyname(hash_algorithm);
	if (!evp) exit_err("hash algorithm %s not found", hash_algorithm);
	digest_size = EVP_MD_size(evp);

	salt_size = 0;
	if (salt_string && *salt_string) {
		salt_size = strlen(salt_string) / 2;
		get_hex(salt_string, &salt_bytes, salt_size, "salt");
	}

	calculated_digest = xmalloc(digest_size);

	if (mode == MODE_VERIFY) {
		get_hex(root_hash, &root_hash_bytes, digest_size, "root_hash");
	}

	calculate_positions();

	create_or_verify();

	fclose(data_file);
	fclose(hash_file);

	if (salt_size)
		free(salt_bytes);
	free(calculated_digest);
	if (mode == MODE_VERIFY)
		free(root_hash_bytes);
	poptFreeContext(popt_context);

	return retval;
}




More information about the dm-devel mailing list