#define _POSIX_C_SOURCE 200809L #include "apt.h" #include "apt-release.h" #include "deb-version.h" #include "../src/config.h" #include "../src/stage.h" #include "../src/fetch.h" #include "../src/import.h" #include "../src/package.h" #include "../src/source.h" #include "../src/state.h" #include "../src/verify.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define APT_INDEX_LIMIT (64u * 1024u * 1024u) struct apt_entry { char *name, *version, *arch, *filename, *sha256, *depends, *pre_depends, *provides; unsigned long long size; unsigned fields; }; struct apt_index { struct apt_entry *entries; size_t count, capacity; char *source, *base; char hash[65]; char source_id[65]; int release_verified; }; static void quote(FILE *out, const char *value) { const unsigned char *p = (const unsigned char *)value; fputc('"', out); for (; *p; ++p) { if (*p == '"' || *p == '\\') fprintf(out, "\\%c", *p); else if (*p < 32 || *p >= 127) fprintf(out, "\\x%02x", *p); else fputc(*p, out); } fputc('"', out); } static int digest(const char *value) { return value && strlen(value) == 64 && strspn(value, "0123456789abcdef") == 64; } static int token(const char *value) { const unsigned char *p = (const unsigned char *)value; if (!*p) return 0; for (; *p; ++p) if (*p <= 32 || *p >= 127 || *p == '/' || *p == ':' || *p == '@') return 0; return 1; } static int safe_filename(const char *path) { const unsigned char *p = (const unsigned char *)path; size_t length = strlen(path); if (!length || length > 1024 || *p == '/') return 0; while (*p) { const unsigned char *end = (const unsigned char *)strchr((const char *)p, '/'); size_t count = end ? (size_t)(end - p) : strlen((const char *)p), i; if (!count || (count == 1 && p[0] == '.') || (count == 2 && p[0] == '.' && p[1] == '.')) return 0; for (i = 0; i < count; ++i) if (!((p[i] >= 'a' && p[i] <= 'z') || (p[i] >= 'A' && p[i] <= 'Z') || (p[i] >= '0' && p[i] <= '9') || p[i] == '_' || p[i] == '-' || p[i] == '.' || p[i] == '+' || p[i] == '~' || p[i] == ':')) return 0; if (!end) return 1; p = end + 1; } return 0; } static void free_index(struct apt_index *index) { size_t i; for (i = 0; i < index->count; ++i) { struct apt_entry *e = &index->entries[i]; free(e->name); free(e->version); free(e->arch); free(e->filename); free(e->sha256); free(e->depends); free(e->pre_depends); free(e->provides); } free(index->entries); free(index->source); free(index->base); } static int entry_order(const void *left, const void *right) { const struct apt_entry *a = left, *b = right; int order = strcmp(a->name, b->name); if (!order) order = strcmp(a->version, b->version); if (!order) order = strcmp(a->arch, b->arch); return order; } static int valid_entry(const struct apt_entry *e) { const unsigned char *p; int order; if ((e->fields & 63) != 63 || !token(e->name) || !token(e->arch) || !e->version[0] || !holy_deb_version_compare(e->version, e->version, &order) || !safe_filename(e->filename) || !digest(e->sha256) || !e->size || e->size > 1024ULL * 1024 * 1024) return 0; for (p = (const unsigned char *)e->name; *p; ++p) if (!((*p >= 'a' && *p <= 'z') || (*p >= '0' && *p <= '9') || *p == '+' || *p == '-' || *p == '.')) return 0; return 1; } static int new_entry(struct apt_index *index) { struct apt_entry *next; size_t capacity; if (index->count == 100000) return 0; if (index->count == index->capacity) { capacity = index->capacity ? index->capacity * 2 : 256; if (capacity > 100000) capacity = 100000; next = realloc(index->entries, capacity * sizeof *next); if (!next) return 0; index->entries = next; index->capacity = capacity; } memset(&index->entries[index->count++], 0, sizeof *index->entries); return 1; } static int append_fold(char **slot, const char *line) { size_t old = *slot ? strlen(*slot) : 0, add = strlen(line); char *next; while (*line == ' ' || *line == '\t') { ++line; --add; } if (old > 65536 || add > 65536 - old - 2) return 0; next = realloc(*slot, old + add + 2); if (!next) return 0; *slot = next; if (old) next[old++] = ' '; memcpy(next + old, line, add + 1); return 1; } static int parse_index(char *data, size_t size, struct apt_index *index) { size_t pos = 0, line_number = 0; int field = 0, in_stanza = 0; while (pos < size) { char *line = data + pos, *end = memchr(line, '\n', size - pos); size_t length = end ? (size_t)(end - line) : size - pos; struct apt_entry *e; char *colon, *value, **slot = NULL; unsigned bit = 0; if (++line_number > 2000000 || length > 65536 || memchr(line, 0, length)) return 0; pos += length + (end != NULL); if (length && line[length - 1] == '\r') --length; line[length] = 0; if (!length) { if (in_stanza && !valid_entry(&index->entries[index->count - 1])) return 0; in_stanza = field = 0; continue; } if (*line == ' ' || *line == '\t') { if (!in_stanza) return 0; e = &index->entries[index->count - 1]; slot = field == 7 ? &e->depends : field == 8 ? &e->pre_depends : field == 9 ? &e->provides : NULL; if (field && field <= 6) return 0; if (slot && !append_fold(slot, line)) return 0; continue; } colon = strchr(line, ':'); if (!colon || colon == line) return 0; *colon = 0; for (value = line; *value; ++value) if (!(isalnum((unsigned char)*value) || *value == '-')) return 0; value = colon + 1; while (*value == ' ' || *value == '\t') ++value; if (!in_stanza) { if (strcasecmp(line, "Package") || !new_entry(index)) return 0; in_stanza = 1; } e = &index->entries[index->count - 1]; if (!strcasecmp(line, "Package")) slot = &e->name, bit = 1, field = 1; else if (!strcasecmp(line, "Version")) slot = &e->version, bit = 2, field = 2; else if (!strcasecmp(line, "Architecture")) slot = &e->arch, bit = 4, field = 3; else if (!strcasecmp(line, "Filename")) slot = &e->filename, bit = 8, field = 4; else if (!strcasecmp(line, "Size")) bit = 16, field = 5; else if (!strcasecmp(line, "SHA256")) slot = &e->sha256, bit = 32, field = 6; else if (!strcasecmp(line, "Depends")) slot = &e->depends, field = 7; else if (!strcasecmp(line, "Pre-Depends")) slot = &e->pre_depends, field = 8; else if (!strcasecmp(line, "Provides")) slot = &e->provides, field = 9; else field = 0; if (bit && (e->fields & bit)) return 0; e->fields |= bit; if (slot) { if (!*value || *slot) return 0; *slot = strdup(value); if (!*slot) return 0; } if (field == 5) { const char *p = value; if (!*p || *p == '0') return 0; for (; *p; ++p) { if (*p < '0' || *p > '9' || e->size > (UINT64_MAX - (unsigned)(*p - '0')) / 10) return 0; e->size = e->size * 10 + (unsigned)(*p - '0'); } } } if (in_stanza && !valid_entry(&index->entries[index->count - 1])) return 0; if (!index->count) return 0; qsort(index->entries, index->count, sizeof *index->entries, entry_order); for (pos = 1; pos < index->count; ++pos) if (!entry_order(&index->entries[pos - 1], &index->entries[pos])) return 0; return 1; } static int decompress(const char *path, char **data, size_t *size) { struct archive *archive = archive_read_new(); struct archive_entry *entry; char *buffer = NULL; size_t used = 0, capacity = 0; int status, ok = 0; if (!archive || archive_read_support_filter_all(archive) != ARCHIVE_OK || archive_read_support_format_raw(archive) != ARCHIVE_OK || archive_read_open_filename(archive, path, 65536) != ARCHIVE_OK) goto done; if (archive_read_next_header(archive, &entry) != ARCHIVE_OK) goto done; for (;;) { char chunk[65536]; la_ssize_t got = archive_read_data(archive, chunk, sizeof chunk); if (got < 0 || used > APT_INDEX_LIMIT) goto done; if (!got) break; if ((size_t)got > APT_INDEX_LIMIT - used) goto done; if (used + (size_t)got + 1 > capacity) { size_t next_size = capacity ? capacity * 2 : 65536; char *next; if (next_size < used + (size_t)got + 1) next_size = used + (size_t)got + 1; if (next_size > APT_INDEX_LIMIT + 1u) next_size = APT_INDEX_LIMIT + 1u; next = realloc(buffer, next_size); if (!next) goto done; buffer = next; capacity = next_size; } memcpy(buffer + used, chunk, (size_t)got); used += (size_t)got; } status = archive_read_next_header(archive, &entry); if (status != ARCHIVE_EOF || !used || !buffer) goto done; buffer[used] = 0; *data = buffer; *size = used; buffer = NULL; ok = 1; done: free(buffer); if (archive) archive_read_free(archive); return ok; } static int file_hash(int fd, char hex[65]) { EVP_MD_CTX *ctx = EVP_MD_CTX_new(); unsigned char buffer[65536], bytes[32]; unsigned length; ssize_t got; size_t i; int ok = 0; if (!ctx || lseek(fd, 0, SEEK_SET) < 0 || EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) goto done; for (;;) { got = read(fd, buffer, sizeof buffer); if (got < 0 && errno == EINTR) continue; if (got <= 0) break; if (EVP_DigestUpdate(ctx, buffer, (size_t)got) != 1) goto done; } if (got < 0 || EVP_DigestFinal_ex(ctx, bytes, &length) != 1 || length != 32) goto done; for (i = 0; i < 32; ++i) snprintf(hex + i * 2, 3, "%02x", bytes[i]); ok = 1; done: EVP_MD_CTX_free(ctx); return ok; } static int copy_original(const char *snapshot, int output) { int source = open(snapshot, O_RDONLY | O_CLOEXEC); int target = -1; char bytes[65536]; ssize_t got; int ok = 0; if (source < 0) goto done; target = openat(output, "original", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); if (target < 0) goto done; for (;;) { got = read(source, bytes, sizeof bytes); if (got < 0 && errno == EINTR) continue; if (got <= 0) break; size_t used = 0; while (used < (size_t)got) { ssize_t n = write(target, bytes + used, (size_t)got - used); if (n < 0 && errno == EINTR) continue; if (n <= 0) goto done; used += (size_t)n; } } ok = !got && !fsync(target) && !fsync(output); done: if (source >= 0) close(source); if (target >= 0) close(target); return ok; } static int write_index(const char *input, const char *expected, const char *source, const char *base, const char *output, int emit) { struct apt_index index = {0}; struct stat st; char *snapshot = NULL, *data = NULL, *url_check = NULL, actual[65]; size_t size = 0; FILE *record = NULL; int input_fd = -1, copy = -1, dir = -1, result = 1; if (!input || !digest(expected) || !token(source) || !strcmp(source, "local") || !base) return 2; url_check = holy_fetch_child_url(base, "probe"); if (!url_check) return 2; free(url_check); input_fd = open(input, O_RDONLY | O_NONBLOCK | O_CLOEXEC); if (input_fd < 0 || fstat(input_fd, &st) || !S_ISREG(st.st_mode) || st.st_size <= 0 || st.st_size > APT_INDEX_LIMIT) { result = 6; goto done; } snapshot = holy_stage_fd(input_fd, "holy-apt-index"); if (!snapshot) goto done; copy = open(snapshot, O_RDONLY | O_CLOEXEC); if (copy < 0 || !file_hash(copy, actual)) goto done; close(copy); copy = -1; if (strcmp(actual, expected)) { result = 4; goto done; } if (!decompress(snapshot, &data, &size) || !parse_index(data, size, &index)) { result = 2; goto done; } if (mkdir(output, 0700)) goto done; dir = open(output, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (dir < 0 || fstat(dir, &st) || st.st_uid != geteuid() || (st.st_mode & 0777) != 0700 || !copy_original(snapshot, dir)) goto done; copy = openat(dir, "original", O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (copy < 0 || !file_hash(copy, actual) || strcmp(actual, expected)) goto done; close(copy); copy = -1; { int fd = openat(dir, "conversion", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); if (fd < 0) goto done; record = fdopen(fd, "w"); if (!record) { close(fd); goto done; } } fputs("format holy-apt-index-1\nsource-name ", record); quote(record, source); fputs("\nbase-url ", record); quote(record, base); fprintf(record, "\nindex-sha256 %s\npackages %zu\nverification pinned-unverified\nstate complete\n", expected, index.count); { int failed = ferror(record); if (fflush(record) || fsync(fileno(record))) failed = 1; if (fclose(record)) failed = 1; record = NULL; if (failed || fsync(dir)) goto done; } if (emit) printf("apt index %s packages %zu sha256 %s\n", output, index.count, expected); result = 0; done: if (result) fprintf(stderr, "holypkg: APT index incomplete (status %d)\n", result); if (record) fclose(record); if (copy >= 0) close(copy); if (dir >= 0) close(dir); if (input_fd >= 0) close(input_fd); if (snapshot) { unlink(snapshot); free(snapshot); } free(data); free_index(&index); return result; } int holy_apt_index(const char *input, const char *expected, const char *source, const char *base, const char *output) { return write_index(input, expected, source, base, output, 1); } int holy_apt_index_quiet(const char *input, const char *expected, const char *source, const char *base, const char *output) { return write_index(input, expected, source, base, output, 0); } int holy_apt_sync(const char *url, const char *expected, const char *source, const char *base, const char *output, const char *ca_file) { char temporary[] = "/tmp/holy-apt-sync-XXXXXX"; char actual[65] = {0}; char *downloaded = NULL, *probe = NULL; struct stat st; int result; if (!url || !digest(expected) || !token(source) || !strcmp(source, "local") || !base || !output || !*output || (lstat(output, &st) == 0 || errno != ENOENT)) return 2; probe = holy_fetch_child_url(base, "probe"); if (!probe) return 2; free(probe); if (!mkdtemp(temporary)) return 1; result = holy_fetch_https_foreign(url, temporary, ca_file, actual); if (result) goto done; downloaded = malloc(strlen(temporary) + 66); if (!downloaded) { result = 1; goto done; } sprintf(downloaded, "%s/%s", temporary, actual); if (strcmp(actual, expected)) { fprintf(stderr, "holypkg: APT index hash mismatch: expected %s, received %s\n", expected, actual); result = 4; goto done; } result = holy_apt_index(downloaded, expected, source, base, output); done: if (downloaded) unlink(downloaded); free(downloaded); rmdir(temporary); return result; } static int read_catalog(const char *catalog, struct apt_index *index) { char *line = NULL, *data = NULL, *error = NULL; size_t capacity = 0, size = 0; FILE *record = NULL; int dir = open(catalog, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); int fd = -1, status = 2, seen = 0, release_required = 0; struct stat st; if (dir < 0) return 6; fd = openat(dir, "conversion", O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size > 4096) goto done; record = fdopen(fd, "r"); if (!record) goto done; fd = -1; while (getline(&line, &capacity, record) > 0) { char **v = NULL; size_t n = 0; if (!holy_lex(line, strlen(line), &v, &n, "APT conversion", 0, &error) || n != 2) { holy_tokens_free(v, n); goto done; } if (!strcmp(v[0], "format") && !strcmp(v[1], "holy-apt-index-1")) seen |= 1; else if (!strcmp(v[0], "source-name") && !index->source) index->source = strdup(v[1]); else if (!strcmp(v[0], "base-url") && !index->base) index->base = strdup(v[1]); else if (!strcmp(v[0], "index-sha256") && !index->hash[0] && digest(v[1])) strcpy(index->hash, v[1]); else if (!strcmp(v[0], "source-id") && !index->source_id[0] && digest(v[1])) strcpy(index->source_id, v[1]); else if (!strcmp(v[0], "state") && !strcmp(v[1], "complete")) seen |= 2; else if (!strcmp(v[0], "release-required") && !strcmp(v[1], "yes")) release_required = 1; holy_tokens_free(v, n); } if (ferror(record) || seen != 3 || !token(index->source) || !index->base || !digest(index->hash)) goto done; { char *probe = holy_fetch_child_url(index->base, "probe"); if (!probe) goto done; free(probe); } fclose(record); record = NULL; fd = openat(dir, "original", O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size > APT_INDEX_LIMIT) goto done; { char actual[65], descriptor[64]; snprintf(descriptor, sizeof descriptor, "/proc/self/fd/%d", fd); if (!file_hash(fd, actual) || strcmp(actual, index->hash) || !decompress(descriptor, &data, &size) || !parse_index(data, size, index)) goto done; } index->release_verified = holy_apt_verify_release(catalog, index->hash); if (index->release_verified == -2) { status = 6; goto done; } if (index->release_verified < 0 || (release_required && !index->release_verified)) goto done; if (holy_apt_verify_files(catalog) < 0) goto done; status = 0; done: if (record) fclose(record); if (fd >= 0) close(fd); if (dir >= 0) close(dir); free(line); free(data); free(error); return status; } static int check_source(const char *catalog, const struct apt_index *index, const char *root, const char *source) { char id[65], key[65], actual[65]; char *base = NULL, *trust = NULL, *key_file = NULL; int result; if (!root && !source) return 0; if (!root || !source) return 2; result = holy_source_apt(root, source, id, &base, &trust, key); if (result) goto done; if (!index->release_verified || !index->source_id[0] || strcmp(index->source_id, id) || strcmp(index->base, base) || !key[0]) { result = 4; goto done; } key_file = malloc(strlen(catalog) + sizeof "/keyring"); if (!key_file) { result = 1; goto done; } sprintf(key_file, "%s/keyring", catalog); if (!holy_apt_key_fingerprint(key_file, actual) || strcmp(actual, key)) result = 4; done: free(base); free(trust); free(key_file); return result; } static int file_query(const char *catalog, const struct apt_index *index, const char *query, const char *required_package) { char *path = NULL, line[65536], **found = NULL; const char *wanted = query[0] == '/' ? query + 1 : query; gzFile gz = NULL; size_t bytes = 0, matches = 0, i; int result = 2, code = Z_OK; if (!*wanted) return 2; if (holy_apt_verify_files(catalog) != 1) { fputs("holypkg: APT file index coverage unavailable\n", stderr); return 6; } path = malloc(strlen(catalog) + sizeof "/contents.gz"); if (!path) return 1; sprintf(path, "%s/contents.gz", catalog); { int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (fd < 0) goto done; gz = gzdopen(fd, "rb"); if (!gz) { close(fd); goto done; } } while (gzgets(gz, line, sizeof line)) { char *end = strrchr(line, '\n'), *location, *name; size_t length = strlen(line); if (!end || length > sizeof line - 2 || bytes > 512u * 1024u * 1024u - length) goto done; bytes += length; *end = 0; location = line + strlen(line); while (location > line && location[-1] != ' ' && location[-1] != '\t') --location; if (location == line) continue; name = location; while (name > line && (name[-1] == ' ' || name[-1] == '\t')) --name; *name = 0; if (strcmp(line, wanted)) continue; for (name = strtok(location, ","); name; name = strtok(NULL, ",")) { char *slash = strrchr(name, '/'); const char *package = slash ? slash + 1 : name; if (!token(package)) continue; for (i = 0; i < index->count; ++i) if (!strcmp(index->entries[i].name, package)) break; if (i == index->count) continue; if (required_package) { if (!strcmp(required_package, package)) matches = 1; continue; } for (i = 0; i < matches; ++i) if (!strcmp(found[i], package)) break; if (i == matches) { char **next; if (matches == 100000) { result = 6; goto done; } next = realloc(found, (matches + 1) * sizeof *next); if (!next) { result = 1; goto done; } found = next; found[matches] = strdup(package); if (!found[matches]) { result = 1; goto done; } ++matches; } } } gzerror(gz, &code); if (code != Z_OK && code != Z_STREAM_END) goto done; result = matches ? 0 : 6; done: if (gz && gzclose(gz) != Z_OK) result = 2; if (result == 6 && !matches) fputs("holypkg: no match in partial APT file index; absence is unknown\n", stderr); if (!result && !required_package) for (i = 0; i < matches; ++i) printf("%s /%s\n", found[i], wanted); if (!required_package) for (i = 0; i < matches; ++i) free(found[i]); free(found); free(path); return result; } int holy_apt_query(const char *catalog, const char *query, int info, int file_search, const char *root, const char *source) { struct apt_index index = {0}; size_t i, matches = 0; int result; if (!catalog || !query || !*query) return 2; result = read_catalog(catalog, &index); if (result) goto done; result = check_source(catalog, &index, root, source); if (result) goto done; if (file_search) { result = file_query(catalog, &index, query, NULL); goto done; } for (i = 0; i < index.count; ++i) { const struct apt_entry *e = &index.entries[i]; if (info ? strcmp(e->name, query) : !strstr(e->name, query)) continue; ++matches; if (info) { printf("package %s\nversion %s\narch %s\nfilename %s\nsize %llu\nsha256 %s\n", e->name, e->version, e->arch, e->filename, e->size, e->sha256); printf("verification %s\n", index.release_verified == 2 ? "inrelease-gpgv-user-key" : index.release_verified == 1 ? "release-gpgv-user-key" : "pinned-unverified"); if (root) printf("source-id %s\nsource-binding checked\n", index.source_id); fputs("depends ", stdout); quote(stdout, e->depends ? e->depends : "-"); fputc('\n', stdout); fputs("pre-depends ", stdout); quote(stdout, e->pre_depends ? e->pre_depends : "-"); fputc('\n', stdout); fputs("provides ", stdout); quote(stdout, e->provides ? e->provides : "-"); fputc('\n', stdout); } else printf("%s %s %s\n", e->name, e->version, e->arch); } result = matches ? info && matches > 1 ? 3 : 0 : 4; done: if (result == 2) fputs("holypkg: malformed APT index or catalog\n", stderr); free_index(&index); return result; } static char *package_url(const char *base, const char *filename) { char *path = strdup(filename), *save = NULL, *part, *url = strdup(base); if (!path || !url) { free(path); free(url); return NULL; } for (part = strtok_r(path, "/", &save); part; part = strtok_r(NULL, "/", &save)) { char *next = holy_fetch_child_url(url, part); free(url); if (!next) { free(path); return NULL; } if (save && *save) { size_t length = strlen(next); url = realloc(next, length + 2); if (!url) { free(next); free(path); return NULL; } url[length] = '/'; url[length + 1] = 0; } else url = next; } free(path); return url; } struct required_file_visit { const char *path; int found; }; static int collect_required_file(void *opaque, const struct holy_manifest_entry *entry) { struct required_file_visit *required = opaque; if (!entry->directory && !strcmp(entry->path, required->path)) required->found = 1; return 1; } static int safe_required_file(const char *path) { const char *part, *end; size_t length; if (!path || path[0] != '/' || !path[1] || strlen(path) > 1024) return 0; part = path + 1; while (*part) { size_t i; end = strchr(part, '/'); length = end ? (size_t)(end - part) : strlen(part); if (!length || (length == 1 && part[0] == '.') || (length == 2 && part[0] == '.' && part[1] == '.')) return 0; for (i = 0; i < length; ++i) if ((unsigned char)part[i] < 32 || (unsigned char)part[i] == 127) return 0; if (!end) break; part = end + 1; } return *part != 0; } static int imported_identity(const char *path, const struct apt_entry *selected, const char *required_file) { DIR *dir = opendir(path); struct dirent *entry; struct required_file_visit required = {required_file ? required_file + 1 : NULL, 0}; size_t count = 0; int ok = 0; if (!dir) return 0; errno = 0; while ((entry = readdir(dir))) { struct holy_package_identity identity = {0}; char *filename; size_t length = strlen(entry->d_name); if (length < 6 || strcmp(entry->d_name + length - 5, ".holy")) continue; filename = malloc(strlen(path) + length + 2); if (!filename) goto done; sprintf(filename, "%s/%s", path, entry->d_name); if (!holy_package_identity(filename, &identity)) { free(filename); goto done; } if (strcmp(identity.name, selected->name) || strcmp(identity.version, selected->version) || (!strcmp(selected->arch, "all") && strcmp(identity.arch, "noarch")) || (!strcmp(selected->arch, "amd64") && strcmp(identity.arch, "x86_64") && strcmp(identity.arch, "noarch")) || (!strcmp(selected->arch, "i386") && strcmp(identity.arch, "x86") && strcmp(identity.arch, "noarch"))) { holy_package_identity_free(&identity); free(filename); goto done; } holy_package_identity_free(&identity); if (required_file && !holy_verify_visit(filename, collect_required_file, &required)) { free(filename); goto done; } free(filename); ++count; errno = 0; } ok = count && !errno && (!required_file || required.found); done: closedir(dir); return ok; } static int apt_proof_hashes(const char *catalog, int inline_signature, char key_hash[65], char signature_hash[65]) { struct stat st; int dir = open(catalog, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); int fd = -1, ok = 0; if (dir < 0) return 0; fd = openat(dir, "keyring", O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size > 1024 * 1024 || !file_hash(fd, key_hash)) goto done; close(fd); fd = -1; fd = openat(dir, inline_signature ? "inrelease" : "release.gpg", O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size > (inline_signature ? 8 : 1) * 1024 * 1024 || !file_hash(fd, signature_hash)) goto done; ok = 1; done: if (fd >= 0) close(fd); close(dir); return ok; } int holy_apt_fetch(const char *catalog, const char *name, const char *version, const char *arch, const char *output, const char *ca_file, int import, const char *required_file, const char *root, const char *source) { struct apt_index index = {0}; const struct apt_entry *selected = NULL; char downloaded[65] = {0}, *url = NULL, *converted = NULL, *original = NULL; char key_hash[65] = {0}, signature_hash[65] = {0}; const char *verification; struct stat st; FILE *receipt = NULL; size_t i; int dir = -1, fd = -1, result; { int order; if (!catalog || !token(name) || !version || !holy_deb_version_compare(version, version, &order) || !token(arch) || !output || (required_file && (!import || !safe_required_file(required_file)))) return 2; } result = read_catalog(catalog, &index); if (result) goto done; result = check_source(catalog, &index, root, source); if (result) goto done; verification = index.release_verified == 2 ? "inrelease-gpgv-user-key" : index.release_verified == 1 ? "release-gpgv-user-key" : "pinned-unverified"; for (i = 0; i < index.count; ++i) { const struct apt_entry *e = &index.entries[i]; if (!strcmp(e->name, name) && !strcmp(e->version, version) && !strcmp(e->arch, arch)) { selected = e; break; } } if (!selected) { result = 4; goto done; } if (required_file) { result = file_query(catalog, &index, required_file, name); if (result) goto done; } url = package_url(index.base, selected->filename); if (!url) { result = 2; goto done; } result = 1; if (mkdir(output, 0700)) goto done; dir = open(output, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (dir < 0 || fstat(dir, &st) || st.st_uid != geteuid() || (st.st_mode & 0777) != 0700) goto done; result = holy_fetch_https_foreign(url, output, ca_file, downloaded); if (result) goto done; if (strcmp(downloaded, selected->sha256)) { result = 4; goto done; } fd = openat(dir, downloaded, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || (unsigned long long)st.st_size != selected->size) { result = 4; goto done; } close(fd); fd = -1; result = 1; if (linkat(dir, downloaded, dir, "original", 0) || fsync(dir)) goto done; if (import) { if (index.release_verified && !apt_proof_hashes(catalog, index.release_verified == 2, key_hash, signature_hash)) { result = 4; goto done; } if (index.release_verified && holy_apt_verify_release(catalog, index.hash) != index.release_verified) { result = 4; goto done; } converted = malloc(strlen(output) + sizeof "/converted"); if (!converted) goto done; sprintf(converted, "%s/converted", output); original = malloc(strlen(output) + sizeof "/original"); if (!original) { result = 1; goto done; } sprintf(original, "%s/original", output); result = holy_import_deb_verified(original, index.source, converted, selected->sha256, verification, index.release_verified ? key_hash : NULL, index.release_verified ? signature_hash : NULL, index.hash, index.base); if (result) goto done; if (!imported_identity(converted, selected, required_file)) { result = 4; goto done; } } fd = openat(dir, "selection", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); if (fd < 0) { result = 1; goto done; } receipt = fdopen(fd, "w"); if (!receipt) { close(fd); fd = -1; result = 1; goto done; } fd = -1; fputs("format holy-apt-selection-1\nsource-name ", receipt); quote(receipt, index.source); fputs("\nbase-url ", receipt); quote(receipt, index.base); fputs("\nurl ", receipt); quote(receipt, url); fputs("\nname ", receipt); quote(receipt, name); fputs("\nversion ", receipt); quote(receipt, version); fputs("\narch ", receipt); quote(receipt, arch); if (required_file) { fputs("\nrequired-file ", receipt); quote(receipt, required_file); fputs("\nfile-provider verified-payload", receipt); } if (root) fprintf(receipt, "\nsource-id %s\nsource-binding checked", index.source_id); fprintf(receipt, "\nindex-sha256 %s\nartifact-sha256 %s\nsize %llu\nverification %s\nimported %s\nstate complete\n", index.hash, selected->sha256, selected->size, verification, import ? "yes" : "no"); { int failed = ferror(receipt); if (fflush(receipt) || fsync(fileno(receipt))) failed = 1; if (fclose(receipt)) failed = 1; receipt = NULL; if (failed || fsync(dir)) { result = 1; goto done; } } printf("apt fetched %s %s %s sha256 %s%s\n", name, version, arch, downloaded, import ? " imported" : ""); result = 0; done: if (result) fprintf(stderr, "holypkg: APT fetch incomplete (status %d)\n", result); if (receipt) fclose(receipt); if (fd >= 0) close(fd); if (dir >= 0) close(dir); free(url); free(converted); free(original); free_index(&index); return result; } int holy_apt_catalog_identity(const char *catalog, const char *root, const char *source, char source_id[65], char index_hash[65]) { struct apt_index index = {0}; int result; source_id[0] = index_hash[0] = 0; if (!catalog || !root || !source) return 2; result = read_catalog(catalog, &index); if (!result) result = check_source(catalog, &index, root, source); if (!result) { memcpy(source_id, index.source_id, 65); memcpy(index_hash, index.hash, 65); } free_index(&index); return result; }