Skip to content

Instantly share code, notes, and snippets.

@alexlarsson
Created May 23, 2022 09:02
Show Gist options
  • Save alexlarsson/765c7f2344c03d80514f0c20ee08560d to your computer and use it in GitHub Desktop.
Save alexlarsson/765c7f2344c03d80514f0c20ee08560d to your computer and use it in GitHub Desktop.
#include <glib.h>
#include <gio/gio.h>
#include <gio/gunixinputstream.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
struct fsverity_descriptor {
guint8 version; /* must be 1 */
guint8 hash_algorithm; /* Merkle tree hash algorithm */
guint8 log_blocksize; /* log2 of size of data and tree blocks */
guint8 salt_size; /* size of salt in bytes; 0 if none */
guint32 reserved1; /* must be 0 */
guint64 data_size_be; /* size of file the Merkle tree is built over */
guint8 root_hash[64]; /* Merkle tree root hash */
guint8 salt[32]; /* salt prepended to each hashed block */
guint8 reserved2[144]; /* must be 0's */
};
typedef struct {
GInputStream *in;
goffset current_pos;
goffset file_size;
goffset block_size;
GChecksum *checksum;
gsize digest_len;
size_t n_blocks_at_level[8];
} FsVerityData;
static gboolean
compute_block_digest (FsVerityData *data,
goffset block_nr,
guint8 *digest_out,
GError **error)
{
guint8 buf[data->block_size];
gsize bytes_read;
gsize digest_len;
/* We don't really use current_pos other that to internally verify
that we're reading the right place from the stream */
g_assert (data->current_pos == block_nr * data->block_size);
if (!g_input_stream_read_all (data->in, buf, sizeof(buf),
&bytes_read, NULL, error))
return FALSE;
data->current_pos += bytes_read;
/* Clear rest */
if (bytes_read < sizeof(buf))
memset (buf + bytes_read, 0, sizeof(buf) - bytes_read);
g_checksum_reset (data->checksum);
g_checksum_update (data->checksum, buf, sizeof(buf));
digest_len = data->digest_len;
g_checksum_get_digest (data->checksum,
digest_out,
&digest_len);
g_assert (digest_len == data->digest_len);
return TRUE;
}
static gboolean
compute_tree_digest (FsVerityData *data,
int level,
goffset block_nr,
guint8 *digest_out,
GError **error)
{
guint8 buf[data->block_size];
gsize digests_per_block = data->block_size / data->digest_len;
gsize i, sub_block_start, sub_block_end, n_sub_blocks;
gsize digest_len;
if (level == 0)
return compute_block_digest (data, block_nr, digest_out, error);
sub_block_start = block_nr * digests_per_block;
sub_block_end = MIN (sub_block_start + digests_per_block, data->n_blocks_at_level[level-1]);
n_sub_blocks = sub_block_end - sub_block_start;
for (i = 0; i < n_sub_blocks; i++)
{
if (!compute_tree_digest (data, level - 1, sub_block_start + i, &buf[i * data->digest_len], error))
return FALSE;
}
/* Zero remainder of buf */
memset (&buf[n_sub_blocks * data->digest_len], 0, (digests_per_block - n_sub_blocks) * data->digest_len);
g_checksum_reset (data->checksum);
g_checksum_update (data->checksum, buf, sizeof(buf));
digest_len = data->digest_len;
g_checksum_get_digest (data->checksum,
digest_out,
&digest_len);
g_assert (digest_len == data->digest_len);
return TRUE;
}
static GChecksum *
fs_verity_compute (GInputStream *in,
goffset file_size,
goffset block_size,
GChecksumType checksum_type,
GError **error)
{
FsVerityData data;
gsize digests_per_block;
gsize max_level;
gsize digest_len;
struct fsverity_descriptor descriptor;
guint32 hash_alg;
switch (checksum_type)
{
case G_CHECKSUM_SHA256:
hash_alg = 1;
break;
case G_CHECKSUM_SHA512:
hash_alg = 2;
break;
default:
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Unsupported checksum type");
return NULL;
}
digest_len = g_checksum_type_get_length (checksum_type);
/* Gotta be an even number of digests per block */
if (block_size % digest_len != 0)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Unsupported block size");
return NULL;
}
data.in = in;
data.current_pos = 0;
data.file_size = file_size;
data.block_size = block_size;
data.checksum = g_checksum_new (checksum_type);
data.digest_len = digest_len;
data.n_blocks_at_level[0] = (data.file_size + data.block_size - 1) / data.block_size;
digests_per_block = data.block_size / data.digest_len;
for (max_level = 0; data.n_blocks_at_level[max_level] > 1; max_level++)
{
g_assert (max_level + 1 < G_N_ELEMENTS(data.n_blocks_at_level));
data.n_blocks_at_level[max_level+1] = (data.n_blocks_at_level[max_level] + digests_per_block - 1) / digests_per_block;
}
memset(&descriptor, 0, sizeof(descriptor));
descriptor.version = 1;
descriptor.hash_algorithm = hash_alg;
descriptor.log_blocksize = g_bit_storage (data.block_size-1); /* log2(block_size) */
descriptor.salt_size = 0;
descriptor.data_size_be = GUINT64_TO_LE(data.file_size);
if (!compute_tree_digest (&data, max_level, 0, descriptor.root_hash, error))
return FALSE;
g_checksum_reset (data.checksum);
g_checksum_update (data.checksum, (guint8 *)&descriptor, sizeof(descriptor));
return data.checksum;
}
int
main(int argc, const char *argv[])
{
const char *path;
int fd;
GChecksum *checksum;
GInputStream *input;
struct stat statbuf;
if (argc != 2)
{
g_print ("No file specified\n");
return 1;
}
path = argv[1];
fd = open(path, O_RDONLY);
if (fd < 0)
{
perror("Can't open file");
return 1;
}
if (fstat(fd, &statbuf) != 0)
{
perror("Can't stat file");
return 1;
}
input = g_unix_input_stream_new (fd, TRUE);
checksum = fs_verity_compute(input, statbuf.st_size, 4096, G_CHECKSUM_SHA256, NULL);
g_print ("fs-verify digest: %s\n", g_checksum_get_string (checksum));
g_checksum_free (checksum);
close(fd);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment