_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file src/verify.c
#define _POSIX_C_SOURCE 200809L
#include "verify.h"
#include "config.h"
#include "package.h"
#include "stage.h"
#include <archive.h>
#include <archive_entry.h>
#include <errno.h>
#include <openssl/evp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
struct payload {
char *path;
char *link;
char *hardlink;
char *group;
int directory;
int config, mutable;
unsigned char hash[32];
long long size;
unsigned int mode;
long long uid, gid;
int matched;
};
int holy_safe_link(const char *path, const char *target)
{
const char *p, *end;
size_t depth = 0;
if (!target || !*target) return 0;
if (target[0] != '/') {
for (p = path; *p; ++p) if (*p == '/') ++depth;
}
for (p = target; *p;) {
size_t n;
while (*p == '/') ++p;
if (!*p) break;
end = strchr(p, '/');
n = end ? (size_t)(end - p) : strlen(p);
if (n == 1 && p[0] == '.') { p += n; continue; }
if (n == 2 && p[0] == '.' && p[1] == '.') {
if (!depth) return 0;
--depth;
} else ++depth;
p += n;
}
return 1;
}
char *holy_relative_link_path(const char *path, size_t alias_length,
const char *target, const char *suffix)
{
char *joined, *part, *save, *out;
char **parts;
size_t prefix = 0, depth = 0, length = 0, capacity, i;
if (!path || !target || !suffix || !*target || target[0] == '/' ||
alias_length > strlen(path) || strlen(path) + strlen(target) + strlen(suffix) > 65536) return NULL;
for (i = 0; i < alias_length; ++i) if (path[i] == '/') prefix = i + 1;
capacity = prefix + strlen(target) + strlen(suffix) + 2;
joined = malloc(capacity);
if (!joined) return NULL;
snprintf(joined, capacity, "%.*s%s%s", (int)prefix, path, target, suffix);
parts = calloc(capacity, sizeof *parts);
if (!parts) { free(joined); return NULL; }
for (part = strtok_r(joined, "/", &save); part; part = strtok_r(NULL, "/", &save)) {
if (!strcmp(part, ".")) continue;
if (!strcmp(part, "..")) {
if (!depth) { free(parts); free(joined); return NULL; }
--depth;
} else parts[depth++] = part;
}
if (!depth) { free(parts); free(joined); return NULL; }
out = malloc(capacity);
if (out) {
for (i = 0; i < depth; ++i) {
size_t n = strlen(parts[i]);
if (i) out[length++] = '/';
memcpy(out + length, parts[i], n);
length += n;
}
out[length] = 0;
}
free(parts); free(joined);
return out;
}
static int compare(const void *a, const void *b)
{
const struct payload *x = a, *y = b;
return strcmp(x->path, y->path);
}
static int number(const char *s, int base, unsigned long long *out)
{
char *end;
errno = 0;
*out = strtoull(s, &end, base);
return s[0] && s[0] != '-' && !errno && !*end;
}
static int valid_group(const char *s)
{
const unsigned char *p = (const unsigned char *)s;
if (!*p || (p[0] == '-' && !p[1])) return 0;
for (; *p; ++p)
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
(*p >= '0' && *p <= '9') || *p == '_' || *p == '-' || *p == '.'))
return 0;
return 1;
}
static int payload_group_order(const void *a, const void *b)
{
return strcmp((*(const struct payload *const *)a)->group,
(*(const struct payload *const *)b)->group);
}
static int unique_group_anchors(const char *path, const struct payload *files, size_t count)
{
const struct payload **anchors;
size_t i, used = 0;
int ok = 1;
if (count > (size_t)-1 / sizeof *anchors) return 0;
anchors = calloc(count ? count : 1, sizeof *anchors);
if (!anchors) return 0;
for (i = 0; i < count; ++i)
if (files[i].group && !files[i].hardlink) anchors[used++] = &files[i];
qsort(anchors, used, sizeof *anchors, payload_group_order);
for (i = 1; i < used; ++i) if (!strcmp(anchors[i-1]->group, anchors[i]->group)) {
fprintf(stderr, "%s: hardlink group has multiple regular anchors\n", path);
ok = 0; break;
}
free(anchors);
return ok;
}
static int resolve_hardlinks(const char *path, struct payload *files, size_t count)
{
size_t i;
for (i = 0; i < count; ++i) {
struct payload key, *target;
if (!files[i].hardlink) continue;
key.path = files[i].hardlink;
target = bsearch(&key, files, count, sizeof *files, compare);
if (!target || target->directory || target->link || target->hardlink ||
files[i].size != 0 || files[i].mode != target->mode ||
files[i].uid != target->uid || files[i].gid != target->gid) {
fprintf(stderr, "%s: invalid hardlink target or attributes\n", path);
return 0;
}
files[i].size = target->size;
memcpy(files[i].hash, target->hash, sizeof files[i].hash);
}
return 1;
}
static int validate_manifest(const char *path, char *text, size_t size,
struct payload *files, size_t count)
{
size_t start = 0, i, line = 0, seen = 0;
char *error = NULL;
for (i = 0; i <= size; ++i) {
char **v = NULL;
size_t n = 0;
struct payload key, *found;
unsigned long long mode, uid, gid, length;
size_t j;
int symlink, directory, hardlink;
if (i < size && text[i] != '\n') continue;
++line;
if (memchr(text + start, '\0', i - start) ||
!holy_lex(text + start, i - start, &v, &n, path, line, &error)) {
fprintf(stderr, "%s: HOLY/files:%zu: %s\n", path, line,
error ? error : "NUL or invalid record");
free(error);
return 0;
}
start = i + 1;
if (!n) { holy_tokens_free(v, n); continue; }
symlink = n && !strcmp(v[0], "symlink");
directory = n && !strcmp(v[0], "dir");
hardlink = n && !strcmp(v[0], "hardlink");
if (((symlink || hardlink) ? n != 13 : n != 12 ||
(strcmp(v[0], "file") && !directory)) ||
!number(v[2], 8, &mode) || !number(v[5], 10, &uid) ||
!number(v[6], 10, &gid) || !number(v[7], 10, &length) ||
((symlink || directory) ? strcmp(v[8], "-") || length != 0 :
strlen(v[8]) != 64) ||
(strcmp(v[9], "none") && strcmp(v[9], "config") &&
strcmp(v[9], "mutable") && strcmp(v[9], "config,mutable")) ||
(strcmp(v[9], "none") && (symlink || directory || hardlink)) ||
strcmp(v[10], "-") ||
((symlink || directory) ? strcmp(v[11], "-") :
strcmp(v[11], "-") && !valid_group(v[11])) ||
(hardlink && (!valid_group(v[11]) || !v[12][0])) ||
(symlink && (!v[12][0] || !holy_safe_link(v[1], v[12]))) ||
mode > 07777 || uid > 0x7fffffff || gid > 0x7fffffff ||
length > 0x7fffffffffffffffULL) {
fprintf(stderr, "%s: HOLY/files:%zu: unsupported or invalid record\n", path, line);
holy_tokens_free(v, n);
return 0;
}
key.path = v[1];
found = count ? bsearch(&key, files, count, sizeof *files, compare) : NULL;
if (!found || found->matched || !!found->link != !!symlink ||
!!found->hardlink != !!hardlink ||
found->directory != directory ||
(symlink && strcmp(found->link, v[12])) ||
(hardlink && strcmp(found->hardlink, v[12])) ||
(unsigned long long)found->size != length ||
found->mode != mode || (unsigned long long)found->uid != uid ||
(unsigned long long)found->gid != gid) {
fprintf(stderr, "%s: HOLY/files:%zu: payload or attributes mismatch\n", path, line);
holy_tokens_free(v, n);
return 0;
}
if (!symlink && !directory) {
for (j = 0; j < 32; ++j) {
char byte[3] = {v[8][j * 2], v[8][j * 2 + 1], 0};
unsigned long long hex;
if (!number(byte, 16, &hex) || hex != found->hash[j]) break;
}
if (j != 32) {
fprintf(stderr, "%s: HOLY/files:%zu: payload SHA-256 mismatch\n", path, line);
holy_tokens_free(v, n);
return 0;
}
}
if (!symlink && !directory && strcmp(v[11], "-")) {
found->group = strdup(v[11]);
if (!found->group) { holy_tokens_free(v, n); return 0; }
}
found->config = !strcmp(v[9], "config") || !strcmp(v[9], "config,mutable");
found->mutable = !strcmp(v[9], "mutable") || !strcmp(v[9], "config,mutable");
found->matched = 1;
++seen;
holy_tokens_free(v, n);
}
if (seen != count) {
fprintf(stderr, "%s: unlisted payload object\n", path);
return 0;
}
for (i = 0; i < count; ++i) if (files[i].hardlink) {
struct payload key, *target;
key.path = files[i].hardlink;
target = bsearch(&key, files, count, sizeof *files, compare);
if (!target->matched || !files[i].group || !target->group ||
strcmp(files[i].group, target->group)) {
fprintf(stderr, "%s: hardlink group mismatch\n", path);
return 0;
}
}
return unique_group_anchors(path, files, count);
}
static int verify_archive(const char *path, int emit,
holy_manifest_visit visitor, void *context)
{
struct archive *a = NULL;
struct archive_entry *entry;
struct payload *files = NULL;
size_t count = 0, i, manifest_size = 0, symlinks = 0, directories = 0, hardlinks = 0;
char *manifest = NULL;
char buffer[8192];
int status, seen = 0, ok = 0;
if (!holy_package_inspect(path, emit)) return 0;
a = archive_read_new();
if (!a || archive_read_support_filter_lz4(a) != ARCHIVE_OK ||
archive_read_support_format_tar(a) != ARCHIVE_OK ||
archive_read_open_filename(a, path, 8192) != ARCHIVE_OK) {
fprintf(stderr, "%s: archive open failed\n", path);
goto done;
}
while ((status = archive_read_next_header(a, &entry)) == ARCHIVE_OK) {
const char *name = archive_entry_pathname(entry);
int is_manifest = name && !strcmp(name, "HOLY/files");
int is_data = name && !strncmp(name, "DATA/", 5) && name[5];
EVP_MD_CTX *hash = NULL;
la_ssize_t got;
unsigned int digest_size;
unsigned long long actual = 0;
if (!holy_safe_archive_path(name)) {
fprintf(stderr, "%s: unsafe archive path\n", path);
goto done;
}
if (is_data && archive_entry_xattr_count(entry) > 0) {
fprintf(stderr, "%s: unsupported payload xattrs\n", path);
goto done;
}
if (is_data && archive_entry_acl_types(entry)) {
fprintf(stderr, "%s: unsupported payload ACL\n", path);
goto done;
}
if ((!strcmp(name, "HOLY") || !strcmp(name, "HOLY/") ||
!strcmp(name, "DATA") || !strcmp(name, "DATA/")) &&
archive_entry_filetype(entry) != AE_IFDIR) {
fprintf(stderr, "%s: archive root marker is not a directory\n", path);
goto done;
}
if (is_manifest) {
if (seen++ || archive_entry_filetype(entry) != AE_IFREG ||
archive_entry_size(entry) < 0 || archive_entry_size(entry) > 16 * 1024 * 1024) {
fprintf(stderr, "%s: invalid HOLY/files\n", path);
goto done;
}
}
if (is_data) {
struct payload *next;
size_t pathlen = strlen(name + 5);
const char *target_path = archive_entry_hardlink(entry);
if ((archive_entry_filetype(entry) != AE_IFREG &&
archive_entry_filetype(entry) != AE_IFLNK &&
archive_entry_filetype(entry) != AE_IFDIR &&
!(target_path && archive_entry_filetype(entry) == 0)) ||
archive_entry_size(entry) < 0 || count == (size_t)-1 / sizeof *files) {
fprintf(stderr, "%s: unsupported payload type\n", path);
goto done;
}
next = realloc(files, (count + 1) * sizeof *files);
if (!next) goto done;
files = next;
files[count].path = strdup(name + 5);
if (!files[count].path) goto done;
if (files[count].path[pathlen - 1] == '/' &&
archive_entry_filetype(entry) != AE_IFDIR) {
free(files[count].path);
fprintf(stderr, "%s: non-directory path ends in slash\n", path);
goto done;
}
if (files[count].path[pathlen - 1] == '/')
files[count].path[pathlen - 1] = '\0';
files[count].link = NULL;
files[count].hardlink = NULL;
files[count].group = NULL;
files[count].directory = archive_entry_filetype(entry) == AE_IFDIR;
files[count].size = archive_entry_size(entry);
files[count].mode = archive_entry_perm(entry);
files[count].uid = archive_entry_uid(entry);
files[count].gid = archive_entry_gid(entry);
files[count].matched = 0;
if (target_path) {
if ((archive_entry_filetype(entry) != AE_IFREG &&
archive_entry_filetype(entry) != 0) ||
!holy_safe_archive_path(target_path) ||
strncmp(target_path, "DATA/", 5) || !target_path[5]) {
free(files[count].path);
fprintf(stderr, "%s: unsafe hardlink target\n", path);
goto done;
}
files[count].hardlink = strdup(target_path + 5);
if (!files[count].hardlink) { free(files[count].path); goto done; }
++hardlinks;
} else if (files[count].directory) {
if (files[count].size != 0) {
free(files[count].path);
fprintf(stderr, "%s: directory contains archive data\n", path);
goto done;
}
++directories;
} else if (archive_entry_filetype(entry) == AE_IFLNK) {
const char *target = archive_entry_symlink(entry);
if (!holy_safe_link(files[count].path, target)) {
free(files[count].path);
fprintf(stderr, "%s: unsafe symlink target\n", path);
goto done;
}
files[count].link = strdup(target);
if (!files[count].link) { free(files[count].path); goto done; }
++symlinks;
} else {
hash = EVP_MD_CTX_new();
if (!hash || EVP_DigestInit_ex(hash, EVP_sha256(), NULL) != 1) {
EVP_MD_CTX_free(hash);
free(files[count].path);
goto done;
}
}
}
while ((got = archive_read_data(a, buffer, sizeof buffer)) > 0) {
if (is_data) actual += (unsigned long long)got;
if (hash && EVP_DigestUpdate(hash, buffer, (size_t)got) != 1) break;
if (is_manifest) {
char *next;
if ((size_t)got > 16 * 1024 * 1024 - manifest_size) break;
next = realloc(manifest, manifest_size + (size_t)got + 1);
if (!next) break;
manifest = next;
memcpy(manifest + manifest_size, buffer, (size_t)got);
manifest_size += (size_t)got;
manifest[manifest_size] = '\0';
}
}
if (got < 0 || got > 0 ||
(is_data && actual != (unsigned long long)files[count].size) ||
(hash && (EVP_DigestFinal_ex(hash, files[count].hash, &digest_size) != 1 ||
digest_size != 32))) {
EVP_MD_CTX_free(hash);
if (is_data) { free(files[count].path); free(files[count].link); free(files[count].hardlink); }
fprintf(stderr, "%s: invalid archive data\n", path);
goto done;
}
if (is_data) ++count;
EVP_MD_CTX_free(hash);
}
if (status != ARCHIVE_EOF || !seen) {
fprintf(stderr, "%s: missing HOLY/files or truncated archive\n", path);
goto done;
}
if (count) qsort(files, count, sizeof *files, compare);
for (i = 1; i < count; ++i)
if (!strcmp(files[i - 1].path, files[i].path)) {
fprintf(stderr, "%s: duplicate payload path\n", path);
goto done;
}
if (!resolve_hardlinks(path, files, count)) goto done;
ok = validate_manifest(path, manifest ? manifest : "", manifest_size, files, count);
if (ok && visitor) for (i = 0; i < count; ++i) {
const struct holy_manifest_entry entry = {
files[i].path, files[i].link, files[i].hardlink, files[i].group,
files[i].hash, files[i].size, files[i].mode, files[i].uid,
files[i].gid, files[i].directory, files[i].config, files[i].mutable
};
if (!visitor(context, &entry)) { ok = 0; break; }
}
if (ok && emit) printf("verified %zu regular files, %zu symlinks, %zu directories, %zu hardlinks\n",
count - symlinks - directories - hardlinks, symlinks, directories, hardlinks);
done:
for (i = 0; i < count; ++i) {
free(files[i].path);
free(files[i].link);
free(files[i].hardlink);
free(files[i].group);
}
free(files);
free(manifest);
if (a) archive_read_free(a);
return ok;
}
int holy_verify_with_output(const char *path, int emit)
{
return verify_archive(path, emit, NULL, NULL);
}
int holy_verify_visit(const char *path, holy_manifest_visit visitor, void *context)
{
return visitor && verify_archive(path, 0, visitor, context);
}
static void print_escaped(const char *text)
{
const unsigned char *p = (const unsigned char *)text;
for (; *p; ++p)
if (*p == '\\' || *p <= 32 || *p >= 127)
printf("\\x%02x", (unsigned int)*p);
else putchar(*p);
}
static int print_manifest(void *context, const struct holy_manifest_entry *entry)
{
size_t i;
(void)context;
printf("%s ", entry->directory ? "dir" : entry->link ? "symlink" :
entry->hardlink ? "hardlink" : "file");
print_escaped(entry->path);
printf(" mode=%04o uid=%lld gid=%lld size=%lld",
entry->mode, entry->uid, entry->gid, entry->size);
if (entry->config) fputs(" config", stdout);
if (entry->mutable) fputs(" mutable", stdout);
if (entry->link || entry->hardlink) {
fputs(" target=", stdout);
print_escaped(entry->link ? entry->link : entry->hardlink);
} else if (!entry->directory) {
fputs(" sha256=", stdout);
for (i = 0; i < 32; ++i) printf("%02x", entry->hash[i]);
}
if (entry->group) {
fputs(" hardlink-group=", stdout);
print_escaped(entry->group);
}
putchar('\n');
return !ferror(stdout);
}
int holy_manifest_local(const char *path)
{
char *snapshot = holy_stage_local(path, "holy-manifest");
int ok;
if (!snapshot) {
fprintf(stderr, "holypkg: could not stage regular local input\n");
return 0;
}
ok = holy_verify_visit(snapshot, print_manifest, NULL);
unlink(snapshot);
free(snapshot);
return ok;
}
int holy_verify(const char *path)
{
return holy_verify_with_output(path, 1);
}